diff --git a/cli/archive_test.go b/cli/archive_test.go deleted file mode 100644 index 93966607617..00000000000 --- a/cli/archive_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package cli - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "io" - "io/fs" - "os" - "path/filepath" - "strings" - "testing" - - "go.viam.com/test" -) - -func TestArchive(t *testing.T) { - tempDir := t.TempDir() - - regularFiles := []string{"file1.txt", "file2.txt"} - allFiles := append([]string{"file1link"}, regularFiles...) - - for _, filename := range regularFiles { - fullPath := filepath.Join(tempDir, filename) - err := os.WriteFile(fullPath, []byte("content"), fs.ModePerm) - test.That(t, err, test.ShouldBeNil) - } - - // Create file1link as a symlink to file1.txt - linkName := filepath.Join(tempDir, "file1link") - err := os.Symlink("file1.txt", linkName) - test.That(t, err, test.ShouldBeNil) - - t.Run("archive file paths", func(t *testing.T) { - foundFiles, err := getArchiveFilePaths([]string{tempDir}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, foundFiles, test.ShouldHaveLength, 3) - for _, f := range foundFiles { - if _, err := os.Stat(f); os.IsNotExist(err) { - t.Errorf("file %s was not found", f) - } - } - }) - - t.Run("create archive", func(t *testing.T) { - paths, err := getArchiveFilePaths([]string{tempDir}) - test.That(t, err, test.ShouldBeNil) - - var buf bytes.Buffer - err = createArchive(paths, &buf, nil) - test.That(t, err, test.ShouldBeNil) - - gzr, err := gzip.NewReader(&buf) - test.That(t, err, test.ShouldBeNil) - defer gzr.Close() - - tr := tar.NewReader(gzr) - - // Map to track which files we've checked - verifiedFiles := make(map[string]bool) - for range allFiles { - header, err := tr.Next() - test.That(t, err, test.ShouldBeNil) - - expectedFile := false - for _, fileName := range allFiles { - if strings.HasSuffix(header.Name, fileName) { - expectedFile = true - verifiedFiles[fileName] = true - break - } - } - test.That(t, expectedFile, test.ShouldBeTrue) - - fileContent := make([]byte, header.Size) - _, err = tr.Read(fileContent) - test.That(t, err, test.ShouldEqual, io.EOF) - // Note that even the symlink should have a value of `content` - test.That(t, string(fileContent), test.ShouldEqual, "content") - } - - // Ensure we visited all files - test.That(t, len(verifiedFiles), test.ShouldEqual, len(allFiles)) - for _, file := range allFiles { - test.That(t, verifiedFiles[file], test.ShouldBeTrue) - } - }) -} diff --git a/cli/auth_test.go b/cli/auth_test.go deleted file mode 100644 index dd3934f852d..00000000000 --- a/cli/auth_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package cli - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strings" - "testing" - - apppb "go.viam.com/api/app/v1" - "go.viam.com/test" - "google.golang.org/grpc" - - "go.viam.com/rdk/testutils/inject" -) - -func TestLoginAction(t *testing.T) { - cCtx, ac, out, errOut := setup(nil, nil, nil, nil, "token") - - test.That(t, ac.loginAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 1) - test.That(t, out.messages[0], test.ShouldContainSubstring, - fmt.Sprintf("Already logged in as %q", testEmail)) -} - -func TestAPIKeyAuth(t *testing.T) { - _, ac, _, errOut := setup(nil, nil, nil, nil, "apiKey") - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - APIKey, isAPIKey := ac.conf.Auth.(*apiKey) - test.That(t, isAPIKey, test.ShouldBeTrue) - test.That(t, APIKey.KeyID, test.ShouldEqual, testKeyID) - test.That(t, APIKey.KeyCrypto, test.ShouldEqual, testKeyCrypto) -} - -func TestPrintAccessTokenAction(t *testing.T) { - // AppServiceClient needed for any Action that calls ensureLoggedIn. - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, nil, nil, "token") - - test.That(t, ac.printAccessTokenAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 1) - test.That(t, out.messages[0], test.ShouldContainSubstring, testToken) -} - -func TestAPIKeyCreateAction(t *testing.T) { - createKeyFunc := func(ctx context.Context, in *apppb.CreateKeyRequest, - opts ...grpc.CallOption, - ) (*apppb.CreateKeyResponse, error) { - return &apppb.CreateKeyResponse{Id: "id-xxx", Key: "key-yyy"}, nil - } - asc := &inject.AppServiceClient{ - CreateKeyFunc: createKeyFunc, - } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") - - test.That(t, ac.organizationsAPIKeyCreateAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 8) - test.That(t, strings.Join(out.messages, ""), test.ShouldContainSubstring, "id-xxx") - test.That(t, strings.Join(out.messages, ""), test.ShouldContainSubstring, "key-yyy") -} - -func TestRobotAPIKeyCreateAction(t *testing.T) { - createKeyFunc := func(ctx context.Context, in *apppb.CreateKeyRequest, - opts ...grpc.CallOption, - ) (*apppb.CreateKeyResponse, error) { - return &apppb.CreateKeyResponse{Id: "id-xxx", Key: "key-yyy"}, nil - } - - fakeOrgID := "fake-org-id" - fakeRobotID := "fake-robot" - - asc := &inject.AppServiceClient{ - CreateKeyFunc: createKeyFunc, - } - - flags := make(map[string]any) - flags[generalFlagOrgID] = fakeOrgID - flags[generalFlagMachineID] = fakeRobotID - flags[apiKeyCreateFlagName] = "my-name" - cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "token") - - test.That(t, ac.robotAPIKeyCreateAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 6) - test.That(t, out.messages[1], test.ShouldContainSubstring, "Successfully created key") - test.That(t, out.messages[2], test.ShouldContainSubstring, "Key ID: id-xxx") - test.That(t, out.messages[3], test.ShouldContainSubstring, "Key Value: key-yyy") - - // test that without name still works - - cCtx.Set(apiKeyCreateFlagName, "") - test.That(t, cCtx.Value(apiKeyCreateFlagName), test.ShouldEqual, "") - - test.That(t, ac.robotAPIKeyCreateAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, strings.Join(out.messages, " "), test.ShouldContainSubstring, "using default key name of") - - // test without an orgID - cCtx.Set(generalFlagOrgID, "") - test.That(t, cCtx.Value(generalFlagOrgID), test.ShouldEqual, "") - - test.That(t, ac.robotAPIKeyCreateAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - - allMessages := strings.Join(out.messages, " ") - test.That(t, allMessages, test.ShouldContainSubstring, "using default key name of ") - - test.That(t, allMessages, test.ShouldContainSubstring, "Successfully created key") - test.That(t, allMessages, test.ShouldContainSubstring, "Key ID: id-xxx") - test.That(t, allMessages, test.ShouldContainSubstring, "Key Value: key-yyy") - - // test without a robot ID should fail - cCtx.Set(generalFlagMachineID, "") - test.That(t, cCtx.Value(generalFlagMachineID), test.ShouldEqual, "") - err := ac.robotAPIKeyCreateAction(cCtx) - test.That(t, err, test.ShouldNotBeNil) - - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot create an api-key for a machine without an ID") - - // test for a location with multiple orgs doesn't work if you don't provide an orgID - createKeyFunc = func(ctx context.Context, in *apppb.CreateKeyRequest, - opts ...grpc.CallOption, - ) (*apppb.CreateKeyResponse, error) { - return nil, errors.New("multiple orgs on the location") - } - - asc = &inject.AppServiceClient{ - CreateKeyFunc: createKeyFunc, - } - - flags = make(map[string]any) - flags[generalFlagMachineID] = fakeRobotID - flags[generalFlagOrgID] = "" - flags[apiKeyCreateFlagName] = "test-me" - cCtx, ac, out, _ = setup(asc, nil, nil, flags, "token") - err = ac.robotAPIKeyCreateAction(cCtx) - test.That(t, err, test.ShouldNotBeNil) - - test.That(t, len(out.messages), test.ShouldEqual, 0) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot create the machine api-key as there are multiple orgs on the location.") -} - -func TestLocationAPIKeyCreateAction(t *testing.T) { - fakeLocID := "fake-loc-id" - fakeOrgID := "fake-org-id" - - createKeyFunc := func(ctx context.Context, in *apppb.CreateKeyRequest, - opts ...grpc.CallOption, - ) (*apppb.CreateKeyResponse, error) { - return &apppb.CreateKeyResponse{Id: "id-xxx", Key: "key-yyy"}, nil - } - - asc := &inject.AppServiceClient{ - CreateKeyFunc: createKeyFunc, - } - - flags := make(map[string]any) - flags[generalFlagLocationID] = "" - flags[generalFlagOrgID] = "" - flags[apiKeyCreateFlagName] = "" // testing no locationID - - cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "token") - err := ac.locationAPIKeyCreateAction(cCtx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot create an api-key for a location without an ID") - - cCtx.Set(generalFlagLocationID, fakeLocID) - // will create an api-key with a default name - test.That(t, ac.locationAPIKeyCreateAction(cCtx), test.ShouldBeNil) - allMessages := strings.Join(out.messages, " ") - - test.That(t, allMessages, test.ShouldContainSubstring, "using default key name of ") - test.That(t, allMessages, test.ShouldContainSubstring, "Successfully created key") - test.That(t, allMessages, test.ShouldContainSubstring, "Key ID: id-xxx") - test.That(t, allMessages, test.ShouldContainSubstring, "Key Value: key-yyy") - - // test with an orgID is fine - cCtx.Set(generalFlagOrgID, fakeOrgID) - test.That(t, ac.c.Value(generalFlagOrgID), test.ShouldNotBeEmpty) - test.That(t, ac.locationAPIKeyCreateAction(cCtx), test.ShouldBeNil) - allMessages = strings.Join(out.messages, " ") - - test.That(t, allMessages, test.ShouldContainSubstring, "Successfully created key") - test.That(t, allMessages, test.ShouldContainSubstring, "Key ID: id-xxx") - test.That(t, allMessages, test.ShouldContainSubstring, "Key Value: key-yyy") - // test that multiple organizations on the location will error out} - createKeyFunc = func(ctx context.Context, in *apppb.CreateKeyRequest, - opts ...grpc.CallOption, - ) (*apppb.CreateKeyResponse, error) { - return nil, errors.New("multiple orgs on the location") - } - - asc = &inject.AppServiceClient{ - CreateKeyFunc: createKeyFunc, - } - - flags = make(map[string]any) - flags[generalFlagLocationID] = fakeLocID - flags[generalFlagOrgID] = "" - flags[apiKeyCreateFlagName] = "test-name" - - cCtx, ac, _, _ = setup(asc, nil, nil, flags, "token") - - err = ac.locationAPIKeyCreateAction(cCtx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, - fmt.Sprintf("cannot create api-key for location: %s as there are multiple orgs on the location", fakeLocID)) -} - -func TestLogoutAction(t *testing.T) { - cCtx, ac, out, errOut := setup(nil, nil, nil, nil, "token") - - test.That(t, ac.logoutAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 1) - test.That(t, out.messages[0], test.ShouldContainSubstring, - fmt.Sprintf("Logged out from %q", testEmail)) -} - -func TestWhoAmIAction(t *testing.T) { - cCtx, ac, out, errOut := setup(nil, nil, nil, nil, "token") - - test.That(t, ac.whoAmIAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 1) - test.That(t, out.messages[0], test.ShouldContainSubstring, testEmail) -} - -func TestConfigMarshalling(t *testing.T) { - t.Run("token config", func(t *testing.T) { - conf := config{ - BaseURL: "https://guthib.com:443", - Auth: &token{ - AccessToken: "secret-token", - User: userData{ - Email: "tipsy@viam.com", - Subject: "MAIV", - }, - }, - } - - bytes, err := json.Marshal(conf) - test.That(t, err, test.ShouldBeNil) - var newConf config - test.That(t, newConf.tryUnmarshallWithAPIKey(bytes), test.ShouldBeError) - test.That(t, newConf.tryUnmarshallWithToken(bytes), test.ShouldBeNil) - test.That(t, newConf.BaseURL, test.ShouldEqual, "https://guthib.com:443") - auth, ok := newConf.Auth.(*token) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, auth.AccessToken, test.ShouldEqual, "secret-token") - test.That(t, auth.User.Email, test.ShouldEqual, "tipsy@viam.com") - test.That(t, auth.User.Subject, test.ShouldEqual, "MAIV") - }) - - t.Run("api-key config", func(t *testing.T) { - conf := config{ - BaseURL: "https://docs.viam.com:443", - Auth: &apiKey{ - KeyID: "42", - KeyCrypto: "secret", - }, - } - - bytes, err := json.Marshal(conf) - test.That(t, err, test.ShouldBeNil) - var newConf config - test.That(t, newConf.tryUnmarshallWithToken(bytes), test.ShouldBeError) - test.That(t, newConf.tryUnmarshallWithAPIKey(bytes), test.ShouldBeNil) - test.That(t, newConf.BaseURL, test.ShouldEqual, "https://docs.viam.com:443") - auth, ok := newConf.Auth.(*apiKey) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, auth.KeyID, test.ShouldEqual, "42") - test.That(t, auth.KeyCrypto, test.ShouldEqual, "secret") - }) -} diff --git a/cli/client_test.go b/cli/client_test.go deleted file mode 100644 index 1e8d22725a6..00000000000 --- a/cli/client_test.go +++ /dev/null @@ -1,819 +0,0 @@ -package cli - -import ( - "context" - "errors" - "flag" - "fmt" - "io/fs" - "maps" - "os" - "path/filepath" - "strconv" - "strings" - "testing" - "time" - - "github.com/google/uuid" - "github.com/urfave/cli/v2" - "go.uber.org/zap/zapcore" - buildpb "go.viam.com/api/app/build/v1" - datapb "go.viam.com/api/app/data/v1" - apppb "go.viam.com/api/app/v1" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/structpb" - - robotconfig "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/services/shell" - _ "go.viam.com/rdk/services/shell/register" - shelltestutils "go.viam.com/rdk/services/shell/testutils" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/testutils/robottestutils" - "go.viam.com/rdk/utils" -) - -var ( - testEmail = "grogu@viam.com" - testToken = "thisistheway" - testKeyID = "testkeyid" - testKeyCrypto = "testkeycrypto" -) - -type testWriter struct { - messages []string -} - -// Write implements io.Writer. -func (tw *testWriter) Write(b []byte) (int, error) { - tw.messages = append(tw.messages, string(b)) - return len(b), nil -} - -// populateFlags populates a FlagSet from a map. -func populateFlags(m map[string]any, args ...string) *flag.FlagSet { - flags := &flag.FlagSet{} - // init all the default flags from the input - for name, val := range m { - switch v := val.(type) { - case int: - flags.Int(name, v, "") - case string: - flags.String(name, v, "") - case bool: - flags.Bool(name, v, "") - default: - // non-int and non-string flags not yet supported - continue - } - } - if err := flags.Parse(args); err != nil { - panic(err) - } - return flags -} - -func newTestContext(t *testing.T, flags map[string]any) *cli.Context { - t.Helper() - out := &testWriter{} - errOut := &testWriter{} - return cli.NewContext(NewApp(out, errOut), populateFlags(flags), nil) -} - -// setup creates a new cli.Context and viamClient with fake auth and the passed -// in AppServiceClient and DataServiceClient. It also returns testWriters that capture Stdout and -// Stdin. -func setup( - asc apppb.AppServiceClient, - dataClient datapb.DataServiceClient, - buildClient buildpb.BuildServiceClient, - defaultFlags map[string]any, - authMethod string, - cliArgs ...string, -) (*cli.Context, *viamClient, *testWriter, *testWriter) { - out := &testWriter{} - errOut := &testWriter{} - flags := populateFlags(defaultFlags, cliArgs...) - - if dataClient != nil { - // these flags are only relevant when testing a dataClient - flags.String(dataFlagDataType, dataTypeTabular, "") - flags.String(dataFlagDestination, utils.ResolveFile(""), "") - } - - cCtx := cli.NewContext(NewApp(out, errOut), flags, nil) - conf := &config{} - if authMethod == "token" { - conf.Auth = &token{ - AccessToken: testToken, - ExpiresAt: time.Now().Add(time.Hour), - User: userData{ - Email: testEmail, - }, - } - } else if authMethod == "apiKey" { - conf.Auth = &apiKey{ - KeyID: testKeyID, - KeyCrypto: testKeyCrypto, - } - } - - ac := &viamClient{ - client: asc, - conf: conf, - c: cCtx, - dataClient: dataClient, - buildClient: buildClient, - selectedOrg: &apppb.Organization{}, - selectedLoc: &apppb.Location{}, - } - return cCtx, ac, out, errOut -} - -//nolint:unparam -func setupWithRunningPart( - t *testing.T, - asc apppb.AppServiceClient, - dataClient datapb.DataServiceClient, - buildClient buildpb.BuildServiceClient, - defaultFlags map[string]any, - authMethod string, - partFQDN string, - cliArgs ...string, -) (*cli.Context, *viamClient, *testWriter, *testWriter, func()) { - t.Helper() - - cCtx, ac, out, errOut := setup(asc, dataClient, buildClient, defaultFlags, authMethod, cliArgs...) - - // this config could later become a parameter - r, err := robotimpl.New(cCtx.Context, &robotconfig.Config{ - Services: []resource.Config{ - { - Name: "shell1", - API: shell.API, - Model: resource.DefaultServiceModel, - }, - }, - }, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - options.FQDN = partFQDN - err = r.StartWeb(cCtx.Context, options) - test.That(t, err, test.ShouldBeNil) - - baseURL, rpcOpts, err := parseBaseURL(fmt.Sprintf("http://%s", addr), false) - test.That(t, err, test.ShouldBeNil) - // this will be the URL we use to make new clients. In a backwards way, this - // lets the robot be the one with external auth handling (if auth were being used) - ac.baseURL = baseURL - ac.rpcOpts = rpcOpts - - return cCtx, ac, out, errOut, func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - } -} - -func TestListOrganizationsAction(t *testing.T) { - listOrganizationsFunc := func(ctx context.Context, in *apppb.ListOrganizationsRequest, - opts ...grpc.CallOption, - ) (*apppb.ListOrganizationsResponse, error) { - orgs := []*apppb.Organization{{Name: "jedi", PublicNamespace: "anakin"}, {Name: "mandalorians"}} - return &apppb.ListOrganizationsResponse{Organizations: orgs}, nil - } - asc := &inject.AppServiceClient{ - ListOrganizationsFunc: listOrganizationsFunc, - } - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "token") - - test.That(t, ac.listOrganizationsAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 3) - test.That(t, out.messages[0], test.ShouldEqual, fmt.Sprintf("Organizations for %q:\n", testEmail)) - test.That(t, out.messages[1], test.ShouldContainSubstring, "jedi") - test.That(t, out.messages[1], test.ShouldContainSubstring, "anakin") - test.That(t, out.messages[2], test.ShouldContainSubstring, "mandalorians") -} - -func TestTabularDataByFilterAction(t *testing.T) { - pbStruct, err := protoutils.StructToStructPb(map[string]interface{}{"bool": true, "string": "true", "float": float64(1)}) - test.That(t, err, test.ShouldBeNil) - - // calls to `TabularDataByFilter` will repeat so long as data continue to be returned, - // so we need a way of telling our injected method when data has already been sent so we - // can send an empty response - var dataRequested bool - tabularDataByFilterFunc := func(ctx context.Context, in *datapb.TabularDataByFilterRequest, opts ...grpc.CallOption, - ) (*datapb.TabularDataByFilterResponse, error) { - if dataRequested { - return &datapb.TabularDataByFilterResponse{}, nil - } - dataRequested = true - return &datapb.TabularDataByFilterResponse{ - Data: []*datapb.TabularData{{Data: pbStruct}}, - Metadata: []*datapb.CaptureMetadata{{LocationId: "loc-id"}}, - }, nil - } - - dsc := &inject.DataServiceClient{ - TabularDataByFilterFunc: tabularDataByFilterFunc, - } - - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, dsc, nil, nil, "token") - - test.That(t, ac.dataExportAction(cCtx), test.ShouldBeNil) - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - test.That(t, len(out.messages), test.ShouldEqual, 4) - test.That(t, out.messages[0], test.ShouldEqual, "Downloading..") - test.That(t, out.messages[1], test.ShouldEqual, ".") - test.That(t, out.messages[2], test.ShouldEqual, ".") - test.That(t, out.messages[3], test.ShouldEqual, "\n") - - // expectedDataSize is the expected string length of the data returned by the injected call - expectedDataSize := 98 - b := make([]byte, expectedDataSize) - - // `data.ndjson` is the standardized name of the file data is written to in the `tabularData` call - filePath := utils.ResolveFile("data/data.ndjson") - file, err := os.Open(filePath) - test.That(t, err, test.ShouldBeNil) - - dataSize, err := file.Read(b) - test.That(t, err, test.ShouldBeNil) - test.That(t, dataSize, test.ShouldEqual, expectedDataSize) - - savedData := string(b) - expectedData := "{\"MetadataIndex\":0,\"TimeReceived\":null,\"TimeRequested\":null,\"bool\":true,\"float\":1,\"string\":\"true\"}" - test.That(t, savedData, test.ShouldEqual, expectedData) - - expectedMetadataSize := 23 - b = make([]byte, expectedMetadataSize) - - // metadata is named `0.json` based on its index in the metadata array - filePath = utils.ResolveFile("metadata/0.json") - file, err = os.Open(filePath) - test.That(t, err, test.ShouldBeNil) - - metadataSize, err := file.Read(b) - test.That(t, err, test.ShouldBeNil) - test.That(t, metadataSize, test.ShouldEqual, expectedMetadataSize) - - savedMetadata := string(b) - test.That(t, savedMetadata, test.ShouldEqual, "{\"locationId\":\"loc-id\"}") -} - -func TestBaseURLParsing(t *testing.T) { - // Test basic parsing - url, rpcOpts, err := parseBaseURL("https://app.viam.com:443", false) - test.That(t, err, test.ShouldBeNil) - test.That(t, url.Port(), test.ShouldEqual, "443") - test.That(t, url.Scheme, test.ShouldEqual, "https") - test.That(t, url.Hostname(), test.ShouldEqual, "app.viam.com") - test.That(t, rpcOpts, test.ShouldBeNil) - - // Test parsing without a port - url, _, err = parseBaseURL("https://app.viam.com", false) - test.That(t, err, test.ShouldBeNil) - test.That(t, url.Port(), test.ShouldEqual, "443") - test.That(t, url.Hostname(), test.ShouldEqual, "app.viam.com") - - // Test parsing locally - url, rpcOpts, err = parseBaseURL("http://127.0.0.1:8081", false) - test.That(t, err, test.ShouldBeNil) - test.That(t, url.Scheme, test.ShouldEqual, "http") - test.That(t, url.Port(), test.ShouldEqual, "8081") - test.That(t, url.Hostname(), test.ShouldEqual, "127.0.0.1") - test.That(t, rpcOpts, test.ShouldHaveLength, 2) - - // Test localhost:8080 - url, _, err = parseBaseURL("http://localhost:8080", false) - test.That(t, err, test.ShouldBeNil) - test.That(t, url.Port(), test.ShouldEqual, "8080") - test.That(t, url.Hostname(), test.ShouldEqual, "localhost") - test.That(t, rpcOpts, test.ShouldHaveLength, 2) - - // Test no scheme remote - url, _, err = parseBaseURL("app.viam.com", false) - test.That(t, err, test.ShouldBeNil) - test.That(t, url.Scheme, test.ShouldEqual, "https") - test.That(t, url.Hostname(), test.ShouldEqual, "app.viam.com") - - // Test invalid url - _, _, err = parseBaseURL(":5", false) - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "missing protocol scheme") -} - -func TestLogEntryFieldsToString(t *testing.T) { - t.Run("normal case", func(t *testing.T) { - f1, err := logging.FieldToProto(zapcore.Field{ - Key: "key1", - Type: zapcore.StringType, - String: "value1", - }) - test.That(t, err, test.ShouldBeNil) - f2, err := logging.FieldToProto(zapcore.Field{ - Key: "key2", - Type: zapcore.Int32Type, - Integer: 123, - }) - test.That(t, err, test.ShouldBeNil) - f3, err := logging.FieldToProto(zapcore.Field{ - Key: "facts", - Type: zapcore.ReflectType, - Interface: map[string]string{"app.viam": "cool", "cli": "cooler"}, - }) - test.That(t, err, test.ShouldBeNil) - fields := []*structpb.Struct{ - f1, f2, f3, - } - - expected := `{"key1": "value1", "key2": 123, "facts": map[app.viam:cool cli:cooler]}` - result, err := logEntryFieldsToString(fields) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldEqual, expected) - }) - - t.Run("empty fields", func(t *testing.T) { - result, err := logEntryFieldsToString([]*structpb.Struct{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldBeEmpty) - }) -} - -func TestGetRobotPartLogs(t *testing.T) { - // Create fake logs of "0"->"9999". - logs := make([]*commonpb.LogEntry, 0, maxNumLogs) - for i := 0; i < maxNumLogs; i++ { - logs = append(logs, &commonpb.LogEntry{Message: fmt.Sprintf("%d", i)}) - } - - getRobotPartLogsFunc := func(ctx context.Context, in *apppb.GetRobotPartLogsRequest, - opts ...grpc.CallOption, - ) (*apppb.GetRobotPartLogsResponse, error) { - // Accept fake page tokens of "2"-"100" and release logs in batches of 100. - // The first page token will be "", which should be interpreted as "1". - pt := 1 - if receivedPt := in.PageToken; receivedPt != nil && *receivedPt != "" { - var err error - pt, err = strconv.Atoi(*receivedPt) - test.That(t, err, test.ShouldBeNil) - } - resp := &apppb.GetRobotPartLogsResponse{ - Logs: logs[(pt-1)*100 : pt*100], - NextPageToken: fmt.Sprintf("%d", pt+1), - } - return resp, nil - } - - listOrganizationsFunc := func(ctx context.Context, in *apppb.ListOrganizationsRequest, - opts ...grpc.CallOption, - ) (*apppb.ListOrganizationsResponse, error) { - orgs := []*apppb.Organization{{Name: "jedi", Id: "123"}} - return &apppb.ListOrganizationsResponse{Organizations: orgs}, nil - } - listLocationsFunc := func(ctx context.Context, in *apppb.ListLocationsRequest, - opts ...grpc.CallOption, - ) (*apppb.ListLocationsResponse, error) { - locs := []*apppb.Location{{Name: "naboo"}} - return &apppb.ListLocationsResponse{Locations: locs}, nil - } - listRobotsFunc := func(ctx context.Context, in *apppb.ListRobotsRequest, - opts ...grpc.CallOption, - ) (*apppb.ListRobotsResponse, error) { - robots := []*apppb.Robot{{Name: "r2d2"}} - return &apppb.ListRobotsResponse{Robots: robots}, nil - } - getRobotPartsFunc := func(ctx context.Context, in *apppb.GetRobotPartsRequest, - opts ...grpc.CallOption, - ) (*apppb.GetRobotPartsResponse, error) { - parts := []*apppb.RobotPart{{Name: "main"}} - return &apppb.GetRobotPartsResponse{Parts: parts}, nil - } - - asc := &inject.AppServiceClient{ - GetRobotPartLogsFunc: getRobotPartLogsFunc, - // Supply some injected functions to avoid a panic when loading - // organizations, locations, robots and parts. - ListOrganizationsFunc: listOrganizationsFunc, - ListLocationsFunc: listLocationsFunc, - ListRobotsFunc: listRobotsFunc, - GetRobotPartsFunc: getRobotPartsFunc, - } - - t.Run("no count", func(t *testing.T) { - cCtx, ac, out, errOut := setup(asc, nil, nil, nil, "") - - test.That(t, ac.robotsPartLogsAction(cCtx), test.ShouldBeNil) - - // No warnings. - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - - // There should be a message for "organization -> location -> robot" - // followed by maxNumLogs messages. - test.That(t, len(out.messages), test.ShouldEqual, defaultNumLogs+1) - test.That(t, out.messages[0], test.ShouldEqual, "jedi -> naboo -> r2d2\n") - // Logs should be printed in order oldest->newest ("99"->"0"). - expectedLogNum := defaultNumLogs - 1 - for i := 1; i <= defaultNumLogs; i++ { - test.That(t, out.messages[i], test.ShouldContainSubstring, - fmt.Sprintf("%d", expectedLogNum)) - expectedLogNum-- - } - }) - t.Run("178 count", func(t *testing.T) { - flags := map[string]any{"count": 178} - cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "") - - test.That(t, ac.robotsPartLogsAction(cCtx), test.ShouldBeNil) - - // No warnings. - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - - // There should be a message for "organization -> location -> robot" - // followed by 178 messages. - test.That(t, len(out.messages), test.ShouldEqual, 179) - test.That(t, out.messages[0], test.ShouldEqual, "jedi -> naboo -> r2d2\n") - // Logs should be printed in order oldest->newest ("177"->"0"). - expectedLogNum := 177 - for i := 1; i <= 178; i++ { - test.That(t, out.messages[i], test.ShouldContainSubstring, - fmt.Sprintf("%d", expectedLogNum)) - expectedLogNum-- - } - }) - t.Run("max count", func(t *testing.T) { - flags := map[string]any{logsFlagCount: maxNumLogs} - cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "") - - test.That(t, ac.robotsPartLogsAction(cCtx), test.ShouldBeNil) - - // No warnings. - test.That(t, len(errOut.messages), test.ShouldEqual, 0) - - // There should be a message for "organization -> location -> robot" - // followed by maxNumLogs messages. - test.That(t, len(out.messages), test.ShouldEqual, maxNumLogs+1) - test.That(t, out.messages[0], test.ShouldEqual, "jedi -> naboo -> r2d2\n") - - // Logs should be printed in order oldest->newest ("9999"->"0"). - expectedLogNum := maxNumLogs - 1 - for i := 1; i <= maxNumLogs; i++ { - test.That(t, out.messages[i], test.ShouldContainSubstring, - fmt.Sprintf("%d", expectedLogNum)) - expectedLogNum-- - } - }) - t.Run("negative count", func(t *testing.T) { - flags := map[string]any{"count": -1} - cCtx, ac, out, errOut := setup(asc, nil, nil, flags, "") - - test.That(t, ac.robotsPartLogsAction(cCtx), test.ShouldBeNil) - - // Warning should read: `Warning:\nProvided negative "count" value. Defaulting to 100`. - test.That(t, len(errOut.messages), test.ShouldEqual, 2) - test.That(t, errOut.messages[0], test.ShouldEqual, "Warning: ") - test.That(t, errOut.messages[1], test.ShouldContainSubstring, `Provided negative "count" value. Defaulting to 100`) - - // There should be a message for "organization -> location -> robot" - // followed by maxNumLogs messages. - test.That(t, len(out.messages), test.ShouldEqual, defaultNumLogs+1) - test.That(t, out.messages[0], test.ShouldEqual, "jedi -> naboo -> r2d2\n") - // Logs should be printed in order oldest->oldest ("99"->"0"). - expectedLogNum := defaultNumLogs - 1 - for i := 1; i <= defaultNumLogs; i++ { - test.That(t, out.messages[i], test.ShouldContainSubstring, - fmt.Sprintf("%d", expectedLogNum)) - expectedLogNum-- - } - }) - t.Run("count too high", func(t *testing.T) { - flags := map[string]any{"count": 1000000} - cCtx, ac, _, _ := setup(asc, nil, nil, flags, "") - - err := ac.robotsPartLogsAction(cCtx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New(`provided too high of a "count" value. Maximum is 10000`)) - }) -} - -func TestShellFileCopy(t *testing.T) { - listOrganizationsFunc := func(ctx context.Context, in *apppb.ListOrganizationsRequest, - opts ...grpc.CallOption, - ) (*apppb.ListOrganizationsResponse, error) { - orgs := []*apppb.Organization{{Name: "jedi", Id: uuid.NewString(), PublicNamespace: "anakin"}, {Name: "mandalorians"}} - return &apppb.ListOrganizationsResponse{Organizations: orgs}, nil - } - listLocationsFunc := func(ctx context.Context, in *apppb.ListLocationsRequest, - opts ...grpc.CallOption, - ) (*apppb.ListLocationsResponse, error) { - locs := []*apppb.Location{{Name: "naboo"}} - return &apppb.ListLocationsResponse{Locations: locs}, nil - } - listRobotsFunc := func(ctx context.Context, in *apppb.ListRobotsRequest, - opts ...grpc.CallOption, - ) (*apppb.ListRobotsResponse, error) { - robots := []*apppb.Robot{{Name: "r2d2"}} - return &apppb.ListRobotsResponse{Robots: robots}, nil - } - - partFqdn := uuid.NewString() - getRobotPartsFunc := func(ctx context.Context, in *apppb.GetRobotPartsRequest, - opts ...grpc.CallOption, - ) (*apppb.GetRobotPartsResponse, error) { - parts := []*apppb.RobotPart{{Name: "main", Fqdn: partFqdn}} - return &apppb.GetRobotPartsResponse{Parts: parts}, nil - } - - asc := &inject.AppServiceClient{ - ListOrganizationsFunc: listOrganizationsFunc, - ListLocationsFunc: listLocationsFunc, - ListRobotsFunc: listRobotsFunc, - GetRobotPartsFunc: getRobotPartsFunc, - } - - partFlags := map[string]any{ - "organization": "jedi", - "location": "naboo", - "robot": "r2d2", - "part": "main", - } - - t.Run("no arguments or files", func(t *testing.T) { - cCtx, viamClient, _, _ := setup(asc, nil, nil, partFlags, "token") - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldEqual, errNoFiles) - }) - - t.Run("one file path is insufficient", func(t *testing.T) { - args := []string{"machine:path"} - cCtx, viamClient, _, _ := setup(asc, nil, nil, partFlags, "token", args...) - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldEqual, errLastArgOfFromMissing) - - args = []string{"path"} - cCtx, viamClient, _, _ = setup(asc, nil, nil, partFlags, "token", args...) - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldEqual, errLastArgOfToMissing) - }) - - t.Run("from has wrong path prefixes", func(t *testing.T) { - args := []string{"machine:path", "path2", "machine:path3", "destination"} - cCtx, viamClient, _, _ := setup(asc, nil, nil, partFlags, "token", args...) - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldHaveSameTypeAs, copyFromPathInvalidError{}) - }) - - tfs := shelltestutils.SetupTestFileSystem(t) - - t.Run("from", func(t *testing.T) { - t.Run("single file", func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{fmt.Sprintf("machine:%s", tfs.SingleFileNested), tempDir} - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlags, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - }) - - t.Run("single file relative", func(t *testing.T) { - tempDir := t.TempDir() - cwd, err := os.Getwd() - test.That(t, err, test.ShouldBeNil) - t.Cleanup(func() { os.Chdir(cwd) }) - test.That(t, os.Chdir(tempDir), test.ShouldBeNil) - - args := []string{fmt.Sprintf("machine:%s", tfs.SingleFileNested), "foo"} - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlags, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, "foo")) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - }) - - t.Run("single directory", func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{fmt.Sprintf("machine:%s", tfs.Root), tempDir} - - t.Log("without recursion set") - cCtx, viamClient, _, _, teardown1 := setupWithRunningPart( - t, asc, nil, nil, partFlags, "token", partFqdn, args...) - defer teardown1() - err := machinesPartCopyFilesAction(cCtx, viamClient) - test.That(t, errors.Is(err, errDirectoryCopyRequestNoRecursion), test.ShouldBeTrue) - _, err = os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, errors.Is(err, fs.ErrNotExist), test.ShouldBeTrue) - - t.Log("with recursion set") - partFlagsCopy := make(map[string]any, len(partFlags)) - maps.Copy(partFlagsCopy, partFlags) - partFlagsCopy["recursive"] = true - cCtx, viamClient, _, _, teardown2 := setupWithRunningPart( - t, asc, nil, nil, partFlagsCopy, "token", partFqdn, args...) - defer teardown2() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - }) - - t.Run("multiple files", func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{ - fmt.Sprintf("machine:%s", tfs.SingleFileNested), - fmt.Sprintf("machine:%s", tfs.InnerDir), - tempDir, - } - partFlagsCopy := make(map[string]any, len(partFlags)) - maps.Copy(partFlagsCopy, partFlags) - partFlagsCopy["recursive"] = true - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlagsCopy, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.InnerDir, filepath.Join(tempDir, filepath.Base(tfs.InnerDir))), test.ShouldBeNil) - }) - - t.Run("preserve permissions on a nested file", func(t *testing.T) { - tfs := shelltestutils.SetupTestFileSystem(t) - - beforeInfo, err := os.Stat(tfs.SingleFileNested) - test.That(t, err, test.ShouldBeNil) - t.Log("start with mode", beforeInfo.Mode()) - newMode := os.FileMode(0o444) - test.That(t, beforeInfo.Mode(), test.ShouldNotEqual, newMode) - test.That(t, os.Chmod(tfs.SingleFileNested, newMode), test.ShouldBeNil) - modTime := time.Date(1988, 1, 2, 3, 0, 0, 0, time.UTC) - test.That(t, os.Chtimes(tfs.SingleFileNested, time.Time{}, modTime), test.ShouldBeNil) - relNestedPath := strings.TrimPrefix(tfs.SingleFileNested, tfs.Root) - - for _, preserve := range []bool{false, true} { - t.Run(fmt.Sprintf("preserve=%t", preserve), func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{fmt.Sprintf("machine:%s", tfs.Root), tempDir} - - partFlagsCopy := make(map[string]any, len(partFlags)) - maps.Copy(partFlagsCopy, partFlags) - partFlagsCopy["recursive"] = true - partFlagsCopy["preserve"] = preserve - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlagsCopy, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - - nestedCopy := filepath.Join(tempDir, filepath.Base(tfs.Root), relNestedPath) - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - afterInfo, err := os.Stat(nestedCopy) - test.That(t, err, test.ShouldBeNil) - if preserve { - test.That(t, afterInfo.ModTime().UTC().String(), test.ShouldEqual, modTime.String()) - test.That(t, afterInfo.Mode(), test.ShouldEqual, newMode) - } else { - test.That(t, afterInfo.ModTime().UTC().String(), test.ShouldNotEqual, modTime.String()) - test.That(t, afterInfo.Mode(), test.ShouldNotEqual, newMode) - } - }) - } - }) - }) - - t.Run("to", func(t *testing.T) { - t.Run("single file", func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{tfs.SingleFileNested, fmt.Sprintf("machine:%s", tempDir)} - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlags, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - }) - - t.Run("single file relative", func(t *testing.T) { - homeDir, err := os.UserHomeDir() - test.That(t, err, test.ShouldBeNil) - randomName := uuid.NewString() - randomPath := filepath.Join(homeDir, randomName) - defer os.Remove(randomPath) - args := []string{tfs.SingleFileNested, fmt.Sprintf("machine:%s", randomName)} - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlags, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - - rd, err := os.ReadFile(randomPath) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - }) - - t.Run("single directory", func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{tfs.Root, fmt.Sprintf("machine:%s", tempDir)} - - t.Log("without recursion set") - cCtx, viamClient, _, _, teardown1 := setupWithRunningPart( - t, asc, nil, nil, partFlags, "token", partFqdn, args...) - defer teardown1() - err := machinesPartCopyFilesAction(cCtx, viamClient) - test.That(t, errors.Is(err, errDirectoryCopyRequestNoRecursion), test.ShouldBeTrue) - _, err = os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, errors.Is(err, fs.ErrNotExist), test.ShouldBeTrue) - - t.Log("with recursion set") - partFlagsCopy := make(map[string]any, len(partFlags)) - maps.Copy(partFlagsCopy, partFlags) - partFlagsCopy["recursive"] = true - cCtx, viamClient, _, _, teardown2 := setupWithRunningPart( - t, asc, nil, nil, partFlagsCopy, "token", partFqdn, args...) - defer teardown2() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - }) - - t.Run("multiple files", func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{ - tfs.SingleFileNested, - tfs.InnerDir, - fmt.Sprintf("machine:%s", tempDir), - } - partFlagsCopy := make(map[string]any, len(partFlags)) - maps.Copy(partFlagsCopy, partFlags) - partFlagsCopy["recursive"] = true - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlagsCopy, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.InnerDir, filepath.Join(tempDir, filepath.Base(tfs.InnerDir))), test.ShouldBeNil) - }) - - t.Run("preserve permissions on a nested file", func(t *testing.T) { - tfs := shelltestutils.SetupTestFileSystem(t) - - beforeInfo, err := os.Stat(tfs.SingleFileNested) - test.That(t, err, test.ShouldBeNil) - t.Log("start with mode", beforeInfo.Mode()) - newMode := os.FileMode(0o444) - test.That(t, beforeInfo.Mode(), test.ShouldNotEqual, newMode) - test.That(t, os.Chmod(tfs.SingleFileNested, newMode), test.ShouldBeNil) - modTime := time.Date(1988, 1, 2, 3, 0, 0, 0, time.UTC) - test.That(t, os.Chtimes(tfs.SingleFileNested, time.Time{}, modTime), test.ShouldBeNil) - relNestedPath := strings.TrimPrefix(tfs.SingleFileNested, tfs.Root) - - for _, preserve := range []bool{false, true} { - t.Run(fmt.Sprintf("preserve=%t", preserve), func(t *testing.T) { - tempDir := t.TempDir() - - args := []string{tfs.Root, fmt.Sprintf("machine:%s", tempDir)} - - partFlagsCopy := make(map[string]any, len(partFlags)) - maps.Copy(partFlagsCopy, partFlags) - partFlagsCopy["recursive"] = true - partFlagsCopy["preserve"] = preserve - cCtx, viamClient, _, _, teardown := setupWithRunningPart( - t, asc, nil, nil, partFlagsCopy, "token", partFqdn, args...) - defer teardown() - test.That(t, machinesPartCopyFilesAction(cCtx, viamClient), test.ShouldBeNil) - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - - nestedCopy := filepath.Join(tempDir, filepath.Base(tfs.Root), relNestedPath) - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - afterInfo, err := os.Stat(nestedCopy) - test.That(t, err, test.ShouldBeNil) - if preserve { - test.That(t, afterInfo.ModTime().UTC().String(), test.ShouldEqual, modTime.String()) - test.That(t, afterInfo.Mode(), test.ShouldEqual, newMode) - } else { - test.That(t, afterInfo.ModTime().UTC().String(), test.ShouldNotEqual, modTime.String()) - test.That(t, afterInfo.Mode(), test.ShouldNotEqual, newMode) - } - }) - } - }) - }) -} diff --git a/cli/data_test.go b/cli/data_test.go deleted file mode 100644 index 59a7234e0b0..00000000000 --- a/cli/data_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package cli - -import ( - "testing" - - datapb "go.viam.com/api/app/data/v1" - "go.viam.com/test" -) - -func TestFilenameForDownload(t *testing.T) { - const expectedUTC = "1970-01-01T00_00_00Z" - noFilename := filenameForDownload(&datapb.BinaryMetadata{Id: "my-id"}) - test.That(t, noFilename, test.ShouldEqual, expectedUTC+"_my-id") - - normalExt := filenameForDownload(&datapb.BinaryMetadata{FileName: "whatever.txt"}) - test.That(t, normalExt, test.ShouldEqual, expectedUTC+"_whatever.txt") - - inFolder := filenameForDownload(&datapb.BinaryMetadata{FileName: "dir/whatever.txt"}) - test.That(t, inFolder, test.ShouldEqual, "dir/whatever.txt") - - gzAtRoot := filenameForDownload(&datapb.BinaryMetadata{FileName: "whatever.gz"}) - test.That(t, gzAtRoot, test.ShouldEqual, expectedUTC+"_whatever") - - gzInFolder := filenameForDownload(&datapb.BinaryMetadata{FileName: "dir/whatever.gz"}) - test.That(t, gzInFolder, test.ShouldEqual, "dir/whatever") -} diff --git a/cli/flags_test.go b/cli/flags_test.go deleted file mode 100644 index 7928825e48b..00000000000 --- a/cli/flags_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package cli - -import ( - "testing" - - "github.com/urfave/cli/v2" - "go.viam.com/test" -) - -func TestAliasStringFlag(t *testing.T) { - f := AliasStringFlag{ - cli.StringFlag{ - Name: "foo", - }, - } - test.That(t, f.Names(), test.ShouldResemble, []string{"foo"}) - test.That(t, f.String(), test.ShouldEqual, f.StringFlag.String()) - - f = AliasStringFlag{ - cli.StringFlag{ - Name: "foo", - Aliases: []string{"hello"}, - }, - } - test.That(t, f.Names(), test.ShouldResemble, []string{"hello", "foo"}) - test.That(t, f.String(), test.ShouldEqual, f.StringFlag.String()) - - f = AliasStringFlag{ - cli.StringFlag{ - Name: "foo", - Aliases: []string{"hello", "world"}, - }, - } - test.That(t, f.Names(), test.ShouldResemble, []string{"hello", "world", "foo"}) - test.That(t, f.String(), test.ShouldEqual, f.StringFlag.String()) -} diff --git a/cli/module_build_test.go b/cli/module_build_test.go deleted file mode 100644 index 10db294db33..00000000000 --- a/cli/module_build_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package cli - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" - "time" - - v1 "go.viam.com/api/app/build/v1" - "go.viam.com/test" - "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/testutils/inject" -) - -func createTestManifest(t *testing.T, path string) string { - t.Helper() - if len(path) == 0 { - path = filepath.Join(t.TempDir(), "meta.json") - } - fi, err := os.Create(path) - test.That(t, err, test.ShouldBeNil) - _, err = fi.WriteString(`{ - "module_id": "test:test", - "visibility": "private", - "url": "https://github.com/", - "description": "a", - "models": [ - { - "api": "a:b:c", - "model": "a:b:c" - } - ], - "build": { - "setup": "./setup.sh", - "build": "make build", - "path": "module", - "arch": ["linux/amd64"] - }, - "entrypoint": "bin/module" -} -`) - test.That(t, err, test.ShouldBeNil) - err = fi.Close() - test.That(t, err, test.ShouldBeNil) - return path -} - -func TestStartBuild(t *testing.T) { - manifest := filepath.Join(t.TempDir(), "meta.json") - createTestManifest(t, manifest) - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, &inject.BuildServiceClient{ - StartBuildFunc: func(ctx context.Context, in *v1.StartBuildRequest, opts ...grpc.CallOption) (*v1.StartBuildResponse, error) { - return &v1.StartBuildResponse{BuildId: "xyz123"}, nil - }, - }, map[string]any{moduleBuildFlagPath: manifest, moduleBuildFlagVersion: "1.2.3"}, "token") - err := ac.moduleBuildStartAction(cCtx) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.messages, test.ShouldHaveLength, 1) - test.That(t, out.messages[0], test.ShouldEqual, "xyz123\n") - test.That(t, errOut.messages, test.ShouldHaveLength, 1) -} - -func TestListBuild(t *testing.T) { - manifest := filepath.Join(t.TempDir(), "meta.json") - createTestManifest(t, manifest) - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, &inject.BuildServiceClient{ - ListJobsFunc: func(ctx context.Context, in *v1.ListJobsRequest, opts ...grpc.CallOption) (*v1.ListJobsResponse, error) { - return &v1.ListJobsResponse{Jobs: []*v1.JobInfo{ - { - BuildId: "xyz123", - Platform: "linux/amd64", - Version: "1.2.3", - Status: v1.JobStatus_JOB_STATUS_DONE, - EndTime: timestamppb.New(time.Unix(0, 0)), // Jan 1 1970 - }, - }}, nil - }, - }, map[string]any{moduleBuildFlagPath: manifest}, "token") - err := ac.moduleBuildListAction(cCtx) - test.That(t, err, test.ShouldBeNil) - joinedOutput := strings.Join(out.messages, "") - test.That(t, joinedOutput, test.ShouldEqual, `ID PLATFORM STATUS VERSION TIME -xyz123 linux/amd64 Done 1.2.3 1970-01-01T00:00:00Z -`) - test.That(t, errOut.messages, test.ShouldHaveLength, 0) -} - -func TestBuildError(t *testing.T) { - err := buildError(map[string]jobStatus{"ok": jobStatusDone}) - test.That(t, err, test.ShouldBeNil) - err = buildError(map[string]jobStatus{"bad": jobStatusFailed}) - test.That(t, err.Error(), test.ShouldEqual, "some platforms failed to build: bad") - err = buildError(map[string]jobStatus{"ok": jobStatusDone, "bad": jobStatusFailed}) - test.That(t, err.Error(), test.ShouldEqual, "some platforms failed to build: bad") -} - -func TestModuleBuildWait(t *testing.T) { - // this creates a race condiition if there are multiple tests testing the moduleBuildPollingInterval - originalPollingInterval := moduleBuildPollingInterval - moduleBuildPollingInterval = 200 * time.Millisecond - defer func() { moduleBuildPollingInterval = originalPollingInterval }() - startTime := time.Now() - //nolint:dogsled - _, ac, _, _ := setup(&inject.AppServiceClient{}, nil, &inject.BuildServiceClient{ - ListJobsFunc: func(ctx context.Context, in *v1.ListJobsRequest, opts ...grpc.CallOption) (*v1.ListJobsResponse, error) { - // this will only report DONE after 2.5 polling intervals - status := v1.JobStatus_JOB_STATUS_DONE - if time.Since(startTime).Seconds() < moduleBuildPollingInterval.Seconds()*2.5 { - status = v1.JobStatus_JOB_STATUS_IN_PROGRESS - } - return &v1.ListJobsResponse{Jobs: []*v1.JobInfo{ - { - BuildId: "xyz123", - Platform: "linux/amd64", - Version: "1.2.3", - Status: status, - }, - }}, nil - }, - }, map[string]any{}, "token") - startWaitTime := time.Now() - statuses, err := ac.waitForBuildToFinish("xyz123", "") - test.That(t, err, test.ShouldBeNil) - test.That(t, statuses, test.ShouldResemble, map[string]jobStatus{"linux/amd64": "Done"}) - // ensure that we had to wait for at least 2, but no more than 5 polling intervals - test.That(t, - time.Since(startWaitTime).Seconds(), - test.ShouldBeBetween, - 2*moduleBuildPollingInterval.Seconds(), - 5*moduleBuildPollingInterval.Seconds()) -} - -func TestModuleGetPlatformsForModule(t *testing.T) { - //nolint:dogsled - _, ac, _, _ := setup(&inject.AppServiceClient{}, nil, &inject.BuildServiceClient{ - ListJobsFunc: func(ctx context.Context, in *v1.ListJobsRequest, opts ...grpc.CallOption) (*v1.ListJobsResponse, error) { - return &v1.ListJobsResponse{Jobs: []*v1.JobInfo{ - { - BuildId: "xyz123", - Platform: "linux/amd64", - Version: "1.2.3", - Status: v1.JobStatus_JOB_STATUS_DONE, - }, - - { - BuildId: "xyz123", - Platform: "linux/arm64", - Version: "1.2.3", - Status: v1.JobStatus_JOB_STATUS_DONE, - }, - }}, nil - }, - }, map[string]any{}, "token") - platforms, err := ac.getPlatformsForModuleBuild("xyz123") - test.That(t, err, test.ShouldBeNil) - test.That(t, platforms, test.ShouldResemble, []string{"linux/amd64", "linux/arm64"}) -} - -// testChdir is os.Chdir scoped to a test. -// Necessary because Getwd() fails if run on a deleted path. -func testChdir(t *testing.T, dest string) { - t.Helper() - orig, err := os.Getwd() - test.That(t, err, test.ShouldBeNil) - os.Chdir(dest) - t.Cleanup(func() { os.Chdir(orig) }) -} - -func TestLocalBuild(t *testing.T) { - testDir := t.TempDir() - testChdir(t, testDir) - - // write manifest and setup.sh - // the manifest contains a: - // "setup": "./setup.sh" - // and a "build": "make build" - manifestPath := createTestManifest(t, "") - err := os.WriteFile( - filepath.Join(testDir, "setup.sh"), - []byte("echo setup step msg"), - 0o700, - ) - test.That(t, err, test.ShouldBeNil) - - err = os.WriteFile( - filepath.Join(testDir, "Makefile"), - []byte("make build:\n\techo build step msg"), - 0o700, - ) - test.That(t, err, test.ShouldBeNil) - - // run the build local action - cCtx, _, out, errOut := setup(&inject.AppServiceClient{}, nil, &inject.BuildServiceClient{}, - map[string]any{moduleBuildFlagPath: manifestPath, moduleBuildFlagVersion: "1.2.3"}, "token") - manifest, err := loadManifest(manifestPath) - test.That(t, err, test.ShouldBeNil) - err = moduleBuildLocalAction(cCtx, &manifest) - test.That(t, err, test.ShouldBeNil) - test.That(t, errOut.messages, test.ShouldHaveLength, 0) - - outMsg := strings.Join(out.messages, "") - test.That(t, outMsg, test.ShouldContainSubstring, "setup step msg") - test.That(t, outMsg, test.ShouldContainSubstring, "build step msg") -} diff --git a/cli/module_reload_test.go b/cli/module_reload_test.go deleted file mode 100644 index 8b1a1a49526..00000000000 --- a/cli/module_reload_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package cli - -import ( - "context" - "os" - "path/filepath" - "testing" - - v1 "go.viam.com/api/app/build/v1" - apppb "go.viam.com/api/app/v1" - "go.viam.com/test" - "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/structpb" - - rdkConfig "go.viam.com/rdk/config" - "go.viam.com/rdk/testutils/inject" -) - -func TestConfigureModule(t *testing.T) { - manifestPath := createTestManifest(t, "") - cCtx, ac, out, errOut := setup(&inject.AppServiceClient{}, nil, &inject.BuildServiceClient{ - StartBuildFunc: func(ctx context.Context, in *v1.StartBuildRequest, opts ...grpc.CallOption) (*v1.StartBuildResponse, error) { - return &v1.StartBuildResponse{BuildId: "xyz123"}, nil - }, - }, map[string]any{moduleBuildFlagPath: manifestPath, moduleBuildFlagVersion: "1.2.3"}, "token") - err := ac.moduleBuildStartAction(cCtx) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.messages, test.ShouldHaveLength, 1) - test.That(t, out.messages[0], test.ShouldEqual, "xyz123\n") - test.That(t, errOut.messages, test.ShouldHaveLength, 1) -} - -func TestFullReloadFlow(t *testing.T) { - manifestPath := createTestManifest(t, "") - confStruct, err := structpb.NewStruct(map[string]any{ - "modules": []any{}, - }) - test.That(t, err, test.ShouldBeNil) - updateCount := 0 - cCtx, vc, _, _ := setup(&inject.AppServiceClient{ - GetRobotPartFunc: func(ctx context.Context, req *apppb.GetRobotPartRequest, - opts ...grpc.CallOption, - ) (*apppb.GetRobotPartResponse, error) { - return &apppb.GetRobotPartResponse{Part: &apppb.RobotPart{ - RobotConfig: confStruct, - Fqdn: "restart-module-robot.local", - }, ConfigJson: ``}, nil - }, - UpdateRobotPartFunc: func(ctx context.Context, req *apppb.UpdateRobotPartRequest, - opts ...grpc.CallOption, - ) (*apppb.UpdateRobotPartResponse, error) { - updateCount++ - return &apppb.UpdateRobotPartResponse{Part: &apppb.RobotPart{}}, nil - }, - GetRobotAPIKeysFunc: func(ctx context.Context, in *apppb.GetRobotAPIKeysRequest, - opts ...grpc.CallOption, - ) (*apppb.GetRobotAPIKeysResponse, error) { - return &apppb.GetRobotAPIKeysResponse{ApiKeys: []*apppb.APIKeyWithAuthorizations{ - {ApiKey: &apppb.APIKey{}}, - }}, nil - }, - }, nil, &inject.BuildServiceClient{}, - map[string]any{moduleBuildFlagPath: manifestPath, partFlag: "part-123", moduleBuildFlagNoBuild: true}, "token") - test.That(t, vc.loginAction(cCtx), test.ShouldBeNil) - err = reloadModuleAction(cCtx, vc) - test.That(t, err, test.ShouldBeNil) - test.That(t, updateCount, test.ShouldEqual, 1) -} - -func TestRestartModule(t *testing.T) { - t.Skip("restartModule test requires fake robot client") -} - -func TestResolvePartId(t *testing.T) { - c := newTestContext(t, map[string]any{}) - // empty flag, no path - partID, err := resolvePartID(c.Context, c.String(partFlag), "") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, partID, test.ShouldBeEmpty) - - // empty flag, fake path - missingPath := filepath.Join(t.TempDir(), "MISSING.json") - _, err = resolvePartID(c.Context, c.String(partFlag), missingPath) - test.That(t, err, test.ShouldNotBeNil) - - // empty flag, valid path - path := filepath.Join(t.TempDir(), "viam.json") - fi, err := os.Create(path) - test.That(t, err, test.ShouldBeNil) - _, err = fi.WriteString(`{"cloud":{"app_address":"https://app.viam.com:443","id":"JSON-PART","secret":"SECRET"}}`) - test.That(t, err, test.ShouldBeNil) - partID, err = resolvePartID(c.Context, c.String(partFlag), path) - test.That(t, err, test.ShouldBeNil) - test.That(t, partID, test.ShouldEqual, "JSON-PART") - - // given flag, valid path - c = newTestContext(t, map[string]any{partFlag: "FLAG-PART"}) - partID, err = resolvePartID(c.Context, c.String(partFlag), path) - test.That(t, err, test.ShouldBeNil) - test.That(t, partID, test.ShouldEqual, "FLAG-PART") -} - -func TestMutateModuleConfig(t *testing.T) { - c := newTestContext(t, map[string]any{}) - manifest := moduleManifest{ModuleID: "viam-labs:test-module", Entrypoint: "/bin/mod"} - - // correct ExePath (do nothing) - modules := []ModuleMap{{"module_id": manifest.ModuleID, "executable_path": manifest.Entrypoint}} - _, dirty, err := mutateModuleConfig(c, modules, manifest) - test.That(t, err, test.ShouldBeNil) - test.That(t, dirty, test.ShouldBeFalse) - - // wrong ExePath - modules = []ModuleMap{{"module_id": manifest.ModuleID, "executable_path": "WRONG"}} - _, dirty, err = mutateModuleConfig(c, modules, manifest) - test.That(t, err, test.ShouldBeNil) - test.That(t, dirty, test.ShouldBeTrue) - test.That(t, modules[0]["executable_path"], test.ShouldEqual, manifest.Entrypoint) - - // wrong ExePath with localName - modules = []ModuleMap{{"name": localizeModuleID(manifest.ModuleID)}} - mutateModuleConfig(c, modules, manifest) - test.That(t, modules[0]["executable_path"], test.ShouldEqual, manifest.Entrypoint) - - // insert case - modules = []ModuleMap{} - modules, _, _ = mutateModuleConfig(c, modules, manifest) - test.That(t, modules[0]["executable_path"], test.ShouldEqual, manifest.Entrypoint) - - // registry to local - // todo(RSDK-6712): this goes away once we reconcile registry + local modules - modules = []ModuleMap{{"module_id": manifest.ModuleID, "executable_path": "WRONG", "type": string(rdkConfig.ModuleTypeRegistry)}} - mutateModuleConfig(c, modules, manifest) - test.That(t, modules[0]["name"], test.ShouldEqual, localizeModuleID(manifest.ModuleID)) -} diff --git a/cli/utils_test.go b/cli/utils_test.go deleted file mode 100644 index 66ee99268a4..00000000000 --- a/cli/utils_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package cli - -import ( - "testing" - - "go.viam.com/test" -) - -func TestMapOver(t *testing.T) { - mapped, _ := mapOver([]int{1, 2}, func(x int) (int, error) { return x + 1, nil }) - test.That(t, mapped, test.ShouldResemble, []int{2, 3}) -} - -func TestSamePath(t *testing.T) { - equal, _ := samePath("/x", "/x") - test.That(t, equal, test.ShouldBeTrue) - equal, _ = samePath("/x", "x") - test.That(t, equal, test.ShouldBeFalse) -} - -func TestGetMapString(t *testing.T) { - m := map[string]any{ - "x": "x", - "y": 10, - } - test.That(t, getMapString(m, "x"), test.ShouldEqual, "x") - test.That(t, getMapString(m, "y"), test.ShouldEqual, "") - test.That(t, getMapString(m, "z"), test.ShouldEqual, "") -} diff --git a/cli/verify_main_test.go b/cli/verify_main_test.go deleted file mode 100644 index 1f48ab185bc..00000000000 --- a/cli/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package cli - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/arm/arm_test.go b/components/arm/arm_test.go deleted file mode 100644 index c35991a76ac..00000000000 --- a/components/arm/arm_test.go +++ /dev/null @@ -1,588 +0,0 @@ -package arm_test - -import ( - "context" - "errors" - "strings" - "testing" - - "github.com/go-viper/mapstructure/v2" - "github.com/golang/geo/r3" - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/arm/fake" - ur "go.viam.com/rdk/components/arm/universalrobots" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -const ( - testArmName = "arm1" - testArmName2 = "arm2" - failArmName = "arm3" - missingArmName = "arm4" -) - -var pose = spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 2, Z: 3}) - -func TestStatusValid(t *testing.T) { - status := &pb.Status{ - EndPosition: spatialmath.PoseToProtobuf(pose), - JointPositions: &pb.JointPositions{Values: []float64{1.1, 2.2, 3.3}}, - IsMoving: true, - } - newStruct, err := protoutils.StructToStructPb(status) - test.That(t, err, test.ShouldBeNil) - test.That( - t, - newStruct.AsMap(), - test.ShouldResemble, - map[string]interface{}{ - "end_position": map[string]interface{}{"o_z": 1.0, "x": 1.0, "y": 2.0, "z": 3.0}, - "joint_positions": map[string]interface{}{"values": []interface{}{1.1, 2.2, 3.3}}, - "is_moving": true, - }, - ) - - convMap := &pb.Status{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(newStruct.AsMap()) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, status) -} - -func TestCreateStatus(t *testing.T) { - successfulPose := spatialmath.NewPose( - r3.Vector{-802.801508917897990613710135, -248.284077946287368376943050, 9.115758604150467903082244}, - &spatialmath.R4AA{1.5810814917942602, 0.992515011486776, -0.0953988491934626, 0.07624310818669232}, - ) - successfulStatus := &pb.Status{ - EndPosition: spatialmath.PoseToProtobuf(successfulPose), - JointPositions: &pb.JointPositions{Values: []float64{1.1, 2.2, 3.3, 1.1, 2.2, 3.3}}, - IsMoving: true, - } - - injectArm := &inject.Arm{} - - //nolint:unparam - successfulJointPositionsFunc := func(context.Context, map[string]interface{}) (*pb.JointPositions, error) { - return successfulStatus.JointPositions, nil - } - - successfulIsMovingFunc := func(context.Context) (bool, error) { - return true, nil - } - - successfulModelFrameFunc := func() referenceframe.Model { - model, _ := ur.MakeModelFrame("ur5e") - return model - } - - t.Run("working", func(t *testing.T) { - injectArm.JointPositionsFunc = successfulJointPositionsFunc - injectArm.IsMovingFunc = successfulIsMovingFunc - injectArm.ModelFrameFunc = successfulModelFrameFunc - - expectedPose := successfulPose - expectedStatus := successfulStatus - - actualStatus, err := arm.CreateStatus(context.Background(), injectArm) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualStatus.IsMoving, test.ShouldEqual, expectedStatus.IsMoving) - test.That(t, actualStatus.JointPositions, test.ShouldResemble, expectedStatus.JointPositions) - - actualPose := spatialmath.NewPoseFromProtobuf(actualStatus.EndPosition) - test.That(t, spatialmath.PoseAlmostEqualEps(actualPose, expectedPose, 0.01), test.ShouldBeTrue) - - resourceAPI, ok, err := resource.LookupAPIRegistration[arm.Arm](arm.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - statusInterface, err := resourceAPI.Status(context.Background(), injectArm) - test.That(t, err, test.ShouldBeNil) - - statusMap, err := protoutils.InterfaceToMap(statusInterface) - test.That(t, err, test.ShouldBeNil) - - endPosMap, err := protoutils.InterfaceToMap(statusMap["end_position"]) - test.That(t, err, test.ShouldBeNil) - actualPose = spatialmath.NewPose( - r3.Vector{endPosMap["x"].(float64), endPosMap["y"].(float64), endPosMap["z"].(float64)}, - &spatialmath.OrientationVectorDegrees{ - endPosMap["theta"].(float64), endPosMap["o_x"].(float64), - endPosMap["o_y"].(float64), endPosMap["o_z"].(float64), - }, - ) - test.That(t, spatialmath.PoseAlmostEqualEps(actualPose, expectedPose, 0.01), test.ShouldBeTrue) - - moving := statusMap["is_moving"].(bool) - test.That(t, moving, test.ShouldEqual, expectedStatus.IsMoving) - - jPosFace := statusMap["joint_positions"].(map[string]interface{})["values"].([]interface{}) - actualJointPositions := []float64{ - jPosFace[0].(float64), jPosFace[1].(float64), jPosFace[2].(float64), - jPosFace[3].(float64), jPosFace[4].(float64), jPosFace[5].(float64), - } - test.That(t, actualJointPositions, test.ShouldResemble, expectedStatus.JointPositions.Values) - }) - - t.Run("not moving", func(t *testing.T) { - injectArm.JointPositionsFunc = successfulJointPositionsFunc - injectArm.ModelFrameFunc = successfulModelFrameFunc - - injectArm.IsMovingFunc = func(context.Context) (bool, error) { - return false, nil - } - - expectedPose := successfulPose - expectedStatus := &pb.Status{ - EndPosition: successfulStatus.EndPosition, //nolint:govet - JointPositions: successfulStatus.JointPositions, - IsMoving: false, - } - - actualStatus, err := arm.CreateStatus(context.Background(), injectArm) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualStatus.IsMoving, test.ShouldEqual, expectedStatus.IsMoving) - test.That(t, actualStatus.JointPositions, test.ShouldResemble, expectedStatus.JointPositions) - actualPose := spatialmath.NewPoseFromProtobuf(actualStatus.EndPosition) - test.That(t, spatialmath.PoseAlmostEqualEps(actualPose, expectedPose, 0.01), test.ShouldBeTrue) - }) - - t.Run("fail on JointPositions", func(t *testing.T) { - injectArm.IsMovingFunc = successfulIsMovingFunc - injectArm.ModelFrameFunc = successfulModelFrameFunc - - errFail := errors.New("can't get joint positions") - injectArm.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) { - return nil, errFail - } - - actualStatus, err := arm.CreateStatus(context.Background(), injectArm) - test.That(t, err, test.ShouldBeError, errFail) - test.That(t, actualStatus, test.ShouldBeNil) - }) - - t.Run("nil JointPositions", func(t *testing.T) { - injectArm.IsMovingFunc = successfulIsMovingFunc - injectArm.ModelFrameFunc = successfulModelFrameFunc - - injectArm.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) { - return nil, nil //nolint:nilnil - } - - expectedStatus := &pb.Status{ - EndPosition: nil, - JointPositions: nil, - IsMoving: successfulStatus.IsMoving, - } - - actualStatus, err := arm.CreateStatus(context.Background(), injectArm) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualStatus.EndPosition, test.ShouldEqual, expectedStatus.EndPosition) - test.That(t, actualStatus.JointPositions, test.ShouldEqual, expectedStatus.JointPositions) - test.That(t, actualStatus.IsMoving, test.ShouldEqual, expectedStatus.IsMoving) - }) - - t.Run("nil model frame", func(t *testing.T) { - injectArm.IsMovingFunc = successfulIsMovingFunc - injectArm.JointPositionsFunc = successfulJointPositionsFunc - - injectArm.ModelFrameFunc = func() referenceframe.Model { - return nil - } - - expectedStatus := &pb.Status{ - EndPosition: nil, - JointPositions: successfulStatus.JointPositions, - IsMoving: successfulStatus.IsMoving, - } - - actualStatus, err := arm.CreateStatus(context.Background(), injectArm) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualStatus.EndPosition, test.ShouldEqual, expectedStatus.EndPosition) - test.That(t, actualStatus.JointPositions, test.ShouldResemble, expectedStatus.JointPositions) - test.That(t, actualStatus.IsMoving, test.ShouldEqual, expectedStatus.IsMoving) - }) -} - -func TestOOBArm(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg := resource.Config{ - Name: arm.API.String(), - Model: resource.DefaultModelFamily.WithModel("ur5e"), - ConvertedAttributes: &fake.Config{ - ArmModel: "ur5e", - }, - } - - notReal, err := fake.NewArm(context.Background(), nil, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - injectedArm := &inject.Arm{ - Arm: notReal, - } - - jPositions := pb.JointPositions{Values: []float64{0, 0, 0, 0, 0, 720}} - injectedArm.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) { - return &jPositions, nil - } - - // instantiate OOB arm - positions, err := injectedArm.JointPositions(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, positions, test.ShouldResemble, &jPositions) - - t.Run("EndPosition works when OOB", func(t *testing.T) { - jPositions := pb.JointPositions{Values: []float64{0, 0, 0, 0, 0, 720}} - pose, err := motionplan.ComputeOOBPosition(injectedArm.ModelFrame(), &jPositions) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose, test.ShouldNotBeNil) - }) - - t.Run("Move fails when OOB", func(t *testing.T) { - pose = spatialmath.NewPoseFromPoint(r3.Vector{200, 200, 200}) - err := arm.Move(context.Background(), logger, injectedArm, pose) - u := "cartesian movements are not allowed when arm joints are out of bounds" - v := "joint 0 input out of bounds, input 12.56637 needs to be within range [6.28319 -6.28319]" - s := strings.Join([]string{u, v}, ": ") - test.That(t, err.Error(), test.ShouldEqual, s) - }) - - t.Run("MoveToJointPositions fails if more OOB", func(t *testing.T) { - vals := referenceframe.FloatsToInputs([]float64{0, 0, 0, 0, 0, 800}) - err := arm.CheckDesiredJointPositions(context.Background(), injectedArm, vals) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldEqual, - "joint 5 needs to be within range [-6.283185307179586, 12.566370614359172] and cannot be moved to 800", - ) - }) - - t.Run("GoToInputs fails if more OOB", func(t *testing.T) { - goal := []referenceframe.Input{{Value: 11}, {Value: 10}, {Value: 10}, {Value: 11}, {Value: 10}, {Value: 10}} - err := arm.CheckDesiredJointPositions(context.Background(), injectedArm, goal) - test.That( - t, - err.Error(), - test.ShouldEqual, - "joint 0 needs to be within range [-6.283185307179586, 6.283185307179586] and cannot be moved to 11", - ) - }) - - t.Run("MoveToJointPositions works if more in bounds", func(t *testing.T) { - vals := referenceframe.FloatsToInputs([]float64{0, 0, 0, 0, 0, utils.DegToRad(400)}) - err := arm.CheckDesiredJointPositions(context.Background(), injectedArm, vals) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("MoveToJointPositions works if completely in bounds", func(t *testing.T) { - vals := []float64{0, 0, 0, 0, 0, 0} - err := injectedArm.MoveToJointPositions(context.Background(), &pb.JointPositions{Values: vals}, nil) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("MoveToJointPositions fails if causes OOB from IB", func(t *testing.T) { - vals := []float64{0, 0, 0, 0, 0, 400} - err := injectedArm.MoveToJointPositions(context.Background(), &pb.JointPositions{Values: vals}, nil) - output := "joint 5 needs to be within range [-6.283185307179586, 6.283185307179586] and cannot be moved to 6.981317007977318" - test.That(t, err.Error(), test.ShouldEqual, output) - }) - - t.Run("MoveToPosition works when IB", func(t *testing.T) { - homePose, err := injectedArm.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - testLinearMove := r3.Vector{homePose.Point().X + 20, homePose.Point().Y, homePose.Point().Z} - testPose := spatialmath.NewPoseFromPoint(testLinearMove) - err = injectedArm.MoveToPosition(context.Background(), testPose, nil) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("GoToInputs works when IB", func(t *testing.T) { - goal := []referenceframe.Input{{Value: 0}, {Value: 0}, {Value: 0}, {Value: 0}, {Value: 0}, {Value: 0}} - err := injectedArm.GoToInputs(context.Background(), goal) - test.That(t, err, test.ShouldBeNil) - }) -} - -func TestXArm6Locations(t *testing.T) { - // check the exact values/locations of arm geometries at a couple different poses - logger := logging.NewTestLogger(t) - cfg := resource.Config{ - Name: arm.API.String(), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ArmModel: "xArm6", - }, - } - - notReal, err := fake.NewArm(context.Background(), nil, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("home location check", func(t *testing.T) { - checkMap := make(map[string]r3.Vector) - checkMap["rdk:component:arm:base_top"] = r3.Vector{ - 0.000000000000000000000000, - 0.000000000000000000000000, - 160.000000000000000000000000, - } - checkMap["rdk:component:arm:upper_arm"] = r3.Vector{ - 0.000000000000000000000000, - 0.000000000000000000000000, - 402.000000000000000000000000, - } - checkMap["rdk:component:arm:upper_forearm"] = r3.Vector{ - 102.997474683058328537299531, - 0.000000000000000000000000, - 502.002525316941671462700469, - } - checkMap["rdk:component:arm:lower_forearm"] = r3.Vector{ - 131.000000000000000000000000, - -27.500000000000000000000000, - 274.200000000000000000000000, - } - checkMap["rdk:component:arm:wrist_link"] = r3.Vector{ - 206.000000000000000000000000, - 10.000000000000000000000000, - 141.500000000000000000000000, - } - - in := make([]referenceframe.Input, 6) - geoms, err := notReal.ModelFrame().Geometries(in) - test.That(t, err, test.ShouldBeNil) - geomMap := geoms.Geometries() - b := locationCheckTestHelper(geomMap, checkMap) - test.That(t, b, test.ShouldBeTrue) - }) - //nolint:dupl - t.Run("location check1", func(t *testing.T) { - checkMap := make(map[string]r3.Vector) - checkMap["rdk:component:arm:base_top"] = r3.Vector{ - 0.000000000000000000000000, - 0.000000000000000000000000, - 160.000000000000000000000000, - } - checkMap["rdk:component:arm:upper_arm"] = r3.Vector{ - -13.477511247321800169629569, - 0.000000000000000000000000, - 401.325562312533520525903441, - } - checkMap["rdk:component:arm:upper_forearm"] = r3.Vector{ - 83.174566602088916056345624, - 0.000000000000000000000000, - 516.742582359123957758129109, - } - checkMap["rdk:component:arm:lower_forearm"] = r3.Vector{ - 158.566974381217988820935716, - -27.362614545145717670493468, - 299.589614460540474283334333, - } - checkMap["rdk:component:arm:wrist_link"] = r3.Vector{ - 259.050559200277916716004256, - 18.072894555453149934010071, - 192.543551746158527748775668, - } - - in := []referenceframe.Input{{Value: 0}, {Value: -0.1}, {Value: -0.1}, {Value: -0.1}, {Value: -0.1}, {Value: -0.1}} - geoms, err := notReal.ModelFrame().Geometries(in) - test.That(t, err, test.ShouldBeNil) - geomMap := geoms.Geometries() - b := locationCheckTestHelper(geomMap, checkMap) - test.That(t, b, test.ShouldBeTrue) - }) - //nolint:dupl - t.Run("location check2", func(t *testing.T) { - checkMap := make(map[string]r3.Vector) - checkMap["rdk:component:arm:base_top"] = r3.Vector{ - 0.000000000000000000000000, - 0.000000000000000000000000, - 160.000000000000000000000000, - } - checkMap["rdk:component:arm:upper_arm"] = r3.Vector{ - -26.820359657333263214695762, - 0.000000000000000000000000, - 399.308988008567553151806351, - } - checkMap["rdk:component:arm:upper_forearm"] = r3.Vector{ - 60.777555075062835499011271, - 0.000000000000000000000000, - 530.142781900699674224597402, - } - checkMap["rdk:component:arm:lower_forearm"] = r3.Vector{ - 180.312201371473605604478507, - -26.951830890634148829576588, - 333.355009225598280409030849, - } - checkMap["rdk:component:arm:wrist_link"] = r3.Vector{ - 297.258065257027055849903263, - 27.068045067389423508075197, - 256.363984524505951867467957, - } - - in := []referenceframe.Input{{Value: 0}, {Value: -0.2}, {Value: -0.2}, {Value: -0.2}, {Value: -0.2}, {Value: -0.2}} - geoms, err := notReal.ModelFrame().Geometries(in) - test.That(t, err, test.ShouldBeNil) - geomMap := geoms.Geometries() - b := locationCheckTestHelper(geomMap, checkMap) - test.That(t, b, test.ShouldBeTrue) - }) -} - -func TestUR5ELocations(t *testing.T) { - // check the exact values/locations of arm geometries at a couple different poses - logger := logging.NewTestLogger(t) - cfg := resource.Config{ - Name: arm.API.String(), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ArmModel: "ur5e", - }, - } - - notReal, err := fake.NewArm(context.Background(), nil, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("home location check", func(t *testing.T) { - checkMap := make(map[string]r3.Vector) - checkMap["rdk:component:arm:wrist_1_link"] = r3.Vector{ - -817.200000000000045474735089, - -66.649999999999948840923025, - 162.500000000000000000000000, - } - checkMap["rdk:component:arm:wrist_2_link"] = r3.Vector{ - -817.200000000000045474735089, - -133.300000000000011368683772, - 112.650000000000005684341886, - } - checkMap["rdk:component:arm:ee_link"] = r3.Vector{ - -817.200000000000045474735089, - -183.149999999999920419213595, - 62.799999999999940314410196, - } - checkMap["rdk:component:arm:base_link"] = r3.Vector{ - 0.000000000000000000000000, - 0.000000000000000000000000, - 120.000000000000000000000000, - } - checkMap["rdk:component:arm:upper_arm_link"] = r3.Vector{ - -212.500000000000000000000000, - -130.000000000000000000000000, - 162.499999999999971578290570, - } - checkMap["rdk:component:arm:forearm_link"] = r3.Vector{ - -621.100000000000022737367544, - 0.000000000000000000000000, - 162.500000000000000000000000, - } - - in := make([]referenceframe.Input, 6) - geoms, err := notReal.ModelFrame().Geometries(in) - test.That(t, err, test.ShouldBeNil) - geomMap := geoms.Geometries() - b := locationCheckTestHelper(geomMap, checkMap) - test.That(t, b, test.ShouldBeTrue) - }) - //nolint:dupl - t.Run("location check1", func(t *testing.T) { - checkMap := make(map[string]r3.Vector) - checkMap["rdk:component:arm:wrist_2_link"] = r3.Vector{ - -821.990564374563746241619810, - -133.300000000000068212102633, - 235.223789629813609280972742, - } - checkMap["rdk:component:arm:wrist_1_link"] = r3.Vector{ - -807.258882072496135151595809, - -66.650000000000062527760747, - 282.847313612725088205479551, - } - checkMap["rdk:component:arm:ee_link"] = r3.Vector{ - -831.967827564655408423277549, - -182.900957639109606134297792, - 186.129551469731126189799397, - } - checkMap["rdk:component:arm:base_link"] = r3.Vector{ - 0.000000000000000000000000, - 0.000000000000000000000000, - 120.000000000000000000000000, - } - checkMap["rdk:component:arm:upper_arm_link"] = r3.Vector{ - -211.438385121580523673401331, - -130.000000000000028421709430, - 183.714601037450989906574250, - } - checkMap["rdk:component:arm:forearm_link"] = r3.Vector{ - -615.067826157828449140652083, - -0.000000000000000000000000, - 243.888257843813590852732887, - } - - in := []referenceframe.Input{{Value: 0}, {Value: -0.1}, {Value: -0.1}, {Value: -0.1}, {Value: -0.1}, {Value: -0.1}} - geoms, err := notReal.ModelFrame().Geometries(in) - test.That(t, err, test.ShouldBeNil) - geomMap := geoms.Geometries() - b := locationCheckTestHelper(geomMap, checkMap) - test.That(t, b, test.ShouldBeTrue) - }) - //nolint:dupl - t.Run("location check2", func(t *testing.T) { - checkMap := make(map[string]r3.Vector) - checkMap["rdk:component:arm:wrist_1_link"] = r3.Vector{ - -777.768417430459294337197207, - -66.650000000000005684341886, - 399.664339441353661186440149, - } - checkMap["rdk:component:arm:wrist_2_link"] = r3.Vector{ - -805.915844729201694462972227, - -133.300000000000011368683772, - 358.521359038106197658635210, - } - checkMap["rdk:component:arm:ee_link"] = r3.Vector{ - -825.889423644316707395773847, - -182.156318905385916195882601, - 311.786348089814850936818402, - } - checkMap["rdk:component:arm:base_link"] = r3.Vector{ - 0.000000000000000000000000, - 0.000000000000000000000000, - 120.000000000000000000000000, - } - checkMap["rdk:component:arm:upper_arm_link"] = r3.Vector{ - -208.264147791263866338340449, - -130.000000000000028421709430, - 204.717232793950500990831642, - } - checkMap["rdk:component:arm:forearm_link"] = r3.Vector{ - -597.148356506493314554973040, - -0.000000000000000000000000, - 323.299402514627388427470578, - } - - in := []referenceframe.Input{{Value: 0}, {Value: -0.2}, {Value: -0.2}, {Value: -0.2}, {Value: -0.2}, {Value: -0.2}} - geoms, err := notReal.ModelFrame().Geometries(in) - test.That(t, err, test.ShouldBeNil) - geomMap := geoms.Geometries() - b := locationCheckTestHelper(geomMap, checkMap) - test.That(t, b, test.ShouldBeTrue) - }) -} - -func locationCheckTestHelper(geomList []spatialmath.Geometry, checkMap map[string]r3.Vector) bool { - for _, g := range geomList { - vecCheck := checkMap[g.Label()] - vecActual := g.Pose().Point() - if !spatialmath.R3VectorAlmostEqual(vecCheck, vecActual, 1e-2) { - return false - } - } - return true -} diff --git a/components/arm/client_test.go b/components/arm/client_test.go deleted file mode 100644 index db2e7e6c979..00000000000 --- a/components/arm/client_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package arm_test - -import ( - "context" - "net" - "testing" - - "github.com/golang/geo/r3" - componentpb "go.viam.com/api/component/arm/v1" - robotpb "go.viam.com/api/robot/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/arm" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/robot/server" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - var ( - capArmPos spatialmath.Pose - capArmJointPos *componentpb.JointPositions - extraOptions map[string]interface{} - ) - - pos1 := spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 2, Z: 3}) - jointPos1 := &componentpb.JointPositions{Values: []float64{1.0, 2.0, 3.0}} - expectedGeometries := []spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{1, 2, 3}, "")} - injectArm := &inject.Arm{} - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - extraOptions = extra - return pos1, nil - } - injectArm.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*componentpb.JointPositions, error) { - extraOptions = extra - return jointPos1, nil - } - injectArm.MoveToPositionFunc = func(ctx context.Context, ap spatialmath.Pose, extra map[string]interface{}) error { - capArmPos = ap - extraOptions = extra - return nil - } - - injectArm.MoveToJointPositionsFunc = func(ctx context.Context, jp *componentpb.JointPositions, extra map[string]interface{}) error { - capArmJointPos = jp - extraOptions = extra - return nil - } - injectArm.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - return errStopUnimplemented - } - injectArm.ModelFrameFunc = func() referenceframe.Model { - data := []byte("{\"links\": [{\"parent\": \"world\"}]}") - model, err := referenceframe.UnmarshalModelJSON(data, "") - test.That(t, err, test.ShouldBeNil) - return model - } - injectArm.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { - return expectedGeometries, nil - } - - pos2 := spatialmath.NewPoseFromPoint(r3.Vector{X: 4, Y: 5, Z: 6}) - jointPos2 := &componentpb.JointPositions{Values: []float64{4.0, 5.0, 6.0}} - injectArm2 := &inject.Arm{} - injectArm2.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return pos2, nil - } - injectArm2.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*componentpb.JointPositions, error) { - return jointPos2, nil - } - injectArm2.MoveToPositionFunc = func(ctx context.Context, ap spatialmath.Pose, extra map[string]interface{}) error { - capArmPos = ap - return nil - } - - injectArm2.MoveToJointPositionsFunc = func(ctx context.Context, jp *componentpb.JointPositions, extra map[string]interface{}) error { - capArmJointPos = jp - return nil - } - injectArm2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - injectArm2.ModelFrameFunc = func() referenceframe.Model { - data := []byte("{\"links\": [{\"parent\": \"world\"}]}") - model, err := referenceframe.UnmarshalModelJSON(data, "") - test.That(t, err, test.ShouldBeNil) - return model - } - - armSvc, err := resource.NewAPIResourceCollection( - arm.API, map[resource.Name]arm.Arm{ - arm.Named(testArmName): injectArm, - arm.Named(testArmName2): injectArm2, - }) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[arm.Arm](arm.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, armSvc), test.ShouldBeNil) - - injectRobot := &inject.Robot{} - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - test.That(t, rpcServer.RegisterServiceServer( - context.Background(), - &robotpb.RobotService_ServiceDesc, - server.New(injectRobot), - ), test.ShouldBeNil) - - injectArm.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - // working - t.Run("arm client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - arm1Client, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(testArmName), logger) - test.That(t, err, test.ShouldBeNil) - - // DoCommand - resp, err := arm1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - pos, err := arm1Client.EndPosition(context.Background(), map[string]interface{}{"foo": "EndPosition"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostEqual(pos, pos1), test.ShouldBeTrue) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "EndPosition"}) - - jointPos, err := arm1Client.JointPositions(context.Background(), map[string]interface{}{"foo": "JointPositions"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, jointPos.String(), test.ShouldResemble, jointPos1.String()) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "JointPositions"}) - - err = arm1Client.MoveToPosition(context.Background(), pos2, map[string]interface{}{"foo": "MoveToPosition"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostEqual(capArmPos, pos2), test.ShouldBeTrue) - - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "MoveToPosition"}) - - err = arm1Client.MoveToJointPositions(context.Background(), jointPos2, map[string]interface{}{"foo": "MoveToJointPositions"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, capArmJointPos.String(), test.ShouldResemble, jointPos2.String()) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "MoveToJointPositions"}) - - err = arm1Client.Stop(context.Background(), map[string]interface{}{"foo": "Stop"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopUnimplemented.Error()) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "Stop"}) - - geometries, err := arm1Client.Geometries(context.Background(), map[string]interface{}{"foo": "Geometries"}) - test.That(t, err, test.ShouldBeNil) - for i, geometry := range geometries { - test.That(t, spatialmath.GeometriesAlmostEqual(expectedGeometries[i], geometry), test.ShouldBeTrue) - } - - test.That(t, arm1Client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("arm client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", arm.Named(testArmName2), logger) - test.That(t, err, test.ShouldBeNil) - - pos, err := client2.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostEqual(pos, pos2), test.ShouldBeTrue) - - err = client2.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/arm/collectors_test.go b/components/arm/collectors_test.go deleted file mode 100644 index bd737b91ba2..00000000000 --- a/components/arm/collectors_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package arm_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - "github.com/golang/geo/r3" - v1 "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/data" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/spatialmath" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - componentName = "arm" - captureInterval = time.Second - numRetries = 5 -) - -var floatList = []float64{1.0, 2.0, 3.0} - -func TestCollectors(t *testing.T) { - tests := []struct { - name string - collector data.CollectorConstructor - expected map[string]any - }{ - { - name: "End position collector should write a pose", - collector: arm.NewEndPositionCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetEndPositionResponse{ - Pose: &v1.Pose{ - OX: 0, - OY: 0, - OZ: 1, - Theta: 0, - X: 1, - Y: 2, - Z: 3, - }, - }), - }, - { - name: "Joint positions collector should write a list of positions", - collector: arm.NewJointPositionsCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetJointPositionsResponse{ - Positions: &pb.JointPositions{ - Values: floatList, - }, - }), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: componentName, - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Clock: mockClock, - Target: &buf, - } - - arm := newArm() - col, err := tc.collector(arm, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, tc.expected) - }) - } -} - -func newArm() arm.Arm { - a := &inject.Arm{} - a.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 2, Z: 3}), nil - } - a.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) { - return &pb.JointPositions{ - Values: floatList, - }, nil - } - return a -} diff --git a/components/arm/export_collectors_test.go b/components/arm/export_collectors_test.go deleted file mode 100644 index c6d581ab12c..00000000000 --- a/components/arm/export_collectors_test.go +++ /dev/null @@ -1,8 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package arm - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var ( - NewEndPositionCollector = newEndPositionCollector - NewJointPositionsCollector = newJointPositionsCollector -) diff --git a/components/arm/fake/fake_test.go b/components/arm/fake/fake_test.go deleted file mode 100644 index 97690c2c17e..00000000000 --- a/components/arm/fake/fake_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package fake - -import ( - "context" - "testing" - - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" -) - -func TestReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - - cfg := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ArmModel: "ur5e", - }, - } - - conf1 := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ArmModel: "xArm6", - }, - } - - conf2 := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ModelFilePath: "fake_model.json", - }, - } - - conf1Err := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ArmModel: "DNE", - }, - } - - conf2Err := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ModelFilePath: "DNE", - }, - } - - conf, err := resource.NativeConfig[*Config](cfg) - test.That(t, err, test.ShouldBeNil) - - model, err := modelFromName(conf.ArmModel, cfg.Name) - test.That(t, err, test.ShouldBeNil) - - fakeArm := &Arm{ - Named: cfg.ResourceName().AsNamed(), - joints: &pb.JointPositions{Values: make([]float64, len(model.DoF()))}, - model: model, - logger: logger, - } - - test.That(t, fakeArm.Reconfigure(context.Background(), nil, conf1), test.ShouldBeNil) - model, err = modelFromName(conf1.ConvertedAttributes.(*Config).ArmModel, cfg.Name) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakeArm.joints.Values, test.ShouldResemble, make([]float64, len(model.DoF()))) - test.That(t, fakeArm.model, test.ShouldResemble, model) - - test.That(t, fakeArm.Reconfigure(context.Background(), nil, conf2), test.ShouldBeNil) - model, err = referenceframe.ParseModelJSONFile(conf2.ConvertedAttributes.(*Config).ModelFilePath, cfg.Name) - test.That(t, err, test.ShouldBeNil) - modelJoints := make([]float64, len(model.DoF())) - test.That(t, fakeArm.joints.Values, test.ShouldResemble, modelJoints) - test.That(t, fakeArm.model, test.ShouldResemble, model) - - err = fakeArm.Reconfigure(context.Background(), nil, conf1Err) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "unsupported") - test.That(t, fakeArm.joints.Values, test.ShouldResemble, modelJoints) - test.That(t, fakeArm.model, test.ShouldResemble, model) - - err = fakeArm.Reconfigure(context.Background(), nil, conf2Err) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "only files") - test.That(t, fakeArm.joints.Values, test.ShouldResemble, modelJoints) - test.That(t, fakeArm.model, test.ShouldResemble, model) -} diff --git a/components/arm/server_test.go b/components/arm/server_test.go deleted file mode 100644 index 39f95e7c0a8..00000000000 --- a/components/arm/server_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package arm_test - -import ( - "context" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/referenceframe/urdf" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -var ( - errGetPoseFailed = errors.New("can't get pose") - errGetJointsFailed = errors.New("can't get joint positions") - errMoveToPositionFailed = errors.New("can't move to pose") - errMoveToJointPositionFailed = errors.New("can't move to joint positions") - errStopUnimplemented = errors.New("Stop unimplemented") - errArmUnimplemented = errors.New("not found") -) - -func newServer() (pb.ArmServiceServer, *inject.Arm, *inject.Arm, error) { - injectArm := &inject.Arm{} - injectArm2 := &inject.Arm{} - arms := map[resource.Name]arm.Arm{ - arm.Named(testArmName): injectArm, - arm.Named(failArmName): injectArm2, - } - armSvc, err := resource.NewAPIResourceCollection(arm.API, arms) - if err != nil { - return nil, nil, nil, err - } - return arm.NewRPCServiceServer(armSvc).(pb.ArmServiceServer), injectArm, injectArm2, nil -} - -func TestServer(t *testing.T) { - armServer, injectArm, injectArm2, err := newServer() - test.That(t, err, test.ShouldBeNil) - - var ( - capArmPos spatialmath.Pose - capArmJointPos *pb.JointPositions - extraOptions map[string]interface{} - ) - - pose1 := spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 2, Z: 3}) - positionDegs1 := &pb.JointPositions{Values: []float64{1.0, 2.0, 3.0}} - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - extraOptions = extra - return pose1, nil - } - injectArm.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) { - extraOptions = extra - return positionDegs1, nil - } - injectArm.MoveToPositionFunc = func(ctx context.Context, ap spatialmath.Pose, extra map[string]interface{}) error { - capArmPos = ap - extraOptions = extra - return nil - } - - injectArm.MoveToJointPositionsFunc = func(ctx context.Context, jp *pb.JointPositions, extra map[string]interface{}) error { - capArmJointPos = jp - extraOptions = extra - return nil - } - injectArm.ModelFrameFunc = func() referenceframe.Model { - model, err := urdf.ParseModelXMLFile(utils.ResolveFile("referenceframe/urdf/testfiles/ur5e.urdf"), "foo") - if err != nil { - return nil - } - return model - } - injectArm.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - return nil - } - - pose2 := &commonpb.Pose{X: 4, Y: 5, Z: 6} - positionDegs2 := &pb.JointPositions{Values: []float64{4.0, 5.0, 6.0}} - injectArm2.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return nil, errGetPoseFailed - } - injectArm2.JointPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) { - return nil, errGetJointsFailed - } - injectArm2.MoveToPositionFunc = func(ctx context.Context, ap spatialmath.Pose, extra map[string]interface{}) error { - capArmPos = ap - return errMoveToPositionFailed - } - - injectArm2.MoveToJointPositionsFunc = func(ctx context.Context, jp *pb.JointPositions, extra map[string]interface{}) error { - capArmJointPos = jp - return errMoveToJointPositionFailed - } - injectArm2.ModelFrameFunc = func() referenceframe.Model { - return nil - } - injectArm2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopUnimplemented - } - - t.Run("arm position", func(t *testing.T) { - _, err := armServer.GetEndPosition(context.Background(), &pb.GetEndPositionRequest{Name: missingArmName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errArmUnimplemented.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "EndPosition"}) - test.That(t, err, test.ShouldBeNil) - resp, err := armServer.GetEndPosition(context.Background(), &pb.GetEndPositionRequest{Name: testArmName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Pose.String(), test.ShouldResemble, spatialmath.PoseToProtobuf(pose1).String()) - - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "EndPosition"}) - - _, err = armServer.GetEndPosition(context.Background(), &pb.GetEndPositionRequest{Name: failArmName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGetPoseFailed.Error()) - }) - - t.Run("move to position", func(t *testing.T) { - _, err = armServer.MoveToPosition(context.Background(), &pb.MoveToPositionRequest{Name: missingArmName, To: pose2}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errArmUnimplemented.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "MoveToPosition"}) - test.That(t, err, test.ShouldBeNil) - _, err = armServer.MoveToPosition(context.Background(), &pb.MoveToPositionRequest{Name: testArmName, To: pose2, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincident(capArmPos, spatialmath.NewPoseFromProtobuf(pose2)), test.ShouldBeTrue) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "MoveToPosition"}) - - _, err = armServer.MoveToPosition(context.Background(), &pb.MoveToPositionRequest{ - Name: failArmName, - To: spatialmath.PoseToProtobuf(pose1), - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errMoveToPositionFailed.Error()) - test.That(t, spatialmath.PoseAlmostCoincident(capArmPos, pose1), test.ShouldBeTrue) - }) - - t.Run("arm joint position", func(t *testing.T) { - _, err := armServer.GetJointPositions(context.Background(), &pb.GetJointPositionsRequest{Name: missingArmName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errArmUnimplemented.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "JointPositions"}) - test.That(t, err, test.ShouldBeNil) - resp, err := armServer.GetJointPositions(context.Background(), &pb.GetJointPositionsRequest{Name: testArmName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Positions.String(), test.ShouldResemble, positionDegs1.String()) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "JointPositions"}) - - _, err = armServer.GetJointPositions(context.Background(), &pb.GetJointPositionsRequest{Name: failArmName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGetJointsFailed.Error()) - }) - - t.Run("move to joint position", func(t *testing.T) { - _, err = armServer.MoveToJointPositions( - context.Background(), - &pb.MoveToJointPositionsRequest{Name: missingArmName, Positions: positionDegs2}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errArmUnimplemented.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "MoveToJointPositions"}) - test.That(t, err, test.ShouldBeNil) - _, err = armServer.MoveToJointPositions( - context.Background(), - &pb.MoveToJointPositionsRequest{Name: testArmName, Positions: positionDegs2, Extra: ext}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, capArmJointPos.String(), test.ShouldResemble, positionDegs2.String()) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "MoveToJointPositions"}) - - _, err = armServer.MoveToJointPositions( - context.Background(), - &pb.MoveToJointPositionsRequest{Name: failArmName, Positions: positionDegs1}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errMoveToJointPositionFailed.Error()) - test.That(t, capArmJointPos.String(), test.ShouldResemble, positionDegs1.String()) - }) - - t.Run("get kinematics", func(t *testing.T) { - _, err = armServer.GetKinematics(context.Background(), &commonpb.GetKinematicsRequest{Name: missingArmName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errArmUnimplemented.Error()) - - kinematics, err := armServer.GetKinematics(context.Background(), &commonpb.GetKinematicsRequest{Name: testArmName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, kinematics.Format, test.ShouldResemble, commonpb.KinematicsFileFormat_KINEMATICS_FILE_FORMAT_URDF) - - kinematics, err = armServer.GetKinematics(context.Background(), &commonpb.GetKinematicsRequest{Name: failArmName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, kinematics.Format, test.ShouldResemble, commonpb.KinematicsFileFormat_KINEMATICS_FILE_FORMAT_UNSPECIFIED) - }) - - t.Run("stop", func(t *testing.T) { - _, err = armServer.Stop(context.Background(), &pb.StopRequest{Name: missingArmName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errArmUnimplemented.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "Stop"}) - test.That(t, err, test.ShouldBeNil) - _, err = armServer.Stop(context.Background(), &pb.StopRequest{Name: testArmName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "Stop"}) - - _, err = armServer.Stop(context.Background(), &pb.StopRequest{Name: failArmName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopUnimplemented.Error()) - }) -} diff --git a/components/arm/universalrobots/ur5e_test.go b/components/arm/universalrobots/ur5e_test.go deleted file mode 100644 index 8c4af333a39..00000000000 --- a/components/arm/universalrobots/ur5e_test.go +++ /dev/null @@ -1,439 +0,0 @@ -package universalrobots - -import ( - "bufio" - "context" - "fmt" - "math" - "net" - "os" - "sync/atomic" - "testing" - "time" - - "github.com/go-gl/mathgl/mgl64" - "github.com/golang/geo/r3" - "go.viam.com/test" - goutils "go.viam.com/utils" - "go.viam.com/utils/artifact" - "go.viam.com/utils/testutils" - "gonum.org/v1/gonum/mat" - "gonum.org/v1/gonum/num/quat" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func testUR5eForwardKinematics(t *testing.T, jointRadians []float64, correct r3.Vector) { - t.Helper() - m, err := referenceframe.UnmarshalModelJSON(ur5modeljson, "") - test.That(t, err, test.ShouldBeNil) - - pos, err := motionplan.ComputePosition(m, referenceframe.JointPositionsFromRadians(jointRadians)) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincidentEps(pos, spatialmath.NewPoseFromPoint(correct), 0.01), test.ShouldBeTrue) - - fromDH := computeUR5ePosition(t, jointRadians) - test.That(t, spatialmath.PoseAlmostEqual(pos, fromDH), test.ShouldBeTrue) -} - -func testUR5eInverseKinematics(t *testing.T, pos spatialmath.Pose) { - t.Helper() - ctx := context.Background() - logger := logging.NewTestLogger(t) - - m, err := referenceframe.UnmarshalModelJSON(ur5modeljson, "") - test.That(t, err, test.ShouldBeNil) - steps, err := motionplan.PlanFrameMotion(ctx, logger, pos, m, referenceframe.FloatsToInputs([]float64{0, 0, 0, 0, 0, 0}), nil, nil) - - test.That(t, err, test.ShouldBeNil) - solution := steps[len(steps)-1] - - // we test that if we go forward from these joints, we end up in the same place - jointRadians := referenceframe.InputsToFloats(solution) - fromDH := computeUR5ePosition(t, jointRadians) - test.That(t, spatialmath.PoseAlmostCoincidentEps(pos, fromDH, 0.01), test.ShouldBeTrue) -} - -func TestKin1(t *testing.T) { - // data came from excel file found here - // https://www.universal-robots.com/articles/ur/application-installation/dh-parameters-for-calculations-of-kinematics-and-dynamics/ - // https://s3-eu-west-1.amazonaws.com/ur-support-site/45257/DH-Transformation.xlsx - // Note: we use millimeters, they use meters - - // Section 1 - first we test each joint independently - - // Home - testUR5eForwardKinematics(t, []float64{0, 0, 0, 0, 0, 0}, r3.Vector{X: -817.2, Y: -232.90, Z: 62.80}) - - // Joint 0 - testUR5eForwardKinematics(t, []float64{math.Pi / 2, 0, 0, 0, 0, 0}, r3.Vector{X: 232.90, Y: -817.2, Z: 62.80}) - testUR5eForwardKinematics(t, []float64{math.Pi, 0, 0, 0, 0, 0}, r3.Vector{X: 817.2, Y: 232.90, Z: 62.80}) - - // Joint 1 - testUR5eForwardKinematics(t, []float64{0, math.Pi / -2, 0, 0, 0, 0}, r3.Vector{X: -99.7, Y: -232.90, Z: 979.70}) - testUR5eForwardKinematics(t, []float64{0, math.Pi / 2, 0, 0, 0, 0}, r3.Vector{X: 99.7, Y: -232.90, Z: -654.70}) - testUR5eForwardKinematics(t, []float64{0, math.Pi, 0, 0, 0, 0}, r3.Vector{X: 817.2, Y: -232.90, Z: 262.2}) - - // Joint 2 - testUR5eForwardKinematics(t, []float64{0, 0, math.Pi / 2, 0, 0, 0}, r3.Vector{X: -325.3, Y: -232.90, Z: -229.7}) - testUR5eForwardKinematics(t, []float64{0, 0, math.Pi, 0, 0, 0}, r3.Vector{X: -32.8, Y: -232.90, Z: 262.2}) - - // Joint 3 - testUR5eForwardKinematics(t, []float64{0, 0, 0, math.Pi / 2, 0, 0}, r3.Vector{X: -717.5, Y: -232.90, Z: 162.5}) - testUR5eForwardKinematics(t, []float64{0, 0, 0, math.Pi, 0, 0}, r3.Vector{X: -817.2, Y: -232.90, Z: 262.2}) - - // Joint 4 - testUR5eForwardKinematics(t, []float64{0, 0, 0, 0, math.Pi / 2, 0}, r3.Vector{X: -916.80, Y: -133.3, Z: 62.8}) - testUR5eForwardKinematics(t, []float64{0, 0, 0, 0, math.Pi, 0}, r3.Vector{X: -817.2, Y: -33.7, Z: 62.8}) - - // Joint 5 - testUR5eForwardKinematics(t, []float64{0, 0, 0, 0, 0, math.Pi / 2}, r3.Vector{X: -817.2, Y: -232.90, Z: 62.80}) - testUR5eForwardKinematics(t, []float64{0, 0, 0, 0, 0, math.Pi}, r3.Vector{X: -817.2, Y: -232.90, Z: 62.80}) - - // Section 2 - try some consistent angle - rad := math.Pi / 4 - testUR5eForwardKinematics(t, []float64{rad, rad, rad, rad, rad, rad}, r3.Vector{X: 16.62, Y: -271.49, Z: -509.52}) - - rad = math.Pi / 2 - testUR5eForwardKinematics(t, []float64{rad, rad, rad, rad, rad, rad}, r3.Vector{X: 133.3, Y: 292.5, Z: -162.9}) - - rad = math.Pi - testUR5eForwardKinematics(t, []float64{rad, rad, rad, rad, rad, rad}, r3.Vector{X: -32.8, Y: 33.7, Z: 262.2}) - - // Section 3 - try some random angles - testUR5eForwardKinematics(t, - []float64{math.Pi / 4, math.Pi / 2, 0, math.Pi / 4, math.Pi / 2, 0}, - r3.Vector{X: 193.91, Y: 5.39, Z: -654.63}, - ) - testUR5eForwardKinematics(t, - []float64{0, math.Pi / 4, math.Pi / 2, 0, math.Pi / 4, math.Pi / 2}, - r3.Vector{X: 97.11, Y: -203.73, Z: -394.65}, - ) - - testUR5eInverseKinematics(t, spatialmath.NewPose( - r3.Vector{X: -202.31, Y: -577.75, Z: 318.58}, - &spatialmath.OrientationVectorDegrees{Theta: 51.84, OX: 0.47, OY: -.42, OZ: -.78}, - )) -} - -type dhConstants struct { - a, d, alpha float64 -} - -func (d dhConstants) matrix(theta float64) *mat.Dense { - m := mat.NewDense(4, 4, nil) - - m.Set(0, 0, math.Cos(theta)) - m.Set(0, 1, -1*math.Sin(theta)*math.Cos(d.alpha)) - m.Set(0, 2, math.Sin(theta)*math.Sin(d.alpha)) - m.Set(0, 3, d.a*math.Cos(theta)) - - m.Set(1, 0, math.Sin(theta)) - m.Set(1, 1, math.Cos(theta)*math.Cos(d.alpha)) - m.Set(1, 2, -1*math.Cos(theta)*math.Sin(d.alpha)) - m.Set(1, 3, d.a*math.Sin(theta)) - - m.Set(2, 0, 0) - m.Set(2, 1, math.Sin(d.alpha)) - m.Set(2, 2, math.Cos(d.alpha)) - m.Set(2, 3, d.d) - - m.Set(3, 3, 1) - - return m -} - -var jointConstants = []dhConstants{ - {0.0000, 0.1625, math.Pi / 2}, - {-0.4250, 0.0000, 0}, - {-0.3922, 0.0000, 0}, - {0.0000, 0.1333, math.Pi / 2}, - {0.0000, 0.0997, -1 * math.Pi / 2}, - {0.0000, 0.0996, 0}, -} - -var orientationDH = dhConstants{0, 1, math.Pi / -2} - -func computeUR5ePosition(t *testing.T, jointRadians []float64) spatialmath.Pose { - t.Helper() - res := jointConstants[0].matrix(jointRadians[0]) - for x, theta := range jointRadians { - if x == 0 { - continue - } - - temp := mat.NewDense(4, 4, nil) - temp.Mul(res, jointConstants[x].matrix(theta)) - res = temp - } - - var o mat.Dense - o.Mul(res, orientationDH.matrix(0)) - - ov := spatialmath.OrientationVector{ - OX: o.At(0, 3) - res.At(0, 3), - OY: o.At(1, 3) - res.At(1, 3), - OZ: o.At(2, 3) - res.At(2, 3), - } - ov.Normalize() - - resMgl := mgl64.Ident4() - // Copy to a mgl64 4x4 to convert to quaternion - for r := 0; r < 4; r++ { - for c := 0; c < 4; c++ { - resMgl.Set(r, c, res.At(r, c)) - } - } - q := mgl64.Mat4ToQuat(resMgl) - poseOV := spatialmath.QuatToOV(quat.Number{q.W, q.X(), q.Y(), q.Z()}) - - // Confirm that our matrix -> quaternion -> OV conversion yields the same result as the OV calculated from the DH param - test.That(t, poseOV.OX, test.ShouldAlmostEqual, ov.OX, .01) - test.That(t, poseOV.OY, test.ShouldAlmostEqual, ov.OY, .01) - test.That(t, poseOV.OZ, test.ShouldAlmostEqual, ov.OZ, .01) - - return spatialmath.NewPose( - r3.Vector{X: res.At(0, 3), Y: res.At(1, 3), Z: res.At(2, 3)}.Mul(1000), - &spatialmath.OrientationVectorDegrees{OX: poseOV.OX, OY: poseOV.OY, OZ: poseOV.OZ, Theta: utils.RadToDeg(poseOV.Theta)}, - ) -} - -func setupListeners(ctx context.Context, statusBlob []byte, - remote *atomic.Bool, -) (func(), error) { - listener29999, err := net.Listen("tcp", "localhost:29999") - if err != nil { - return nil, err - } - - listener30001, err := net.Listen("tcp", "localhost:30001") - if err != nil { - return nil, err - } - - listener30011, err := net.Listen("tcp", "localhost:30011") - if err != nil { - return nil, err - } - - goutils.PanicCapturingGo(func() { - for { - if ctx.Err() != nil { - break - } - conn, err := listener29999.Accept() - if err != nil { - break - } - ioReader := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) - if _, err = ioReader.WriteString("hello test dashboard\n"); err != nil { - break - } - - if ioReader.Flush() != nil { - break - } - for { - _, _, err := ioReader.ReadLine() - if err != nil { - return - } - if _, err = ioReader.WriteString(fmt.Sprintf("%v\n", remote.Load())); err != nil { - break - } - if ioReader.Flush() != nil { - break - } - timeout := time.NewTimer(100 * time.Millisecond) - select { - case <-ctx.Done(): - return - case <-timeout.C: - continue - } - } - } - }) - goutils.PanicCapturingGo(func() { - for { - if ctx.Err() != nil { - break - } - if _, err := listener30001.Accept(); err != nil { - break - } - } - }) - goutils.PanicCapturingGo(func() { - for { - if ctx.Err() != nil { - break - } - conn, err := listener30011.Accept() - if err != nil { - break - } - for { - if ctx.Err() != nil { - break - } - _, err = conn.Write(statusBlob) - if err != nil { - break - } - if !goutils.SelectContextOrWait(ctx, 100*time.Millisecond) { - return - } - } - } - }) - - closer := func() { - listener30001.Close() - listener29999.Close() - listener30011.Close() - } - return closer, nil -} - -func TestArmReconnection(t *testing.T) { - var remote atomic.Bool - - remote.Store(false) - - statusBlob, err := os.ReadFile(artifact.MustPath("components/arm/universalrobots/armBlob")) - test.That(t, err, test.ShouldBeNil) - - logger := logging.NewTestLogger(t) - parentCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - ctx, childCancel := context.WithCancel(parentCtx) - - closer, err := setupListeners(ctx, statusBlob, &remote) - - test.That(t, err, test.ShouldBeNil) - cfg := resource.Config{ - Name: "testarm", - ConvertedAttributes: &Config{ - SpeedDegsPerSec: 0.3, - Host: "localhost", - ArmHostedKinematics: false, - }, - } - - arm, err := urArmConnect(parentCtx, cfg, logger) - - test.That(t, err, test.ShouldBeNil) - ua, ok := arm.(*urArm) - test.That(t, ok, test.ShouldBeTrue) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ua.mu.Lock() - test.That(tb, ua.isConnected, test.ShouldBeTrue) - test.That(tb, ua.inRemoteMode, test.ShouldBeFalse) - ua.mu.Unlock() - }) - - remote.Store(true) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ua.mu.Lock() - test.That(tb, ua.isConnected, test.ShouldBeTrue) - test.That(tb, ua.inRemoteMode, test.ShouldBeTrue) - ua.mu.Unlock() - }) - - remote.Store(false) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ua.mu.Lock() - test.That(tb, ua.isConnected, test.ShouldBeTrue) - test.That(tb, ua.inRemoteMode, test.ShouldBeFalse) - ua.mu.Unlock() - }) - - closer() - childCancel() - - test.That(t, goutils.SelectContextOrWait(parentCtx, time.Millisecond*500), test.ShouldBeTrue) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ua.mu.Lock() - test.That(tb, ua.isConnected, test.ShouldBeFalse) - ua.mu.Unlock() - }) - - ctx, childCancel = context.WithCancel(parentCtx) - - closer, err = setupListeners(ctx, statusBlob, &remote) - test.That(t, err, test.ShouldBeNil) - remote.Store(true) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ua.mu.Lock() - test.That(tb, ua.isConnected, test.ShouldBeTrue) - test.That(tb, ua.inRemoteMode, test.ShouldBeTrue) - ua.mu.Unlock() - }) - - closer() - childCancel() - _ = ua.Close(ctx) -} - -func TestReconfigure(t *testing.T) { - cfg := resource.Config{ - Name: "testarm", - ConvertedAttributes: &Config{ - SpeedDegsPerSec: 0.3, - Host: "localhost", - ArmHostedKinematics: false, - }, - } - - conf1 := resource.Config{ - Name: "testarm", - ConvertedAttributes: &Config{ - SpeedDegsPerSec: 0.5, - Host: "localhost", - ArmHostedKinematics: false, - }, - } - - conf2 := resource.Config{ - Name: "testarm", - ConvertedAttributes: &Config{ - SpeedDegsPerSec: 0.5, - Host: "new", - ArmHostedKinematics: false, - }, - } - - conf, err := resource.NativeConfig[*Config](cfg) - test.That(t, err, test.ShouldBeNil) - - ur5e := &urArm{ - speedRadPerSec: conf.SpeedDegsPerSec, - urHostedKinematics: conf.ArmHostedKinematics, - host: conf.Host, - } - - // scenario where we do not reconfigure - test.That(t, ur5e.Reconfigure(context.Background(), nil, conf1), test.ShouldBeNil) - test.That(t, ur5e.speedRadPerSec, test.ShouldEqual, utils.DegToRad(0.5)) - - // scenario where we have to configure - test.That(t, ur5e.Reconfigure(context.Background(), nil, conf2), test.ShouldBeNil) - test.That(t, ur5e.speedRadPerSec, test.ShouldEqual, utils.DegToRad(0.5)) - test.That(t, ur5e.host, test.ShouldEqual, "new") -} diff --git a/components/arm/universalrobots/ur_parser_test.go b/components/arm/universalrobots/ur_parser_test.go deleted file mode 100644 index 5ff99ac2070..00000000000 --- a/components/arm/universalrobots/ur_parser_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package universalrobots - -import ( - "context" - "math" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" -) - -func Test1(t *testing.T) { - logger := logging.NewTestLogger(t) - data, err := os.ReadFile(artifact.MustPath("robots/universalrobots/test1.raw")) - test.That(t, err, test.ShouldBeNil) - - state, err := readRobotStateMessage(context.Background(), data, logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, int(math.Round(state.Joints[0].degrees())), test.ShouldEqual, 90) - test.That(t, int(math.Round(state.Joints[1].degrees())), test.ShouldEqual, -90) - test.That(t, int(math.Round(state.Joints[2].degrees())), test.ShouldEqual, 5) - test.That(t, int(math.Round(state.Joints[3].degrees())), test.ShouldEqual, 10) - test.That(t, int(math.Round(state.Joints[4].degrees())), test.ShouldEqual, 15) - test.That(t, int(math.Round(state.Joints[5].degrees())), test.ShouldEqual, 20) -} diff --git a/components/arm/universalrobots/verify_main_test.go b/components/arm/universalrobots/verify_main_test.go deleted file mode 100644 index d638e121699..00000000000 --- a/components/arm/universalrobots/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package universalrobots - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/arm/verify_main_test.go b/components/arm/verify_main_test.go deleted file mode 100644 index 48239d3f6f2..00000000000 --- a/components/arm/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package arm - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/arm/wrapper/wrapper_test.go b/components/arm/wrapper/wrapper_test.go deleted file mode 100644 index b0d9bbd11af..00000000000 --- a/components/arm/wrapper/wrapper_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package wrapper - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func TestReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - - cfg := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ModelFilePath: "../universalrobots/ur5e.json", - ArmName: "does not exist0", - }, - } - - armName := arm.Named("foo") - cfg1 := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ModelFilePath: "../xarm/xarm6_kinematics.json", - ArmName: armName.ShortName(), - }, - } - - cfg1Err := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ModelFilePath: "../xarm/xarm6_kinematics.json", - ArmName: "dne1", - }, - } - - cfg2Err := resource.Config{ - Name: "testArm", - ConvertedAttributes: &Config{ - ModelFilePath: "DNE", - ArmName: armName.ShortName(), - }, - } - - conf, err := resource.NativeConfig[*Config](cfg) - test.That(t, err, test.ShouldBeNil) - - model, err := modelFromPath(conf.ModelFilePath, cfg.Name) - test.That(t, err, test.ShouldBeNil) - - actualArm := &inject.Arm{} - - wrapperArm := &Arm{ - Named: cfg.ResourceName().AsNamed(), - model: model, - actual: &inject.Arm{}, - logger: logger, - } - - deps := resource.Dependencies{armName: actualArm} - - test.That(t, wrapperArm.Reconfigure(context.Background(), deps, cfg1), test.ShouldBeNil) - - err = wrapperArm.Reconfigure(context.Background(), deps, cfg1Err) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "missing from dep") - - err = wrapperArm.Reconfigure(context.Background(), deps, cfg2Err) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "only files") -} diff --git a/components/arm/xarm/xarm_test.go b/components/arm/xarm/xarm_test.go deleted file mode 100644 index f7f38ad7bb9..00000000000 --- a/components/arm/xarm/xarm_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package xarm - -import ( - "context" - "net" - "strconv" - "testing" - - "github.com/golang/geo/r3" - pb "go.viam.com/api/common/v1" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - frame "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var ( - home7 = frame.FloatsToInputs([]float64{0, 0, 0, 0, 0, 0, 0}) - wbY = -426. -) - -// This will test solving the path to write the word "VIAM" on a whiteboard. -func TestWriteViam(t *testing.T) { - fs := frame.NewEmptyFrameSystem("test") - - ctx := context.Background() - logger := logging.NewTestLogger(t) - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm7_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - - err = fs.AddFrame(m, fs.World()) - test.That(t, err, test.ShouldBeNil) - - markerOriginFrame, err := frame.NewStaticFrame( - "marker_origin", - spatial.NewPoseFromOrientation(&spatial.OrientationVectorDegrees{OY: -1, OZ: 1}), - ) - test.That(t, err, test.ShouldBeNil) - markerFrame, err := frame.NewStaticFrame("marker", spatial.NewPoseFromPoint(r3.Vector{0, 0, 160})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(markerOriginFrame, m) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(markerFrame, markerOriginFrame) - test.That(t, err, test.ShouldBeNil) - - eraserOriginFrame, err := frame.NewStaticFrame( - "eraser_origin", - spatial.NewPoseFromOrientation(&spatial.OrientationVectorDegrees{OY: 1, OZ: 1}), - ) - test.That(t, err, test.ShouldBeNil) - eraserFrame, err := frame.NewStaticFrame("eraser", spatial.NewPoseFromPoint(r3.Vector{0, 0, 160})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(eraserOriginFrame, m) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(eraserFrame, eraserOriginFrame) - test.That(t, err, test.ShouldBeNil) - - moveFrame := eraserFrame - - // draw pos start - goal := spatial.NewPoseFromProtobuf(&pb.Pose{ - X: 230, - Y: wbY + 10, - Z: 600, - OY: -1, - }) - - seedMap := map[string][]frame.Input{} - - seedMap[m.Name()] = home7 - - plan, err := motionplan.PlanMotion(ctx, &motionplan.PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal), - Frame: moveFrame, - StartConfiguration: seedMap, - FrameSystem: fs, - }) - test.That(t, err, test.ShouldBeNil) - - opt := map[string]interface{}{"motion_profile": motionplan.LinearMotionProfile} - goToGoal := func(seedMap map[string][]frame.Input, goal spatial.Pose) map[string][]frame.Input { - plan, err := motionplan.PlanMotion(ctx, &motionplan.PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(fs.World().Name(), goal), - Frame: moveFrame, - StartConfiguration: seedMap, - FrameSystem: fs, - Options: opt, - }) - test.That(t, err, test.ShouldBeNil) - return plan.Trajectory()[len(plan.Trajectory())-1] - } - - seed := plan.Trajectory()[len(plan.Trajectory())-1] - for _, goal = range viamPoints { - seed = goToGoal(seed, goal) - } -} - -var viamPoints = []spatial.Pose{ - spatial.NewPoseFromProtobuf(&pb.Pose{X: 200, Y: wbY + 1.5, Z: 595, OY: -1}), - spatial.NewPoseFromProtobuf(&pb.Pose{X: 120, Y: wbY + 1.5, Z: 595, OY: -1}), -} - -func TestReconfigure(t *testing.T) { - listener1, err := net.Listen("tcp4", "127.0.0.1:0") - test.That(t, err, test.ShouldBeNil) - defer listener1.Close() - addr1 := listener1.Addr().String() - listener2, err := net.Listen("tcp4", "127.0.0.1:0") - test.That(t, err, test.ShouldBeNil) - defer listener2.Close() - addr2 := listener2.Addr().String() - host1, port1Str, err := net.SplitHostPort(addr1) - test.That(t, err, test.ShouldBeNil) - host2, port2Str, err := net.SplitHostPort(addr2) - test.That(t, err, test.ShouldBeNil) - - port1, err := strconv.ParseInt(port1Str, 10, 32) - test.That(t, err, test.ShouldBeNil) - port2, err := strconv.ParseInt(port2Str, 10, 32) - test.That(t, err, test.ShouldBeNil) - - cfg := resource.Config{ - Name: "testarm", - ConvertedAttributes: &Config{ - Speed: 0.3, - Host: host1, - Port: int(port1), - Acceleration: 0.1, - }, - } - - shouldNotReconnectCfg := resource.Config{ - Name: "testarm", - ConvertedAttributes: &Config{ - Speed: 0.5, - Host: host1, - Port: int(port1), - Acceleration: 0.3, - }, - } - - shouldReconnectCfg := resource.Config{ - Name: "testarm", - ConvertedAttributes: &Config{ - Speed: 0.6, - Host: host2, - Port: int(port2), - Acceleration: 0.34, - }, - } - - conf, err := resource.NativeConfig[*Config](cfg) - test.That(t, err, test.ShouldBeNil) - confNotReconnect, ok := shouldNotReconnectCfg.ConvertedAttributes.(*Config) - test.That(t, ok, test.ShouldBeTrue) - - conn1, err := net.Dial("tcp", listener1.Addr().String()) - test.That(t, err, test.ShouldBeNil) - xArm := &xArm{ - speed: float32(utils.DegToRad(float64(conf.Speed))), - logger: logging.NewTestLogger(t), - } - xArm.mu.Lock() - xArm.conn = conn1 - xArm.mu.Unlock() - - ctx := context.Background() - - // scenario where we do no nothing - prevSpeed := xArm.speed - test.That(t, xArm.Reconfigure(ctx, nil, cfg), test.ShouldBeNil) - - xArm.mu.Lock() - currentConn := xArm.conn - xArm.mu.Unlock() - test.That(t, currentConn, test.ShouldEqual, conn1) - test.That(t, xArm.speed, test.ShouldEqual, prevSpeed) - - // scenario where we do not reconnect - test.That(t, xArm.Reconfigure(ctx, nil, shouldNotReconnectCfg), test.ShouldBeNil) - - xArm.mu.Lock() - currentConn = xArm.conn - xArm.mu.Unlock() - test.That(t, currentConn, test.ShouldEqual, conn1) - test.That(t, xArm.speed, test.ShouldEqual, float32(utils.DegToRad(float64(confNotReconnect.Speed)))) - - // scenario where we have to reconnect - err = xArm.Reconfigure(ctx, nil, shouldReconnectCfg) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failed to start") - - xArm.mu.Lock() - currentConn = xArm.conn - xArm.mu.Unlock() - test.That(t, currentConn, test.ShouldNotEqual, conn1) - test.That(t, xArm.speed, test.ShouldEqual, float32(utils.DegToRad(float64(confNotReconnect.Speed)))) -} diff --git a/components/audioinput/audio_input.go b/components/audioinput/audio_input.go deleted file mode 100644 index 5a56366c278..00000000000 --- a/components/audioinput/audio_input.go +++ /dev/null @@ -1,146 +0,0 @@ -// Package audioinput defines an audio capturing device. -package audioinput - -import ( - "context" - "errors" - - "github.com/pion/mediadevices/pkg/prop" - pb "go.viam.com/api/component/audioinput/v1" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[AudioInput]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterAudioInputServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.AudioInputService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - - // TODO(RSDK-562): Add RegisterCollector -} - -// SubtypeName is a constant that identifies the audio input resource subtype string. -const SubtypeName = "audio_input" - -// API is a variable that identifies the audio input resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named audio inputs's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// An AudioInput is a resource that can capture audio. -type AudioInput interface { - resource.Resource - AudioSource -} - -// An AudioSource represents anything that can capture audio. -type AudioSource interface { - gostream.AudioSource - gostream.AudioPropertyProvider -} - -// A LivenessMonitor is responsible for monitoring the liveness of an audio input. An example -// is connectivity. Since the model itself knows best about how to maintain this state, -// the reconfigurable offers a safe way to notify if a state needs to be reset due -// to some exceptional event (like a reconnect). -// It is expected that the monitoring code is tied to the lifetime of the resource -// and once the resource is closed, so should the monitor. That is, it should -// no longer send any resets once a Close on its associated resource has returned. -type LivenessMonitor interface { - Monitor(notifyReset func()) -} - -// FromDependencies is a helper for getting the named audio input from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (AudioInput, error) { - return resource.FromDependencies[AudioInput](deps, Named(name)) -} - -// FromRobot is a helper for getting the named audio input from the given Robot. -func FromRobot(r robot.Robot, name string) (AudioInput, error) { - return robot.ResourceFromRobot[AudioInput](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all audio input names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -type audioPropertiesFunc func(ctx context.Context) (prop.Audio, error) - -func (apf audioPropertiesFunc) MediaProperties(ctx context.Context) (prop.Audio, error) { - return apf(ctx) -} - -// NewAudioSourceFromReader creates an AudioSource from a reader. -func NewAudioSourceFromReader(reader gostream.AudioReader, props prop.Audio) (AudioSource, error) { - if reader == nil { - return nil, errors.New("cannot have a nil reader") - } - as := gostream.NewAudioSource(reader, props) - return &audioSource{ - as: as, - prov: audioPropertiesFunc(func(ctx context.Context) (prop.Audio, error) { - return props, nil - }), - }, nil -} - -// FromAudioSource creates an AudioInput resource either from a AudioSource. -func FromAudioSource(name resource.Name, src AudioSource) (AudioInput, error) { - return &sourceBasedInput{ - Named: name.AsNamed(), - AudioSource: src, - }, nil -} - -type sourceBasedInput struct { - resource.Named - resource.AlwaysRebuild - AudioSource -} - -// NewAudioSourceFromGostreamSource creates an AudioSource from a gostream.AudioSource. -func NewAudioSourceFromGostreamSource(audSrc gostream.AudioSource) (AudioSource, error) { - if audSrc == nil { - return nil, errors.New("cannot have a nil audio source") - } - provider, ok := audSrc.(gostream.AudioPropertyProvider) - if !ok { - return nil, errors.New("source must have property provider") - } - return &audioSource{ - as: audSrc, - prov: provider, - }, nil -} - -// AudioSource implements an AudioInput with a gostream.AudioSource. -type audioSource struct { - as gostream.AudioSource - prov gostream.AudioPropertyProvider -} - -func (as *audioSource) Stream( - ctx context.Context, - errHandlers ...gostream.ErrorHandler, -) (gostream.AudioStream, error) { - return as.as.Stream(ctx, errHandlers...) -} - -func (as *audioSource) MediaProperties(ctx context.Context) (prop.Audio, error) { - return as.prov.MediaProperties(ctx) -} - -// Close closes the underlying AudioSource. -func (as *audioSource) Close(ctx context.Context) error { - return as.as.Close(ctx) -} diff --git a/components/audioinput/client.go b/components/audioinput/client.go deleted file mode 100644 index 6ef4426b851..00000000000 --- a/components/audioinput/client.go +++ /dev/null @@ -1,230 +0,0 @@ -package audioinput - -import ( - "context" - "io" - "math" - "sync" - - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "github.com/pkg/errors" - pb "go.viam.com/api/component/audioinput/v1" - "go.viam.com/utils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// client is an audio input client. -type client struct { - resource.Named - resource.TriviallyReconfigurable - conn rpc.ClientConn - client pb.AudioInputServiceClient - logger logging.Logger - mu sync.Mutex - name string - activeBackgroundWorkers sync.WaitGroup - healthyClientCh chan struct{} -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (AudioInput, error) { - c := pb.NewAudioInputServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - conn: conn, - client: c, - logger: logger, - }, nil -} - -func (c *client) Read(ctx context.Context) (wave.Audio, func(), error) { - stream, err := c.Stream(ctx) - if err != nil { - return nil, nil, err - } - defer func() { - if err := stream.Close(ctx); err != nil { - c.logger.CErrorw(ctx, "error closing stream", "error", err) - } - }() - return stream.Next(ctx) -} - -func (c *client) Stream( - ctx context.Context, - errHandlers ...gostream.ErrorHandler, -) (gostream.AudioStream, error) { - // RSDK-6340: The resource manager closes remote resources when the underlying - // connection goes bad. However, when the connection is re-established, the client - // objects these resources represent are not re-initialized/marked "healthy". - // `healthyClientCh` helps track these transitions between healthy and unhealthy - // states. - // - // When a new `client.Stream()` is created we will either use the existing - // `healthyClientCh` or create a new one. - // - // The goroutine a `Stream()` method spins off will listen to its version of the - // `healthyClientCh` to be notified when the connection has died so it can gracefully - // terminate. - // - // When a connection becomes unhealthy, the resource manager will call `Close` on the - // audioinput client object. Closing the client will: - // 1. close its `client.healthyClientCh` channel - // 2. wait for existing "stream" goroutines to drain - // 3. nil out the `client.healthyClientCh` member variable - // - // New streams concurrent with closing cannot start until this drain completes. There - // will never be stream goroutines from the old "generation" running concurrently - // with those from the new "generation". - c.mu.Lock() - if c.healthyClientCh == nil { - c.healthyClientCh = make(chan struct{}) - } - healthyClientCh := c.healthyClientCh - c.mu.Unlock() - - streamCtx, stream, chunkCh := gostream.NewMediaStreamForChannel[wave.Audio](context.Background()) - - chunksClient, err := c.client.Chunks(ctx, &pb.ChunksRequest{ - Name: c.name, - SampleFormat: pb.SampleFormat_SAMPLE_FORMAT_FLOAT32_INTERLEAVED, - }) - if err != nil { - return nil, err - } - - infoResp, err := chunksClient.Recv() - if err != nil { - return nil, err - } - infoProto := infoResp.GetInfo() - - c.mu.Lock() - if err := streamCtx.Err(); err != nil { - c.mu.Unlock() - return nil, err - } - c.activeBackgroundWorkers.Add(1) - c.mu.Unlock() - - utils.PanicCapturingGo(func() { - defer c.activeBackgroundWorkers.Done() - defer close(chunkCh) - - for { - if streamCtx.Err() != nil { - return - } - - var nextErr error - - chunkResp, err := chunksClient.Recv() - - var chunk wave.Audio - if err != nil { - if errors.Is(err, io.EOF) { - return - } - for _, handler := range errHandlers { - handler(streamCtx, err) - } - nextErr = err - } else { - chunkProto := chunkResp.GetChunk() - info := wave.ChunkInfo{ - Len: int(chunkProto.Length), - Channels: int(infoProto.Channels), - SamplingRate: int(infoProto.SamplingRate), - } - - switch infoProto.SampleFormat { - case pb.SampleFormat_SAMPLE_FORMAT_INT16_INTERLEAVED: - chunkActual := wave.NewInt16Interleaved(info) - for i := 0; i < info.Len; i++ { - chunkActual.Data[i] = int16(HostEndian.Uint16(chunkProto.Data[i*2:])) - } - chunk = chunkActual - case pb.SampleFormat_SAMPLE_FORMAT_FLOAT32_INTERLEAVED: - chunkActual := wave.NewFloat32Interleaved(info) - for i := 0; i < info.Len; i++ { - chunkActual.Data[i] = math.Float32frombits(HostEndian.Uint32(chunkProto.Data[i*4:])) - } - chunk = chunkActual - case pb.SampleFormat_SAMPLE_FORMAT_UNSPECIFIED: - fallthrough - default: - nextErr = errors.Errorf("unknown type of audio sample format %v", infoProto.SampleFormat) - } - } - - select { - case <-streamCtx.Done(): - return - case <-healthyClientCh: - if err := stream.Close(context.Background()); err != nil { - c.logger.Warn("error closing stream", err) - } - return - case chunkCh <- gostream.MediaReleasePairWithError[wave.Audio]{ - Media: chunk, - Release: func() {}, - Err: nextErr, - }: - } - } - }) - - return stream, nil -} - -func (c *client) MediaProperties(ctx context.Context) (prop.Audio, error) { - resp, err := c.client.Properties(ctx, &pb.PropertiesRequest{ - Name: c.name, - }) - if err != nil { - return prop.Audio{}, err - } - return prop.Audio{ - ChannelCount: int(resp.ChannelCount), - Latency: resp.Latency.AsDuration(), - SampleRate: int(resp.SampleRate), - SampleSize: int(resp.SampleSize), - IsBigEndian: resp.IsBigEndian, - IsFloat: resp.IsFloat, - IsInterleaved: resp.IsInterleaved, - }, nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return protoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} - -// TODO(RSDK-6433): This method can be called more than once during a client's lifecycle. -// For example, consider a case where a remote audioinput goes offline and then back -// online. We will call `Close` on the audioinput client when we detect the disconnection -// to remove active streams but then reuse the client when the connection is -// re-established. -func (c *client) Close(ctx context.Context) error { - c.mu.Lock() - defer c.mu.Unlock() - - if c.healthyClientCh != nil { - close(c.healthyClientCh) - } - c.activeBackgroundWorkers.Wait() - c.healthyClientCh = nil - return nil -} diff --git a/components/audioinput/client_test.go b/components/audioinput/client_test.go deleted file mode 100644 index 6fe33dd9d9a..00000000000 --- a/components/audioinput/client_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package audioinput_test - -import ( - "context" - "errors" - "net" - "testing" - - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/audioinput" - "go.viam.com/rdk/gostream" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testAudioInputName = "audio1" - failAudioInputName = "audio2" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - audioData := &wave.Float32Interleaved{ - Data: []float32{ - 0.1, -0.5, 0.2, -0.6, 0.3, -0.7, 0.4, -0.8, 0.5, -0.9, 0.6, -1.0, 0.7, -1.1, 0.8, -1.2, - }, - Size: wave.ChunkInfo{8, 2, 48000}, - } - - injectAudioInput := &inject.AudioInput{} - - // good audio input - injectAudioInput.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.AudioStream, error) { - return gostream.NewEmbeddedAudioStreamFromReader(gostream.AudioReaderFunc(func(ctx context.Context) (wave.Audio, func(), error) { - return audioData, func() {}, nil - })), nil - } - expectedProps := prop.Audio{ - ChannelCount: 1, - SampleRate: 2, - IsBigEndian: true, - IsInterleaved: true, - Latency: 5, - } - injectAudioInput.MediaPropertiesFunc = func(ctx context.Context) (prop.Audio, error) { - return expectedProps, nil - } - // bad audio input - injectAudioInput2 := &inject.AudioInput{} - injectAudioInput2.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.AudioStream, error) { - return nil, errors.New("can't generate stream") - } - - resources := map[resource.Name]audioinput.AudioInput{ - audioinput.Named(testAudioInputName): injectAudioInput, - audioinput.Named(failAudioInputName): injectAudioInput2, - } - audioInputSvc, err := resource.NewAPIResourceCollection(audioinput.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[audioinput.AudioInput](audioinput.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, audioInputSvc), test.ShouldBeNil) - - injectAudioInput.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - t.Run("audio input client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - audioInput1Client, err := audioinput.NewClientFromConn(context.Background(), conn, "", audioinput.Named(testAudioInputName), logger) - test.That(t, err, test.ShouldBeNil) - chunk, _, err := gostream.ReadAudio(context.Background(), audioInput1Client) - test.That(t, err, test.ShouldBeNil) - - audioData := wave.NewFloat32Interleaved(chunk.ChunkInfo()) - // convert - for i := 0; i < chunk.ChunkInfo().Len; i++ { - for j := 0; j < chunk.ChunkInfo().Channels; j++ { - audioData.Set(i, j, chunk.At(i, j)) - } - } - - test.That(t, chunk, test.ShouldResemble, audioData) - - props, err := audioInput1Client.MediaProperties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, props, test.ShouldResemble, expectedProps) - - // DoCommand - resp, err := audioInput1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, audioInput1Client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("audio input client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", audioinput.Named(failAudioInputName), logger) - test.That(t, err, test.ShouldBeNil) - - _, _, err = gostream.ReadAudio(context.Background(), client2) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "can't generate stream") - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} - -func TestClientStreamAfterClose(t *testing.T) { - // Set up gRPC server - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - // Set up audioinput that can stream audio - - audioData := &wave.Float32Interleaved{ - Data: []float32{ - 0.1, -0.5, 0.2, -0.6, 0.3, -0.7, 0.4, -0.8, 0.5, -0.9, 0.6, -1.0, 0.7, -1.1, 0.8, -1.2, - }, - Size: wave.ChunkInfo{8, 2, 48000}, - } - - injectAudioInput := &inject.AudioInput{} - - // good audio input - injectAudioInput.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.AudioStream, error) { - return gostream.NewEmbeddedAudioStreamFromReader(gostream.AudioReaderFunc(func(ctx context.Context) (wave.Audio, func(), error) { - return audioData, func() {}, nil - })), nil - } - - expectedProps := prop.Audio{ - ChannelCount: 1, - SampleRate: 2, - IsBigEndian: true, - IsInterleaved: true, - Latency: 5, - } - injectAudioInput.MediaPropertiesFunc = func(ctx context.Context) (prop.Audio, error) { - return expectedProps, nil - } - - // Register AudioInputService API in our gRPC server. - resources := map[resource.Name]audioinput.AudioInput{ - audioinput.Named(testAudioInputName): injectAudioInput, - } - audioinputSvc, err := resource.NewAPIResourceCollection(audioinput.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[audioinput.AudioInput](audioinput.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, audioinputSvc), test.ShouldBeNil) - - // Start serving requests. - go rpcServer.Serve(listener) - defer rpcServer.Stop() - - // Make client connection - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := audioinput.NewClientFromConn(context.Background(), conn, "", audioinput.Named(testAudioInputName), logger) - test.That(t, err, test.ShouldBeNil) - - // Get a stream - stream, err := client.Stream(context.Background()) - test.That(t, stream, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Read from stream - media, _, err := stream.Next(context.Background()) - test.That(t, media, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Close client and read from stream - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - media, _, err = stream.Next(context.Background()) - test.That(t, media, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "context canceled") - - // Get a new stream - stream, err = client.Stream(context.Background()) - test.That(t, stream, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Read from the new stream - media, _, err = stream.Next(context.Background()) - test.That(t, media, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Close client and connection - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) -} diff --git a/components/audioinput/fake/audio_input.go b/components/audioinput/fake/audio_input.go deleted file mode 100644 index 860aed83a59..00000000000 --- a/components/audioinput/fake/audio_input.go +++ /dev/null @@ -1,161 +0,0 @@ -//go:build !no_cgo - -// Package fake implements a fake audio input. -package fake - -import ( - "context" - "encoding/binary" - "math" - "sync" - "sync/atomic" - "time" - - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "go.viam.com/utils" - - "go.viam.com/rdk/components/audioinput" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func init() { - resource.RegisterComponent( - audioinput.API, - resource.DefaultModelFamily.WithModel("fake"), - resource.Registration[audioinput.AudioInput, resource.NoNativeConfig]{Constructor: func( - _ context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (audioinput.AudioInput, error) { - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - var condMu sync.RWMutex - cond := sync.NewCond(condMu.RLocker()) - input := &audioInput{ - Named: conf.ResourceName().AsNamed(), - toneHz: 440, - cancel: cancelFunc, - cancelCtx: cancelCtx, - cond: cond, - } - input.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - ticker := time.NewTicker(latencyMillis * time.Millisecond) - for { - if !utils.SelectContextOrWaitChan(cancelCtx, ticker.C) { - return - } - atomic.AddInt64(&input.step, 1) - cond.Broadcast() - } - }, input.activeBackgroundWorkers.Done) - as := gostream.NewAudioSource(gostream.AudioReaderFunc(input.Read), prop.Audio{ - ChannelCount: channelCount, - SampleRate: samplingRate, - IsBigEndian: audioinput.HostEndian == binary.BigEndian, - IsInterleaved: true, - Latency: time.Millisecond * latencyMillis, - }) - input.AudioSource = as - return audioinput.FromAudioSource(conf.ResourceName(), input) - }}) -} - -// audioInput is a fake audioinput that always returns the same chunk. -type audioInput struct { - resource.Named - resource.TriviallyReconfigurable - gostream.AudioSource - mu sync.RWMutex - step int64 - toneHz float64 - cancel func() - cancelCtx context.Context - activeBackgroundWorkers sync.WaitGroup - cond *sync.Cond -} - -const ( - latencyMillis = 20 - samplingRate = 48000 - channelCount = 1 -) - -func (i *audioInput) Read(ctx context.Context) (wave.Audio, func(), error) { - select { - case <-i.cancelCtx.Done(): - return nil, nil, i.cancelCtx.Err() - case <-ctx.Done(): - return nil, nil, ctx.Err() - default: - } - - i.cond.L.Lock() - i.cond.Wait() - i.cond.L.Unlock() - - select { - case <-i.cancelCtx.Done(): - return nil, nil, i.cancelCtx.Err() - case <-ctx.Done(): - return nil, nil, ctx.Err() - default: - } - - const length = samplingRate * latencyMillis / 1000 - const numChunks = samplingRate / length - angle := math.Pi * 2 / (float64(length) * numChunks) - - i.mu.RLock() - toneHz := i.toneHz - i.mu.RUnlock() - - step := int(atomic.LoadInt64(&i.step) % numChunks) - chunk := wave.NewFloat32Interleaved(wave.ChunkInfo{ - Len: length, - Channels: channelCount, - SamplingRate: samplingRate, - }) - - for sample := 0; sample < length; sample++ { - val := wave.Float32Sample(math.Sin(angle * toneHz * (float64((length * step) + sample)))) - chunk.Set(sample, 0, val) - } - return chunk, func() {}, nil -} - -func (i *audioInput) MediaProperties(_ context.Context) (prop.Audio, error) { - return prop.Audio{ - ChannelCount: channelCount, - SampleRate: samplingRate, - IsBigEndian: audioinput.HostEndian == binary.BigEndian, - IsInterleaved: true, - Latency: time.Millisecond * latencyMillis, - }, nil -} - -// DoCommand allows setting of tone. -func (i *audioInput) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - i.mu.Lock() - defer i.mu.Unlock() - newTone, ok := cmd["set_tone_hz"].(float64) - if !ok { - return map[string]interface{}{}, nil - } - oldTone := i.toneHz - i.toneHz = newTone - return map[string]interface{}{"prev_tone_hz": oldTone}, nil -} - -// Close stops the generator routine. -func (i *audioInput) Close(ctx context.Context) error { - i.cancel() - i.activeBackgroundWorkers.Wait() - i.cond.L.Lock() - i.cond.Signal() - i.cond.L.Unlock() - return i.AudioSource.Close(ctx) -} diff --git a/components/audioinput/microphone/microphone.go b/components/audioinput/microphone/microphone.go deleted file mode 100644 index d1b3646a0ef..00000000000 --- a/components/audioinput/microphone/microphone.go +++ /dev/null @@ -1,122 +0,0 @@ -//go:build !no_cgo - -// Package microphone implements a microphone audio input. Really the microphone -// is any audio input device that can be found via gostream. -package microphone - -import ( - "context" - "errors" - "path/filepath" - "regexp" - - "github.com/pion/mediadevices" - - "go.viam.com/rdk/components/audioinput" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("microphone") - -func init() { - resource.RegisterComponent( - audioinput.API, - model, - resource.Registration[audioinput.AudioInput, *Config]{ - Constructor: func( - _ context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (audioinput.AudioInput, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - src, err := newMicrophoneSource(newConf, logger) - if err != nil { - return nil, err - } - // This always rebuilds on reconfiguration right now. A better system - // would be to reuse the monitored webcam code. - return audioinput.FromAudioSource(conf.ResourceName(), src) - }, - }) -} - -// Config is the attribute struct for microphones. -type Config struct { - resource.TriviallyValidateConfig - Path string `json:"audio_path"` - PathPattern string `json:"audio_path_pattern"` - Debug bool `json:"debug"` -} - -// newMicrophoneSource returns a new source based on a microphone discovered from the given attributes. -func newMicrophoneSource(conf *Config, logger logging.Logger) (audioinput.AudioSource, error) { - var err error - - debug := conf.Debug - - if conf.Path != "" { - return tryMicrophoneOpen(conf.Path, gostream.DefaultConstraints, logger) - } - - var pattern *regexp.Regexp - if conf.PathPattern != "" { - pattern, err = regexp.Compile(conf.PathPattern) - if err != nil { - return nil, err - } - } - all := gostream.QueryAudioDevices() - - for _, info := range all { - logger.Debugf("%s", info.ID) - logger.Debugf("\t labels: %v", info.Labels) - for _, label := range info.Labels { - if pattern != nil && !pattern.MatchString(label) { - if debug { - logger.Debug("\t skipping because of pattern") - } - continue - } - for _, p := range info.Properties { - logger.Debugf("\t %+v", p.Audio) - if p.Audio.ChannelCount == 0 { - if debug { - logger.Debug("\t skipping because audio channels are empty") - } - continue - } - s, err := tryMicrophoneOpen(label, gostream.DefaultConstraints, logger) - if err == nil { - if debug { - logger.Debug("\t USING") - } - return s, nil - } - if debug { - logger.Debugw("cannot open driver with properties", "properties", p, - "error", err) - } - } - } - } - return nil, errors.New("found no microphones") -} - -func tryMicrophoneOpen( - path string, - constraints mediadevices.MediaStreamConstraints, - logger logging.Logger, -) (audioinput.AudioSource, error) { - source, err := gostream.GetNamedAudioSource(filepath.Base(path), constraints, logger.AsZap()) - if err != nil { - return nil, err - } - // TODO(XXX): implement LivenessMonitor - return audioinput.NewAudioSourceFromGostreamSource(source) -} diff --git a/components/audioinput/register/register.go b/components/audioinput/register/register.go deleted file mode 100644 index 7dee5cf2d6b..00000000000 --- a/components/audioinput/register/register.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !no_cgo - -// Package register registers all relevant audio inputs and also API specific functions -package register - -import ( - // for audio inputs. - _ "go.viam.com/rdk/components/audioinput/fake" - _ "go.viam.com/rdk/components/audioinput/microphone" -) diff --git a/components/audioinput/server.go b/components/audioinput/server.go deleted file mode 100644 index 35b9479ab5d..00000000000 --- a/components/audioinput/server.go +++ /dev/null @@ -1,320 +0,0 @@ -package audioinput - -import ( - "bytes" - "context" - "encoding/binary" - "fmt" - "io" - "time" - "unsafe" - - "github.com/go-audio/audio" - "github.com/go-audio/transforms" - "github.com/go-audio/wav" - "github.com/pion/mediadevices/pkg/wave" - "github.com/pkg/errors" - "go.opencensus.io/trace" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/audioinput/v1" - "go.viam.com/utils" - "google.golang.org/genproto/googleapis/api/httpbody" - "google.golang.org/protobuf/types/known/durationpb" - "gopkg.in/src-d/go-billy.v4/memfs" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// HostEndian indicates the byte ordering this host natively uses. -var HostEndian binary.ByteOrder - -func init() { - // from github.com/pion/mediadevices/pkg/wave/decoder.go - //nolint:gosec - switch v := *(*uint16)(unsafe.Pointer(&([]byte{0x12, 0x34}[0]))); v { - case 0x1234: - HostEndian = binary.BigEndian - case 0x3412: - HostEndian = binary.LittleEndian - default: - panic(fmt.Sprintf("failed to determine host endianness: %x", v)) - } -} - -// serviceServer implements the AudioInputService from audioinput.proto. -type serviceServer struct { - pb.UnimplementedAudioInputServiceServer - coll resource.APIResourceCollection[AudioInput] -} - -// NewRPCServiceServer constructs an audio input gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[AudioInput]) interface{} { - return &serviceServer{coll: coll} -} - -// Chunks returns audio chunks (samples) forever from an audio input of the underlying robot. A specific sampling -// format can be requested but may not necessarily be the same one returned. -func (s *serviceServer) Chunks(req *pb.ChunksRequest, server pb.AudioInputService_ChunksServer) error { - audioInput, err := s.coll.Resource(req.Name) - if err != nil { - return err - } - - chunkStream, err := audioInput.Stream(server.Context()) - if err != nil { - return err - } - defer func() { - utils.UncheckedError(chunkStream.Close(server.Context())) - }() - - firstChunk, release, err := chunkStream.Next(server.Context()) - if err != nil { - return err - } - info := firstChunk.ChunkInfo() - release() - - var sf wave.SampleFormat - sfProto := req.SampleFormat - switch req.SampleFormat { - case pb.SampleFormat_SAMPLE_FORMAT_UNSPECIFIED: - sfProto = pb.SampleFormat_SAMPLE_FORMAT_INT16_INTERLEAVED - fallthrough - case pb.SampleFormat_SAMPLE_FORMAT_INT16_INTERLEAVED: - sf = wave.Int16SampleFormat - case pb.SampleFormat_SAMPLE_FORMAT_FLOAT32_INTERLEAVED: - sf = wave.Float32SampleFormat - default: - return errors.Errorf("unknown type of audio sample format %v", req.SampleFormat) - } - - if err := server.Send(&pb.ChunksResponse{ - Type: &pb.ChunksResponse_Info{ - Info: &pb.AudioChunkInfo{ - SampleFormat: sfProto, - Channels: uint32(info.Channels), - SamplingRate: int64(info.SamplingRate), - }, - }, - }); err != nil { - return err - } - - sendNextChunk := func() error { - chunk, release, err := chunkStream.Next(server.Context()) - if err != nil { - return err - } - defer release() - - var outBytes []byte - switch c := chunk.(type) { - case *wave.Int16Interleaved: - outBytes = make([]byte, len(c.Data)*2) - buf := bytes.NewBuffer(outBytes[:0]) - chunkCopy := c - if sf != chunk.SampleFormat() { - chunkCopy = wave.NewInt16Interleaved(info) - // convert - for i := 0; i < c.Size.Len; i++ { - for j := 0; j < c.Size.Channels; j++ { - chunkCopy.Set(i, j, chunk.At(i, j)) - } - } - } - if err := binary.Write(buf, HostEndian, chunkCopy.Data); err != nil { - return err - } - case *wave.Float32Interleaved: - outBytes = make([]byte, len(c.Data)*4) - buf := bytes.NewBuffer(outBytes[:0]) - chunkCopy := c - if sf != chunk.SampleFormat() { - chunkCopy = wave.NewFloat32Interleaved(info) - // convert - for i := 0; i < c.Size.Len; i++ { - for j := 0; j < c.Size.Channels; j++ { - chunkCopy.Set(i, j, chunk.At(i, j)) - } - } - } - if err := binary.Write(buf, HostEndian, chunkCopy.Data); err != nil { - return err - } - default: - return errors.Errorf("unknown type of audio buffer %T", chunk) - } - - return server.Send(&pb.ChunksResponse{ - Type: &pb.ChunksResponse_Chunk{ - Chunk: &pb.AudioChunk{ - Data: outBytes, - Length: uint32(info.Len), - }, - }, - }) - } - - for { - if err := sendNextChunk(); err != nil { - return err - } - } -} - -// Properties returns properties of an audio input of the underlying robot. -func (s *serviceServer) Properties( - ctx context.Context, - req *pb.PropertiesRequest, -) (*pb.PropertiesResponse, error) { - audioInput, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - props, err := audioInput.MediaProperties(ctx) - if err != nil { - return nil, err - } - - return &pb.PropertiesResponse{ - ChannelCount: uint32(props.ChannelCount), - Latency: durationpb.New(props.Latency), - SampleRate: uint32(props.SampleRate), - SampleSize: uint32(props.SampleSize), - IsBigEndian: props.IsBigEndian, - IsFloat: props.IsFloat, - IsInterleaved: props.IsInterleaved, - }, nil -} - -// Record renders an audio chunk from an audio input of the underlying robot -// to an HTTP response. A specific MIME type cannot be requested and may not necessarily -// be the same one returned each time. -func (s *serviceServer) Record( - ctx context.Context, - req *pb.RecordRequest, -) (*httpbody.HttpBody, error) { - ctx, span := trace.StartSpan(ctx, "audioinput::server::Record") - defer span.End() - audioInput, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - chunkStream, err := audioInput.Stream(ctx) - if err != nil { - return nil, err - } - defer func() { - utils.UncheckedError(chunkStream.Close(ctx)) - }() - - firstChunk, release, err := chunkStream.Next(ctx) - if err != nil { - return nil, err - } - info := firstChunk.ChunkInfo() - release() - - duration := req.Duration.AsDuration() - if duration == 0 { - duration = time.Second - } - if duration > 5*time.Second { - return nil, errors.New("can only record up to 5 seconds") - } - - ms := memfs.New() - fd, err := ms.Create("dummy") - if err != nil { - return nil, err - } - - wavEnc := wav.NewEncoder(fd, - info.SamplingRate, - 24, - info.Channels, - 1, // PCM - ) - - nextChunk := func() error { - chunk, release, err := chunkStream.Next(ctx) - if err != nil { - return err - } - defer release() - - switch c := chunk.(type) { - case *wave.Int16Interleaved: - cData := make([]int, len(c.Data)) - for i := 0; i < len(c.Data); i++ { - cData[i] = int(c.Data[i]) - } - buf := &audio.IntBuffer{ - Format: &audio.Format{ - NumChannels: info.Channels, - SampleRate: info.SamplingRate, - }, - Data: cData, - SourceBitDepth: 16, - } - - return wavEnc.Write(buf) - case *wave.Float32Interleaved: - dataCopy := make([]float32, len(c.Data)) - copy(dataCopy, c.Data) - buf := &audio.Float32Buffer{ - Format: &audio.Format{ - NumChannels: info.Channels, - SampleRate: info.SamplingRate, - }, - Data: dataCopy, - SourceBitDepth: 32, - } - if err := transforms.PCMScaleF32(buf, 24); err != nil { - return err - } - - return wavEnc.Write(buf.AsIntBuffer()) - default: - return errors.Errorf("unknown type of audio buffer %T", chunk) - } - } - numChunks := int(duration.Seconds() * float64(info.SamplingRate/info.Len)) - for i := 0; i < numChunks; i++ { - if err := nextChunk(); err != nil { - return nil, err - } - } - - if err := wavEnc.Close(); err != nil { - return nil, err - } - if _, err := fd.Seek(0, io.SeekStart); err != nil { - return nil, err - } - rd, err := io.ReadAll(fd) - if err != nil { - return nil, err - } - - return &httpbody.HttpBody{ - ContentType: "audio/wav", - Data: rd, - }, nil -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - audioInput, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, audioInput, req) -} diff --git a/components/audioinput/verify_main_test.go b/components/audioinput/verify_main_test.go deleted file mode 100644 index beafd196237..00000000000 --- a/components/audioinput/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package audioinput - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/base/base.go b/components/base/base.go deleted file mode 100644 index 3409007386e..00000000000 --- a/components/base/base.go +++ /dev/null @@ -1,136 +0,0 @@ -// Package base defines the base that a robot uses to move around. -package base - -import ( - "context" - - "github.com/golang/geo/r3" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/base/v1" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Base]{ - Status: resource.StatusFunc(CreateStatus), - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterBaseServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.BaseService_ServiceDesc, - RPCClient: NewClientFromConn, - }) -} - -// SubtypeName is a constant that identifies the component resource API string "base". -const SubtypeName = "base" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named Base's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// A Base represents a physical base of a robot. -type Base interface { - resource.Resource - resource.Actuator - resource.Shaped - - // MoveStraight moves the robot straight a given distance at a given speed. - // If a distance or speed of zero is given, the base will stop. - // This method blocks until completed or cancelled - // - // myBase, err := base.FromRobot(machine, "my_base") - // // Move the base forward 40 mm at a velocity of 90 mm/s. - // myBase.MoveStraight(context.Background(), 40, 90, nil) - // - // // Move the base backward 40 mm at a velocity of -90 mm/s. - // myBase.MoveStraight(context.Background(), 40, -90, nil) - MoveStraight(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error - - // Spin spins the robot by a given angle in degrees at a given speed. - // If a speed of 0 the base will stop. - // Given a positive speed and a positive angle, the base turns to the left (for built-in RDK drivers) - // This method blocks until completed or cancelled - // - // myBase, err := base.FromRobot(machine, "my_base") - // - // // Spin the base 10 degrees at an angular velocity of 15 deg/sec. - // myBase.Spin(context.Background(), 10, 15, nil) - Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error - - // For linear power, positive Y moves forwards for built-in RDK drivers - // For angular power, positive Z turns to the left for built-in RDK drivers - // myBase, err := base.FromRobot(machine, "my_base") - // - // // Make your wheeled base move forward. Set linear power to 75%. - // logger.Info("move forward") - // err = myBase.SetPower(context.Background(), r3.Vector{Y: .75}, r3.Vector{}, nil) - // - // // Make your wheeled base move backward. Set linear power to -100%. - // logger.Info("move backward") - // err = myBase.SetPower(context.Background(), r3.Vector{Y: -1}, r3.Vector{}, nil) - // - // // Make your wheeled base spin left. Set angular power to 100%. - // logger.Info("spin left") - // err = myBase.SetPower(context.Background(), r3.Vector{}, r3.Vector{Z: 1}, nil) - // - // // Make your wheeled base spin right. Set angular power to -75%. - // logger.Info("spin right") - // err = mybase.SetPower(context.Background(), r3.Vector{}, r3.Vector{Z: -.75}, nil) - SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error - - // linear is in mmPerSec (positive Y moves forwards for built-in RDK drivers) - // angular is in degsPerSec (positive Z turns to the left for built-in RDK drivers) - // - // myBase, err := base.FromRobot(machine, "my_base") - // - // // Set the linear velocity to 50 mm/sec and the angular velocity to 15 deg/sec. - // myBase.SetVelocity(context.Background(), r3.Vector{Y: 50}, r3.Vector{Z: 15}, nil) - SetVelocity(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error - - // Properties returns the width, turning radius, and wheel circumference of the physical base in meters. - // - // myBase, err := base.FromRobot(machine, "my_base") - // - // // Get the width and turning radius of the base - // properties, err := myBase.Properties(context.Background(), nil) - // - // // Get the width - // myBaseWidth := properties.WidthMeters - // - // // Get the turning radius - // myBaseTurningRadius := properties.TurningRadiusMeters - // - // // Get the wheel circumference - // myBaseWheelCircumference := properties.WheelCircumferenceMeters - Properties(ctx context.Context, extra map[string]interface{}) (Properties, error) -} - -// FromDependencies is a helper for getting the named base from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Base, error) { - return resource.FromDependencies[Base](deps, Named(name)) -} - -// FromRobot is a helper for getting the named base from the given Robot. -func FromRobot(r robot.Robot, name string) (Base, error) { - return robot.ResourceFromRobot[Base](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all base names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// CreateStatus creates a status from the base. -func CreateStatus(ctx context.Context, b Base) (*commonpb.ActuatorStatus, error) { - isMoving, err := b.IsMoving(ctx) - if err != nil { - return nil, err - } - return &commonpb.ActuatorStatus{IsMoving: isMoving}, nil -} diff --git a/components/base/base_test.go b/components/base/base_test.go deleted file mode 100644 index b829c5fb427..00000000000 --- a/components/base/base_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package base_test - -import ( - "context" - "testing" - - "github.com/go-viper/mapstructure/v2" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testBaseName = "base1" - failBaseName = "base2" -) - -func TestStatusValid(t *testing.T) { - status := &commonpb.ActuatorStatus{ - IsMoving: true, - } - newStruct, err := protoutils.StructToStructPb(status) - test.That(t, err, test.ShouldBeNil) - test.That( - t, - newStruct.AsMap(), - test.ShouldResemble, - map[string]interface{}{ - "is_moving": true, - }, - ) - - convMap := &commonpb.ActuatorStatus{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(newStruct.AsMap()) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, status) -} - -func TestCreateStatus(t *testing.T) { - t.Run("is moving", func(t *testing.T) { - status := &commonpb.ActuatorStatus{ - IsMoving: true, - } - - injectBase := &inject.Base{} - injectBase.IsMovingFunc = func(context.Context) (bool, error) { - return true, nil - } - status1, err := base.CreateStatus(context.Background(), injectBase) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - - resourceAPI, ok, err := resource.LookupAPIRegistration[base.Base](base.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - status2, err := resourceAPI.Status(context.Background(), injectBase) - test.That(t, err, test.ShouldBeNil) - test.That(t, status2, test.ShouldResemble, status) - }) - - t.Run("is not moving", func(t *testing.T) { - status := &commonpb.ActuatorStatus{ - IsMoving: false, - } - - injectBase := &inject.Base{} - injectBase.IsMovingFunc = func(context.Context) (bool, error) { - return false, nil - } - status1, err := base.CreateStatus(context.Background(), injectBase) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - }) -} diff --git a/components/base/client.go b/components/base/client.go deleted file mode 100644 index edb6f05e0c4..00000000000 --- a/components/base/client.go +++ /dev/null @@ -1,166 +0,0 @@ -// Package base contains a gRPC based base client -package base - -import ( - "context" - - "github.com/golang/geo/r3" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/base/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// client implements BaseServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.BaseServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Base, error) { - c := pb.NewBaseServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - }, nil -} - -func (c *client) MoveStraight(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.MoveStraight(ctx, &pb.MoveStraightRequest{ - Name: c.name, - DistanceMm: int64(distanceMm), - MmPerSec: mmPerSec, - Extra: ext, - }) - if err != nil { - return err - } - return nil -} - -func (c *client) Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.Spin(ctx, &pb.SpinRequest{ - Name: c.name, - AngleDeg: angleDeg, - DegsPerSec: degsPerSec, - Extra: ext, - }) - if err != nil { - return err - } - return nil -} - -func (c *client) SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.SetPower(ctx, &pb.SetPowerRequest{ - Name: c.name, - Linear: &commonpb.Vector3{X: linear.X, Y: linear.Y, Z: linear.Z}, - Angular: &commonpb.Vector3{X: angular.X, Y: angular.Y, Z: angular.Z}, - Extra: ext, - }) - if err != nil { - return err - } - return nil -} - -func (c *client) SetVelocity(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.SetVelocity(ctx, &pb.SetVelocityRequest{ - Name: c.name, - Linear: &commonpb.Vector3{X: linear.X, Y: linear.Y, Z: linear.Z}, - Angular: &commonpb.Vector3{X: angular.X, Y: angular.Y, Z: angular.Z}, - Extra: ext, - }) - if err != nil { - return err - } - return nil -} - -func (c *client) Stop(ctx context.Context, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.Stop(ctx, &pb.StopRequest{Name: c.name, Extra: ext}) - if err != nil { - return err - } - return nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} - -func (c *client) IsMoving(ctx context.Context) (bool, error) { - resp, err := c.client.IsMoving(ctx, &pb.IsMovingRequest{Name: c.name}) - if err != nil { - return false, err - } - return resp.IsMoving, nil -} - -func (c *client) Properties(ctx context.Context, extra map[string]interface{}) (Properties, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return Properties{}, err - } - - req := &pb.GetPropertiesRequest{Name: c.name, Extra: ext} - resp, err := c.client.GetProperties(ctx, req) - if err != nil { - return Properties{}, err - } - return ProtoFeaturesToProperties(resp), nil -} - -func (c *client) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetGeometries(ctx, &commonpb.GetGeometriesRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - return spatialmath.NewGeometriesFromProto(resp.GetGeometries()) -} diff --git a/components/base/client_test.go b/components/base/client_test.go deleted file mode 100644 index 1c13604614f..00000000000 --- a/components/base/client_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package base_test - -import ( - "context" - "net" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/base" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func setupWorkingBase( - workingBase *inject.Base, - argsReceived map[string][]interface{}, - expectedFeatures base.Properties, - geometries []spatialmath.Geometry, -) { - workingBase.MoveStraightFunc = func( - _ context.Context, distanceMm int, - mmPerSec float64, - extra map[string]interface{}, - ) error { - argsReceived["MoveStraight"] = []interface{}{distanceMm, mmPerSec, extra} - return nil - } - - workingBase.SpinFunc = func( - _ context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}, - ) error { - argsReceived["Spin"] = []interface{}{angleDeg, degsPerSec, extra} - return nil - } - - workingBase.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - - workingBase.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return expectedFeatures, nil - } - - workingBase.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { - return geometries, nil - } -} - -func setupBrokenBase(brokenBase *inject.Base) { - brokenBase.MoveStraightFunc = func( - ctx context.Context, - distanceMm int, mmPerSec float64, - extra map[string]interface{}, - ) error { - return errMoveStraight - } - brokenBase.SpinFunc = func( - ctx context.Context, - angleDeg, degsPerSec float64, - extra map[string]interface{}, - ) error { - return errSpinFailed - } - brokenBase.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopFailed - } - - brokenBase.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{}, errPropertiesFailed - } -} - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - argsReceived := map[string][]interface{}{} - - workingBase := &inject.Base{} - expectedFeatures := base.Properties{ - TurningRadiusMeters: 1.2, - WidthMeters: float64(100) * 0.001, - } - expectedGeometries := []spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{1, 2, 3}, "")} - setupWorkingBase(workingBase, argsReceived, expectedFeatures, expectedGeometries) - - brokenBase := &inject.Base{} - setupBrokenBase(brokenBase) - - resMap := map[resource.Name]base.Base{ - base.Named(testBaseName): workingBase, - base.Named(failBaseName): brokenBase, - } - - baseSvc, err := resource.NewAPIResourceCollection(base.API, resMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[base.Base](base.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, baseSvc), test.ShouldBeNil) - - workingBase.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingBaseClient, err := base.NewClientFromConn(context.Background(), conn, "", base.Named(testBaseName), logger) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, workingBaseClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }() - - t.Run("working base client", func(t *testing.T) { - expectedExtra := map[string]interface{}{"foo": "bar"} - - t.Run("working MoveStraight", func(t *testing.T) { - distance := 42 - mmPerSec := 42.0 - err = workingBaseClient.MoveStraight( - context.Background(), - distance, - mmPerSec, - map[string]interface{}{"foo": "bar"}, - ) - test.That(t, err, test.ShouldBeNil) - expectedArgs := []interface{}{distance, mmPerSec, expectedExtra} - test.That(t, argsReceived["MoveStraight"], test.ShouldResemble, expectedArgs) - }) - - t.Run("working DoCommand", func(t *testing.T) { - resp, err := workingBaseClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - }) - - t.Run("working Spin", func(t *testing.T) { - angleDeg := 90.0 - degsPerSec := 30.0 - err = workingBaseClient.Spin( - context.Background(), - angleDeg, - degsPerSec, - map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - expectedArgs := []interface{}{angleDeg, degsPerSec, expectedExtra} - test.That(t, argsReceived["Spin"], test.ShouldResemble, expectedArgs) - }) - - t.Run("working Properties", func(t *testing.T) { - features, err := workingBaseClient.Properties(context.Background(), expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, features, test.ShouldResemble, expectedFeatures) - }) - - t.Run("working Stop", func(t *testing.T) { - err = workingBaseClient.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("working Geometries", func(t *testing.T) { - geometries, err := workingBaseClient.Geometries(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - for i, geometry := range geometries { - test.That(t, spatialmath.GeometriesAlmostEqual(geometry, expectedGeometries[i]), test.ShouldBeTrue) - } - }) - }) - - t.Run("working base client by dialing", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := resourceAPI.RPCClient(context.Background(), conn, "", base.Named(testBaseName), logger) - test.That(t, err, test.ShouldBeNil) - - degsPerSec := 42.0 - angleDeg := 30.0 - - err = client.Spin(context.Background(), angleDeg, degsPerSec, nil) - test.That(t, err, test.ShouldBeNil) - expectedArgs := []interface{}{angleDeg, degsPerSec, map[string]interface{}{}} - test.That(t, argsReceived["Spin"], test.ShouldResemble, expectedArgs) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("failing base client", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingBaseClient, err := base.NewClientFromConn(context.Background(), conn, "", base.Named(failBaseName), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - - err = failingBaseClient.MoveStraight(context.Background(), 42, 42.0, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, errMoveStraight.Error()) - - err = failingBaseClient.Spin(context.Background(), 42.0, 42.0, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, errSpinFailed.Error()) - - _, err = failingBaseClient.Properties(context.Background(), nil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPropertiesFailed.Error()) - - err = failingBaseClient.Stop(context.Background(), nil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopFailed.Error()) - - test.That(t, failingBaseClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/base/fake/fake.go b/components/base/fake/fake.go deleted file mode 100644 index 076f6f7f59c..00000000000 --- a/components/base/fake/fake.go +++ /dev/null @@ -1,108 +0,0 @@ -// Package fake implements a fake base. -package fake - -import ( - "context" - - "github.com/golang/geo/r3" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -func init() { - resource.RegisterComponent( - base.API, - resource.DefaultModelFamily.WithModel("fake"), - resource.Registration[base.Base, resource.NoNativeConfig]{Constructor: NewBase}, - ) -} - -const ( - defaultWidthMm = 600 - defaultMinimumTurningRadiusM = 0 - defaultWheelCircumferenceM = 3 -) - -// Base is a fake base that returns what it was provided in each method. -type Base struct { - resource.Named - resource.TriviallyReconfigurable - CloseCount int - WidthMeters float64 - TurningRadius float64 - WheelCircumferenceMeters float64 - Geometry []spatialmath.Geometry - logger logging.Logger -} - -// NewBase instantiates a new base of the fake model type. -func NewBase(_ context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger) (base.Base, error) { - b := &Base{ - Named: conf.ResourceName().AsNamed(), - Geometry: []spatialmath.Geometry{}, - logger: logger, - } - if conf.Frame != nil && conf.Frame.Geometry != nil { - geometry, err := conf.Frame.Geometry.ParseConfig() - if err != nil { - return nil, err - } - b.Geometry = []spatialmath.Geometry{geometry} - } - b.WidthMeters = defaultWidthMm * 0.001 - b.TurningRadius = defaultMinimumTurningRadiusM - return b, nil -} - -// MoveStraight does nothing. -func (b *Base) MoveStraight(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - return nil -} - -// Spin does nothing. -func (b *Base) Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - return nil -} - -// SetPower does nothing. -func (b *Base) SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return nil -} - -// SetVelocity does nothing. -func (b *Base) SetVelocity(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return nil -} - -// Stop does nothing. -func (b *Base) Stop(ctx context.Context, extra map[string]interface{}) error { - return nil -} - -// IsMoving always returns false. -func (b *Base) IsMoving(ctx context.Context) (bool, error) { - return false, nil -} - -// Close does nothing. -func (b *Base) Close(ctx context.Context) error { - b.CloseCount++ - return nil -} - -// Properties returns the base's properties. -func (b *Base) Properties(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{ - TurningRadiusMeters: b.TurningRadius, - WidthMeters: b.WidthMeters, - WheelCircumferenceMeters: b.WheelCircumferenceMeters, - }, nil -} - -// Geometries returns the geometries associated with the fake base. -func (b *Base) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - return b.Geometry, nil -} diff --git a/components/base/kinematicbase/differentialDrive.go b/components/base/kinematicbase/differentialDrive.go deleted file mode 100644 index 4e1180b2259..00000000000 --- a/components/base/kinematicbase/differentialDrive.go +++ /dev/null @@ -1,396 +0,0 @@ -//go:build !no_cgo - -// Package kinematicbase contains wrappers that augment bases with information needed for higher level -// control over the base -package kinematicbase - -import ( - "context" - "errors" - "math" - "sync" - "time" - - "github.com/golang/geo/r3" - utils "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan/ik" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" -) - -// The pause time when not using a localizer before moving on to next move step. -const ( - defaultNoLocalizerDelay = 250 * time.Millisecond - defaultCollisionBufferMM = 1e-8 -) - -var ( - // errMovementTimeout is used for when a movement call times out after no movement for some time. - errMovementTimeout = errors.New("movement has timed out") - // Input representation of origin. - originInputs = []referenceframe.Input{{Value: 0}, {Value: 0}, {Value: 0}} -) - -// wrapWithDifferentialDriveKinematics takes a wheeledBase component and adds a localizer to it -// It also adds kinematic model so that it can be controlled. -func wrapWithDifferentialDriveKinematics( - ctx context.Context, - b base.Base, - logger logging.Logger, - localizer motion.Localizer, - limits []referenceframe.Limit, - options Options, -) (KinematicBase, error) { - ddk := &differentialDriveKinematics{ - Base: b, - Localizer: localizer, - logger: logger, - options: options, - } - ddk.mutex.Lock() - defer ddk.mutex.Unlock() - - geometries, err := b.Geometries(ctx, nil) - if err != nil { - return nil, err - } - // RSDK-4131 will update this so it is no longer necessary - var geometry, boundingSphere spatialmath.Geometry - if len(geometries) > 1 { - ddk.logger.CWarn(ctx, "multiple geometries specified for differential drive kinematic base, only can use the first at this time") - } - if len(geometries) > 0 { - geometry = geometries[0] - } - if geometry != nil { - boundingSphere, err = spatialmath.BoundingSphere(geometry) - } - if boundingSphere == nil || err != nil { - logger.CWarn(ctx, "base %s not configured with a geometry, will be considered a 300mm sphere for collision detection purposes.") - boundingSphere, err = spatialmath.NewSphere(spatialmath.NewZeroPose(), 150., b.Name().ShortName()) - if err != nil { - return nil, err - } - } - - ddk.executionFrame, err = referenceframe.New2DMobileModelFrame(b.Name().ShortName(), limits, boundingSphere) - if err != nil { - return nil, err - } - - if options.PositionOnlyMode { - ddk.planningFrame, err = referenceframe.New2DMobileModelFrame(b.Name().ShortName(), limits[:2], boundingSphere) - if err != nil { - return nil, err - } - } else { - ddk.planningFrame = ddk.executionFrame - } - - ddk.noLocalizerCacheInputs = originInputs - return ddk, nil -} - -type differentialDriveKinematics struct { - base.Base - motion.Localizer - logger logging.Logger - planningFrame, executionFrame referenceframe.Model - options Options - noLocalizerCacheInputs []referenceframe.Input - currentTrajectory [][]referenceframe.Input - currentIdx int - mutex sync.RWMutex -} - -func (ddk *differentialDriveKinematics) Kinematics() referenceframe.Frame { - return ddk.planningFrame -} - -func (ddk *differentialDriveKinematics) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - // If no localizer is present, CurrentInputs returns the expected position of the robot assuming after - // each part of move command was completed accurately. - if ddk.Localizer == nil { - ddk.mutex.RLock() - defer ddk.mutex.RUnlock() - currentInputs := ddk.noLocalizerCacheInputs - - return currentInputs, nil - } - - // TODO(rb): make a transformation from the component reference to the base frame - pif, err := ddk.CurrentPosition(ctx) - if err != nil { - return nil, err - } - pt := pif.Pose().Point() - // We should not have a problem with Gimbal lock by looking at yaw in the domain that most bases will be moving. - // This could potentially be made more robust in the future, though. - theta := math.Mod(pif.Pose().Orientation().EulerAngles().Yaw, 2*math.Pi) - return []referenceframe.Input{{Value: pt.X}, {Value: pt.Y}, {Value: theta}}, nil -} - -func (ddk *differentialDriveKinematics) GoToInputs(ctx context.Context, desiredSteps ...[]referenceframe.Input) error { - ddk.mutex.Lock() - ddk.currentTrajectory = desiredSteps - ddk.mutex.Unlock() - for i, desired := range desiredSteps { - ddk.mutex.Lock() - ddk.currentIdx = i - ddk.mutex.Unlock() - err := ddk.goToInputs(ctx, desired) - if err != nil { - return err - } - } - return nil -} - -func (ddk *differentialDriveKinematics) goToInputs(ctx context.Context, desired []referenceframe.Input) error { - // create capsule which defines the valid region for a base to be when driving to desired waypoint - // deviationThreshold defines max distance base can be from path without error being thrown - var err error - current, inputsErr := ddk.CurrentInputs(ctx) - if inputsErr != nil { - return inputsErr - } - validRegion, capsuleErr := ddk.newValidRegionCapsule(current, desired) - if capsuleErr != nil { - return capsuleErr - } - movementErr := make(chan error, 1) - defer close(movementErr) - - cancelContext, cancel := context.WithCancel(ctx) - defer cancel() - - if ddk.Localizer == nil { - defer func() { - ddk.mutex.Lock() - defer ddk.mutex.Unlock() - ddk.noLocalizerCacheInputs = originInputs - }() - } - - utils.PanicCapturingGo(func() { - // this loop polls the error state and issues a corresponding command to move the base to the objective - // when the base is within the positional threshold of the goal, exit the loop - for err := cancelContext.Err(); err == nil; err = cancelContext.Err() { - utils.SelectContextOrWait(ctx, 10*time.Millisecond) - point := spatialmath.NewPoint(r3.Vector{X: current[0].Value, Y: current[1].Value}, "") - col, err := validRegion.CollidesWith(point, defaultCollisionBufferMM) - if err != nil { - movementErr <- err - return - } - if !col { - movementErr <- errors.New("base has deviated too far from path") - return - } - - // get to the x, y location first - note that from the base's perspective +y is forward - desiredHeading := math.Atan2(desired[1].Value-current[1].Value, desired[0].Value-current[0].Value) - commanded, err := ddk.issueCommand(cancelContext, current, []referenceframe.Input{desired[0], desired[1], {Value: desiredHeading}}) - if err != nil { - movementErr <- err - return - } - - if !commanded { - // no command to move to the x, y location was issued, correct the heading and then exit - // 2DOF model indicates position-only mode so heading doesn't need to be corrected, exit function - if len(ddk.planningFrame.DoF()) == 2 { - movementErr <- err - return - } - if commanded, err := ddk.issueCommand(cancelContext, current, []referenceframe.Input{current[0], current[1], desired[2]}); err == nil { - if !commanded { - movementErr <- nil - return - } - } else { - movementErr <- err - return - } - } - current, err = ddk.CurrentInputs(cancelContext) - if err != nil { - movementErr <- err - return - } - ddk.logger.CInfof(ctx, "current inputs: %v", current) - } - movementErr <- err - }) - - // watching for movement timeout - lastUpdate := time.Now() - var prevInputs []referenceframe.Input - - for { - utils.SelectContextOrWait(ctx, 100*time.Millisecond) - select { - case err := <-movementErr: - return err - default: - } - - currentInputs, err := ddk.CurrentInputs(ctx) - if err != nil { - cancel() - <-movementErr - return err - } - if prevInputs == nil { - prevInputs = currentInputs - } - positionChange := ik.L2InputMetric(&ik.Segment{ - StartConfiguration: prevInputs, - EndConfiguration: currentInputs, - }) - if positionChange > ddk.options.MinimumMovementThresholdMM { - lastUpdate = time.Now() - prevInputs = currentInputs - } else if time.Since(lastUpdate) > ddk.options.Timeout { - cancel() - <-movementErr - return errMovementTimeout - } - } -} - -// issueCommand issues a relevant command to move the base to the given desired inputs and returns the boolean describing -// if it issued a command successfully. If it is already at the location it will not need to issue another command and can therefore -// return a false. -func (ddk *differentialDriveKinematics) issueCommand(ctx context.Context, current, desired []referenceframe.Input) (bool, error) { - distErr, headingErr, err := ddk.inputDiff(current, desired) - if err != nil { - return false, err - } - ddk.logger.CDebugf(ctx, "distErr: %.2f\theadingErr %.2f", distErr, headingErr) - if distErr > ddk.options.GoalRadiusMM && math.Abs(headingErr) > ddk.options.HeadingThresholdDegrees { - // base is headed off course; spin to correct - err := ddk.Spin(ctx, math.Min(headingErr, ddk.options.MaxSpinAngleDeg), ddk.options.AngularVelocityDegsPerSec, nil) - - // Update the cached current inputs to the resultant position of the spin command when the localizer is nil - if ddk.Localizer == nil { - ddk.mutex.Lock() - defer ddk.mutex.Unlock() - ddk.noLocalizerCacheInputs = []referenceframe.Input{{Value: 0}, {Value: 0}, desired[2]} - time.Sleep(defaultNoLocalizerDelay) - } - return true, err - } else if distErr > ddk.options.GoalRadiusMM { - // base is pointed the correct direction but not there yet; forge onward - err := ddk.MoveStraight(ctx, int(math.Min(distErr, ddk.options.MaxMoveStraightMM)), ddk.options.LinearVelocityMMPerSec, nil) - - // Update the cached current inputs to the resultant position of the move straight command when the localizer is nil - if ddk.Localizer == nil { - ddk.mutex.Lock() - defer ddk.mutex.Unlock() - ddk.noLocalizerCacheInputs = desired - time.Sleep(defaultNoLocalizerDelay) - } - return true, err - } - return false, nil -} - -// create a function for the error state, which is defined as [positional error, heading error]. -func (ddk *differentialDriveKinematics) inputDiff(current, desired []referenceframe.Input) (float64, float64, error) { - // create a goal pose in the world frame - goal := spatialmath.NewPose( - r3.Vector{X: desired[0].Value, Y: desired[1].Value}, - &spatialmath.OrientationVector{OZ: 1, Theta: desired[2].Value}, - ) - - // transform the goal pose such that it is in the base frame - currentPose, err := ddk.executionFrame.Transform(current) - if err != nil { - return 0, 0, err - } - delta := spatialmath.PoseBetween(currentPose, goal) - - // calculate the error state - headingErr := math.Mod(delta.Orientation().OrientationVectorDegrees().Theta, 360) - positionErr := delta.Point().Norm() - return positionErr, headingErr, nil -} - -// newValidRegionCapsule returns a capsule which defines the valid regions for a base to be when moving to a waypoint. -// The valid region is all points that are deviationThreshold (mm) distance away from the line segment between the -// starting and ending waypoints. This capsule is used to detect whether a base leaves this region and has thus deviated -// too far from its path. -func (ddk *differentialDriveKinematics) newValidRegionCapsule(starting, desired []referenceframe.Input) (spatialmath.Geometry, error) { - pt := r3.Vector{X: (desired[0].Value + starting[0].Value) / 2, Y: (desired[1].Value + starting[1].Value) / 2} - positionErr, _, err := ddk.inputDiff(starting, []referenceframe.Input{desired[0], desired[1], {Value: 0}}) - if err != nil { - return nil, err - } - - desiredHeading := math.Atan2(starting[0].Value-desired[0].Value, starting[1].Value-desired[1].Value) - - // rotate such that y is forward direction to match the frame for movement of a base - // rotate around the z-axis such that the capsule points in the direction of the end waypoint - r, err := spatialmath.NewRotationMatrix([]float64{ - math.Cos(desiredHeading), -math.Sin(desiredHeading), 0, - 0, 0, -1, - math.Sin(desiredHeading), math.Cos(desiredHeading), 0, - }) - if err != nil { - return nil, err - } - - center := spatialmath.NewPose(pt, r) - capsule, err := spatialmath.NewCapsule( - center, - ddk.options.PlanDeviationThresholdMM, - 2*ddk.options.PlanDeviationThresholdMM+positionErr, - "") - if err != nil { - return nil, err - } - - return capsule, nil -} - -func (ddk *differentialDriveKinematics) ErrorState(ctx context.Context) (spatialmath.Pose, error) { - ddk.mutex.RLock() - waypoints := ddk.currentTrajectory - currentNode := ddk.currentIdx - ddk.mutex.RUnlock() - - // Get pose-in-frame of the base via its localizer. The offset between the localizer and its base should already be accounted for. - actualPIF, err := ddk.CurrentPosition(ctx) - if err != nil { - return nil, err - } - - var nominalPose spatialmath.Pose - - // Determine the nominal pose, that is, the pose where the robot ought be if it had followed the plan perfectly up until this point. - // This is done differently depending on what sort of frame we are working with. - if len(waypoints) < 2 { - return nil, errors.New("diff drive motion plan must have at least two waypoints") - } - nominalPose, err = ddk.planningFrame.Transform(waypoints[currentNode]) - if err != nil { - return nil, err - } - pastPose, err := ddk.planningFrame.Transform(waypoints[currentNode-1]) - if err != nil { - return nil, err - } - // diff drive bases don't have a notion of "distance along the trajectory between waypoints", so instead we compare to the - // nearest point on the straight line path. - nominalPoint := spatialmath.ClosestPointSegmentPoint(pastPose.Point(), nominalPose.Point(), actualPIF.Pose().Point()) - pointDiff := nominalPose.Point().Sub(pastPose.Point()) - desiredHeading := math.Atan2(pointDiff.Y, pointDiff.X) - nominalPose = spatialmath.NewPose(nominalPoint, &spatialmath.OrientationVector{OZ: 1, Theta: desiredHeading}) - - return spatialmath.PoseBetween(nominalPose, actualPIF.Pose()), nil -} diff --git a/components/base/kinematicbase/differentialDrive_test.go b/components/base/kinematicbase/differentialDrive_test.go deleted file mode 100644 index cf5f42e0488..00000000000 --- a/components/base/kinematicbase/differentialDrive_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package kinematicbase - -import ( - "context" - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/components/base" - fakebase "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/slam/fake" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -// Limits when localizer isn't present. -const testNilLocalizerMoveLimit = 10000. - -func testConfig() resource.Config { - return resource.Config{ - Name: "test", - API: base.API, - Frame: &referenceframe.LinkConfig{ - Geometry: &spatialmath.GeometryConfig{ - R: 5, - X: 8, - Y: 6, - L: 10, - TranslationOffset: r3.Vector{X: 3, Y: 4, Z: 0}, - Label: "ddk", - }, - }, - } -} - -func TestWrapWithDifferentialDriveKinematics(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - testCases := []struct { - geoType spatialmath.GeometryType - success bool - }{ - {spatialmath.SphereType, true}, - {spatialmath.BoxType, true}, - {spatialmath.CapsuleType, true}, - {spatialmath.UnknownType, true}, - {spatialmath.GeometryType("bad"), false}, - } - - expectedSphere, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "") - test.That(t, err, test.ShouldBeNil) - - for _, tc := range testCases { - t.Run(string(tc.geoType), func(t *testing.T) { - testCfg := testConfig() - testCfg.Frame.Geometry.Type = tc.geoType - ddk, err := buildTestDDK(ctx, testCfg, true, defaultLinearVelocityMMPerSec, defaultAngularVelocityDegsPerSec, logger) - test.That(t, err == nil, test.ShouldEqual, tc.success) - if err != nil { - return - } - limits := ddk.executionFrame.DoF() - test.That(t, limits[0].Min, test.ShouldBeLessThan, 0) - test.That(t, limits[1].Min, test.ShouldBeLessThan, 0) - test.That(t, limits[0].Max, test.ShouldBeGreaterThan, 0) - test.That(t, limits[1].Max, test.ShouldBeGreaterThan, 0) - geometry, err := ddk.executionFrame.(*referenceframe.SimpleModel).Geometries(make([]referenceframe.Input, len(limits))) - test.That(t, err, test.ShouldBeNil) - equivalent := spatialmath.GeometriesAlmostEqual( - geometry.GeometryByName(testCfg.Name+":"+testCfg.Frame.Geometry.Label), - expectedSphere, - ) - test.That(t, equivalent, test.ShouldBeTrue) - }) - } - - t.Run("Successful setting of velocities", func(t *testing.T) { - velocities := []struct { - linear float64 - angular float64 - }{ - {10.1, 20.2}, - {0, -1.5}, - {-1.9, 0}, - {-1, 2}, - {3, -4}, - } - for _, vels := range velocities { - ddk, err := buildTestDDK(ctx, testConfig(), true, vels.linear, vels.angular, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, ddk.options.LinearVelocityMMPerSec, test.ShouldAlmostEqual, vels.linear) - test.That(t, ddk.options.AngularVelocityDegsPerSec, test.ShouldAlmostEqual, vels.angular) - } - }) -} - -func TestCurrentInputs(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - t.Run("with Localizer", func(t *testing.T) { - ddk, err := buildTestDDK(ctx, testConfig(), true, - defaultLinearVelocityMMPerSec, defaultAngularVelocityDegsPerSec, logger) - test.That(t, err, test.ShouldBeNil) - for i := 0; i < 10; i++ { - _, err := ddk.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - } - }) - - t.Run("without Localizer", func(t *testing.T) { - ddk, err := buildTestDDK(ctx, testConfig(), false, - defaultLinearVelocityMMPerSec, defaultAngularVelocityDegsPerSec, logger) - test.That(t, err, test.ShouldBeNil) - for i := 0; i < 10; i++ { - input, err := ddk.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, input, test.ShouldResemble, []referenceframe.Input{{Value: 0}, {Value: 0}, {Value: 0}}) - } - }) -} - -func TestInputDiff(t *testing.T) { - ctx := context.Background() - - // make injected slam service - slam := inject.NewSLAMService("the slammer") - slam.PositionFunc = func(ctx context.Context) (spatialmath.Pose, string, error) { - return spatialmath.NewZeroPose(), "", nil - } - - // build base - logger := logging.NewTestLogger(t) - ddk, err := buildTestDDK(ctx, testConfig(), true, - defaultLinearVelocityMMPerSec, defaultAngularVelocityDegsPerSec, logger) - test.That(t, err, test.ShouldBeNil) - ddk.Localizer = motion.NewSLAMLocalizer(slam) - - desiredInput := []referenceframe.Input{{Value: 3}, {Value: 4}, {Value: utils.DegToRad(30)}} - distErr, headingErr, err := ddk.inputDiff(make([]referenceframe.Input, 3), desiredInput) - test.That(t, err, test.ShouldBeNil) - test.That(t, distErr, test.ShouldEqual, r3.Vector{X: desiredInput[0].Value, Y: desiredInput[1].Value, Z: 0}.Norm()) - test.That(t, headingErr, test.ShouldAlmostEqual, 30) -} - -func buildTestDDK( - ctx context.Context, - cfg resource.Config, - hasLocalizer bool, - linVel, angVel float64, - logger logging.Logger, -) (*differentialDriveKinematics, error) { - // make fake base - b, err := fakebase.NewBase(ctx, resource.Dependencies{}, cfg, logger) - if err != nil { - return nil, err - } - - // make a SLAM service and get its limits - var localizer motion.Localizer - var limits []referenceframe.Limit - if hasLocalizer { - fakeSLAM := fake.NewSLAM(slam.Named("test"), logger) - limits, err = fakeSLAM.Limits(ctx, true) - if err != nil { - return nil, err - } - localizer = motion.NewSLAMLocalizer(fakeSLAM) - } else { - limits = []referenceframe.Limit{ - {Min: testNilLocalizerMoveLimit, Max: testNilLocalizerMoveLimit}, - {Min: testNilLocalizerMoveLimit, Max: testNilLocalizerMoveLimit}, - } - } - limits = append(limits, referenceframe.Limit{Min: -2 * math.Pi, Max: 2 * math.Pi}) - - // construct differential drive kinematic base - options := NewKinematicBaseOptions() - options.LinearVelocityMMPerSec = linVel - options.AngularVelocityDegsPerSec = angVel - kb, err := wrapWithDifferentialDriveKinematics(ctx, b, logger, localizer, limits, options) - if err != nil { - return nil, err - } - ddk, ok := kb.(*differentialDriveKinematics) - if !ok { - return nil, err - } - return ddk, nil -} - -func TestNewValidRegionCapsule(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - ddk, err := buildTestDDK(ctx, testConfig(), true, defaultLinearVelocityMMPerSec, defaultAngularVelocityDegsPerSec, logger) - test.That(t, err, test.ShouldBeNil) - - starting := referenceframe.FloatsToInputs([]float64{400, 0, 0}) - desired := referenceframe.FloatsToInputs([]float64{0, 400, 0}) - c, err := ddk.newValidRegionCapsule(starting, desired) - test.That(t, err, test.ShouldBeNil) - - col, err := c.CollidesWith(spatialmath.NewPoint(r3.Vector{X: -176, Y: 576, Z: 0}, ""), defaultCollisionBufferMM) - test.That(t, err, test.ShouldBeNil) - test.That(t, col, test.ShouldBeTrue) - - col, err = c.CollidesWith(spatialmath.NewPoint( - r3.Vector{X: -defaultPlanDeviationThresholdMM, Y: -defaultPlanDeviationThresholdMM, Z: 0}, - "", - ), defaultCollisionBufferMM) - test.That(t, err, test.ShouldBeNil) - test.That(t, col, test.ShouldBeFalse) -} diff --git a/components/base/kinematicbase/execution.go b/components/base/kinematicbase/execution.go deleted file mode 100644 index 42a468a3837..00000000000 --- a/components/base/kinematicbase/execution.go +++ /dev/null @@ -1,503 +0,0 @@ -// Package kinematicbase contains wrappers that augment bases with information needed for higher level -// control over the base -package kinematicbase - -import ( - "context" - "errors" - "fmt" - "math" - "time" - - "github.com/golang/geo/r3" - "go.uber.org/multierr" - utils "go.viam.com/utils" - - "go.viam.com/rdk/motionplan/ik" - "go.viam.com/rdk/motionplan/tpspace" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" -) - -const ( - updateStepSeconds = 0.35 // Update CurrentInputs and check deviation every this many seconds. - lookaheadDistMult = 2. // Look ahead distance for path correction will be this times the turning radius - goalsToAttempt = 10 // Divide the lookahead distance into this many discrete goals to attempt to correct towards. - - // Before post-processing trajectory will have velocities every this many mm (or degs if spinning in place). - stepDistResolution = 1. - - // Used to determine minimum linear deviation allowed before correction attempt. Determined by multiplying max linear speed by - // inputUpdateStepSeconds, and will correct if deviation is larger than this percent of that amount. - minDeviationToCorrectPct = 50. - microsecondsPerSecond = 1e6 -) - -type arcStep struct { - linVelMMps r3.Vector - angVelDegps r3.Vector - durationSeconds float64 - - // arcSegment.StartPosition is the pose at dist=0 for the PTG these traj nodes are derived from, such that - // Compose(arcSegment.StartPosition, subTraj[n].Pose) is the expected pose at that node. - // A single trajectory may be broken into multiple arcSteps, so we need to be able to track the total distance elapsed through - // the trajectory. - arcSegment ik.Segment - - subTraj []*tpspace.TrajNode -} - -func (step *arcStep) String() string { - return fmt.Sprintf("Step: lin velocity: %f,\n\t ang velocity: %f,\n\t duration: %f s,\n\t arcSegment %s,\n\t arc start pose %v", - step.linVelMMps, - step.angVelDegps, - step.durationSeconds, - step.arcSegment.String(), - spatialmath.PoseToProtobuf(step.arcSegment.StartPosition), - ) -} - -type courseCorrectionGoal struct { - Goal spatialmath.Pose - Solution []referenceframe.Input - stepIdx int - trajIdx int -} - -func (ptgk *ptgBaseKinematics) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - var err error - // Cancel any prior GoToInputs calls - if ptgk.cancelFunc != nil { - ptgk.cancelFunc() - } - ctx, cancelFunc := context.WithCancel(ctx) - ptgk.cancelFunc = cancelFunc - - defer func() { - ptgk.inputLock.Lock() - ptgk.currentInputs = zeroInput - ptgk.inputLock.Unlock() - }() - - tryStop := func(errToWrap error) error { - stopCtx, cancelFn := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFn() - return multierr.Combine(errToWrap, ptgk.Base.Stop(stopCtx, nil)) - } - - startPose := spatialmath.NewZeroPose() // This is the location of the base at call time - if ptgk.Localizer != nil { - startPoseInFrame, err := ptgk.Localizer.CurrentPosition(ctx) - if err != nil { - return tryStop(err) - } - startPose = startPoseInFrame.Pose() - } - - // Pre-process all steps into a series of velocities - ptgk.inputLock.Lock() - ptgk.currentExecutingSteps, err = ptgk.arcStepsFromInputs(inputSteps, startPose) - arcSteps := ptgk.currentExecutingSteps - ptgk.inputLock.Unlock() - if err != nil { - return tryStop(err) - } - - for i := 0; i < len(arcSteps); i++ { - if ctx.Err() != nil { - return ctx.Err() - } - step := arcSteps[i] - ptgk.inputLock.Lock() // In the case where there's actual contention here, this could cause timing issues; how to solve? - ptgk.currentIdx = i - ptgk.currentInputs = step.arcSegment.StartConfiguration - ptgk.inputLock.Unlock() - - ptgk.logger.Debugf("step, i %d \n %s", i, step.String()) - - err := ptgk.Base.SetVelocity( - ctx, - step.linVelMMps, - step.angVelDegps, - nil, - ) - if err != nil { - return tryStop(err) - } - arcStartTime := time.Now() - // Now we are moving. We need to do several things simultaneously: - // - move until we think we have finished the arc, then move on to the next step - // - update our CurrentInputs tracking where we are through the arc - // - Check where we are relative to where we think we are, and tweak velocities accordingly - for timeElapsedSeconds := updateStepSeconds; timeElapsedSeconds <= step.durationSeconds; timeElapsedSeconds += updateStepSeconds { - if ctx.Err() != nil { - return ctx.Err() - } - // Account for 1) timeElapsedSeconds being inputUpdateStepSeconds ahead of actual elapsed time, and the fact that the loop takes - // nonzero time to run especially when using the localizer. - actualTimeElapsed := time.Since(arcStartTime) - // Time durations are ints, not floats. 0.9 * time.Second is zero. Thus we use microseconds for math. - remainingTimeStep := time.Duration(microsecondsPerSecond*timeElapsedSeconds)*time.Microsecond - actualTimeElapsed - - if remainingTimeStep > 0 { - utils.SelectContextOrWait(ctx, remainingTimeStep) - if ctx.Err() != nil { - return tryStop(ctx.Err()) - } - } - distIncVel := step.linVelMMps.Y - if distIncVel == 0 { - distIncVel = step.angVelDegps.Z - } - currentInputs := []referenceframe.Input{ - step.arcSegment.StartConfiguration[ptgIndex], - step.arcSegment.StartConfiguration[trajectoryAlphaWithinPTG], - {step.arcSegment.StartConfiguration[startDistanceAlongTrajectoryIndex].Value + math.Abs(distIncVel)*timeElapsedSeconds}, - step.arcSegment.StartConfiguration[endDistanceAlongTrajectoryIndex], - } - ptgk.inputLock.Lock() - ptgk.currentInputs = currentInputs - ptgk.inputLock.Unlock() - - // If we have a localizer, we are able to attempt to correct to stay on the path. - // For now we do not try to correct while in a correction. - if ptgk.Localizer != nil { - newArcSteps, err := ptgk.courseCorrect(ctx, currentInputs, arcSteps, i) - if err != nil { - // If this (or anywhere else in this closure) has an error, the only consequence is that we are unable to solve a - // valid course correction trajectory. We are still continuing to follow the plan, so if we ignore this error, we - // will either try to course correct again and succeed or fail, or else the motion service will replan due to - // either position or obstacles. - ptgk.logger.Debugf("encountered an error while course correcting: %v", err) - } - if newArcSteps != nil { - // newArcSteps will be nil if there is no course correction needed - ptgk.inputLock.Lock() - ptgk.currentExecutingSteps = newArcSteps - ptgk.inputLock.Unlock() - arcSteps = newArcSteps - break - } - } - } - } - return tryStop(nil) -} - -func (ptgk *ptgBaseKinematics) arcStepsFromInputs(inputSteps [][]referenceframe.Input, startPose spatialmath.Pose) ([]arcStep, error) { - var arcSteps []arcStep - runningPose := startPose - for _, inputs := range inputSteps { - trajArcSteps, err := ptgk.trajectoryArcSteps(runningPose, inputs) - if err != nil { - return nil, err - } - - arcSteps = append(arcSteps, trajArcSteps...) - runningPose = trajArcSteps[len(trajArcSteps)-1].arcSegment.EndPosition - } - return arcSteps, nil -} - -// trajectoryArcSteps takes a set of inputs and breaks the trajectory apart into steps of velocities. It returns the list of -// steps to execute, including the timing of how long to maintain each velocity, and the expected starting and ending positions. -func (ptgk *ptgBaseKinematics) trajectoryArcSteps( - startPose spatialmath.Pose, - inputs []referenceframe.Input, -) ([]arcStep, error) { - selectedPTG := int(math.Round(inputs[ptgIndex].Value)) - - traj, err := ptgk.ptgs[selectedPTG].Trajectory( - inputs[trajectoryAlphaWithinPTG].Value, - inputs[startDistanceAlongTrajectoryIndex].Value, - inputs[endDistanceAlongTrajectoryIndex].Value, - stepDistResolution, - ) - if err != nil { - return nil, err - } - - finalSteps := []arcStep{} - timeStep := 0. - curDist := 0. - curInputs := []referenceframe.Input{ - inputs[ptgIndex], - inputs[trajectoryAlphaWithinPTG], - {curDist}, - inputs[endDistanceAlongTrajectoryIndex], - } - runningPose := startPose - segment := ik.Segment{ - StartConfiguration: curInputs, - StartPosition: runningPose, - Frame: ptgk.Kinematics(), - } - // Trajectory distance is either length in mm, or if linear distance is not increasing, number of degrees to rotate in place. - lastLinVel := r3.Vector{0, traj[0].LinVel * ptgk.linVelocityMMPerSecond, 0} - lastAngVel := r3.Vector{0, 0, traj[0].AngVel * ptgk.angVelocityDegsPerSecond} - nextStep := arcStep{ - linVelMMps: lastLinVel, - angVelDegps: lastAngVel, - arcSegment: segment, - durationSeconds: 0., - } - for _, trajPt := range traj { - nextStep.subTraj = append(nextStep.subTraj, trajPt) - nextLinVel := r3.Vector{0, trajPt.LinVel * ptgk.linVelocityMMPerSecond, 0} - nextAngVel := r3.Vector{0, 0, trajPt.AngVel * ptgk.angVelocityDegsPerSecond} - // Check if this traj node has different velocities from the last one. If so, end our segment and start a new segment. - if nextStep.linVelMMps.Sub(nextLinVel).Norm2() > 1e-6 || nextStep.angVelDegps.Sub(nextAngVel).Norm2() > 1e-6 { - // Changed velocity, make a new step - nextStep.durationSeconds = timeStep - - nextStep.arcSegment.StartConfiguration[endDistanceAlongTrajectoryIndex].Value = curDist - nextStep.arcSegment.EndConfiguration = nextStep.arcSegment.StartConfiguration - arcPose, err := ptgk.Kinematics().Transform(nextStep.arcSegment.StartConfiguration) - if err != nil { - return nil, err - } - runningPose = spatialmath.Compose(runningPose, arcPose) - - nextStep.arcSegment.EndPosition = runningPose - finalSteps = append(finalSteps, nextStep) - - curInputs = []referenceframe.Input{ - inputs[ptgIndex], - inputs[trajectoryAlphaWithinPTG], - {curDist}, - inputs[endDistanceAlongTrajectoryIndex], - } - segment = ik.Segment{ - StartConfiguration: curInputs, - StartPosition: runningPose, - Frame: ptgk.Kinematics(), - } - nextStep = arcStep{ - linVelMMps: nextLinVel, - angVelDegps: nextAngVel, - arcSegment: segment, - durationSeconds: 0, - } - timeStep = 0. - } - distIncrement := trajPt.Dist - curDist - curDist += distIncrement - if nextStep.linVelMMps.Y != 0 { - timeStep += distIncrement / (math.Abs(nextStep.linVelMMps.Y)) - } else if nextStep.angVelDegps.Z != 0 { - timeStep += distIncrement / (math.Abs(nextStep.angVelDegps.Z)) - } - } - nextStep.durationSeconds = timeStep - nextStep.arcSegment.EndConfiguration = nextStep.arcSegment.StartConfiguration - arcPose, err := ptgk.Kinematics().Transform(nextStep.arcSegment.StartConfiguration) - if err != nil { - return nil, err - } - runningPose = spatialmath.Compose(runningPose, arcPose) - nextStep.arcSegment.EndConfiguration = curInputs - nextStep.arcSegment.EndPosition = runningPose - finalSteps = append(finalSteps, nextStep) - - return finalSteps, nil -} - -// courseCorrect will check whether the base is sufficiently off-course, and if so, attempt to calculate a set of corrective arcs to arrive -// back at a point along the planned path. If successful will return the new set of steps to execute to reflect the correction. -func (ptgk *ptgBaseKinematics) courseCorrect( - ctx context.Context, - currentInputs []referenceframe.Input, - arcSteps []arcStep, - arcIdx int, -) ([]arcStep, error) { - actualPose, err := ptgk.Localizer.CurrentPosition(ctx) - if err != nil { - return nil, err - } - // Current distance traveled on PTG is currentInputs[start dist]. Since the step may not have started at 0, we construct the query - // to get the pose travelled along the arc. - execInputs := []referenceframe.Input{ - currentInputs[ptgIndex], - currentInputs[trajectoryAlphaWithinPTG], - arcSteps[arcIdx].arcSegment.StartConfiguration[startDistanceAlongTrajectoryIndex], - currentInputs[startDistanceAlongTrajectoryIndex], - } - trajPose, err := ptgk.frame.Transform(execInputs) - if err != nil { - return nil, err - } - - // This is where we expected to be on the trajectory. - expectedPose := spatialmath.Compose(arcSteps[arcIdx].arcSegment.StartPosition, trajPose) - - // This is where actually are on the trajectory - poseDiff := spatialmath.PoseBetween(actualPose.Pose(), expectedPose) - - allowableDiff := ptgk.linVelocityMMPerSecond * updateStepSeconds * (minDeviationToCorrectPct / 100) - ptgk.logger.Debug( - "allowable diff ", allowableDiff, " diff now ", poseDiff.Point().Norm(), " angle diff ", poseDiff.Orientation().AxisAngles().Theta, - ) - - if poseDiff.Point().Norm() > allowableDiff || poseDiff.Orientation().AxisAngles().Theta > 0.25 { - ptgk.logger.Debug("expected to be at ", spatialmath.PoseToProtobuf(expectedPose)) - ptgk.logger.Debug("Localizer says at ", spatialmath.PoseToProtobuf(actualPose.Pose())) - // Accumulate list of points along the path to try to connect to - goals := ptgk.makeCourseCorrectionGoals( - goalsToAttempt, - arcIdx, - actualPose.Pose(), - arcSteps, - currentInputs, - ) - - ptgk.logger.Debug("wanted to attempt ", goalsToAttempt, " goals, got ", len(goals)) - // Attempt to solve from `actualPose` to each of those points - solution, err := ptgk.getCorrectionSolution(ctx, goals) - if err != nil { - return nil, err - } - if solution.Solution != nil { - ptgk.logger.Debug("successful course correction", solution.Solution) - - correctiveArcSteps := []arcStep{} - actualPoseTracked := actualPose.Pose() - for i := 0; i < len(solution.Solution); i += 2 { - // We've got a course correction solution. Swap out the relevant arcsteps. - newArcSteps, err := ptgk.trajectoryArcSteps( - actualPoseTracked, - []referenceframe.Input{{float64(ptgk.courseCorrectionIdx)}, solution.Solution[i], {0}, solution.Solution[i+1]}, - ) - if err != nil { - return nil, err - } - for _, newArcStep := range newArcSteps { - actualPoseTracked = spatialmath.Compose( - actualPoseTracked, - spatialmath.PoseBetween(newArcStep.arcSegment.StartPosition, newArcStep.arcSegment.EndPosition), - ) - } - correctiveArcSteps = append(correctiveArcSteps, newArcSteps...) - } - - // Update the connection point - connectionPoint := arcSteps[solution.stepIdx] - - // Use distances to calculate the % completion of the arc, used to update the time remaining. - // We can't use step.durationSeconds because we might connect to a different arc than we're currently in. - pctTrajRemaining := (connectionPoint.subTraj[len(connectionPoint.subTraj)-1].Dist - - connectionPoint.subTraj[solution.trajIdx].Dist) / - (connectionPoint.subTraj[len(connectionPoint.subTraj)-1].Dist - connectionPoint.arcSegment.StartConfiguration[2].Value) - - connectionPoint.arcSegment.StartConfiguration[2].Value = connectionPoint.subTraj[solution.trajIdx].Dist - connectionPoint.durationSeconds *= pctTrajRemaining - connectionPoint.subTraj = connectionPoint.subTraj[solution.trajIdx:] - - // Start with the already-executed steps. - // We need to include the i-th step because we're about to increment i and want to start with the correction, then - // continue with the connection point. - var newArcSteps []arcStep - newArcSteps = append(newArcSteps, arcSteps[:arcIdx+1]...) - newArcSteps = append(newArcSteps, correctiveArcSteps...) - newArcSteps = append(newArcSteps, connectionPoint) - if solution.stepIdx < len(arcSteps)-1 { - newArcSteps = append(newArcSteps, arcSteps[solution.stepIdx+1:]...) - } - return newArcSteps, nil - } - return nil, errors.New("failed to find valid course correction") - } - return nil, nil -} - -func (ptgk *ptgBaseKinematics) getCorrectionSolution(ctx context.Context, goals []courseCorrectionGoal) (courseCorrectionGoal, error) { - for _, goal := range goals { - solveMetric := ik.NewSquaredNormMetric(goal.Goal) - solutionChan := make(chan *ik.Solution, 1) - ptgk.logger.Debug("attempting goal ", spatialmath.PoseToProtobuf(goal.Goal)) - seed := []referenceframe.Input{{math.Pi / 2}, {ptgk.linVelocityMMPerSecond / 2}, {math.Pi / 2}, {ptgk.linVelocityMMPerSecond / 2}} - if goal.Goal.Point().X > 0 { - seed[0].Value *= -1 - } else { - seed[2].Value *= -1 - } - // Attempt to use our course correction solver to solve for a new set of trajectories which will get us from our current position - // to our goal point along our original trajectory. - err := ptgk.ptgs[ptgk.courseCorrectionIdx].Solve( - ctx, - solutionChan, - seed, - solveMetric, - 0, - ) - if err != nil { - return courseCorrectionGoal{}, err - } - var solution *ik.Solution - select { - case solution = <-solutionChan: - default: - } - ptgk.logger.Debug("solution ", solution) - if solution.Score < 100. { - goal.Solution = solution.Configuration - return goal, nil - } - } - return courseCorrectionGoal{}, nil -} - -// This function will select `nGoals` poses in the future from the current position, rectifying them to be relatice to `currPose`. -// It will create `courseCorrectionGoal` structs for each. The goals will be approximately evenly spaced. -func (ptgk *ptgBaseKinematics) makeCourseCorrectionGoals( - nGoals, currStep int, - currPose spatialmath.Pose, - steps []arcStep, - currentInputs []referenceframe.Input, -) []courseCorrectionGoal { - goals := []courseCorrectionGoal{} - currDist := currentInputs[startDistanceAlongTrajectoryIndex].Value - stepsPerGoal := int((ptgk.nonzeroBaseTurningRadiusMeters*lookaheadDistMult*1000)/stepDistResolution) / nGoals - - if stepsPerGoal < 1 { - return []courseCorrectionGoal{} - } - - startingTrajPt := 0 - for i := 0; i < len(steps[currStep].subTraj); i++ { - if steps[currStep].subTraj[i].Dist >= currDist { - startingTrajPt = i - break - } - } - - totalTrajSteps := 0 - for i := currStep; i < len(steps); i++ { - totalTrajSteps += len(steps[i].subTraj) - } - totalTrajSteps -= startingTrajPt - // If we have fewer steps left than needed to fill our goal list, shrink the spacing of goals - if stepsPerGoal*nGoals > totalTrajSteps { - stepsPerGoal = totalTrajSteps / nGoals // int division is what we want here - } - - stepsRemainingThisGoal := stepsPerGoal - for i := currStep; i < len(steps); i++ { - for len(steps[i].subTraj)-startingTrajPt > stepsRemainingThisGoal { - goalTrajPtIdx := startingTrajPt + stepsRemainingThisGoal - goalPose := spatialmath.PoseBetween( - currPose, - spatialmath.Compose(steps[i].arcSegment.StartPosition, steps[i].subTraj[goalTrajPtIdx].Pose), - ) - goals = append(goals, courseCorrectionGoal{Goal: goalPose, stepIdx: i, trajIdx: goalTrajPtIdx}) - if len(goals) == nGoals { - return goals - } - - startingTrajPt = goalTrajPtIdx - stepsRemainingThisGoal = stepsPerGoal - } - stepsRemainingThisGoal -= len(steps[i].subTraj) - startingTrajPt - startingTrajPt = 0 - } - return goals -} diff --git a/components/base/kinematicbase/fake_kinematics.go b/components/base/kinematicbase/fake_kinematics.go deleted file mode 100644 index 6f3b8840941..00000000000 --- a/components/base/kinematicbase/fake_kinematics.go +++ /dev/null @@ -1,300 +0,0 @@ -//go:build !no_cgo - -package kinematicbase - -import ( - "context" - "errors" - "sync" - "time" - - "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/motionplan/tpspace" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -type fakeDiffDriveKinematics struct { - *fake.Base - parentFrame string - planningFrame, executionFrame referenceframe.Frame - inputs []referenceframe.Input - options Options - sensorNoise spatialmath.Pose - lock sync.RWMutex -} - -// WrapWithFakeDiffDriveKinematics creates a DiffDrive KinematicBase from the fake Base so that it satisfies the ModelFramer and -// InputEnabled interfaces. -func WrapWithFakeDiffDriveKinematics( - ctx context.Context, - b *fake.Base, - localizer motion.Localizer, - limits []referenceframe.Limit, - options Options, - sensorNoise spatialmath.Pose, -) (KinematicBase, error) { - position, err := localizer.CurrentPosition(ctx) - if err != nil { - return nil, err - } - pt := position.Pose().Point() - if sensorNoise == nil { - sensorNoise = spatialmath.NewZeroPose() - } - fk := &fakeDiffDriveKinematics{ - Base: b, - parentFrame: position.Parent(), - inputs: referenceframe.FloatsToInputs([]float64{pt.X, pt.Y}), - sensorNoise: sensorNoise, - } - var geometry spatialmath.Geometry - if len(fk.Base.Geometry) != 0 { - geometry = fk.Base.Geometry[0] - } - - fk.executionFrame, err = referenceframe.New2DMobileModelFrame(b.Name().ShortName(), limits, geometry) - if err != nil { - return nil, err - } - - if options.PositionOnlyMode { - fk.planningFrame, err = referenceframe.New2DMobileModelFrame(b.Name().ShortName(), limits[:2], geometry) - if err != nil { - return nil, err - } - } else { - fk.planningFrame = fk.executionFrame - } - - fk.options = options - return fk, nil -} - -func (fk *fakeDiffDriveKinematics) Kinematics() referenceframe.Frame { - return fk.planningFrame -} - -func (fk *fakeDiffDriveKinematics) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - fk.lock.RLock() - defer fk.lock.RUnlock() - return fk.inputs, nil -} - -func (fk *fakeDiffDriveKinematics) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - for _, inputs := range inputSteps { - _, err := fk.planningFrame.Transform(inputs) - if err != nil { - return err - } - fk.lock.Lock() - fk.inputs = inputs - fk.lock.Unlock() - - // Sleep for a short amount to time to simulate a base taking some amount of time to reach the inputs - time.Sleep(150 * time.Millisecond) - } - return nil -} - -func (fk *fakeDiffDriveKinematics) ErrorState(ctx context.Context) (spatialmath.Pose, error) { - return fk.sensorNoise, nil -} - -func (fk *fakeDiffDriveKinematics) CurrentPosition(ctx context.Context) (*referenceframe.PoseInFrame, error) { - fk.lock.RLock() - inputs := fk.inputs - fk.lock.RUnlock() - currentPose, err := fk.planningFrame.Transform(inputs) - if err != nil { - return nil, err - } - return referenceframe.NewPoseInFrame(fk.parentFrame, spatialmath.Compose(currentPose, fk.sensorNoise)), nil -} - -type fakePTGKinematics struct { - *fake.Base - localizer motion.Localizer - frame referenceframe.Frame - options Options - sensorNoise spatialmath.Pose - ptgs []tpspace.PTGSolver - currentInput []referenceframe.Input - origin *referenceframe.PoseInFrame - positionlock sync.RWMutex - inputLock sync.RWMutex - logger logging.Logger - sleepTime int -} - -// WrapWithFakePTGKinematics creates a PTG KinematicBase from the fake Base so that it satisfies the ModelFramer and InputEnabled -// interfaces. -func WrapWithFakePTGKinematics( - ctx context.Context, - b *fake.Base, - logger logging.Logger, - origin *referenceframe.PoseInFrame, - options Options, - sensorNoise spatialmath.Pose, - sleepTime int, -) (KinematicBase, error) { - properties, err := b.Properties(ctx, nil) - if err != nil { - return nil, err - } - - baseMillimetersPerSecond := options.LinearVelocityMMPerSec - if baseMillimetersPerSecond == 0 { - baseMillimetersPerSecond = defaultLinearVelocityMMPerSec - } - - baseTurningRadiusMeters := properties.TurningRadiusMeters - if baseTurningRadiusMeters < 0 { - return nil, errors.New("can only wrap with PTG kinematics if turning radius is greater than or equal to zero") - } - - angVelocityDegsPerSecond, err := correctAngularVelocityWithTurnRadius( - logger, - baseTurningRadiusMeters, - baseMillimetersPerSecond, - options.AngularVelocityDegsPerSec, - ) - if err != nil { - return nil, err - } - - geometries, err := b.Geometries(ctx, nil) - if err != nil { - return nil, err - } - - nonzeroBaseTurningRadiusMeters := (baseMillimetersPerSecond / rdkutils.DegToRad(angVelocityDegsPerSecond)) / 1000. - - frame, err := tpspace.NewPTGFrameFromKinematicOptions( - b.Name().ShortName(), - logger, - nonzeroBaseTurningRadiusMeters, - 0, // If zero, will use default on the receiver end. - geometries, - options.NoSkidSteer, - baseTurningRadiusMeters == 0, - ) - if err != nil { - return nil, err - } - - if sensorNoise == nil { - sensorNoise = spatialmath.NewZeroPose() - } - - ptgProv, ok := frame.(tpspace.PTGProvider) - if !ok { - return nil, errors.New("unable to cast ptgk frame to a PTG Provider") - } - ptgs := ptgProv.PTGSolvers() - - fk := &fakePTGKinematics{ - Base: b, - frame: frame, - origin: origin, - ptgs: ptgs, - currentInput: zeroInput, - sensorNoise: sensorNoise, - logger: logger, - sleepTime: sleepTime, - } - initLocalizer := &fakePTGKinematicsLocalizer{fk} - fk.localizer = motion.TwoDLocalizer(initLocalizer) - - fk.options = options - return fk, nil -} - -func (fk *fakePTGKinematics) Kinematics() referenceframe.Frame { - return fk.frame -} - -func (fk *fakePTGKinematics) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - fk.inputLock.RLock() - defer fk.inputLock.RUnlock() - return fk.currentInput, nil -} - -func (fk *fakePTGKinematics) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - defer func() { - fk.inputLock.Lock() - fk.currentInput = zeroInput - fk.inputLock.Unlock() - }() - - for _, inputs := range inputSteps { - fk.positionlock.RLock() - startingPose := fk.origin - fk.positionlock.RUnlock() - - fk.inputLock.Lock() - fk.currentInput = []referenceframe.Input{inputs[0], inputs[1], {Value: 0}} - fk.inputLock.Unlock() - - finalPose, err := fk.frame.Transform(inputs) - if err != nil { - return err - } - - steps := motionplan.PathStepCount(spatialmath.NewZeroPose(), finalPose, 2) - startCfg := referenceframe.FloatsToInputs([]float64{inputs[0].Value, inputs[1].Value, 0}) - var interpolatedConfigurations [][]referenceframe.Input - for i := 0; i <= steps; i++ { - interp := float64(i) / float64(steps) - interpConfig, err := fk.frame.Interpolate(startCfg, inputs, interp) - if err != nil { - return err - } - interpolatedConfigurations = append(interpolatedConfigurations, interpConfig) - } - for _, inter := range interpolatedConfigurations { - if ctx.Err() != nil { - return ctx.Err() - } - relativePose, err := fk.frame.Transform(inter) - if err != nil { - return err - } - newPose := spatialmath.Compose(startingPose.Pose(), relativePose) - - fk.positionlock.Lock() - fk.origin = referenceframe.NewPoseInFrame(fk.origin.Parent(), newPose) - fk.positionlock.Unlock() - - fk.inputLock.Lock() - fk.currentInput = []referenceframe.Input{inputs[0], inputs[1], inter[2]} - fk.inputLock.Unlock() - - time.Sleep(time.Duration(fk.sleepTime) * time.Microsecond * 10) - } - } - return nil -} - -func (fk *fakePTGKinematics) ErrorState(ctx context.Context) (spatialmath.Pose, error) { - return fk.sensorNoise, nil -} - -func (fk *fakePTGKinematics) CurrentPosition(ctx context.Context) (*referenceframe.PoseInFrame, error) { - return fk.localizer.CurrentPosition(ctx) -} - -type fakePTGKinematicsLocalizer struct { - fk *fakePTGKinematics -} - -func (fkl *fakePTGKinematicsLocalizer) CurrentPosition(ctx context.Context) (*referenceframe.PoseInFrame, error) { - fkl.fk.positionlock.RLock() - defer fkl.fk.positionlock.RUnlock() - origin := fkl.fk.origin - return referenceframe.NewPoseInFrame(origin.Parent(), spatialmath.Compose(origin.Pose(), fkl.fk.sensorNoise)), nil -} diff --git a/components/base/kinematicbase/fake_kinematics_test.go b/components/base/kinematicbase/fake_kinematics_test.go deleted file mode 100644 index ac15be9befd..00000000000 --- a/components/base/kinematicbase/fake_kinematics_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package kinematicbase - -import ( - "context" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - fakebase "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -func TestNewFakeDiffDriveKinematics(t *testing.T) { - conf := resource.Config{ - Name: "test", - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - Geometry: &spatialmath.GeometryConfig{ - R: 10, - }, - }, - } - - ctx := context.Background() - logger := logging.NewTestLogger(t) - b, err := fakebase.NewBase(ctx, resource.Dependencies{}, conf, logger) - test.That(t, err, test.ShouldBeNil) - ms := inject.NewMovementSensor("test") - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(0, 0), 0, nil - } - ms.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil - } - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{CompassHeadingSupported: true}, nil - } - localizer := motion.NewMovementSensorLocalizer(ms, geo.NewPoint(0, 0), spatialmath.NewZeroPose()) - limits := []referenceframe.Limit{{Min: -100, Max: 100}, {Min: -100, Max: 100}} - - options := NewKinematicBaseOptions() - options.PositionOnlyMode = false - noise := spatialmath.NewPoseFromPoint(r3.Vector{1, 0, 0}) - kb, err := WrapWithFakeDiffDriveKinematics(ctx, b.(*fakebase.Base), localizer, limits, options, noise) - test.That(t, err, test.ShouldBeNil) - expected := referenceframe.FloatsToInputs([]float64{10, 11}) - test.That(t, kb.GoToInputs(ctx, expected), test.ShouldBeNil) - inputs, err := kb.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, inputs, test.ShouldResemble, expected) - pose, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincident(pose.Pose(), spatialmath.NewPoseFromPoint(r3.Vector{11, 11, 0})), test.ShouldBeTrue) -} - -func TestNewFakePTGKinematics(t *testing.T) { - conf := resource.Config{ - Name: "test", - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - Geometry: &spatialmath.GeometryConfig{ - R: 10, - }, - }, - } - - ctx := context.Background() - logger := logging.NewTestLogger(t) - b, err := fakebase.NewBase(ctx, resource.Dependencies{}, conf, logger) - test.That(t, err, test.ShouldBeNil) - ms := inject.NewMovementSensor("test") - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(0, 0), 0, nil - } - ms.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil - } - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{CompassHeadingSupported: true}, nil - } - - options := NewKinematicBaseOptions() - options.PositionOnlyMode = false - noise := spatialmath.NewPoseFromPoint(r3.Vector{1, 0, 0}) - origin := referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewZeroPose()) - kb, err := WrapWithFakePTGKinematics(ctx, b.(*fakebase.Base), logger, origin, options, noise, 5) - test.That(t, err, test.ShouldBeNil) - - startpose, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - realStartPose := spatialmath.PoseBetweenInverse(noise, startpose.Pose()) - - expected := referenceframe.FloatsToInputs([]float64{0, 1.23, 0, 110}) - expectedPose, err := kb.Kinematics().Transform(expected) - test.That(t, err, test.ShouldBeNil) - expectedPose = spatialmath.Compose(spatialmath.Compose(realStartPose, expectedPose), noise) - test.That(t, kb.GoToInputs(ctx, expected), test.ShouldBeNil) - pose, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincidentEps(pose.Pose(), expectedPose, 2), test.ShouldBeTrue) -} diff --git a/components/base/kinematicbase/kinematics.go b/components/base/kinematicbase/kinematics.go deleted file mode 100644 index d2be85a304c..00000000000 --- a/components/base/kinematicbase/kinematics.go +++ /dev/null @@ -1,166 +0,0 @@ -//go:build !no_cgo - -// Package kinematicbase contains wrappers that augment bases with information needed for higher level -// control over the base -package kinematicbase - -import ( - "context" - "errors" - "time" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" -) - -// KinematicBase is an interface for Bases that also satisfy the ModelFramer and InputEnabled interfaces. -type KinematicBase interface { - base.Base - motion.Localizer - referenceframe.InputEnabled - - Kinematics() referenceframe.Frame - // ErrorState takes a complete motionplan, as well as the index of the currently-executing set of inputs, and computes the pose - // difference between where the robot in fact is, and where it ought to be. - ErrorState(context.Context) (spatialmath.Pose, error) -} - -const ( - // LinearVelocityMMPerSec is the linear velocity the base will drive at in mm/s. - defaultLinearVelocityMMPerSec = 200. - - // AngularVelocityMMPerSec is the angular velocity the base will turn with in deg/s. - defaultAngularVelocityDegsPerSec = 60. - - // distThresholdMM is used when the base is moving to a goal. It is considered successful if it is within this radius. - defaultGoalRadiusMM = 300. - - // headingThresholdDegrees is used when the base is moving to a goal. - // If its heading is within this angle it is considered on the correct path. - defaultHeadingThresholdDegrees = 8. - - // planDeviationThresholdMM is the amount that the base is allowed to deviate from the straight line path it is intended to travel. - // If it ever exceeds this amount the movement will fail and an error will be returned. - defaultPlanDeviationThresholdMM = 600.0 // mm - - // timeout is the maximum amount of time that the base is allowed to remain stationary during a movement, else an error is thrown. - defaultTimeout = time.Second * 10 - - // minimumMovementThresholdMM is the amount that a base needs to move for it not to be considered stationary. - defaultMinimumMovementThresholdMM = 20. // mm - - // maxMoveStraightMM is the maximum distance the base should move with a single MoveStraight command. - // used to break up large driving segments to prevent error from building up due to slightly incorrect angle. - // Only used for diff drive kinematics, as PTGs do not use MoveStraight. - defaultMaxMoveStraightMM = 2000. - - // maxSpinAngleDeg is the maximum amount of degrees the base should turn with a single Spin command. - // used to break up large turns into smaller chunks to prevent error from building up. - defaultMaxSpinAngleDeg = 45. - - // positionOnlyMode defines whether motion planning should be done in 2DOF or 3DOF. - defaultPositionOnlyMode = true - - // defaultUsePTGs defines whether motion planning should use PTGs. - defaultUsePTGs = true - - // defaultNoSkidSteer defines whether motion planning should plan for diff drive bases using skid steer. If true, it will plan using - // only rotations and straight lines. - defaultNoSkidSteer = false -) - -// Options contains values used for execution of base movement. -type Options struct { - // LinearVelocityMMPerSec is the linear velocity the base will drive at in mm/s - LinearVelocityMMPerSec float64 - - // AngularVelocityMMPerSec is the angular velocity the base will turn with in deg/s - AngularVelocityDegsPerSec float64 - - // GoalRadiusMM is used when the base is moving to a goal. It is considered successful if it is within this radius. - GoalRadiusMM float64 - - // HeadingThresholdDegrees is used when the base is moving to a goal. - // If its heading is within this angle it is considered to be on the correct path. - HeadingThresholdDegrees float64 - - // PlanDeviationThresholdMM is the amount that the base is allowed to deviate from the straight line path it is intended to travel. - // If it ever exceeds this amount the movement will fail and an error will be returned. - PlanDeviationThresholdMM float64 - - // Timeout is the maximum amount of time that the base is allowed to remain stationary during a movement, else an error is thrown. - Timeout time.Duration - - // MinimumMovementThresholdMM is the amount that a base needs to move for it not to be considered stationary. - MinimumMovementThresholdMM float64 - - // MaxMoveStraightMM is the maximum distance the base should move with a single MoveStraight command. - // used to break up large driving segments to prevent error from building up due to slightly incorrect angle. - MaxMoveStraightMM float64 - - // MaxSpinAngleDeg is the maximum amount of degrees the base should turn with a single Spin command. - // used to break up large turns into smaller chunks to prevent error from building up. - MaxSpinAngleDeg float64 - - // PositionOnlyMode defines whether motion planning should be done in 2DOF or 3DOF. - // If value is true, planning is done in [x,y]. If value is false, planning is done in [x,y,theta]. - PositionOnlyMode bool - - // UsePTGs defines whether motion planning should plan using PTGs. - UsePTGs bool - - // NoSkidSteer defines whether motion planning should plan for diff drive bases using skid steer. If true, it will plan using - // only rotations and straight lines. Not used if turning radius > 0, or if UsePTGs is false. - NoSkidSteer bool -} - -// NewKinematicBaseOptions creates a struct with values used for execution of base movement. -// all values are pre-set to reasonable default values and can be changed if desired. -func NewKinematicBaseOptions() Options { - options := Options{ - LinearVelocityMMPerSec: defaultLinearVelocityMMPerSec, - AngularVelocityDegsPerSec: defaultAngularVelocityDegsPerSec, - GoalRadiusMM: defaultGoalRadiusMM, - HeadingThresholdDegrees: defaultHeadingThresholdDegrees, - PlanDeviationThresholdMM: defaultPlanDeviationThresholdMM, - Timeout: defaultTimeout, - MinimumMovementThresholdMM: defaultMinimumMovementThresholdMM, - MaxMoveStraightMM: defaultMaxMoveStraightMM, - MaxSpinAngleDeg: defaultMaxSpinAngleDeg, - PositionOnlyMode: defaultPositionOnlyMode, - UsePTGs: defaultUsePTGs, - NoSkidSteer: defaultNoSkidSteer, - } - return options -} - -// WrapWithKinematics will wrap a Base with the appropriate type of kinematics, allowing it to provide a Frame which can be planned with -// and making it InputEnabled. -func WrapWithKinematics( - ctx context.Context, - b base.Base, - logger logging.Logger, - localizer motion.Localizer, - limits []referenceframe.Limit, - options Options, -) (KinematicBase, error) { - if kb, ok := b.(KinematicBase); ok { - return kb, nil - } - - properties, err := b.Properties(ctx, nil) - if err != nil { - return nil, err - } - - if !options.UsePTGs { - if properties.TurningRadiusMeters == 0 { - return wrapWithDifferentialDriveKinematics(ctx, b, logger, localizer, limits, options) - } - return nil, errors.New("must use PTGs with nonzero turning radius") - } - return wrapWithPTGKinematics(ctx, b, logger, localizer, options) -} diff --git a/components/base/kinematicbase/ptgKinematics.go b/components/base/kinematicbase/ptgKinematics.go deleted file mode 100644 index 9325933b06a..00000000000 --- a/components/base/kinematicbase/ptgKinematics.go +++ /dev/null @@ -1,241 +0,0 @@ -//go:build !no_cgo - -// Package kinematicbase contains wrappers that augment bases with information needed for higher level -// control over the base -package kinematicbase - -import ( - "context" - "errors" - "sync" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan/tpspace" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -var zeroInput = make([]referenceframe.Input, 4) - -const ( - ptgIndex int = iota // The first input is the index of the associated PTG in the `ptgs` array - trajectoryAlphaWithinPTG // The second input is the alpha value of the ptg to use - startDistanceAlongTrajectoryIndex // The third input is the start distance of the arc that will be executed - endDistanceAlongTrajectoryIndex // The fourth input is the end distance of the arc that will be executed -) - -type ptgBaseKinematics struct { - base.Base - motion.Localizer - logger logging.Logger - frame referenceframe.Frame - ptgs []tpspace.PTGSolver - courseCorrectionIdx int - linVelocityMMPerSecond float64 - angVelocityDegsPerSecond float64 - nonzeroBaseTurningRadiusMeters float64 - - inputLock sync.RWMutex - currentIdx int - currentInputs []referenceframe.Input - currentExecutingSteps []arcStep - - origin spatialmath.Pose - geometries []spatialmath.Geometry - cancelFunc context.CancelFunc -} - -// wrapWithPTGKinematics takes a Base component and adds a PTG kinematic model so that it can be controlled. -func wrapWithPTGKinematics( - ctx context.Context, - b base.Base, - logger logging.Logger, - localizer motion.Localizer, - options Options, -) (KinematicBase, error) { - properties, err := b.Properties(ctx, nil) - if err != nil { - return nil, err - } - - linVelocityMMPerSecond := options.LinearVelocityMMPerSec - if linVelocityMMPerSecond == 0 { - linVelocityMMPerSecond = defaultLinearVelocityMMPerSec - } - - // Update our angular velocity and our - baseTurningRadiusMeters := properties.TurningRadiusMeters - if baseTurningRadiusMeters < 0 { - return nil, errors.New("can only wrap with PTG kinematics if turning radius is greater than or equal to zero") - } - - angVelocityDegsPerSecond, err := correctAngularVelocityWithTurnRadius( - logger, - baseTurningRadiusMeters, - linVelocityMMPerSecond, - options.AngularVelocityDegsPerSec, - ) - if err != nil { - return nil, err - } - - logger.CInfof(ctx, - "using linVelocityMMPerSecond %f, angVelocityDegsPerSecond %f, and baseTurningRadiusMeters %f for PTG base kinematics", - linVelocityMMPerSecond, - angVelocityDegsPerSecond, - baseTurningRadiusMeters, - ) - - geometries, err := b.Geometries(ctx, nil) - if len(geometries) == 0 || err != nil { - logger.CWarn(ctx, "base %s not configured with a geometry, will be considered a 300mm sphere for collision detection purposes.") - sphere, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 150., b.Name().Name) - if err != nil { - return nil, err - } - geometries = []spatialmath.Geometry{sphere} - } - - nonzeroBaseTurningRadiusMeters := (linVelocityMMPerSecond / rdkutils.DegToRad(angVelocityDegsPerSecond)) / 1000. - frame, err := tpspace.NewPTGFrameFromKinematicOptions( - b.Name().ShortName(), - logger, - nonzeroBaseTurningRadiusMeters, - 0, // If zero, will use default trajectory count on the receiver end. - geometries, - options.NoSkidSteer, - baseTurningRadiusMeters == 0, - ) - if err != nil { - return nil, err - } - ptgProv, err := rdkutils.AssertType[tpspace.PTGProvider](frame) - if err != nil { - return nil, err - } - ptgs := ptgProv.PTGSolvers() - origin := spatialmath.NewZeroPose() - - ptgCourseCorrection, err := rdkutils.AssertType[tpspace.PTGCourseCorrection](frame) - if err != nil { - return nil, err - } - courseCorrectionIdx := ptgCourseCorrection.CorrectionSolverIdx() - - if localizer != nil { - originPIF, err := localizer.CurrentPosition(ctx) - if err != nil { - return nil, err - } - origin = originPIF.Pose() - } - - return &ptgBaseKinematics{ - Base: b, - Localizer: localizer, - logger: logger, - frame: frame, - ptgs: ptgs, - courseCorrectionIdx: courseCorrectionIdx, - linVelocityMMPerSecond: linVelocityMMPerSecond, - angVelocityDegsPerSecond: angVelocityDegsPerSecond, - nonzeroBaseTurningRadiusMeters: nonzeroBaseTurningRadiusMeters, - currentInputs: zeroInput, - origin: origin, - geometries: geometries, - }, nil -} - -func (ptgk *ptgBaseKinematics) Kinematics() referenceframe.Frame { - return ptgk.frame -} - -// For a ptgBaseKinematics, `CurrentInputs` returns inputs which reflect what the base is currently doing. -// If the base is not moving, the CurrentInputs will all be zeros, and a `Transform()` will yield the zero pose. -// If the base is moving, then the inputs will be nonzero and the `Transform()` of the CurrentInputs will yield the pose at which the base -// is expected to arrive after completing execution of the current set of inputs. -func (ptgk *ptgBaseKinematics) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - ptgk.inputLock.RLock() - defer ptgk.inputLock.RUnlock() - return ptgk.currentInputs, nil -} - -func (ptgk *ptgBaseKinematics) ErrorState(ctx context.Context) (spatialmath.Pose, error) { - if ptgk.Localizer == nil { - return nil, errors.New("cannot call ErrorState on a base without a localizer") - } - - // Get pose-in-frame of the base via its localizer. The offset between the localizer and its base should already be accounted for. - actualPIF, err := ptgk.CurrentPosition(ctx) - if err != nil { - return nil, err - } - - // Determine the nominal pose, that is, the pose where the robot ought be if it had followed the plan perfectly up until this point. - ptgk.inputLock.RLock() - currentIdx := ptgk.currentIdx - currentExecutingSteps := ptgk.currentExecutingSteps - ptgk.inputLock.RUnlock() - currentInputs, err := ptgk.CurrentInputs(ctx) - if err != nil { - return nil, err - } - // The inputs representing the arc we have already executed can be computed as below. - // The return of CurrentInputs() represents the amount left on the arc, as an external caller will be more interested in where a base - // is going than where it has been. - executedInputs := []referenceframe.Input{ - currentInputs[ptgIndex], - currentInputs[trajectoryAlphaWithinPTG], - currentExecutingSteps[currentIdx].arcSegment.StartConfiguration[startDistanceAlongTrajectoryIndex], - currentInputs[startDistanceAlongTrajectoryIndex], - } - - currPoseInArc, err := ptgk.frame.Transform(executedInputs) - if err != nil { - return nil, err - } - nominalPose := spatialmath.Compose(currentExecutingSteps[currentIdx].arcSegment.StartPosition, currPoseInArc) - - return spatialmath.PoseBetween(nominalPose, actualPIF.Pose()), nil -} - -func correctAngularVelocityWithTurnRadius(logger logging.Logger, turnRadMeters, velocityMMps, angVelocityDegps float64) (float64, error) { - angVelocityRadps := rdkutils.DegToRad(angVelocityDegps) - turnRadMillimeters := turnRadMeters * 1000. - if angVelocityRadps == 0 { - if turnRadMeters == 0 { - return -1, errors.New("cannot create ptg frame, turning radius and angular velocity cannot both be zero") - } - angVelocityRadps = velocityMMps / turnRadMillimeters - } else if turnRadMeters > 0 { - // Compute smallest allowable turning radius permitted by the given speeds. Use the greater of the two. - calcTurnRadius := (velocityMMps / angVelocityRadps) - if calcTurnRadius > turnRadMillimeters { - // This is a debug message because the user will never notice the difference; the trajectories executed by the base will be a - // subset of the ones that would have been had this conditional not been hit. - logger.Debugf( - "given turning radius was %f but a linear velocity of %f "+ - "meters per sec and angular velocity of %f degs per sec only allow a turning radius of %f, using that instead", - turnRadMeters, velocityMMps/1000., angVelocityDegps, calcTurnRadius, - ) - } else if calcTurnRadius < turnRadMillimeters { - // If max allowed angular velocity would turn tighter than given turn radius, shrink the max used angular velocity - // to match the requested tightest turn radius. - angVelocityRadps = velocityMMps / turnRadMillimeters - // This is a warning message because the user will observe the base turning at a different speed than the one requested. - logger.Warnf( - "given turning radius was %f but a linear velocity of %f "+ - "meters per sec and angular velocity of %f degs per sec would turn at a radius of %f. Decreasing angular velocity to %f.", - turnRadMeters, velocityMMps/1000., angVelocityDegps, calcTurnRadius, rdkutils.RadToDeg(angVelocityRadps), - ) - } - } - return rdkutils.RadToDeg(angVelocityRadps), nil -} - -func (ptgk *ptgBaseKinematics) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - return ptgk.geometries, nil -} diff --git a/components/base/kinematicbase/ptgKinematics_test.go b/components/base/kinematicbase/ptgKinematics_test.go deleted file mode 100644 index c046a158777..00000000000 --- a/components/base/kinematicbase/ptgKinematics_test.go +++ /dev/null @@ -1,383 +0,0 @@ -// Package kinematicbase contains wrappers that augment bases with information needed for higher level -// control over the base -package kinematicbase - -import ( - "context" - "math" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/motionplan/tpspace" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -func TestPTGKinematicsNoGeom(t *testing.T) { - logger := logging.NewTestLogger(t) - - name := resource.Name{API: resource.NewAPI("is", "a", "fakebase")} - b := &fake.Base{ - Named: name.AsNamed(), - Geometry: []spatialmath.Geometry{}, - WidthMeters: 0.2, - TurningRadius: 0.3, - } - - ctx := context.Background() - - kb, err := WrapWithKinematics(ctx, b, logger, nil, nil, NewKinematicBaseOptions()) - test.That(t, err, test.ShouldBeNil) - test.That(t, kb, test.ShouldNotBeNil) - ptgBase, ok := kb.(*ptgBaseKinematics) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, ptgBase, test.ShouldNotBeNil) - - dstPIF := referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewPoseFromPoint(r3.Vector{X: 999, Y: 0, Z: 0})) - - fs := referenceframe.NewEmptyFrameSystem("test") - f := kb.Kinematics() - - defaultBaseGeom, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 150., b.Name().Name) - test.That(t, err, test.ShouldBeNil) - t.Run("Kinematics", func(t *testing.T) { - frame, err := tpspace.NewPTGFrameFromKinematicOptions( - b.Name().ShortName(), logger, 0.3, 0, nil, NewKinematicBaseOptions().NoSkidSteer, b.TurningRadius == 0, - ) - test.That(t, frame, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, f.Name(), test.ShouldEqual, b.Name().ShortName()) - test.That(t, f.DoF(), test.ShouldResemble, frame.DoF()) - - gifs, err := f.Geometries(referenceframe.FloatsToInputs([]float64{0, 0, 0, 0})) - test.That(t, err, test.ShouldBeNil) - - test.That(t, gifs.Geometries(), test.ShouldResemble, []spatialmath.Geometry{defaultBaseGeom}) - }) - t.Run("Geometries", func(t *testing.T) { - geoms, err := kb.Geometries(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(geoms), test.ShouldEqual, 1) - test.That(t, geoms[0], test.ShouldResemble, defaultBaseGeom) - }) - - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(f, fs.World()) - inputMap := referenceframe.StartPositions(fs) - - plan, err := motionplan.PlanMotion(ctx, &motionplan.PlanRequest{ - Logger: logger, - Goal: dstPIF, - Frame: f, - StartConfiguration: inputMap, - StartPose: spatialmath.NewZeroPose(), - FrameSystem: fs, - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, plan, test.ShouldNotBeNil) - runningPose := spatialmath.NewZeroPose() - for i, inputMap := range plan.Trajectory() { - inputs := inputMap[""] - arcSteps, err := ptgBase.trajectoryArcSteps(runningPose, inputs) - test.That(t, err, test.ShouldBeNil) - - if i == 0 || i == len(plan.Trajectory())-1 { - // First and last should be all-zero stop commands - test.That(t, len(arcSteps), test.ShouldEqual, 1) - test.That(t, arcSteps[0].durationSeconds, test.ShouldEqual, 0) - test.That(t, arcSteps[0].linVelMMps, test.ShouldResemble, r3.Vector{}) - test.That(t, arcSteps[0].angVelDegps, test.ShouldResemble, r3.Vector{}) - } else { - test.That(t, len(arcSteps), test.ShouldBeGreaterThanOrEqualTo, 1) - } - runningPose = spatialmath.Compose(runningPose, arcSteps[len(arcSteps)-1].subTraj[len(arcSteps[len(arcSteps)-1].subTraj)-1].Pose) - } -} - -func TestPTGKinematicsWithGeom(t *testing.T) { - logger := logging.NewTestLogger(t) - - name := resource.Name{API: resource.NewAPI("is", "a", "fakebase")} - - baseGeom, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{1, 1, 1}, "") - test.That(t, err, test.ShouldBeNil) - - b := &fake.Base{ - Named: name.AsNamed(), - Geometry: []spatialmath.Geometry{baseGeom}, - WidthMeters: 0.2, - TurningRadius: 0.3, - } - - ctx := context.Background() - - kbOpt := NewKinematicBaseOptions() - kbOpt.AngularVelocityDegsPerSec = 20 - - ms := inject.NewMovementSensor("movement_sensor") - gpOrigin := geo.NewPoint(0, 0) - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return gpOrigin, 0, nil - } - ms.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil - } - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{CompassHeadingSupported: true}, nil - } - localizer := motion.NewMovementSensorLocalizer(ms, gpOrigin, spatialmath.NewZeroPose()) - kb, err := WrapWithKinematics(ctx, b, logger, localizer, nil, kbOpt) - test.That(t, err, test.ShouldBeNil) - test.That(t, kb, test.ShouldNotBeNil) - - ptgBase, ok := kb.(*ptgBaseKinematics) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, ptgBase, test.ShouldNotBeNil) - - dstPIF := referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewPoseFromPoint(r3.Vector{X: 6000, Y: 0, Z: 0})) - - fs := referenceframe.NewEmptyFrameSystem("test") - f := kb.Kinematics() - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(f, fs.World()) - inputMap := referenceframe.StartPositions(fs) - - obstacle, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{2000, 0, 0}), r3.Vector{1, 1, 1}, "") - test.That(t, err, test.ShouldBeNil) - - geoms := []spatialmath.Geometry{obstacle} - worldState, err := referenceframe.NewWorldState( - []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(referenceframe.World, geoms)}, - nil, - ) - test.That(t, err, test.ShouldBeNil) - - plan, err := motionplan.PlanMotion(ctx, &motionplan.PlanRequest{ - Logger: logger, - Goal: dstPIF, - Frame: f, - StartConfiguration: inputMap, - FrameSystem: fs, - WorldState: worldState, - StartPose: spatialmath.NewZeroPose(), - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, plan, test.ShouldNotBeNil) - - allInputs := [][]referenceframe.Input{} - - // Spot check each individual trajectory - runningPose := spatialmath.NewZeroPose() - for i, inputMap := range plan.Trajectory() { - inputs := inputMap[""] - allInputs = append(allInputs, inputs) - arcSteps, err := ptgBase.trajectoryArcSteps(runningPose, inputs) - test.That(t, err, test.ShouldBeNil) - - if i == 0 || i == len(plan.Trajectory())-1 { - // First and last should be all-zero stop commands - test.That(t, len(arcSteps), test.ShouldEqual, 1) - test.That(t, arcSteps[0].durationSeconds, test.ShouldEqual, 0) - test.That(t, arcSteps[0].linVelMMps, test.ShouldResemble, r3.Vector{}) - test.That(t, arcSteps[0].angVelDegps, test.ShouldResemble, r3.Vector{}) - } else { - test.That(t, len(arcSteps), test.ShouldBeGreaterThanOrEqualTo, 1) - } - runningPose = spatialmath.Compose(runningPose, arcSteps[len(arcSteps)-1].subTraj[len(arcSteps[len(arcSteps)-1].subTraj)-1].Pose) - } - - // Now check the full set of arcs - arcSteps, err := ptgBase.arcStepsFromInputs(allInputs, spatialmath.NewZeroPose()) - test.That(t, err, test.ShouldBeNil) - arcIdx := 1 - - t.Run("CourseCorrectionPieces", func(t *testing.T) { - currInputs := []referenceframe.Input{ - arcSteps[arcIdx].arcSegment.StartConfiguration[0], - arcSteps[arcIdx].arcSegment.StartConfiguration[1], - {0}, - {1}, - } - ptgBase.inputLock.Lock() - ptgBase.currentIdx = arcIdx - ptgBase.currentExecutingSteps = arcSteps - ptgBase.currentInputs = currInputs - ptgBase.inputLock.Unlock() - // Mock up being off course and try to correct - skewPose := spatialmath.NewPose(r3.Vector{5, -300, 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -4}) - newPose, err := kb.Kinematics().Transform(currInputs) - test.That(t, err, test.ShouldBeNil) - - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - newGeoPose := spatialmath.PoseToGeoPose(spatialmath.NewGeoPose(gpOrigin, 0), spatialmath.Compose(newPose, skewPose)) - return newGeoPose.Location(), 0, nil - } - - goals := ptgBase.makeCourseCorrectionGoals( - goalsToAttempt, - arcIdx, - skewPose, - arcSteps, - currInputs, - ) - test.That(t, goals, test.ShouldNotBeNil) - solution, err := ptgBase.getCorrectionSolution(ctx, goals) - test.That(t, err, test.ShouldBeNil) - test.That(t, solution, test.ShouldNotBeNil) - - t.Run("ErrorState", func(t *testing.T) { - errorState, err := kb.ErrorState(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, errorState, test.ShouldNotBeNil) - - // Error State should be computed based on current inputs, current executing steps, and the localizer's position function - currentPosition, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - arcStartPosition := arcSteps[arcIdx].arcSegment.StartPosition - executedInputs := []referenceframe.Input{ - ptgBase.currentInputs[ptgIndex], - ptgBase.currentInputs[trajectoryAlphaWithinPTG], - arcSteps[arcIdx].arcSegment.StartConfiguration[startDistanceAlongTrajectoryIndex], - ptgBase.currentInputs[startDistanceAlongTrajectoryIndex], - } - onArcPosition, err := kb.Kinematics().Transform(executedInputs) - test.That(t, err, test.ShouldBeNil) - arcPose := spatialmath.Compose(arcStartPosition, onArcPosition) - - test.That( - t, - spatialmath.PoseAlmostCoincidentEps(errorState, spatialmath.PoseBetween(arcPose, currentPosition.Pose()), 1e-5), - test.ShouldBeTrue, - ) - test.That( - t, - spatialmath.PoseAlmostCoincidentEps(errorState, spatialmath.PoseBetween(arcPose, skewPose), 5), - test.ShouldBeTrue, - ) - }) - - t.Run("RunCorrection", func(t *testing.T) { - newArcSteps, err := ptgBase.courseCorrect(ctx, currInputs, arcSteps, arcIdx) - test.That(t, err, test.ShouldBeNil) - arcIdx++ - newInputs := []referenceframe.Input{ - arcSteps[arcIdx].arcSegment.StartConfiguration[0], - arcSteps[arcIdx].arcSegment.StartConfiguration[1], - {0}, - {0}, - } - ptgBase.inputLock.Lock() - ptgBase.currentIdx = arcIdx - ptgBase.currentExecutingSteps = newArcSteps - ptgBase.currentInputs = newInputs - ptgBase.inputLock.Unlock() - // After course correction, error state should always be zero - errorState, err := kb.ErrorState(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, errorState, test.ShouldNotBeNil) - test.That(t, spatialmath.PoseAlmostEqualEps(errorState, spatialmath.NewZeroPose(), 1e-5), test.ShouldBeTrue) - }) - }) - - t.Run("EasyGoal", func(t *testing.T) { - goal := courseCorrectionGoal{ - Goal: spatialmath.NewPose(r3.Vector{X: -0.8564, Y: 234.}, &spatialmath.OrientationVectorDegrees{OZ: 1., Theta: 4.4}), - } - solution, err := ptgBase.getCorrectionSolution(ctx, []courseCorrectionGoal{goal}) - test.That(t, err, test.ShouldBeNil) - test.That(t, solution.Solution, test.ShouldNotBeNil) // Irrelevant what this is as long as filled in - }) - - t.Run("Kinematics", func(t *testing.T) { - kinematics := kb.Kinematics() - f, err := tpspace.NewPTGFrameFromKinematicOptions( - b.Name().ShortName(), logger, 0.3, 0, []spatialmath.Geometry{baseGeom}, kbOpt.NoSkidSteer, b.TurningRadius == 0, - ) - test.That(t, f, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, kinematics.Name(), test.ShouldEqual, b.Name().ShortName()) - test.That(t, kinematics.DoF(), test.ShouldResemble, f.DoF()) - - gifs, err := kinematics.Geometries(referenceframe.FloatsToInputs([]float64{0, 0, 0, 0})) - test.That(t, err, test.ShouldBeNil) - test.That(t, gifs.Geometries(), test.ShouldResemble, []spatialmath.Geometry{baseGeom}) - }) - - t.Run("GoToInputs", func(t *testing.T) { - // The transform of current inputs is the remaining step, i.e. where the base will go when the current inputs are done executing. - // To mock this up correctly, we need to alter the inputs here to simulate the base moving without a localizer - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - ptgBase.inputLock.RLock() - execInputs := []referenceframe.Input{ - ptgBase.currentInputs[0], - ptgBase.currentInputs[1], - {0}, - ptgBase.currentInputs[2], - } - ptgBase.inputLock.RUnlock() - newPose, err := kb.Kinematics().Transform(execInputs) - test.That(t, err, test.ShouldBeNil) - newGeoPose := spatialmath.PoseToGeoPose(spatialmath.NewGeoPose(gpOrigin, 0), newPose) - return newGeoPose.Location(), 0, nil - } - ms.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - ptgBase.inputLock.RLock() - execInputs := []referenceframe.Input{ - ptgBase.currentInputs[0], - ptgBase.currentInputs[1], - {0}, - ptgBase.currentInputs[2], - } - ptgBase.inputLock.RUnlock() - newPose, err := kb.Kinematics().Transform(execInputs) - test.That(t, err, test.ShouldBeNil) - headingRightHanded := newPose.Orientation().OrientationVectorDegrees().Theta - return math.Abs(headingRightHanded) - 360, nil - } - - waypoints, err := plan.Trajectory().GetFrameInputs(kb.Name().ShortName()) - test.That(t, err, test.ShouldBeNil) - // Start by resetting current inputs to 0 - err = kb.GoToInputs(ctx, waypoints[0]) - test.That(t, err, test.ShouldBeNil) - err = kb.GoToInputs(ctx, waypoints[1]) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("CurrentInputs", func(t *testing.T) { - currentInputs, err := kb.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(currentInputs), test.ShouldEqual, 4) - expectedInputs := referenceframe.FloatsToInputs([]float64{0, 0, 0, 0}) - test.That(t, currentInputs, test.ShouldResemble, expectedInputs) - }) - - t.Run("CurrentPosition", func(t *testing.T) { - currentPosition, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, currentPosition, test.ShouldNotBeNil) - expectedPosition, err := kb.Kinematics().Transform(ptgBase.currentInputs) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincidentEps(currentPosition.Pose(), expectedPosition, 1e-5), test.ShouldBeTrue) - }) - - t.Run("Geometries", func(t *testing.T) { - geoms, err := kb.Geometries(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(geoms), test.ShouldEqual, 1) - test.That(t, geoms[0], test.ShouldResemble, baseGeom) - }) -} diff --git a/components/base/properties.go b/components/base/properties.go deleted file mode 100644 index da5052f8a6b..00000000000 --- a/components/base/properties.go +++ /dev/null @@ -1,40 +0,0 @@ -// Package base contains an enum representing optional base features -package base - -import pb "go.viam.com/api/component/base/v1" - -// Properties is a structure representing features -// of a base. -type Properties struct { - TurningRadiusMeters float64 - WidthMeters float64 - WheelCircumferenceMeters float64 -} - -// ProtoFeaturesToProperties takes a GetPropertiesResponse and returns -// an equivalent Properties struct. -func ProtoFeaturesToProperties(resp *pb.GetPropertiesResponse) Properties { - return Properties{ - // A base's truning radius is the minimum radius it can turn around. - // This can be zero for bases that use differential, omni, mecanum - // and zero-turn steering bases. - // Usually non-zero for ackerman, crab and four wheel steered bases - TurningRadiusMeters: resp.TurningRadiusMeters, - // the width of the base's wheelbase - WidthMeters: resp.WidthMeters, - // the circumference of the wheels - WheelCircumferenceMeters: resp.WheelCircumferenceMeters, - } -} - -// PropertiesToProtoResponse takes a map of features to struct and converts it -// to a GetPropertiesResponse. -func PropertiesToProtoResponse( - features Properties, -) (*pb.GetPropertiesResponse, error) { - return &pb.GetPropertiesResponse{ - TurningRadiusMeters: features.TurningRadiusMeters, - WidthMeters: features.WidthMeters, - WheelCircumferenceMeters: features.WheelCircumferenceMeters, - }, nil -} diff --git a/components/base/register/register.go b/components/base/register/register.go deleted file mode 100644 index 5d2cf3ab1e9..00000000000 --- a/components/base/register/register.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package register registers all relevant bases -package register - -import ( - // register bases. - _ "go.viam.com/rdk/components/base/fake" - _ "go.viam.com/rdk/components/base/sensorcontrolled" - _ "go.viam.com/rdk/components/base/wheeled" -) diff --git a/components/base/sensorcontrolled/movestraight.go b/components/base/sensorcontrolled/movestraight.go deleted file mode 100644 index 4f57157d238..00000000000 --- a/components/base/sensorcontrolled/movestraight.go +++ /dev/null @@ -1,174 +0,0 @@ -// Package sensorcontrolled base implements a base with feedback control from a movement sensor -package sensorcontrolled - -import ( - "context" - "errors" - "math" - "time" - - geo "github.com/kellydunn/golang-geo" -) - -const ( - slowDownDistGain = .1 - maxSlowDownDist = 100 // mm - moveStraightErrTarget = 0 // mm - headingGain = 1. -) - -// MoveStraight commands a base to move forward for the desired distanceMm at the given mmPerSec. -// When controls are enabled, MoveStraight calculates the required velocity to reach mmPerSec -// and the distanceMm goal. It then polls the provided velocity movement sensor and corrects any -// error between this calculated velocity and the actual velocity using a PID control loop. -// MoveStraight also monitors the position and stops the base when the goal distanceMm is reached. -// If a compass heading movement sensor is provided, MoveStraight will attempt to keep the heading -// of the base fixed in the original direction it was faced at the beginning of the MoveStraight call. -func (sb *sensorBase) MoveStraight( - ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}, -) error { - sb.opMgr.CancelRunning(ctx) - ctx, done := sb.opMgr.New(ctx) - defer done() - - // If a position movement sensor or controls are not configured, we cannot use this MoveStraight method. - // Instead we need to use the MoveStraight method of the base that the sensorcontrolled base wraps. - // If there is no valid velocity sensor, there won't be a controlLoopConfig. - if sb.position == nil || len(sb.controlLoopConfig.Blocks) == 0 { - sb.logger.CWarnf(ctx, - "Position reporting sensor not available, or control loop not configured, using base %s's MoveStraight", - sb.controlledBase.Name().ShortName()) - if sb.loop != nil { - sb.loop.Pause() - } - return sb.controlledBase.MoveStraight(ctx, distanceMm, mmPerSec, extra) - } - - // make sure the control loop is enabled - if sb.loop == nil { - if err := sb.startControlLoop(); err != nil { - return err - } - } - sb.loop.Resume() - - straightTimeEst := time.Duration(int(time.Second) * int(math.Abs(float64(distanceMm)/mmPerSec))) - startTime := time.Now() - timeOut := 5 * straightTimeEst - if timeOut < 10*time.Second { - timeOut = 10 * time.Second - } - - // grab the initial heading for MoveStraight to clamp to. Will return 0 if no supporting sensors were configured. - initialHeading, _, err := sb.headingFunc(ctx) - if err != nil { - return err - } - - // initialize relevant parameters for moving straight - slowDownDist := calcSlowDownDist(distanceMm) - - var initPos *geo.Point - - if sb.position != nil { - initPos, _, err = sb.position.Position(ctx, nil) - if err != nil { - return err - } - } - - ticker := time.NewTicker(time.Duration(1000./sb.controlLoopConfig.Frequency) * time.Millisecond) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - // do not return context canceled errors, just log them - if errors.Is(ctx.Err(), context.Canceled) { - sb.logger.Error(ctx.Err()) - return nil - } - return ctx.Err() - case <-ticker.C: - var errDist float64 - - angVelDes, err := sb.calcHeadingControl(ctx, initialHeading) - if err != nil { - return err - } - - if sb.position != nil { - errDist, err = sb.calcPositionError(ctx, distanceMm, initPos) - if err != nil { - return err - } - } - - if errDist < moveStraightErrTarget { - return sb.Stop(ctx, nil) - } - - linVelDes := calcLinVel(errDist, mmPerSec, slowDownDist) - if err != nil { - return err - } - - // update velocity controller - if err := sb.updateControlConfig(ctx, linVelDes/1000.0, angVelDes); err != nil { - return err - } - - // exit if the straight takes too long - if time.Since(startTime) > timeOut { - sb.logger.CWarn(ctx, "exceeded time for MoveStraight call, stopping base") - return sb.Stop(ctx, nil) - } - } - } -} - -// calculate the desired angular velocity to correct the heading of the base. -func (sb *sensorBase) calcHeadingControl(ctx context.Context, initHeading float64) (float64, error) { - currHeading, _, err := sb.headingFunc(ctx) - if err != nil { - return 0, err - } - - headingErr := initHeading - currHeading - headingErrWrapped := headingErr - (math.Floor((headingErr+180.)/(2*180.)))*2*180. // [-180;180) - - return headingErrWrapped * headingGain, nil -} - -// calcPositionError calculates the current error in position. -// This results in the distance the base needs to travel to reach the goal. -func (sb *sensorBase) calcPositionError(ctx context.Context, distanceMm int, initPos *geo.Point) (float64, error) { - pos, _, err := sb.position.Position(ctx, nil) - if err != nil { - return 0, err - } - - // the currDist will always return as positive, so we need the goal distanceMm to be positive - currDist := initPos.GreatCircleDistance(pos) * 1000000. - return math.Abs(float64(distanceMm)) - currDist, nil -} - -// calcLinVel computes the desired linear velocity based on how far the base is from reaching the goal. -func calcLinVel(errDist, mmPerSec, slowDownDist float64) float64 { - // have the velocity slow down when appoaching the goal. Otherwise use the desired velocity - linVel := errDist * mmPerSec / slowDownDist - absMmPerSec := math.Abs(mmPerSec) - if math.Abs(linVel) > absMmPerSec { - return absMmPerSec * sign(linVel) - } - return linVel -} - -// calcSlowDownDist computes the distance at which the MoveStraight call should begin to slow down. -// This helps to prevent overshoot when reaching the goal and reduces the jerk on the robot when the straight is complete. -func calcSlowDownDist(distanceMm int) float64 { - slowDownDist := float64(distanceMm) * slowDownDistGain - if math.Abs(slowDownDist) > maxSlowDownDist { - return maxSlowDownDist * sign(float64(distanceMm)) - } - return slowDownDist -} diff --git a/components/base/sensorcontrolled/sensorcontrolled.go b/components/base/sensorcontrolled/sensorcontrolled.go deleted file mode 100644 index 29c974b0749..00000000000 --- a/components/base/sensorcontrolled/sensorcontrolled.go +++ /dev/null @@ -1,304 +0,0 @@ -// Package sensorcontrolled base implements a base with feedback control from a movement sensor -package sensorcontrolled - -import ( - "context" - "sync" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/control" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -const ( - yawPollTime = 5 * time.Millisecond - velocitiesPollTime = 5 * time.Millisecond - sensorDebug = false - typeLinVel = "linear_velocity" - typeAngVel = "angular_velocity" -) - -var ( - // Model is the name of the sensor_controlled model of a base component. - model = resource.DefaultModelFamily.WithModel("sensor-controlled") - errNoGoodSensor = errors.New("no appropriate sensor for orientation or velocity feedback") -) - -// Config configures a sensor controlled base. -type Config struct { - MovementSensor []string `json:"movement_sensor"` - Base string `json:"base"` - ControlParameters []control.PIDConfig `json:"control_parameters,omitempty"` -} - -// Validate validates all parts of the sensor controlled base config. -func (cfg *Config) Validate(path string) ([]string, error) { - deps := []string{} - if len(cfg.MovementSensor) == 0 { - return nil, resource.NewConfigValidationError(path, errors.New("need at least one movement sensor for base")) - } - - deps = append(deps, cfg.MovementSensor...) - if cfg.Base == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "base") - } - - deps = append(deps, cfg.Base) - return deps, nil -} - -type sensorBase struct { - resource.Named - conf *Config - logger logging.Logger - mu sync.Mutex - - activeBackgroundWorkers sync.WaitGroup - controlledBase base.Base // the inherited wheeled base - - opMgr *operation.SingleOperationManager - - allSensors []movementsensor.MovementSensor - velocities movementsensor.MovementSensor - position movementsensor.MovementSensor - // headingFunc returns the current angle between (-180,180) and whether Spin is supported - headingFunc func(ctx context.Context) (float64, bool, error) - - controlLoopConfig control.Config - blockNames map[string][]string - loop *control.Loop -} - -func init() { - resource.RegisterComponent( - base.API, - model, - resource.Registration[base.Base, *Config]{Constructor: createSensorBase}) -} - -func createSensorBase( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (base.Base, error) { - sb := &sensorBase{ - logger: logger, - Named: conf.ResourceName().AsNamed(), - opMgr: operation.NewSingleOperationManager(), - } - - if err := sb.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return sb, nil -} - -func (sb *sensorBase) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - newConf, err := resource.NativeConfig[*Config](conf) - sb.conf = newConf - if err != nil { - return err - } - - if sb.loop != nil { - sb.loop.Stop() - sb.loop = nil - } - - sb.mu.Lock() - defer sb.mu.Unlock() - - // reset all sensors - sb.allSensors = nil - sb.velocities = nil - var orientation movementsensor.MovementSensor - var compassHeading movementsensor.MovementSensor - sb.position = nil - sb.controlledBase = nil - - for _, name := range newConf.MovementSensor { - ms, err := movementsensor.FromDependencies(deps, name) - if err != nil { - return errors.Wrapf(err, "no movement sensor named (%s)", name) - } - sb.allSensors = append(sb.allSensors, ms) - } - - for _, ms := range sb.allSensors { - props, err := ms.Properties(context.Background(), nil) - if err == nil && props.OrientationSupported { - // return first sensor that does not error that satisfies the properties wanted - orientation = ms - sb.logger.CInfof(ctx, "using sensor %s as orientation sensor for base", orientation.Name().ShortName()) - break - } - } - - for _, ms := range sb.allSensors { - props, err := ms.Properties(context.Background(), nil) - if err == nil && props.AngularVelocitySupported && props.LinearVelocitySupported { - // return first sensor that does not error that satisfies the properties wanted - sb.velocities = ms - sb.logger.CInfof(ctx, "using sensor %s as velocity sensor for base", sb.velocities.Name().ShortName()) - break - } - } - - for _, ms := range sb.allSensors { - props, err := ms.Properties(context.Background(), nil) - if err == nil && props.PositionSupported { - // return first sensor that does not error that satisfies the properties wanted - sb.position = ms - sb.logger.CInfof(ctx, "using sensor %s as position sensor for base", sb.position.Name().ShortName()) - break - } - } - - for _, ms := range sb.allSensors { - props, err := ms.Properties(context.Background(), nil) - if err == nil && props.CompassHeadingSupported { - // return first sensor that does not error that satisfies the properties wanted - compassHeading = ms - sb.logger.CInfof(ctx, "using sensor %s as compassHeading sensor for base", compassHeading.Name().ShortName()) - break - } - } - sb.determineHeadingFunc(ctx, orientation, compassHeading) - - if orientation == nil && sb.velocities == nil { - return errNoGoodSensor - } - - sb.controlledBase, err = base.FromDependencies(deps, newConf.Base) - if err != nil { - return errors.Wrapf(err, "no base named (%s)", newConf.Base) - } - - if sb.velocities != nil && len(newConf.ControlParameters) != 0 { - // assign linear and angular PID correctly based on the given type - var linear, angular control.PIDConfig - for _, c := range newConf.ControlParameters { - switch c.Type { - case typeLinVel: - linear = c - case typeAngVel: - angular = c - default: - sb.logger.Warn("control_parameters type must be 'linear_velocity' or 'angular_velocity'") - } - } - - // unlock the mutex before setting up the control loop so that the motors - // are not locked, and can run if any auto-tuning is necessary - sb.mu.Unlock() - if err := sb.setupControlLoop(linear, angular); err != nil { - sb.mu.Lock() - return err - } - // relock the mutex after setting up the control loop since there is still a defer unlock - sb.mu.Lock() - } - - return nil -} - -func (sb *sensorBase) SetPower( - ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}, -) error { - sb.opMgr.CancelRunning(ctx) - if sb.loop != nil { - sb.loop.Pause() - } - return sb.controlledBase.SetPower(ctx, linear, angular, extra) -} - -func (sb *sensorBase) Stop(ctx context.Context, extra map[string]interface{}) error { - sb.opMgr.CancelRunning(ctx) - if sb.loop != nil { - sb.loop.Pause() - } - return sb.controlledBase.Stop(ctx, extra) -} - -func (sb *sensorBase) IsMoving(ctx context.Context) (bool, error) { - return sb.controlledBase.IsMoving(ctx) -} - -func (sb *sensorBase) Properties(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return sb.controlledBase.Properties(ctx, extra) -} - -func (sb *sensorBase) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - return sb.controlledBase.Geometries(ctx, extra) -} - -func (sb *sensorBase) Close(ctx context.Context) error { - if err := sb.Stop(ctx, nil); err != nil { - return err - } - if sb.loop != nil { - sb.loop.Stop() - sb.loop = nil - } - - sb.activeBackgroundWorkers.Wait() - return nil -} - -// determineHeadingFunc determines which movement sensor endpoint should be used for control. -// The priority is Orientation -> Heading -> No heading control. -func (sb *sensorBase) determineHeadingFunc(ctx context.Context, - orientation, compassHeading movementsensor.MovementSensor, -) { - switch { - case orientation != nil: - - sb.logger.CInfof(ctx, "using sensor %s as angular heading sensor for base %v", orientation.Name().ShortName(), sb.Name().ShortName()) - - sb.headingFunc = func(ctx context.Context) (float64, bool, error) { - orient, err := orientation.Orientation(ctx, nil) - if err != nil { - return 0, false, err - } - // this returns (-180-> 180) - yaw := rdkutils.RadToDeg(orient.EulerAngles().Yaw) - - return yaw, true, nil - } - case compassHeading != nil: - sb.logger.CInfof(ctx, "using sensor %s as angular heading sensor for base %v", compassHeading.Name().ShortName(), sb.Name().ShortName()) - - sb.headingFunc = func(ctx context.Context) (float64, bool, error) { - compass, err := compassHeading.CompassHeading(ctx, nil) - if err != nil { - return 0, false, err - } - // flip compass heading to be CCW/Z up - compass = 360 - compass - - // make the compass heading (-180->180) - if compass > 180 { - compass -= 360 - } - - return compass, true, nil - } - default: - sb.logger.CInfof(ctx, "base %v cannot control heading, no heading related sensor given", - sb.Name().ShortName()) - sb.headingFunc = func(ctx context.Context) (float64, bool, error) { - return 0, false, nil - } - } -} diff --git a/components/base/sensorcontrolled/sensorcontrolled_test.go b/components/base/sensorcontrolled/sensorcontrolled_test.go deleted file mode 100644 index 5e2f6c3bf42..00000000000 --- a/components/base/sensorcontrolled/sensorcontrolled_test.go +++ /dev/null @@ -1,501 +0,0 @@ -package sensorcontrolled - -import ( - "context" - "errors" - "strings" - "sync" - "testing" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/control" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" - rdkutils "go.viam.com/rdk/utils" -) - -const ( - // compassValue and orientationValue should be different for tests. - defaultCompassValue = 45. - defaultOrientationValue = 40. -) - -var ( - // compassValue and orientationValue should be different for tests. - compassValue = defaultCompassValue - orientationValue = defaultOrientationValue -) - -func sConfig() resource.Config { - return resource.Config{ - Name: "test", - API: base.API, - Model: resource.Model{Name: "wheeled_base"}, - ConvertedAttributes: &Config{ - MovementSensor: []string{"ms"}, - Base: "test_base", - }, - } -} - -func createDependencies(t *testing.T) resource.Dependencies { - t.Helper() - deps := make(resource.Dependencies) - - counter := 0 - - deps[movementsensor.Named("ms")] = &inject.MovementSensor{ - PropertiesFuncExtraCap: map[string]interface{}{}, - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{OrientationSupported: true}, nil - }, - OrientationFunc: func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - counter++ - return &spatialmath.EulerAngles{Roll: 0, Pitch: 0, Yaw: rdkutils.RadToDeg(float64(counter))}, nil - }, - } - - deps = addBaseDependency(deps) - - return deps -} - -func addBaseDependency(deps resource.Dependencies) resource.Dependencies { - deps[base.Named(("test_base"))] = &inject.Base{ - DoFunc: testutils.EchoFunc, - MoveStraightFunc: func(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - return nil - }, - SpinFunc: func(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - return nil - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { - return nil - }, - IsMovingFunc: func(context.Context) (bool, error) { - return false, nil - }, - CloseFunc: func(ctx context.Context) error { - return nil - }, - SetPowerFunc: func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return nil - }, - SetVelocityFunc: func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return nil - }, - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{ - TurningRadiusMeters: 0.1, - WidthMeters: 0.1, - }, nil - }, - GeometriesFunc: func(ctx context.Context) ([]spatialmath.Geometry, error) { - return nil, nil - }, - } - return deps -} - -func TestSensorBase(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - testCfg := sConfig() - conf, ok := testCfg.ConvertedAttributes.(*Config) - test.That(t, ok, test.ShouldBeTrue) - deps, err := conf.Validate("path") - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldResemble, []string{"ms", "test_base"}) - sbDeps := createDependencies(t) - - sb, err := createSensorBase(ctx, sbDeps, testCfg, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, sb, test.ShouldNotBeNil) - - moving, err := sb.IsMoving(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - - props, err := sb.Properties(ctx, nil) - test.That(t, props.WidthMeters, test.ShouldResemble, 0.1) - test.That(t, err, test.ShouldBeNil) - - geometries, err := sb.Geometries(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, geometries, test.ShouldBeNil) - - test.That(t, sb.SetPower(ctx, r3.Vector{X: 0, Y: 10, Z: 0}, r3.Vector{X: 0, Y: 0, Z: 0}, nil), test.ShouldBeNil) - - // this test does not include a velocities sensor and does not create a sensor base with a control loop - test.That(t, sb.SetVelocity(ctx, r3.Vector{X: 0, Y: 100, Z: 0}, r3.Vector{X: 0, Y: 100, Z: 0}, nil), test.ShouldBeNil) - test.That(t, sb.MoveStraight(ctx, 10, 10, nil), test.ShouldBeNil) - test.That(t, sb.Spin(ctx, 2, 10, nil), test.ShouldBeNil) - test.That(t, sb.Stop(ctx, nil), test.ShouldBeNil) - - test.That(t, sb.Close(ctx), test.ShouldBeNil) -} - -func sBaseTestConfig(msNames []string) resource.Config { - controlParams := make([]control.PIDConfig, 2) - controlParams[0] = control.PIDConfig{ - Type: typeLinVel, - P: 0.5, - I: 0.5, - D: 0.0, - } - controlParams[1] = control.PIDConfig{ - Type: typeAngVel, - P: 0.5, - I: 0.5, - D: 0.0, - } - - return resource.Config{ - Name: "test", - API: base.API, - Model: resource.Model{Name: "controlled_base"}, - ConvertedAttributes: &Config{ - MovementSensor: msNames, - Base: "test_base", - ControlParameters: controlParams, - }, - } -} - -func msDependencies(t *testing.T, msNames []string, -) (resource.Dependencies, resource.Config) { - t.Helper() - - cfg := sBaseTestConfig(msNames) - - deps := make(resource.Dependencies) - - for _, msName := range msNames { - ms := inject.NewMovementSensor(msName) - switch { - case strings.Contains(msName, "orientation"): - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - OrientationSupported: true, - }, nil - } - ms.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return &spatialmath.EulerAngles{Roll: 0, Pitch: 0, Yaw: rdkutils.DegToRad(orientationValue)}, nil - } - deps[movementsensor.Named(msName)] = ms - case strings.Contains(msName, "position"): - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - PositionSupported: true, - }, nil - } - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return &geo.Point{}, 0, nil - } - deps[movementsensor.Named(msName)] = ms - case strings.Contains(msName, "compass"): - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - CompassHeadingSupported: true, - }, nil - } - ms.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return compassValue, nil - } - deps[movementsensor.Named(msName)] = ms - - case strings.Contains(msName, "setvel"): - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - AngularVelocitySupported: true, - LinearVelocitySupported: true, - }, nil - } - ms.LinearVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, nil - } - ms.AngularVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{}, nil - } - deps[movementsensor.Named(msName)] = ms - - case strings.Contains(msName, "Bad"): - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - OrientationSupported: true, - AngularVelocitySupported: true, - LinearVelocitySupported: true, - }, errors.New("bad sensor") - } - deps[movementsensor.Named(msName)] = ms - - default: - } - } - - deps = addBaseDependency(deps) - - return deps, cfg -} - -func TestReconfig(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - deps, cfg := msDependencies(t, []string{"orientation"}) - - b, err := createSensorBase(ctx, deps, cfg, logger) - test.That(t, err, test.ShouldBeNil) - sb, ok := b.(*sensorBase) - test.That(t, ok, test.ShouldBeTrue) - headingOri, headingSupported, err := sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingOri, test.ShouldEqual, orientationValue) - - deps, cfg = msDependencies(t, []string{"orientation1"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingOri, test.ShouldEqual, orientationValue) - - deps, cfg = msDependencies(t, []string{"setvel1"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, sb.velocities.Name().ShortName(), test.ShouldResemble, "setvel1") - - deps, cfg = msDependencies(t, []string{"setvel2"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, sb.velocities.Name().ShortName(), test.ShouldResemble, "setvel2") - headingNone, headingSupported, err := sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeFalse) - test.That(t, headingNone, test.ShouldEqual, 0) - - deps, cfg = msDependencies(t, []string{"orientation3", "setvel3", "Bad"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, err, test.ShouldBeNil) - headingOri, headingSupported, err = sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingOri, test.ShouldEqual, orientationValue) - test.That(t, sb.velocities.Name().ShortName(), test.ShouldResemble, "setvel3") - - deps, cfg = msDependencies(t, []string{"Bad", "orientation4", "setvel4", "orientation5", "setvel5"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, err, test.ShouldBeNil) - headingOri, headingSupported, err = sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingOri, test.ShouldEqual, orientationValue) - test.That(t, sb.velocities.Name().ShortName(), test.ShouldResemble, "setvel4") - - deps, cfg = msDependencies(t, []string{"Bad", "orientation6", "setvel6", "position1", "compass1"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, err, test.ShouldBeNil) - headingOri, headingSupported, err = sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingOri, test.ShouldEqual, orientationValue) - test.That(t, sb.velocities.Name().ShortName(), test.ShouldResemble, "setvel6") - test.That(t, sb.position.Name().ShortName(), test.ShouldResemble, "position1") - - deps, cfg = msDependencies(t, []string{"Bad", "setvel7", "position2", "compass2"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, sb.velocities.Name().ShortName(), test.ShouldResemble, "setvel7") - test.That(t, sb.position.Name().ShortName(), test.ShouldResemble, "position2") - headingCompass, headingSupported, err := sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingCompass, test.ShouldNotEqual, orientationValue) - test.That(t, headingCompass, test.ShouldEqual, -compassValue) - - deps, cfg = msDependencies(t, []string{"Bad"}) - err = b.Reconfigure(ctx, deps, cfg) - test.That(t, sb.velocities, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errNoGoodSensor) - headingBad, headingSupported, err := sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeFalse) - test.That(t, headingBad, test.ShouldEqual, 0) -} - -func TestSensorBaseWithVelocitiesSensor(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps, cfg := msDependencies(t, []string{"setvel1"}) - - b, err := createSensorBase(ctx, deps, cfg, logger) - test.That(t, err, test.ShouldBeNil) - sb, ok := b.(*sensorBase) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - test.That(t, sb.velocities.Name().ShortName(), test.ShouldResemble, "setvel1") - - test.That(t, sb.SetVelocity(ctx, r3.Vector{X: 0, Y: 100, Z: 0}, r3.Vector{X: 0, Y: 100, Z: 0}, nil), test.ShouldBeNil) - test.That(t, sb.loop, test.ShouldNotBeNil) - test.That(t, sb.Stop(ctx, nil), test.ShouldBeNil) - test.That(t, sb.Close(ctx), test.ShouldBeNil) -} - -func TestSensorBaseSpin(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps, cfg := msDependencies(t, []string{"setvel1", "orientation1"}) - b, err := createSensorBase(ctx, deps, cfg, logger) - test.That(t, err, test.ShouldBeNil) - sb, ok := b.(*sensorBase) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - headingOri, headingSupported, err := sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingOri, test.ShouldEqual, orientationValue) - - depsNoOri, cfgNoOri := msDependencies(t, []string{"setvel1"}) - bNoOri, err := createSensorBase(ctx, depsNoOri, cfgNoOri, logger) - test.That(t, err, test.ShouldBeNil) - sbNoOri, ok := bNoOri.(*sensorBase) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - headingOri, headingSupported, err = sbNoOri.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeFalse) - test.That(t, headingOri, test.ShouldEqual, 0) - t.Run("Test canceling a sensor controlled spin", func(t *testing.T) { - // flaky test, will see behavior after RSDK-6164 - t.Skip() - cancelCtx, cancel := context.WithCancel(ctx) - wg := sync.WaitGroup{} - wg.Add(1) - utils.PanicCapturingGo(func() { - defer wg.Done() - err := sb.Spin(cancelCtx, 10, 10, nil) - test.That(t, err, test.ShouldBeError, cancelCtx.Err()) - }) - time.Sleep(4 * time.Second) - cancel() - wg.Wait() - }) - t.Run("Test canceling a sensor controlled spin due to calling another running api", func(t *testing.T) { - // flaky test, will see behavior after RSDK-6164 - t.Skip() - wg := sync.WaitGroup{} - wg.Add(1) - utils.PanicCapturingGo(func() { - defer wg.Done() - err := sb.Spin(ctx, 10, 10, nil) - test.That(t, err, test.ShouldBeNil) - }) - time.Sleep(2 * time.Second) - err := sb.SetPower(context.Background(), r3.Vector{}, r3.Vector{}, nil) - test.That(t, err, test.ShouldBeNil) - wg.Wait() - }) - t.Run("Test not including an orientation ms will use the non controlled spin", func(t *testing.T) { - // the injected base will return nil instead of blocking - err := sbNoOri.Spin(ctx, 10, 10, nil) - test.That(t, err, test.ShouldBeNil) - }) -} - -func TestSensorBaseMoveStraight(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps, cfg := msDependencies(t, []string{"setvel1", "position1", "orientation1"}) - b, err := createSensorBase(ctx, deps, cfg, logger) - test.That(t, err, test.ShouldBeNil) - sb, ok := b.(*sensorBase) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - test.That(t, sb.position.Name().ShortName(), test.ShouldResemble, "position1") - headingOri, headingSupported, err := sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - test.That(t, headingOri, test.ShouldEqual, orientationValue) - test.That(t, headingOri, test.ShouldNotEqual, compassValue) - - depsNoPos, cfgNoPos := msDependencies(t, []string{"setvel1"}) - bNoPos, err := createSensorBase(ctx, depsNoPos, cfgNoPos, logger) - test.That(t, err, test.ShouldBeNil) - sbNoPos, ok := bNoPos.(*sensorBase) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - headingZero, headingSupported, err := sbNoPos.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeFalse) - test.That(t, headingZero, test.ShouldEqual, 0) - t.Run("Test canceling a sensor controlled MoveStraight", func(t *testing.T) { - // flaky test, will see behavior after RSDK-6164 - t.Skip() - cancelCtx, cancel := context.WithCancel(ctx) - wg := sync.WaitGroup{} - wg.Add(1) - utils.PanicCapturingGo(func() { - defer wg.Done() - err := sb.MoveStraight(cancelCtx, 100, 100, nil) - test.That(t, err, test.ShouldBeNil) - }) - time.Sleep(4 * time.Second) - cancel() - wg.Wait() - }) - t.Run("Test canceling a sensor controlled MoveStraight due to calling another running api", func(t *testing.T) { - // flaky test, will see behavior after RSDK-6164 - t.Skip() - wg := sync.WaitGroup{} - wg.Add(1) - utils.PanicCapturingGo(func() { - defer wg.Done() - err := sb.MoveStraight(ctx, 100, 100, nil) - test.That(t, err, test.ShouldBeNil) - }) - time.Sleep(2 * time.Second) - err := sb.SetPower(context.Background(), r3.Vector{}, r3.Vector{}, nil) - test.That(t, err, test.ShouldBeNil) - wg.Wait() - }) - t.Run("Test not including a position ms will use the non controlled MoveStraight", func(t *testing.T) { - // the injected base will return nil instead of blocking - err := sbNoPos.MoveStraight(ctx, 100, 100, nil) - test.That(t, err, test.ShouldBeNil) - }) - t.Run("Test heading error wraps", func(t *testing.T) { - // orientation configured, so update the value for testing - orientationValue = 179 - headingOri, headingSupported, err := sb.headingFunc(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingSupported, test.ShouldBeTrue) - // validate the orientation updated - test.That(t, headingOri, test.ShouldEqual, 179) - - // test -179 -> 179 results in a small error - headingErr, err := sb.calcHeadingControl(ctx, -179) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingErr, test.ShouldEqual, 2) - - // test full circle results in 0 error - headingErr2, err := sb.calcHeadingControl(ctx, 360+179) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingErr2, test.ShouldEqual, 0) - for i := -720; i <= 720; i += 30 { - headingErr, err := sb.calcHeadingControl(ctx, float64(i)) - test.That(t, err, test.ShouldBeNil) - test.That(t, headingErr, test.ShouldBeBetweenOrEqual, -180, 180) - } - orientationValue = defaultOrientationValue - }) -} diff --git a/components/base/sensorcontrolled/spin.go b/components/base/sensorcontrolled/spin.go deleted file mode 100644 index 9d984e49844..00000000000 --- a/components/base/sensorcontrolled/spin.go +++ /dev/null @@ -1,160 +0,0 @@ -package sensorcontrolled - -import ( - "context" - "errors" - "math" - "time" -) - -const ( - increment = 0.01 // angle fraction multiplier to check - oneTurn = 360.0 - maxSlowDownAng = 30. // maximum angle from goal for spin to begin breaking - slowDownAngGain = 0.1 // Use the final 10% of the requested spin to slow down - boundCheckTarget = 1. // error threshold for spin -) - -// Spin commands a base to turn about its center at an angular speed and for a specific angle. -// When controls are enabled, Spin polls the provided orientation movement sensor and corrects -// any error between the desired degsPerSec and the actual degsPerSec using a PID control loop. -// Spin also monitors the angleDeg and stops the base when the goal angle is reached. -func (sb *sensorBase) Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - sb.opMgr.CancelRunning(ctx) - ctx, done := sb.opMgr.New(ctx) - defer done() - - // If an orientation movement sensor or controls are not configured, we cannot use this Spin method. - // Instead we need to use the Spin method of the base that the sensorBase wraps. - // If there is no valid velocity sensor, there won't be a controlLoopConfig. - if len(sb.controlLoopConfig.Blocks) == 0 { - sb.logger.CWarnf(ctx, "control parameters not configured, using %v's Spin method", sb.controlledBase.Name().ShortName()) - return sb.controlledBase.Spin(ctx, angleDeg, degsPerSec, extra) - } - - prevAngle, spinSupported, err := sb.headingFunc(ctx) - if err != nil { - return err - } - - if !spinSupported { - sb.logger.CWarn(ctx, "orientation movement sensor not configured, using %v's spin method", sb.controlledBase.Name().ShortName()) - if sb.loop != nil { - sb.loop.Pause() - } - return sb.controlledBase.Spin(ctx, angleDeg, degsPerSec, extra) - } - - // make sure the control loop is enabled - if sb.loop == nil { - if err := sb.startControlLoop(); err != nil { - return err - } - } - sb.loop.Resume() - var angErr float64 - prevMovedAng := 0. - - // to keep the signs simple, ensure degsPerSec is positive and let angleDeg handle the direction of the spin - if degsPerSec < 0 { - angleDeg = -angleDeg - degsPerSec = -degsPerSec - } - slowDownAng := calcSlowDownAng(angleDeg) - - ticker := time.NewTicker(time.Duration(1000./sb.controlLoopConfig.Frequency) * time.Millisecond) - defer ticker.Stop() - - // timeout duration is a multiplier times the expected time to perform a movement - spinTimeEst := time.Duration(int(time.Second) * int(math.Abs(angleDeg/degsPerSec))) - startTime := time.Now() - timeOut := 5 * spinTimeEst - if timeOut < 10*time.Second { - timeOut = 10 * time.Second - } - - for { - if err := ctx.Err(); err != nil { - ticker.Stop() - return err - } - - select { - case <-ctx.Done(): - // do not return context canceled errors, just log them - if errors.Is(ctx.Err(), context.Canceled) { - sb.logger.Error(ctx.Err()) - return nil - } - return err - case <-ticker.C: - - currYaw, _, err := sb.headingFunc(ctx) - if err != nil { - return err - } - angErr, prevMovedAng = getAngError(currYaw, prevAngle, prevMovedAng, angleDeg) - - if math.Abs(angErr) < boundCheckTarget { - return sb.Stop(ctx, nil) - } - angVel := calcAngVel(angErr, degsPerSec, slowDownAng) - - if err := sb.updateControlConfig(ctx, 0, angVel); err != nil { - return err - } - - // track the previous angle to compute how much we moved with each iteration - prevAngle = currYaw - - // check if the duration of the spin exceeds the expected length of the spin - if time.Since(startTime) > timeOut { - sb.logger.CWarn(ctx, "exceeded time for Spin call, stopping base") - return sb.Stop(ctx, nil) - } - } - } -} - -// calcSlowDownAng computes the angle at which the spin should begin to slow down. -// This helps to prevent overshoot when reaching the goal and reduces the jerk on the robot when the spin is complete. -// This term should always be positive. -func calcSlowDownAng(angleDeg float64) float64 { - return math.Min(math.Abs(angleDeg)*slowDownAngGain, maxSlowDownAng) -} - -// calcAngVel computes the desired angular velocity based on how far the base is from reaching the goal. -func calcAngVel(angErr, degsPerSec, slowDownAng float64) float64 { - // have the velocity slow down when appoaching the goal. Otherwise use the desired velocity - angVel := angErr * degsPerSec / slowDownAng - if math.Abs(angVel) > degsPerSec { - return degsPerSec * sign(angVel) - } - return angVel -} - -// getAngError computes the current distance the spin has moved and returns how much further the base must move to reach the goal. -func getAngError(currYaw, prevAngle, prevMovedAng, desiredAngle float64) (float64, float64) { - // use initial angle to get the current angle the spin has moved - angMoved := getMovedAng(prevAngle, currYaw, prevMovedAng) - - // compute the error - errAng := (desiredAngle - angMoved) - - return errAng, angMoved -} - -// getMovedAng tracks how much the angle has moved between each sensor update. -// This allows us to convert a bounded angle(0 to 360 or -180 to 180) into the raw angle traveled. -func getMovedAng(prevAngle, currAngle, angMoved float64) float64 { - // the angle changed from 180 to -180. this means we are spinning in the negative direction - if currAngle-prevAngle < -300 { - return angMoved + currAngle - prevAngle + 360 - } - // the angle changed from -180 to 180 - if currAngle-prevAngle > 300 { - return angMoved + currAngle - prevAngle - 360 - } - // add the change in angle to the position - return angMoved + currAngle - prevAngle -} diff --git a/components/base/sensorcontrolled/velocities.go b/components/base/sensorcontrolled/velocities.go deleted file mode 100644 index c30987c7908..00000000000 --- a/components/base/sensorcontrolled/velocities.go +++ /dev/null @@ -1,146 +0,0 @@ -package sensorcontrolled - -import ( - "context" - "math" - - "github.com/golang/geo/r3" - - "go.viam.com/rdk/control" -) - -// SetVelocity commands a base to move at the requested linear and angular velocites. -// When controls are enabled, SetVelocity polls the provided velocity movement sensor and corrects -// any error between the desired velocity and the actual velocity using a PID control loop. -func (sb *sensorBase) SetVelocity( - ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}, -) error { - sb.opMgr.CancelRunning(ctx) - ctx, done := sb.opMgr.New(ctx) - defer done() - - if len(sb.controlLoopConfig.Blocks) == 0 { - sb.logger.CWarnf(ctx, "control parameters not configured, using %v's SetVelocity method", sb.controlledBase.Name().ShortName()) - return sb.controlledBase.SetVelocity(ctx, linear, angular, extra) - } - - // make sure the control loop is enabled - if sb.loop == nil { - if err := sb.startControlLoop(); err != nil { - return err - } - } - sb.loop.Resume() - - // convert linear.Y mmPerSec to mPerSec, angular.Z is degPerSec - if err := sb.updateControlConfig(ctx, linear.Y/1000.0, angular.Z); err != nil { - return err - } - - return nil -} - -// startControlLoop uses the control config to initialize a control loop and store it on the sensor controlled base struct. -// The sensor base is the controllable interface that implements State and GetState called from the endpoint block of the control loop. -func (sb *sensorBase) startControlLoop() error { - loop, err := control.NewLoop(sb.logger, sb.controlLoopConfig, sb) - if err != nil { - return err - } - if err := loop.Start(); err != nil { - return err - } - sb.loop = loop - - return nil -} - -func (sb *sensorBase) setupControlLoop(linear, angular control.PIDConfig) error { - // set the necessary options for a sensorcontrolled base - options := control.Options{ - SensorFeedback2DVelocityControl: true, - LoopFrequency: 10, - ControllableType: "base_name", - } - - // check if either linear or angular need to be tuned - if linear.NeedsAutoTuning() || angular.NeedsAutoTuning() { - options.NeedsAutoTuning = true - } - - // combine linear and angular back into one control.PIDConfig, with linear first - pidVals := []control.PIDConfig{linear, angular} - - // fully set up the control config based on the provided options - pl, err := control.SetupPIDControlConfig(pidVals, sb.Name().ShortName(), options, sb, sb.logger) - if err != nil { - return err - } - - sb.controlLoopConfig = pl.ControlConf - sb.loop = pl.ControlLoop - sb.blockNames = pl.BlockNames - - return nil -} - -func (sb *sensorBase) updateControlConfig( - ctx context.Context, linearValue, angularValue float64, -) error { - // set linear setpoint config - if err := control.UpdateConstantBlock(ctx, sb.blockNames[control.BlockNameConstant][0], linearValue, sb.loop); err != nil { - return err - } - - // set angular setpoint config - if err := control.UpdateConstantBlock(ctx, sb.blockNames[control.BlockNameConstant][1], angularValue, sb.loop); err != nil { - return err - } - - return nil -} - -func sign(x float64) float64 { // A quick helper function - if math.Signbit(x) { - return -1.0 - } - return 1.0 -} - -// SetState is called in endpoint.go of the controls package by the control loop -// instantiated in this file. It is a helper function to call the sensor-controlled base's -// SetVelocity from within that package. -func (sb *sensorBase) SetState(ctx context.Context, state []*control.Signal) error { - sb.mu.Lock() - defer sb.mu.Unlock() - - if sb.loop != nil && !sb.loop.Running() { - return nil - } - - sb.logger.CDebug(ctx, "setting state") - linvel := state[0].GetSignalValueAt(0) - // multiply by the direction of the linear velocity so that angular direction - // (cw/ccw) doesn't switch when the base is moving backwards - angvel := (state[1].GetSignalValueAt(0) * sign(linvel)) - - return sb.controlledBase.SetPower(ctx, r3.Vector{Y: linvel}, r3.Vector{Z: angvel}, nil) -} - -// State is called in endpoint.go of the controls package by the control loop -// instantiated in this file. It is a helper function to call the sensor-controlled base's -// movementsensor and insert its LinearVelocity and AngularVelocity values -// in the signal in the control loop's thread in the endpoint code. -func (sb *sensorBase) State(ctx context.Context) ([]float64, error) { - sb.logger.CDebug(ctx, "getting state") - linvel, err := sb.velocities.LinearVelocity(ctx, nil) - if err != nil { - return []float64{}, err - } - - angvel, err := sb.velocities.AngularVelocity(ctx, nil) - if err != nil { - return []float64{}, err - } - return []float64{linvel.Y, angvel.Z}, nil -} diff --git a/components/base/server.go b/components/base/server.go deleted file mode 100644 index 132d5e5d1d2..00000000000 --- a/components/base/server.go +++ /dev/null @@ -1,177 +0,0 @@ -// Package base contains a gRPC based arm service server. -package base - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/base/v1" - - "go.viam.com/rdk/operation" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// serviceServer implements the BaseService from base.proto. -type serviceServer struct { - pb.UnimplementedBaseServiceServer - coll resource.APIResourceCollection[Base] -} - -// NewRPCServiceServer constructs a base gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Base]) interface{} { - return &serviceServer{coll: coll} -} - -// MoveStraight moves a robot's base in a straight line by a given distance, expressed in millimeters -// and a given speed, expressed in millimeters per second. -func (s *serviceServer) MoveStraight( - ctx context.Context, - req *pb.MoveStraightRequest, -) (*pb.MoveStraightResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - base, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - - err = base.MoveStraight(ctx, int(req.GetDistanceMm()), req.GetMmPerSec(), req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.MoveStraightResponse{}, nil -} - -// Spin spins a robot's base by an given angle, expressed in degrees, and a given -// angular speed, expressed in degrees per second. -func (s *serviceServer) Spin( - ctx context.Context, - req *pb.SpinRequest, -) (*pb.SpinResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - base, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - - err = base.Spin(ctx, req.GetAngleDeg(), req.GetDegsPerSec(), req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.SpinResponse{}, nil -} - -func (s *serviceServer) SetPower( - ctx context.Context, - req *pb.SetPowerRequest, -) (*pb.SetPowerResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - base, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - - err = base.SetPower( - ctx, - protoutils.ConvertVectorProtoToR3(req.GetLinear()), - protoutils.ConvertVectorProtoToR3(req.GetAngular()), - req.Extra.AsMap(), - ) - if err != nil { - return nil, err - } - return &pb.SetPowerResponse{}, nil -} - -func (s *serviceServer) SetVelocity( - ctx context.Context, - req *pb.SetVelocityRequest, -) (*pb.SetVelocityResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - base, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - - err = base.SetVelocity( - ctx, - protoutils.ConvertVectorProtoToR3(req.GetLinear()), - protoutils.ConvertVectorProtoToR3(req.GetAngular()), - req.Extra.AsMap(), - ) - if err != nil { - return nil, err - } - return &pb.SetVelocityResponse{}, nil -} - -// Stop stops a robot's base. -func (s *serviceServer) Stop( - ctx context.Context, - req *pb.StopRequest, -) (*pb.StopResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - base, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - if err = base.Stop(ctx, req.Extra.AsMap()); err != nil { - return nil, err - } - return &pb.StopResponse{}, nil -} - -// IsMoving queries of a component is in motion. -func (s *serviceServer) IsMoving(ctx context.Context, req *pb.IsMovingRequest) (*pb.IsMovingResponse, error) { - base, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - moving, err := base.IsMoving(ctx) - if err != nil { - return nil, err - } - return &pb.IsMovingResponse{IsMoving: moving}, nil -} - -func (s *serviceServer) GetProperties( - ctx context.Context, - req *pb.GetPropertiesRequest, -) (*pb.GetPropertiesResponse, error) { - baseName := req.GetName() - base, err := s.coll.Resource(baseName) - if err != nil { - return nil, err - } - - features, err := base.Properties(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return PropertiesToProtoResponse(features) -} - -func (s *serviceServer) GetGeometries(ctx context.Context, req *commonpb.GetGeometriesRequest) (*commonpb.GetGeometriesResponse, error) { - res, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - geometries, err := res.Geometries(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &commonpb.GetGeometriesResponse{Geometries: spatialmath.NewGeometriesToProto(geometries)}, nil -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - base, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, base, req) -} diff --git a/components/base/server_test.go b/components/base/server_test.go deleted file mode 100644 index 8abef1f1f40..00000000000 --- a/components/base/server_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package base_test - -import ( - "context" - "testing" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/base/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errMoveStraight = errors.New("critical failure in MoveStraight") - errSpinFailed = errors.New("critical failure in Spin") - errPropertiesFailed = errors.New("critical failure in Properties") - errStopFailed = errors.New("critical failure in Stop") -) - -func newServer() (pb.BaseServiceServer, *inject.Base, *inject.Base, error) { - workingBase := &inject.Base{} - brokenBase := &inject.Base{} - bases := map[resource.Name]base.Base{ - base.Named(testBaseName): workingBase, - base.Named(failBaseName): brokenBase, - } - baseSvc, err := resource.NewAPIResourceCollection(base.API, bases) - if err != nil { - return nil, nil, nil, err - } - return base.NewRPCServiceServer(baseSvc).(pb.BaseServiceServer), workingBase, brokenBase, nil -} - -func TestServer(t *testing.T) { - server, workingBase, brokenBase, err := newServer() - test.That(t, err, test.ShouldBeNil) - - t.Run("MoveStraight", func(t *testing.T) { - speed := 2.3 - distance := int64(1) - - // on successful move straight - workingBase.MoveStraightFunc = func( - ctx context.Context, distanceMm int, - mmPerSec float64, - extra map[string]interface{}, - ) error { - return nil - } - req := &pb.MoveStraightRequest{ - Name: testBaseName, - MmPerSec: speed, - DistanceMm: distance, - } - resp, err := server.MoveStraight(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, &pb.MoveStraightResponse{}) - - // on failing move straight - brokenBase.MoveStraightFunc = func( - ctx context.Context, distanceMm int, - mmPerSec float64, - extra map[string]interface{}, - ) error { - return errMoveStraight - } - req = &pb.MoveStraightRequest{ - Name: failBaseName, - MmPerSec: speed, - DistanceMm: distance, - } - resp, err = server.MoveStraight(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errMoveStraight) - - // failure on unfound base - req = &pb.MoveStraightRequest{ - Name: "dne", - MmPerSec: speed, - DistanceMm: distance, - } - resp, err = server.MoveStraight(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("Spin", func(t *testing.T) { - angSpeed := 45.0 - angle := 90.0 - // on successful spin - workingBase.SpinFunc = func( - ctx context.Context, - angleDeg, degsPerSec float64, - extra map[string]interface{}, - ) error { - return nil - } - req := &pb.SpinRequest{ - Name: testBaseName, - DegsPerSec: angSpeed, - AngleDeg: angle, - } - resp, err := server.Spin(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, &pb.SpinResponse{}) - - // on failing spin - brokenBase.SpinFunc = func( - ctx context.Context, - angleDeg, degsPerSec float64, - extra map[string]interface{}, - ) error { - return errSpinFailed - } - req = &pb.SpinRequest{ - Name: failBaseName, - DegsPerSec: angSpeed, - AngleDeg: angle, - } - resp, err = server.Spin(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errSpinFailed) - - // failure on unfound base - req = &pb.SpinRequest{ - Name: "dne", - DegsPerSec: angSpeed, - AngleDeg: angle, - } - resp, err = server.Spin(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("Properties", func(t *testing.T) { - turnRadius := 0.1 - width := 0.2 - // on a successful get properties - workingBase.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{ - TurningRadiusMeters: turnRadius, - WidthMeters: width, - }, nil - } - req := &pb.GetPropertiesRequest{Name: testBaseName} - resp, err := server.GetProperties(context.Background(), req) // TODO (rh) rename server to bServer after review - test.That(t, resp, test.ShouldResemble, &pb.GetPropertiesResponse{WidthMeters: width, TurningRadiusMeters: turnRadius}) - test.That(t, err, test.ShouldBeNil) - - // on a failing get properties - brokenBase.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{}, errPropertiesFailed - } - req = &pb.GetPropertiesRequest{Name: failBaseName} - resp, err = server.GetProperties(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errPropertiesFailed) - - // failure on base not found - req = &pb.GetPropertiesRequest{Name: "dne"} - resp, err = server.GetProperties(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("Stop", func(t *testing.T) { - // on successful stop - workingBase.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - req := &pb.StopRequest{Name: testBaseName} - resp, err := server.Stop(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, &pb.StopResponse{}) - - // on failing stop - brokenBase.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopFailed - } - req = &pb.StopRequest{Name: failBaseName} - resp, err = server.Stop(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errStopFailed) - - // failure on base not found - req = &pb.StopRequest{Name: "dne"} - resp, err = server.Stop(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) -} diff --git a/components/base/verify_main_test.go b/components/base/verify_main_test.go deleted file mode 100644 index 82b3453703b..00000000000 --- a/components/base/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package base - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/base/wheeled/verify_main_test.go b/components/base/wheeled/verify_main_test.go deleted file mode 100644 index d4ab01d0943..00000000000 --- a/components/base/wheeled/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package wheeled - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/base/wheeled/wheeled_base.go b/components/base/wheeled/wheeled_base.go deleted file mode 100644 index 14ec1f8cf70..00000000000 --- a/components/base/wheeled/wheeled_base.go +++ /dev/null @@ -1,514 +0,0 @@ -// Package wheeled implements some bases, like a wheeled base. -package wheeled - -/* - The Viam wheeled package implements a wheeled robot base with differential drive control. The base must have an equal - number of motors on its left and right sides. The base's width and wheel circumference dimensions are required to - compute wheel speeds to move the base straight distances or spin to headings at the desired input speeds. A spin slip - factor acts as a multiplier to adjust power delivery to the wheels when each side of the base is undergoing unequal - friction because of the surface it is moving on. - Any motors can be used for the base motors (encoded, un-encoded, steppers, servos) as long as they update their position - continuously (not limited to 0-360 or any other domain). - - Adding a movementsensor that supports Orientation provides feedback to a Spin command to correct the heading. As of - June 2023, this feature is experimental. - - Configuring a base with a frame will create a kinematic base that can be used by Viam's motion service to plan paths - when a SLAM service is also present. As of June 2023 This feature is experimental. - Example Config: - { - "name": "myBase", - "type": "base", - "model": "wheeled", - "attributes": { - "right": ["right1", "right2"], - "left": ["left1", "left2"], - "spin_slip_factor": 1.76, - "wheel_circumference_mm": 217, - "width_mm": 260, - }, - "depends_on": ["left1", "left2", "right1", "right2", "local"], - }, -*/ - -import ( - "context" - "fmt" - "math" - "sync" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.uber.org/multierr" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -// Model is the name of the wheeled model of a base component. -var Model = resource.DefaultModelFamily.WithModel("wheeled") - -// Config is how you configure a wheeled base. -type Config struct { - WidthMM int `json:"width_mm"` - WheelCircumferenceMM int `json:"wheel_circumference_mm"` - SpinSlipFactor float64 `json:"spin_slip_factor,omitempty"` - Left []string `json:"left"` - Right []string `json:"right"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - - if cfg.WidthMM == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "width_mm") - } - - if cfg.WheelCircumferenceMM == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "wheel_circumference_mm") - } - - if len(cfg.Left) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "left") - } - if len(cfg.Right) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "right") - } - - if len(cfg.Left) != len(cfg.Right) { - return nil, resource.NewConfigValidationError(path, - fmt.Errorf("left and right need to have the same number of motors, not %d vs %d", - len(cfg.Left), len(cfg.Right))) - } - - deps = append(deps, cfg.Left...) - deps = append(deps, cfg.Right...) - - return deps, nil -} - -func init() { - resource.RegisterComponent(base.API, Model, resource.Registration[base.Base, *Config]{Constructor: createWheeledBase}) -} - -type wheeledBase struct { - resource.Named - widthMm int - wheelCircumferenceMm int - spinSlipFactor float64 - geometries []spatialmath.Geometry - - left []motor.Motor - right []motor.Motor - allMotors []motor.Motor - - opMgr *operation.SingleOperationManager - logger logging.Logger - - mu sync.Mutex - name string -} - -// Reconfigure reconfigures the base atomically and in place. -func (wb *wheeledBase) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - wb.mu.Lock() - defer wb.mu.Unlock() - - wb.geometries = []spatialmath.Geometry{} - if conf.Frame != nil { - frame, err := conf.Frame.ParseConfig() - if err != nil { - return err - } - if geom := frame.Geometry(); geom != nil { - wb.geometries = append(wb.geometries, geom) - } - } - - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - if newConf.SpinSlipFactor == 0 { - wb.spinSlipFactor = 1 - } else { - wb.spinSlipFactor = newConf.SpinSlipFactor - } - - updateMotors := func(curr []motor.Motor, fromConfig []string, whichMotor string) ([]motor.Motor, error) { - newMotors := make([]motor.Motor, 0) - if len(curr) != len(fromConfig) { - for _, name := range fromConfig { - select { - case <-ctx.Done(): - return newMotors, rdkutils.NewBuildTimeoutError(wb.Name().String()) - default: - } - m, err := motor.FromDependencies(deps, name) - if err != nil { - return newMotors, errors.Wrapf(err, "no %s motor named (%s)", whichMotor, name) - } - newMotors = append(newMotors, m) - } - } else { - // Compare each element of the slices - for i := range curr { - select { - case <-ctx.Done(): - return newMotors, rdkutils.NewBuildTimeoutError(wb.Name().String()) - default: - } - if (curr)[i].Name().String() != (fromConfig)[i] { - for _, name := range fromConfig { - m, err := motor.FromDependencies(deps, name) - if err != nil { - return newMotors, errors.Wrapf(err, "no %s motor named (%s)", whichMotor, name) - } - newMotors = append(newMotors, m) - } - break - } - } - } - return newMotors, nil - } - - left, err := updateMotors(wb.left, newConf.Left, "left") - wb.left = left - if err != nil { - return err - } - right, err := updateMotors(wb.right, newConf.Right, "right") - wb.right = right - if err != nil { - return err - } - - wb.allMotors = append(wb.allMotors, wb.left...) - wb.allMotors = append(wb.allMotors, wb.right...) - - if wb.widthMm != newConf.WidthMM { - wb.widthMm = newConf.WidthMM - } - - if wb.wheelCircumferenceMm != newConf.WheelCircumferenceMM { - wb.wheelCircumferenceMm = newConf.WheelCircumferenceMM - } - - return nil -} - -// createWheeledBase returns a new wheeled base defined by the given config. -func createWheeledBase( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (base.Base, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - wb := wheeledBase{ - Named: conf.ResourceName().AsNamed(), - widthMm: newConf.WidthMM, - wheelCircumferenceMm: newConf.WheelCircumferenceMM, - spinSlipFactor: newConf.SpinSlipFactor, - opMgr: operation.NewSingleOperationManager(), - logger: logger, - name: conf.Name, - } - - if err := wb.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return &wb, nil -} - -// Spin commands a base to turn about its center at a angular speed and for a specific angle. -func (wb *wheeledBase) Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - ctx, done := wb.opMgr.New(ctx) - defer done() - wb.logger.CDebugf(ctx, "received a Spin with angleDeg:%.2f, degsPerSec:%.2f", angleDeg, degsPerSec) - - // Stop the motors if the speed is 0 - if math.Abs(degsPerSec) < 0.0001 { - err := wb.Stop(ctx, nil) - if err != nil { - return errors.Errorf("error when trying to spin at a speed of 0: %v", err) - } - return err - } - - // Spin math - rpm, revolutions := wb.spinMath(angleDeg, degsPerSec) - - return wb.runAllGoFor(ctx, -rpm, revolutions, rpm, revolutions) -} - -// MoveStraight commands a base to drive forward or backwards at a linear speed and for a specific distance. -func (wb *wheeledBase) MoveStraight(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - wb.logger.CDebugf(ctx, "received a MoveStraight with distanceMM:%d, mmPerSec:%.2f", distanceMm, mmPerSec) - - // Stop the motors if the speed or distance are 0 - if math.Abs(mmPerSec) < 0.0001 || distanceMm == 0 { - err := wb.Stop(ctx, nil) - if err != nil { - return errors.Errorf("error when trying to move straight at a speed and/or distance of 0: %v", err) - } - return err - } - - // Straight math - rpm, rotations := wb.straightDistanceToMotorInputs(distanceMm, mmPerSec) - - // start new operation after all calculations are made - ctx, done := wb.opMgr.New(ctx) - defer done() - return wb.runAllGoFor(ctx, rpm, rotations, rpm, rotations) -} - -// runAllGoFor executes `motor.GoFor` commands in parallel for left and right motors, -// with specified speeds and rotations and stops the base if an error occurs. -// All callers must register an operation via `wb.opMgr.New` to ensure the left and right motors -// receive consistent instructions. -func (wb *wheeledBase) runAllGoFor(ctx context.Context, leftRPM, leftRotations, rightRPM, rightRotations float64) error { - goForFuncs := func() []rdkutils.SimpleFunc { - ret := []rdkutils.SimpleFunc{} - - // These reads of `wb.left` and `wb.right` can race with `Reconfigure`. - wb.mu.Lock() - defer wb.mu.Unlock() - for _, m := range wb.left { - motor := m - ret = append(ret, func(ctx context.Context) error { return motor.GoFor(ctx, leftRPM, leftRotations, nil) }) - } - - for _, m := range wb.right { - motor := m - ret = append(ret, func(ctx context.Context) error { return motor.GoFor(ctx, rightRPM, rightRotations, nil) }) - } - return ret - }() - - if _, err := rdkutils.RunInParallel(ctx, goForFuncs); err != nil { - err := multierr.Combine(err, wb.Stop(ctx, nil)) - // Ignore the context canceled error - this occurs when the base is stopped by the user. - if !errors.Is(err, context.Canceled) { - return err - } - } - return nil -} - -// differentialDrive takes forward and left direction inputs from a first person -// perspective on a 2D plane and converts them to left and right motor powers. negative -// forward means backward and negative left means right. -func (wb *wheeledBase) differentialDrive(forward, left float64) (float64, float64) { - if forward < 0 { - // Mirror the forward turning arc if we go in reverse - leftMotor, rightMotor := wb.differentialDrive(-forward, left) - return -leftMotor, -rightMotor - } - - // convert to polar coordinates - r := math.Hypot(forward, left) - t := math.Atan2(left, forward) - - // rotate by 45 degrees - t += math.Pi / 4 - if t == 0 { - // HACK: Fixes a weird ATAN2 corner case. Ensures that when motor that is on the - // same side as the turn has the same power when going left and right. Without - // this, the right motor has ZERO power when going forward/backward turning - // right, when it should have at least some very small value. - t += 1.224647e-16 / 2 - } - - // convert to cartesian - leftMotor := r * math.Cos(t) - rightMotor := r * math.Sin(t) - - // rescale the new coords - leftMotor *= math.Sqrt(2) - rightMotor *= math.Sqrt(2) - - // clamp to -1/+1 - leftMotor = math.Max(-1, math.Min(leftMotor, 1)) - rightMotor = math.Max(-1, math.Min(rightMotor, 1)) - - return leftMotor, rightMotor -} - -// SetVelocity commands the base to move at the input linear and angular velocities. -func (wb *wheeledBase) SetVelocity(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - wb.logger.CDebugf(ctx, - "received a SetVelocity with linear.X: %.2f, linear.Y: %.2f linear.Z: %.2f(mmPerSec),"+ - " angular.X: %.2f, angular.Y: %.2f, angular.Z: %.2f", - linear.X, linear.Y, linear.Z, angular.X, angular.Y, angular.Z) - - // check if we're receiving a vector of magnitude zero (all components zero) from linear and angular velcoity - // and interpret that as a signal to stop the base - if linear.Norm() == 0 && angular.Norm() == 0 { - wb.logger.CDebug(ctx, "received a SetVelocity command of linear 0,0,0, and angular 0,0,0, stopping base") - return wb.Stop(ctx, nil) - } - - leftRPM, rightRPM := wb.velocityMath(linear.Y, angular.Z) - // Passing zero revolutions to `motor.GoFor` will have the motor run until - // interrupted. Moreover, `motor.GoFor` will return immediately when given zero revolutions. - const numRevolutions = 0 - - // start new operation after all calculations are made - ctx, done := wb.opMgr.New(ctx) - defer done() - return wb.runAllGoFor(ctx, leftRPM, numRevolutions, rightRPM, numRevolutions) -} - -// SetPower commands the base motors to run at powers corresponding to input linear and angular powers. -func (wb *wheeledBase) SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - wb.opMgr.CancelRunning(ctx) - - wb.logger.CDebugf(ctx, - "received a SetPower with linear.X: %.2f, linear.Y: %.2f linear.Z: %.2f,"+ - " angular.X: %.2f, angular.Y: %.2f, angular.Z: %.2f", - linear.X, linear.Y, linear.Z, angular.X, angular.Y, angular.Z) - - // check if we're receiving a vector of magnitude zero (all components zero) from linear and angular velcoity - // and interpret that as a signal to stop the base - if linear.Norm() == 0 && angular.Norm() == 0 { - wb.logger.CDebug(ctx, "received a SetPower command of linear 0,0,0, and angular 0,0,0, stopping base") - return wb.Stop(ctx, nil) - } - - lPower, rPower := wb.differentialDrive(linear.Y, angular.Z) - - // Send motor commands - setPowerFuncs := func() []rdkutils.SimpleFunc { - ret := []rdkutils.SimpleFunc{} - - wb.mu.Lock() - defer wb.mu.Unlock() - for _, m := range wb.left { - motor := m - ret = append(ret, func(ctx context.Context) error { return motor.SetPower(ctx, lPower, extra) }) - } - - for _, m := range wb.right { - motor := m - ret = append(ret, func(ctx context.Context) error { return motor.SetPower(ctx, rPower, extra) }) - } - return ret - }() - - if _, err := rdkutils.RunInParallel(ctx, setPowerFuncs); err != nil { - return multierr.Combine(err, wb.Stop(ctx, nil)) - } - return nil -} - -// returns rpm, revolutions for a spin motion. -func (wb *wheeledBase) spinMath(angleDeg, degsPerSec float64) (float64, float64) { - wheelTravel := wb.spinSlipFactor * float64(wb.widthMm) * math.Pi * (angleDeg / 360.0) - revolutions := wheelTravel / float64(wb.wheelCircumferenceMm) - revolutions = math.Abs(revolutions) - - // RPM = revolutions (unit) * deg/sec * (1 rot / 2pi deg) * (60 sec / 1 min) = rot/min - // RPM = (revolutions (unit) / angleDeg) * degPerSec * 60 - rpm := (revolutions / angleDeg) * degsPerSec * 60 - - return rpm, revolutions -} - -// calcualtes wheel rpms from overall base linear and angular movement velocity inputs. -func (wb *wheeledBase) velocityMath(mmPerSec, degsPerSec float64) (float64, float64) { - // Base calculations - v := mmPerSec - r := float64(wb.wheelCircumferenceMm) / (2.0 * math.Pi) - l := float64(wb.widthMm) - - w0 := degsPerSec / 180 * math.Pi - wL := (v / r) - (l * w0 / (2 * r)) - wR := (v / r) + (l * w0 / (2 * r)) - - // RPM = revolutions (unit) * deg/sec * (1 rot / 2pi deg) * (60 sec / 1 min) = rot/min - rpmL := (wL / (2 * math.Pi)) * 60 - rpmR := (wR / (2 * math.Pi)) * 60 - - return rpmL, rpmR -} - -// calculates the motor revolutions and speeds that correspond to the required distance and linear speeds. -func (wb *wheeledBase) straightDistanceToMotorInputs(distanceMm int, mmPerSec float64) (float64, float64) { - // takes in base speed and distance to calculate motor rpm and total rotations - rotations := float64(distanceMm) / float64(wb.wheelCircumferenceMm) - - rotationsPerSec := mmPerSec / float64(wb.wheelCircumferenceMm) - rpm := 60 * rotationsPerSec - - return rpm, rotations -} - -// Stop commands the base to stop moving. -func (wb *wheeledBase) Stop(ctx context.Context, extra map[string]interface{}) error { - stopFuncs := func() []rdkutils.SimpleFunc { - ret := []rdkutils.SimpleFunc{} - - wb.mu.Lock() - defer wb.mu.Unlock() - for _, m := range wb.left { - motor := m - ret = append(ret, func(ctx context.Context) error { return motor.Stop(ctx, extra) }) - } - - for _, m := range wb.right { - motor := m - ret = append(ret, func(ctx context.Context) error { return motor.Stop(ctx, extra) }) - } - return ret - }() - - if _, err := rdkutils.RunInParallel(ctx, stopFuncs); err != nil { - return multierr.Combine(err) - } - return nil -} - -func (wb *wheeledBase) IsMoving(ctx context.Context) (bool, error) { - for _, m := range wb.allMotors { - isMoving, _, err := m.IsPowered(ctx, nil) - if err != nil { - return false, err - } - if isMoving { - return true, err - } - } - return false, nil -} - -// Close is called from the client to close the instance of the wheeledBase. -func (wb *wheeledBase) Close(ctx context.Context) error { - return wb.Stop(ctx, nil) -} - -func (wb *wheeledBase) Properties(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{ - TurningRadiusMeters: 0.0, - WidthMeters: float64(wb.widthMm) * 0.001, // convert to meters from mm - WheelCircumferenceMeters: float64(wb.wheelCircumferenceMm) * 0.001, // convert to meters from mm - }, nil -} - -func (wb *wheeledBase) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - return wb.geometries, nil -} diff --git a/components/base/wheeled/wheeled_base_test.go b/components/base/wheeled/wheeled_base_test.go deleted file mode 100644 index 91a2ca63887..00000000000 --- a/components/base/wheeled/wheeled_base_test.go +++ /dev/null @@ -1,501 +0,0 @@ -package wheeled - -import ( - "context" - "errors" - "math" - "testing" - "time" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func createFakeMotor() motor.Motor { - return &inject.Motor{ - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return errors.New("stop error") }, - } -} - -func newTestCfg() resource.Config { - return resource.Config{ - Name: "test", - API: base.API, - Model: resource.Model{Name: "wheeled_base"}, - ConvertedAttributes: &Config{ - WidthMM: 100, - WheelCircumferenceMM: 1000, - Left: []string{"fl-m", "bl-m"}, - Right: []string{"fr-m", "br-m"}, - }, - } -} - -func createMockDeps(t *testing.T) resource.Dependencies { - t.Helper() - deps := make(resource.Dependencies) - deps[motor.Named("right")] = createFakeMotor() - deps[motor.Named("left")] = createFakeMotor() - return deps -} - -func fakeMotorDependencies(t *testing.T, deps []string) resource.Dependencies { - t.Helper() - logger := logging.NewTestLogger(t) - - result := make(resource.Dependencies) - for _, dep := range deps { - result[motor.Named(dep)] = &fake.Motor{ - Named: motor.Named(dep).AsNamed(), - MaxRPM: 60, - OpMgr: operation.NewSingleOperationManager(), - Logger: logger, - } - } - return result -} - -func TestWheelBaseMath(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - testCfg := newTestCfg() - deps, err := testCfg.Validate("path", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - motorDeps := fakeMotorDependencies(t, deps) - - newBase, err := createWheeledBase(context.Background(), motorDeps, testCfg, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, newBase, test.ShouldNotBeNil) - wb, ok := newBase.(*wheeledBase) - test.That(t, ok, test.ShouldBeTrue) - - t.Run("basics", func(t *testing.T) { - props, err := wb.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, props.WidthMeters, test.ShouldEqual, 100*0.001) - - geometries, err := wb.Geometries(ctx, nil) - test.That(t, len(geometries), test.ShouldBeZeroValue) - test.That(t, err, test.ShouldBeNil) - - err = wb.SetVelocity(ctx, r3.Vector{X: 0, Y: 10, Z: 0}, r3.Vector{X: 0, Y: 0, Z: 10}, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "RPM that is nearly 0") - - err = wb.SetVelocity(ctx, r3.Vector{X: 0, Y: 100, Z: 0}, r3.Vector{X: 0, Y: 0, Z: 100}, nil) - test.That(t, err, test.ShouldBeNil) - - moving, err := wb.IsMoving(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - - err = wb.SetVelocity(ctx, r3.Vector{X: 0, Y: 0, Z: 0}, r3.Vector{X: 0, Y: 0, Z: 0}, nil) - test.That(t, err, test.ShouldBeNil) - - moving, err = wb.IsMoving(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - - err = wb.SetPower(ctx, r3.Vector{X: 0, Y: 10, Z: 0}, r3.Vector{X: 0, Y: 0, Z: 10}, nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, wb.Stop(ctx, nil), test.ShouldBeNil) - - moving, err = wb.IsMoving(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - - err = wb.SetPower(ctx, r3.Vector{X: 0, Y: 10, Z: 0}, r3.Vector{X: 0, Y: 0, Z: 10}, nil) - test.That(t, err, test.ShouldBeNil) - - err = wb.SetPower(ctx, r3.Vector{X: 0, Y: 0, Z: 0}, r3.Vector{X: 0, Y: 0, Z: 0}, nil) - test.That(t, err, test.ShouldBeNil) - - moving, err = wb.IsMoving(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - }) - - t.Run("math_straight", func(t *testing.T) { - rpm, rotations := wb.straightDistanceToMotorInputs(1000, 1000) - test.That(t, rpm, test.ShouldEqual, 60.0) - test.That(t, rotations, test.ShouldEqual, 1.0) - - rpm, rotations = wb.straightDistanceToMotorInputs(-1000, 1000) - test.That(t, rpm, test.ShouldEqual, 60.0) - test.That(t, rotations, test.ShouldEqual, -1.0) - - rpm, rotations = wb.straightDistanceToMotorInputs(1000, -1000) - test.That(t, rpm, test.ShouldEqual, -60.0) - test.That(t, rotations, test.ShouldEqual, 1.0) - - rpm, rotations = wb.straightDistanceToMotorInputs(-1000, -1000) - test.That(t, rpm, test.ShouldEqual, -60.0) - test.That(t, rotations, test.ShouldEqual, -1.0) - }) - - t.Run("straight no speed", func(t *testing.T) { - err := wb.MoveStraight(ctx, 1000, 0, nil) - test.That(t, err, test.ShouldBeNil) - - err = waitForMotorsToStop(ctx, wb) - test.That(t, err, test.ShouldBeNil) - - for _, m := range wb.allMotors { - isOn, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, isOn, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0.0) - } - }) - - t.Run("straight no distance", func(t *testing.T) { - err := wb.MoveStraight(ctx, 0, 1000, nil) - test.That(t, err, test.ShouldBeNil) - - err = waitForMotorsToStop(ctx, wb) - test.That(t, err, test.ShouldBeNil) - - for _, m := range wb.allMotors { - isOn, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, isOn, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0.0) - } - }) - - t.Run("waitForMotorsToStop", func(t *testing.T) { - err := wb.Stop(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - err = wb.allMotors[0].SetPower(ctx, 1, nil) - test.That(t, err, test.ShouldBeNil) - isOn, powerPct, err := wb.allMotors[0].IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, isOn, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 1.0) - - err = waitForMotorsToStop(ctx, wb) - test.That(t, err, test.ShouldBeNil) - - for _, m := range wb.allMotors { - isOn, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, isOn, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0.0) - } - - err = waitForMotorsToStop(ctx, wb) - test.That(t, err, test.ShouldBeNil) - - for _, m := range wb.allMotors { - isOn, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, isOn, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0.0) - } - }) - - // Spin tests - t.Run("spin math", func(t *testing.T) { - rpms, rotations := wb.spinMath(90, 10) - test.That(t, rpms, test.ShouldAlmostEqual, 0.523, 0.001) - test.That(t, rotations, test.ShouldAlmostEqual, 0.0785, 0.001) - - rpms, rotations = wb.spinMath(-90, 10) - test.That(t, rpms, test.ShouldAlmostEqual, -0.523, 0.001) - test.That(t, rotations, test.ShouldAlmostEqual, 0.0785, 0.001) - - rpms, rotations = wb.spinMath(90, -10) - test.That(t, rpms, test.ShouldAlmostEqual, -0.523, 0.001) - test.That(t, rotations, test.ShouldAlmostEqual, 0.0785, 0.001) - - rpms, rotations = wb.spinMath(-90, -10) - test.That(t, rpms, test.ShouldAlmostEqual, 0.523, 0.001) - test.That(t, rotations, test.ShouldAlmostEqual, 0.0785, 0.001) - - rpms, rotations = wb.spinMath(60, 10) - test.That(t, rpms, test.ShouldAlmostEqual, 0.523, 0.001) - test.That(t, rotations, test.ShouldAlmostEqual, 0.0523, 0.001) - - rpms, rotations = wb.spinMath(30, 10) - test.That(t, rpms, test.ShouldAlmostEqual, 0.523, 0.001) - test.That(t, rotations, test.ShouldAlmostEqual, 0.0261, 0.001) - }) - - // Velocity tests - t.Run("velocity math curved", func(t *testing.T) { - l, r := wb.velocityMath(1000, 10) - test.That(t, l, test.ShouldAlmostEqual, 59.476, 0.01) - test.That(t, r, test.ShouldAlmostEqual, 60.523, 0.01) - - l, r = wb.velocityMath(-1000, -10) - test.That(t, l, test.ShouldAlmostEqual, -59.476, 0.01) - test.That(t, r, test.ShouldAlmostEqual, -60.523, 0.01) - - l, r = wb.velocityMath(1000, -10) - test.That(t, l, test.ShouldAlmostEqual, 60.523, 0.01) - test.That(t, r, test.ShouldAlmostEqual, 59.476, 0.01) - - l, r = wb.velocityMath(-1000, 10) - test.That(t, l, test.ShouldAlmostEqual, -60.523, 0.01) - test.That(t, r, test.ShouldAlmostEqual, -59.476, 0.01) - }) - - t.Run("arc math zero angle", func(t *testing.T) { - l, r := wb.velocityMath(1000, 0) - test.That(t, l, test.ShouldEqual, 60.0) - test.That(t, r, test.ShouldEqual, 60.0) - - l, r = wb.velocityMath(-1000, 0) - test.That(t, l, test.ShouldEqual, -60.0) - test.That(t, r, test.ShouldEqual, -60.0) - }) - - t.Run("differentialDrive", func(t *testing.T) { - var fwdL, fwdR, revL, revR float64 - - // Go straight (↑) - t.Logf("Go straight (↑)") - fwdL, fwdR = wb.differentialDrive(1, 0) - test.That(t, fwdL, test.ShouldBeGreaterThan, 0) - test.That(t, fwdR, test.ShouldBeGreaterThan, 0) - test.That(t, math.Abs(fwdL), test.ShouldEqual, math.Abs(fwdR)) - - // Go forward-left (↰) - t.Logf("Go forward-left (↰)") - fwdL, fwdR = wb.differentialDrive(1, 1) - test.That(t, fwdL, test.ShouldBeGreaterThan, 0) - test.That(t, fwdR, test.ShouldBeGreaterThan, 0) - test.That(t, math.Abs(fwdL), test.ShouldBeLessThan, math.Abs(fwdR)) - - // Go reverse-left (↲) - t.Logf("Go reverse-left (↲)") - revL, revR = wb.differentialDrive(-1, 1) - test.That(t, revL, test.ShouldBeLessThan, 0) - test.That(t, revR, test.ShouldBeLessThan, 0) - test.That(t, math.Abs(revL), test.ShouldBeLessThan, math.Abs(revR)) - - // Ensure motor powers are mirrored going left. - test.That(t, math.Abs(fwdL), test.ShouldEqual, math.Abs(revL)) - test.That(t, math.Abs(fwdR), test.ShouldEqual, math.Abs(revR)) - - // Go forward-right (↱) - t.Logf("Go forward-right (↱)") - fwdL, fwdR = wb.differentialDrive(1, -1) - test.That(t, fwdL, test.ShouldBeGreaterThan, 0) - test.That(t, fwdR, test.ShouldBeGreaterThan, 0) - test.That(t, math.Abs(fwdL), test.ShouldBeGreaterThan, math.Abs(revL)) - - // Go reverse-right (↳) - t.Logf("Go reverse-right (↳)") - revL, revR = wb.differentialDrive(-1, -1) - test.That(t, revL, test.ShouldBeLessThan, 0) - test.That(t, revR, test.ShouldBeLessThan, 0) - test.That(t, math.Abs(revL), test.ShouldBeGreaterThan, math.Abs(revR)) - - // Ensure motor powers are mirrored going right. - test.That(t, math.Abs(fwdL), test.ShouldEqual, math.Abs(revL)) - test.That(t, math.Abs(fwdR), test.ShouldEqual, math.Abs(revR)) - - // Test spin left (↺) - t.Logf("Test spin left (↺)") - spinL, spinR := wb.differentialDrive(0, 1) - test.That(t, spinL, test.ShouldBeLessThanOrEqualTo, 0) - test.That(t, spinR, test.ShouldBeGreaterThan, 0) - - // Test spin right (↻) - t.Logf("Test spin right (↻)") - spinL, spinR = wb.differentialDrive(0, -1) - test.That(t, spinL, test.ShouldBeGreaterThan, 0) - test.That(t, spinR, test.ShouldBeLessThanOrEqualTo, 0) - }) -} - -func TestStopError(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps := createMockDeps(t) - - fakecfg := resource.Config{ - Name: "base", - ConvertedAttributes: &Config{ - WidthMM: 100, - WheelCircumferenceMM: 100, - Left: []string{"left"}, - Right: []string{"right"}, - }, - } - base, err := createWheeledBase(ctx, deps, fakecfg, logger) - test.That(t, err, test.ShouldBeNil) - - err = base.Stop(ctx, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "stop error") -} - -func TestWheeledBaseConstructor(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // empty config - cfg := &Config{} - _, err := cfg.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - - // invalid config - cfg = &Config{ - WidthMM: 100, - WheelCircumferenceMM: 1000, - Left: []string{"fl-m", "bl-m"}, - Right: []string{"fr-m"}, - } - _, err = cfg.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - - // valid config - testCfg := newTestCfg() - deps, err := testCfg.Validate("path", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - motorDeps := fakeMotorDependencies(t, deps) - - newBase, err := createWheeledBase(ctx, motorDeps, testCfg, logger) - test.That(t, err, test.ShouldBeNil) - wb, ok := newBase.(*wheeledBase) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(wb.left), test.ShouldEqual, 2) - test.That(t, len(wb.right), test.ShouldEqual, 2) - test.That(t, len(wb.allMotors), test.ShouldEqual, 4) -} - -func TestWheeledBaseReconfigure(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // valid config - testCfg := newTestCfg() - deps, err := testCfg.Validate("path", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - motorDeps := fakeMotorDependencies(t, deps) - - newBase, err := createWheeledBase(ctx, motorDeps, testCfg, logger) - test.That(t, err, test.ShouldBeNil) - wb, ok := newBase.(*wheeledBase) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(wb.left), test.ShouldEqual, 2) - test.That(t, len(wb.right), test.ShouldEqual, 2) - test.That(t, len(wb.allMotors), test.ShouldEqual, 4) - - // invert the motors to confirm that Reconfigure still occurs when array order/naming changes - newTestConf := newTestCfg() - newTestConf.ConvertedAttributes = &Config{ - WidthMM: 100, - WheelCircumferenceMM: 1000, - Left: []string{"fr-m", "br-m"}, - Right: []string{"fl-m", "bl-m"}, - } - deps, err = newTestConf.Validate("path", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - motorDeps = fakeMotorDependencies(t, deps) - test.That(t, wb.Reconfigure(ctx, motorDeps, newTestConf), test.ShouldBeNil) - - // Add a new motor to Left only to confirm that Reconfigure is impossible because cfg validation fails - newerTestCfg := newTestCfg() - newerTestCfg.ConvertedAttributes = &Config{ - WidthMM: 100, - WheelCircumferenceMM: 1000, - Left: []string{"fl-m", "bl-m", "ml-m"}, - Right: []string{"fr-m", "br-m"}, - } - - deps, err = newerTestCfg.Validate("path", resource.APITypeComponentName) - test.That(t, err.Error(), test.ShouldContainSubstring, "left and right need to have the same number of motors") - test.That(t, deps, test.ShouldBeNil) - - // Add a motor to Right so Left and Right are now the same size again, confirm that Reconfigure - // occurs after the motor array size change - newestTestCfg := newTestCfg() - newestTestCfg.ConvertedAttributes = &Config{ - WidthMM: 100, - WheelCircumferenceMM: 1000, - Left: []string{"fl-m", "bl-m", "ml-m"}, - Right: []string{"fr-m", "br-m", "mr-m"}, - } - - deps, err = newestTestCfg.Validate("path", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - motorDeps = fakeMotorDependencies(t, deps) - test.That(t, wb.Reconfigure(ctx, motorDeps, newestTestCfg), test.ShouldBeNil) -} - -func TestValidate(t *testing.T) { - cfg := &Config{} - deps, err := cfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "width_mm") - - cfg.WidthMM = 100 - deps, err = cfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "wheel_circumference_mm") - - cfg.WheelCircumferenceMM = 1000 - deps, err = cfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "left") - - cfg.Left = []string{"fl-m", "bl-m"} - deps, err = cfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "right") - - cfg.Right = []string{"fr-m"} - deps, err = cfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "left and right need to have the same number of motors, not 2 vs 1") - - cfg.Right = append(cfg.Right, "br-m") - deps, err = cfg.Validate("path") - test.That(t, deps, test.ShouldResemble, []string{"fl-m", "bl-m", "fr-m", "br-m"}) - test.That(t, err, test.ShouldBeNil) -} - -// waitForMotorsToStop polls all motors to see if they're on, used only for testing. -func waitForMotorsToStop(ctx context.Context, wb *wheeledBase) error { - for { - if !utils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return ctx.Err() - } - - anyOn := false - anyOff := false - - for _, m := range wb.allMotors { - isOn, _, err := m.IsPowered(ctx, nil) - if err != nil { - return err - } - if isOn { - anyOn = true - } else { - anyOff = true - } - } - - if !anyOn { - return nil - } - - if anyOff { - // once one motor turns off, we turn them all off - return wb.Stop(ctx, nil) - } - } -} diff --git a/components/board/beaglebone/board.go b/components/board/beaglebone/board.go deleted file mode 100644 index f5bd408bbb6..00000000000 --- a/components/board/beaglebone/board.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package beaglebone implements a beaglebone based board. -package beaglebone - -import ( - "github.com/pkg/errors" - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" -) - -const modelName = "beaglebone" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappings(modelName, boardInfoMappings) - var noBoardErr genericlinux.NoBoardFoundError - if errors.As(err, &noBoardErr) { - logging.Global().Debugw("error getting beaglebone GPIO board mapping", "error", err) - } - - genericlinux.RegisterBoard(modelName, gpioMappings) -} diff --git a/components/board/beaglebone/data.go b/components/board/beaglebone/data.go deleted file mode 100644 index f0dcd17ca9c..00000000000 --- a/components/board/beaglebone/data.go +++ /dev/null @@ -1,106 +0,0 @@ -package beaglebone - -import "go.viam.com/rdk/components/board/genericlinux" - -const bbAi = "bb_Ai64" - -var boardInfoMappings = map[string]genericlinux.BoardInformation{ - bbAi: { - PinDefinitions: []genericlinux.PinDefinition{ - // PinNumberBoard {914} -> PinNameCVM3 "P9_14" - - // ******** DATA MAPPING ******************************** - // Hardware PWMs contain a number other than -1 in the last element of map - // beaglebone pwm mapping sys/devices/platform/bus@100000/*.pwm - // NOTE: each hardware PWM device can only drive 1 line at a time, even though 2 lines - // are hooked up to each. For example, you can't have PWM signals running on lines 914 - // and 916 at the same time, even though both of them work on their own. - // NOTE: pins with hardware PWM support don't work as GPIO by default - - // GPIO only pins - // beaglebone gpio mapping uses directory sys/devices/platform/bus@100000/*.gpio - // beaglebone 600000.gpio/ (128 lines) corresponds to gpiochip1 and /sys/class/gpio/gpiochip300/ - // beaglebone 601000.gpio/ (36 lines) corresponds to gpiochip2 and /sys/class/gpio/gpiochip264/ - - {Name: "803", DeviceName: "gpiochip1", LineNumber: 20, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "804", DeviceName: "gpiochip1", LineNumber: 48, PwmChipSysfsDir: "", PwmID: -1}, // BOOTMODE2 - {Name: "805", DeviceName: "gpiochip1", LineNumber: 33, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "806", DeviceName: "gpiochip1", LineNumber: 34, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "807", DeviceName: "gpiochip1", LineNumber: 15, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "808", DeviceName: "gpiochip1", LineNumber: 14, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "809", DeviceName: "gpiochip1", LineNumber: 17, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "810", DeviceName: "gpiochip1", LineNumber: 16, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "811", DeviceName: "gpiochip1", LineNumber: 60, PwmChipSysfsDir: "", PwmID: -1}, // BOOTMODE7 - {Name: "812", DeviceName: "gpiochip1", LineNumber: 59, PwmChipSysfsDir: "", PwmID: -1}, - // We're unable to get GPIO to work on pin 813 despite Beaglebone's docs. PWM works. - {Name: "813", DeviceName: "gpiochip1", LineNumber: 89, PwmChipSysfsDir: "3000000.pwm", PwmID: 1}, // pwmchip0 V27 EHRPWM0_A - {Name: "814", DeviceName: "gpiochip1", LineNumber: 75, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "815", DeviceName: "gpiochip1", LineNumber: 61, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "816", DeviceName: "gpiochip1", LineNumber: 62, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "817", DeviceName: "gpiochip1", LineNumber: 3, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "818", DeviceName: "gpiochip1", LineNumber: 4, PwmChipSysfsDir: "", PwmID: -1}, - // We're unable to get GPIO to work on pin 819 despite Beaglebone's docs. PWM works. - {Name: "819", DeviceName: "gpiochip1", LineNumber: 88, PwmChipSysfsDir: "3000000.pwm", PwmID: 0}, // pwmchip0 V29 EHRPWM0_B - {Name: "820", DeviceName: "gpiochip1", LineNumber: 76, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "821", DeviceName: "gpiochip1", LineNumber: 30, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "822", DeviceName: "gpiochip1", LineNumber: 5, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "823", DeviceName: "gpiochip1", LineNumber: 31, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "824", DeviceName: "gpiochip1", LineNumber: 6, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "825", DeviceName: "gpiochip1", LineNumber: 35, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "826", DeviceName: "gpiochip1", LineNumber: 51, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "827", DeviceName: "gpiochip1", LineNumber: 71, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "828", DeviceName: "gpiochip1", LineNumber: 72, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "829", DeviceName: "gpiochip1", LineNumber: 73, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "830", DeviceName: "gpiochip1", LineNumber: 74, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "831", DeviceName: "gpiochip1", LineNumber: 32, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "832", DeviceName: "gpiochip1", LineNumber: 26, PwmChipSysfsDir: "", PwmID: -1}, // Timer-based PWM - {Name: "833", DeviceName: "gpiochip1", LineNumber: 25, PwmChipSysfsDir: "", PwmID: -1}, // Timer-based PWM - {Name: "834", DeviceName: "gpiochip1", LineNumber: 7, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "835", DeviceName: "gpiochip1", LineNumber: 24, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "836", DeviceName: "gpiochip1", LineNumber: 8, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "837", DeviceName: "gpiochip1", LineNumber: 11, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "838", DeviceName: "gpiochip1", LineNumber: 9, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "839", DeviceName: "gpiochip1", LineNumber: 69, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "840", DeviceName: "gpiochip1", LineNumber: 70, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "841", DeviceName: "gpiochip1", LineNumber: 67, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "842", DeviceName: "gpiochip1", LineNumber: 68, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "843", DeviceName: "gpiochip1", LineNumber: 65, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "844", DeviceName: "gpiochip1", LineNumber: 66, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "845", DeviceName: "gpiochip1", LineNumber: 79, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "846", DeviceName: "gpiochip1", LineNumber: 80, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "911", DeviceName: "gpiochip1", LineNumber: 1, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "912", DeviceName: "gpiochip1", LineNumber: 45, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "913", DeviceName: "gpiochip1", LineNumber: 2, PwmChipSysfsDir: "", PwmID: -1}, - // We're unable to get GPIO to work on pin 914 despite Beaglebone's docs. PWM works. - {Name: "914", DeviceName: "gpiochip1", LineNumber: 93, PwmChipSysfsDir: "3020000.pwm", PwmID: 0}, - {Name: "915", DeviceName: "gpiochip1", LineNumber: 47, PwmChipSysfsDir: "", PwmID: -1}, - // We're unable to get GPIO to work on pin 916 despite Beaglebone's docs. PWM works. - {Name: "916", DeviceName: "gpiochip1", LineNumber: 94, PwmChipSysfsDir: "3020000.pwm", PwmID: 1}, - {Name: "917", DeviceName: "gpiochip1", LineNumber: 28, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "918", DeviceName: "gpiochip1", LineNumber: 40, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "919", DeviceName: "gpiochip1", LineNumber: 78, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "920", DeviceName: "gpiochip1", LineNumber: 77, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "921", DeviceName: "gpiochip1", LineNumber: 39, PwmChipSysfsDir: "3010000.pwm", PwmID: 0}, - {Name: "922", DeviceName: "gpiochip1", LineNumber: 38, PwmChipSysfsDir: "3010000.pwm", PwmID: 1}, - {Name: "923", DeviceName: "gpiochip1", LineNumber: 10, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "924", DeviceName: "gpiochip1", LineNumber: 13, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "925", DeviceName: "gpiochip1", LineNumber: 127, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "926", DeviceName: "gpiochip1", LineNumber: 12, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "927", DeviceName: "gpiochip1", LineNumber: 46, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "928", DeviceName: "gpiochip1", LineNumber: 43, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "929", DeviceName: "gpiochip2", LineNumber: 14, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "930", DeviceName: "gpiochip2", LineNumber: 13, PwmChipSysfsDir: "", PwmID: -1}, // Timer-based PWM - {Name: "931", DeviceName: "gpiochip1", LineNumber: 52, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "933", DeviceName: "gpiochip1", LineNumber: 50, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "935", DeviceName: "gpiochip1", LineNumber: 55, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "936", DeviceName: "gpiochip1", LineNumber: 56, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "937", DeviceName: "gpiochip1", LineNumber: 57, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "938", DeviceName: "gpiochip1", LineNumber: 58, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "939", DeviceName: "gpiochip1", LineNumber: 54, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "940", DeviceName: "gpiochip1", LineNumber: 81, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "941", DeviceName: "gpiochip2", LineNumber: 0, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "942", DeviceName: "gpiochip1", LineNumber: 123, PwmChipSysfsDir: "", PwmID: -1}, // Timer-based PWM - }, - Compats: []string{"beagle,j721e-beagleboneai64", "ti,j721e"}, - }, -} diff --git a/components/board/board.go b/components/board/board.go deleted file mode 100644 index 12b778f4aaa..00000000000 --- a/components/board/board.go +++ /dev/null @@ -1,100 +0,0 @@ -// Package board defines the interfaces that typically live on a single-board computer -// such as a Raspberry Pi. -// -// Besides the board itself, some other interfaces it defines are analog pins and digital interrupts. -package board - -import ( - "context" - "time" - - pb "go.viam.com/api/component/board/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Board]{ - Status: resource.StatusFunc(CreateStatus), - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterBoardServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.BoardService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: analogs.String(), - }, newAnalogCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: gpios.String(), - }, newGPIOCollector) -} - -// SubtypeName is a constant that identifies the component resource API string "board". -const SubtypeName = "board" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named board's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// A Board represents a physical general purpose board that contains various -// components such as analogs, and digital interrupts. -type Board interface { - resource.Resource - - // AnalogByName returns an analog pin by name. - AnalogByName(name string) (Analog, error) - - // DigitalInterruptByName returns a digital interrupt by name. - DigitalInterruptByName(name string) (DigitalInterrupt, error) - - // GPIOPinByName returns a GPIOPin by name. - GPIOPinByName(name string) (GPIOPin, error) - - // AnalogNames returns the names of all known analog pins. - AnalogNames() []string - - // DigitalInterruptNames returns the names of all known digital interrupts. - DigitalInterruptNames() []string - - // SetPowerMode sets the board to the given power mode. If - // provided, the board will exit the given power mode after - // the specified duration. - SetPowerMode(ctx context.Context, mode pb.PowerMode, duration *time.Duration) error - - // StreamTicks starts a stream of digital interrupt ticks. - StreamTicks(ctx context.Context, interrupts []DigitalInterrupt, ch chan Tick, - extra map[string]interface{}) error -} - -// An Analog represents an analog pin that resides on a board. -type Analog interface { - // Read reads off the current value. - Read(ctx context.Context, extra map[string]interface{}) (int, error) - - // Write writes a value to the analog pin. - Write(ctx context.Context, value int, extra map[string]interface{}) error -} - -// FromDependencies is a helper for getting the named board from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Board, error) { - return resource.FromDependencies[Board](deps, Named(name)) -} - -// FromRobot is a helper for getting the named board from the given Robot. -func FromRobot(r robot.Robot, name string) (Board, error) { - return robot.ResourceFromRobot[Board](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all board names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} diff --git a/components/board/client.go b/components/board/client.go deleted file mode 100644 index 05df78fbf04..00000000000 --- a/components/board/client.go +++ /dev/null @@ -1,345 +0,0 @@ -// Package board contains a gRPC based board client. -package board - -import ( - "context" - "math" - "slices" - "sync" - "time" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/board/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/durationpb" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// errUnimplemented is used for any unimplemented methods that should -// eventually be implemented server side or faked client side. -var errUnimplemented = errors.New("unimplemented") - -// client implements BoardServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - client pb.BoardServiceClient - logger logging.Logger - - info boardInfo - - interruptStreams []*interruptStream - - mu sync.Mutex -} - -type boardInfo struct { - name string - analogNames []string - digitalInterruptNames []string -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Board, error) { - info := boardInfo{ - name: name.ShortName(), - analogNames: []string{}, - digitalInterruptNames: []string{}, - } - bClient := pb.NewBoardServiceClient(conn) - c := &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - client: bClient, - logger: logger, - info: info, - } - return c, nil -} - -func (c *client) AnalogByName(name string) (Analog, error) { - if !slices.Contains(c.info.analogNames, name) { - c.info.analogNames = append(c.info.analogNames, name) - } - return &analogClient{ - client: c, - boardName: c.info.name, - analogName: name, - }, nil -} - -func (c *client) DigitalInterruptByName(name string) (DigitalInterrupt, error) { - if !slices.Contains(c.info.digitalInterruptNames, name) { - c.info.digitalInterruptNames = append(c.info.digitalInterruptNames, name) - } - return &digitalInterruptClient{ - client: c, - boardName: c.info.name, - digitalInterruptName: name, - }, nil -} - -func (c *client) GPIOPinByName(name string) (GPIOPin, error) { - return &gpioPinClient{ - client: c, - boardName: c.info.name, - pinName: name, - }, nil -} - -func (c *client) AnalogNames() []string { - if len(c.info.analogNames) == 0 { - c.logger.Debugw("no cached analog readers") - return []string{} - } - return copyStringSlice(c.info.analogNames) -} - -func (c *client) DigitalInterruptNames() []string { - if len(c.info.digitalInterruptNames) == 0 { - c.logger.Debugw("no cached digital interrupts") - return []string{} - } - return copyStringSlice(c.info.digitalInterruptNames) -} - -func (c *client) SetPowerMode(ctx context.Context, mode pb.PowerMode, duration *time.Duration) error { - var dur *durationpb.Duration - if duration != nil { - dur = durationpb.New(*duration) - } - _, err := c.client.SetPowerMode(ctx, &pb.SetPowerModeRequest{Name: c.info.name, PowerMode: mode, Duration: dur}) - return err -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.info.name, cmd) -} - -// analogClient satisfies a gRPC based board.AnalogReader. Refer to the interface -// for descriptions of its methods. -type analogClient struct { - *client - boardName string - analogName string -} - -func (ac *analogClient) Read(ctx context.Context, extra map[string]interface{}) (int, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return 0, err - } - // the api method is named ReadAnalogReader, it is named differently than - // the board interface functions. - resp, err := ac.client.client.ReadAnalogReader(ctx, &pb.ReadAnalogReaderRequest{ - BoardName: ac.boardName, - AnalogReaderName: ac.analogName, - Extra: ext, - }) - if err != nil { - return 0, err - } - return int(resp.Value), nil -} - -func (ac *analogClient) Write(ctx context.Context, value int, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = ac.client.client.WriteAnalog(ctx, &pb.WriteAnalogRequest{ - Name: ac.boardName, - Pin: ac.analogName, - Value: int32(value), - Extra: ext, - }) - if err != nil { - return err - } - return nil -} - -// digitalInterruptClient satisfies a gRPC based board.DigitalInterrupt. Refer to the -// interface for descriptions of its methods. -type digitalInterruptClient struct { - *client - boardName string - digitalInterruptName string -} - -func (dic *digitalInterruptClient) Value(ctx context.Context, extra map[string]interface{}) (int64, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return 0, err - } - resp, err := dic.client.client.GetDigitalInterruptValue(ctx, &pb.GetDigitalInterruptValueRequest{ - BoardName: dic.boardName, - DigitalInterruptName: dic.digitalInterruptName, - Extra: ext, - }) - if err != nil { - return 0, err - } - return resp.Value, nil -} - -func (dic *digitalInterruptClient) Tick(ctx context.Context, high bool, nanoseconds uint64) error { - panic(errUnimplemented) -} - -func (dic *digitalInterruptClient) Name() string { - return dic.digitalInterruptName -} - -func (c *client) StreamTicks(ctx context.Context, interrupts []DigitalInterrupt, ch chan Tick, - extra map[string]interface{}, -) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - stream := &interruptStream{ - extra: ext, - client: c, - } - - c.mu.Lock() - c.interruptStreams = append(c.interruptStreams, stream) - c.mu.Unlock() - - err = stream.startStream(ctx, interrupts, ch) - if err != nil { - return err - } - return nil -} - -func (c *client) removeStream(s *interruptStream) { - c.mu.Lock() - defer c.mu.Unlock() - for i, stream := range s.interruptStreams { - if stream == s { - // To remove this item, we replace it with the last item in the list, then truncate the - // list by 1. - s.client.interruptStreams[i] = s.client.interruptStreams[len(s.client.interruptStreams)-1] - s.client.interruptStreams = s.client.interruptStreams[:len(s.client.interruptStreams)-1] - break - } - } -} - -// gpioPinClient satisfies a gRPC based board.GPIOPin. Refer to the interface -// for descriptions of its methods. -type gpioPinClient struct { - *client - boardName string - pinName string -} - -func (gpc *gpioPinClient) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = gpc.client.client.SetGPIO(ctx, &pb.SetGPIORequest{ - Name: gpc.boardName, - Pin: gpc.pinName, - High: high, - Extra: ext, - }) - return err -} - -func (gpc *gpioPinClient) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return false, err - } - resp, err := gpc.client.client.GetGPIO(ctx, &pb.GetGPIORequest{ - Name: gpc.boardName, - Pin: gpc.pinName, - Extra: ext, - }) - if err != nil { - return false, err - } - return resp.High, nil -} - -func (gpc *gpioPinClient) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return math.NaN(), err - } - resp, err := gpc.client.client.PWM(ctx, &pb.PWMRequest{ - Name: gpc.boardName, - Pin: gpc.pinName, - Extra: ext, - }) - if err != nil { - return math.NaN(), err - } - return resp.DutyCyclePct, nil -} - -func (gpc *gpioPinClient) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = gpc.client.client.SetPWM(ctx, &pb.SetPWMRequest{ - Name: gpc.boardName, - Pin: gpc.pinName, - DutyCyclePct: dutyCyclePct, - Extra: ext, - }) - return err -} - -func (gpc *gpioPinClient) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return 0, err - } - resp, err := gpc.client.client.PWMFrequency(ctx, &pb.PWMFrequencyRequest{ - Name: gpc.boardName, - Pin: gpc.pinName, - Extra: ext, - }) - if err != nil { - return 0, err - } - return uint(resp.FrequencyHz), nil -} - -func (gpc *gpioPinClient) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = gpc.client.client.SetPWMFrequency(ctx, &pb.SetPWMFrequencyRequest{ - Name: gpc.boardName, - Pin: gpc.pinName, - FrequencyHz: uint64(freqHz), - Extra: ext, - }) - return err -} - -// copyStringSlice is a helper to simply copy a string slice -// so that no one mutates it. -func copyStringSlice(src []string) []string { - out := make([]string, len(src)) - copy(out, src) - return out -} diff --git a/components/board/client_test.go b/components/board/client_test.go deleted file mode 100644 index cf183421997..00000000000 --- a/components/board/client_test.go +++ /dev/null @@ -1,290 +0,0 @@ -package board_test - -import ( - "context" - "net" - "slices" - "testing" - "time" - - boardpb "go.viam.com/api/component/board/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/board" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testBoardName = "board1" - missingBoardName = "board2" -) - -func setupService(t *testing.T, injectBoard *inject.Board) (net.Listener, func()) { - t.Helper() - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - boardSvc, err := resource.NewAPIResourceCollection(board.API, map[resource.Name]board.Board{board.Named(testBoardName): injectBoard}) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[board.Board](board.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, boardSvc), test.ShouldBeNil) - - go rpcServer.Serve(listener) - return listener, func() { rpcServer.Stop() } -} - -func TestFailingClient(t *testing.T) { - logger := logging.NewTestLogger(t) - - injectBoard := &inject.Board{} - - listener, cleanup := setupService(t, injectBoard) - defer cleanup() - - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - - _, err := viamgrpc.Dial(cancelCtx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) -} - -func TestWorkingClient(t *testing.T) { - logger := logging.NewTestLogger(t) - injectBoard := &inject.Board{} - - listener, cleanup := setupService(t, injectBoard) - defer cleanup() - - testWorkingClient := func(t *testing.T, client board.Board) { - t.Helper() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - var actualExtra map[string]interface{} - - // DoCommand - injectBoard.DoFunc = testutils.EchoFunc - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - injectGPIOPin := &inject.GPIOPin{} - injectBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return injectGPIOPin, nil - } - - // Set - injectGPIOPin.SetFunc = func(ctx context.Context, high bool, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - onePin, err := client.GPIOPinByName("one") - test.That(t, err, test.ShouldBeNil) - err = onePin.Set(context.Background(), true, expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, injectGPIOPin.SetCap()[1:], test.ShouldResemble, []interface{}{true}) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - - // Get - injectGPIOPin.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - actualExtra = extra - return true, nil - } - isHigh, err := onePin.Get(context.Background(), expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, isHigh, test.ShouldBeTrue) - test.That(t, injectGPIOPin.GetCap()[1:], test.ShouldResemble, []interface{}{}) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - - // SetPWM - injectGPIOPin.SetPWMFunc = func(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - err = onePin.SetPWM(context.Background(), 0.03, expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, injectGPIOPin.SetPWMCap()[1:], test.ShouldResemble, []interface{}{0.03}) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - - // SetPWMFreq - injectGPIOPin.SetPWMFreqFunc = func(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - err = onePin.SetPWMFreq(context.Background(), 11233, expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, injectGPIOPin.SetPWMFreqCap()[1:], test.ShouldResemble, []interface{}{uint(11233)}) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - - // Analog - injectAnalog := &inject.Analog{} - injectBoard.AnalogByNameFunc = func(name string) (board.Analog, error) { - return injectAnalog, nil - } - analog1, err := injectBoard.AnalogByName("analog1") - test.That(t, err, test.ShouldBeNil) - test.That(t, injectBoard.AnalogByNameCap(), test.ShouldResemble, []interface{}{"analog1"}) - - // Analog: Read - injectAnalog.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { - actualExtra = extra - return 6, nil - } - readVal, err := analog1.Read(context.Background(), expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, readVal, test.ShouldEqual, 6) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - - // write analog - injectAnalog.WriteFunc = func(ctx context.Context, value int, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - analog2, err := injectBoard.AnalogByName("pin1") - test.That(t, err, test.ShouldBeNil) - err = analog2.Write(context.Background(), 6, expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - - // Digital Interrupt - injectDigitalInterrupt := &inject.DigitalInterrupt{} - injectBoard.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - return injectDigitalInterrupt, nil - } - digital1, err := injectBoard.DigitalInterruptByName("digital1") - test.That(t, err, test.ShouldBeNil) - test.That(t, injectBoard.DigitalInterruptByNameCap(), test.ShouldResemble, []interface{}{"digital1"}) - - // Digital Interrupt:Value - injectDigitalInterrupt.ValueFunc = func(ctx context.Context, extra map[string]interface{}) (int64, error) { - actualExtra = extra - return 287, nil - } - digital1Val, err := digital1.Value(context.Background(), expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, digital1Val, test.ShouldEqual, 287) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - - // StreamTicks - injectBoard.StreamTicksFunc = func(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, - ) error { - actualExtra = extra - return nil - } - err = injectBoard.StreamTicks(context.Background(), []board.DigitalInterrupt{digital1}, make(chan board.Tick), expectedExtra) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - actualExtra = nil - injectDigitalInterrupt.NameFunc = func() string { - return "digital1" - } - name := digital1.Name() - test.That(t, name, test.ShouldEqual, "digital1") - - // SetPowerMode (currently unimplemented in RDK) - injectBoard.SetPowerModeFunc = func(ctx context.Context, mode boardpb.PowerMode, duration *time.Duration) error { - return viamgrpc.UnimplementedError - } - err = client.SetPowerMode(context.Background(), boardpb.PowerMode_POWER_MODE_OFFLINE_DEEP, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldHaveSameTypeAs, viamgrpc.UnimplementedError) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - } - - t.Run("New client from connection", func(t *testing.T) { - ctx := context.Background() - conn, err := viamgrpc.Dial(ctx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := board.NewClientFromConn(ctx, conn, "", board.Named(testBoardName), logger) - test.That(t, err, test.ShouldBeNil) - - testWorkingClient(t, client) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} - -// these tests validate that for modular boards(which rely on client.go's board interface) -// we will only add new pins to the cached name lists. -func TestClientNames(t *testing.T) { - logger := logging.NewTestLogger(t) - injectBoard := &inject.Board{} - - listener, cleanup := setupService(t, injectBoard) - defer cleanup() - t.Run("test analog names are cached correctly in the client", func(t *testing.T) { - ctx := context.Background() - conn, err := viamgrpc.Dial(ctx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := board.NewClientFromConn(ctx, conn, "", board.Named(testBoardName), logger) - test.That(t, err, test.ShouldBeNil) - - nameFunc := func(name string) error { - _, err = client.AnalogByName(name) - return err - } - testNamesAPI(t, client.AnalogNames, nameFunc, "Analog") - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("test interrupt names are cached correctly in the client", func(t *testing.T) { - ctx := context.Background() - conn, err := viamgrpc.Dial(ctx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := board.NewClientFromConn(ctx, conn, "", board.Named(testBoardName), logger) - test.That(t, err, test.ShouldBeNil) - - nameFunc := func(name string) error { - _, err = client.DigitalInterruptByName(name) - return err - } - testNamesAPI(t, client.DigitalInterruptNames, nameFunc, "DigitalInterrupt") - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} - -func testNamesAPI(t *testing.T, namesFunc func() []string, nameFunc func(string) error, name string) { - t.Helper() - names := namesFunc() - test.That(t, len(names), test.ShouldEqual, 0) - name1 := name + "1" - err := nameFunc(name1) - test.That(t, err, test.ShouldBeNil) - names = namesFunc() - test.That(t, len(names), test.ShouldEqual, 1) - test.That(t, slices.Contains(names, name1), test.ShouldBeTrue) - - name2 := name + "2" - err = nameFunc(name2) - test.That(t, err, test.ShouldBeNil) - - names = namesFunc() - test.That(t, len(names), test.ShouldEqual, 2) - test.That(t, slices.Contains(names, name2), test.ShouldBeTrue) - - err = nameFunc(name1) - test.That(t, err, test.ShouldBeNil) - names = namesFunc() - test.That(t, len(names), test.ShouldEqual, 2) - test.That(t, slices.Contains(names, name1), test.ShouldBeTrue) -} diff --git a/components/board/collector.go b/components/board/collector.go deleted file mode 100644 index 82026cfeb4f..00000000000 --- a/components/board/collector.go +++ /dev/null @@ -1,105 +0,0 @@ -package board - -import ( - "context" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/board/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" -) - -type method int64 - -const ( - // we need analog pins that are readers, not writers, - // to collect data from. - analogReaderNameKey = "reader_name" - gpioPinNameKey = "pin_name" - analogs method = iota - gpios -) - -func (m method) String() string { - if m == analogs { - return "Analogs" - } - if m == gpios { - return "Gpios" - } - return "Unknown" -} - -// newAnalogCollector returns a collector to register an analog reading method. If one is already registered -// with the same MethodMetadata it will panic. -func newAnalogCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - board, err := assertBoard(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, arg map[string]*anypb.Any) (interface{}, error) { - var value int - if _, ok := arg[analogReaderNameKey]; !ok { - return nil, data.FailedToReadErr(params.ComponentName, analogs.String(), - errors.New("Must supply reader_name in additional_params for analog collector")) - } - if reader, err := board.AnalogByName(arg[analogReaderNameKey].String()); err == nil { - value, err = reader.Read(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, analogs.String(), err) - } - } - return pb.ReadAnalogReaderResponse{ - Value: int32(value), - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newGPIOCollector returns a collector to register a gpio get method. If one is already registered -// with the same MethodMetadata it will panic. -func newGPIOCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - board, err := assertBoard(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, arg map[string]*anypb.Any) (interface{}, error) { - var value bool - if _, ok := arg[gpioPinNameKey]; !ok { - return nil, data.FailedToReadErr(params.ComponentName, gpios.String(), - errors.New("Must supply pin_name in additional params for gpio collector")) - } - if gpio, err := board.GPIOPinByName(arg[gpioPinNameKey].String()); err == nil { - value, err = gpio.Get(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, gpios.String(), err) - } - } - return pb.GetGPIOResponse{ - High: value, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -func assertBoard(resource interface{}) (Board, error) { - board, ok := resource.(Board) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - - return board, nil -} diff --git a/components/board/collectors_test.go b/components/board/collectors_test.go deleted file mode 100644 index a2ea313dc5c..00000000000 --- a/components/board/collectors_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package board_test - -import ( - "context" - "encoding/json" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - "github.com/golang/protobuf/ptypes/wrappers" - pb "go.viam.com/api/component/board/v1" - "go.viam.com/test" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/data" - "go.viam.com/rdk/logging" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - componentName = "board" - captureInterval = time.Second - numRetries = 5 -) - -func TestCollectors(t *testing.T) { - tests := []struct { - name string - params data.CollectorParams - collector data.CollectorConstructor - expected map[string]any - shouldError bool - expectedError error - }{ - { - name: "Board analog collector should write an analog response", - params: data.CollectorParams{ - ComponentName: componentName, - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - MethodParams: map[string]*anypb.Any{ - "reader_name": convertInterfaceToAny("analog"), - }, - }, - collector: board.NewAnalogCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.ReadAnalogReaderResponse{ - Value: 1, - }), - shouldError: false, - }, - { - name: "Board gpio collector should write a gpio response", - params: data.CollectorParams{ - ComponentName: componentName, - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - MethodParams: map[string]*anypb.Any{ - "pin_name": convertInterfaceToAny("gpio"), - }, - }, - collector: board.NewGPIOCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetGPIOResponse{ - High: true, - }), - shouldError: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - tc.params.Clock = mockClock - tc.params.Target = &buf - - board := newBoard() - col, err := tc.collector(board, tc.params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, tc.expected) - }) - } -} - -func newBoard() board.Board { - b := &inject.Board{} - analog := &inject.Analog{} - analog.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { - return 1, nil - } - b.AnalogByNameFunc = func(name string) (board.Analog, error) { - return analog, nil - } - gpioPin := &inject.GPIOPin{} - gpioPin.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return true, nil - } - b.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return gpioPin, nil - } - return b -} - -func convertInterfaceToAny(v interface{}) *anypb.Any { - anyValue := &anypb.Any{} - - bytes, err := json.Marshal(v) - if err != nil { - return nil - } - bytesValue := &wrappers.BytesValue{ - Value: bytes, - } - - anypb.MarshalFrom(anyValue, bytesValue, proto.MarshalOptions{}) - return anyValue -} diff --git a/components/board/config.go b/components/board/config.go deleted file mode 100644 index 2c6e7f6da96..00000000000 --- a/components/board/config.go +++ /dev/null @@ -1,67 +0,0 @@ -package board - -import "go.viam.com/rdk/resource" - -// SPIConfig enumerates a specific, shareable SPI bus. -type SPIConfig struct { - Name string `json:"name"` - BusSelect string `json:"bus_select"` // "0" or "1" for main/aux in libpigpio -} - -// Validate ensures all parts of the config are valid. -func (config *SPIConfig) Validate(path string) error { - if config.Name == "" { - return resource.NewConfigValidationFieldRequiredError(path, "name") - } - return nil -} - -// I2CConfig enumerates a specific, shareable I2C bus. -type I2CConfig struct { - Name string `json:"name"` - Bus string `json:"bus"` -} - -// Validate ensures all parts of the config are valid. -func (config *I2CConfig) Validate(path string) error { - if config.Name == "" { - return resource.NewConfigValidationFieldRequiredError(path, "name") - } - if config.Bus == "" { - return resource.NewConfigValidationFieldRequiredError(path, "bus") - } - return nil -} - -// AnalogReaderConfig describes the configuration of an analog reader on a board. -type AnalogReaderConfig struct { - Name string `json:"name"` - Pin string `json:"pin"` - AverageOverMillis int `json:"average_over_ms,omitempty"` - SamplesPerSecond int `json:"samples_per_sec,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (config *AnalogReaderConfig) Validate(path string) error { - if config.Name == "" { - return resource.NewConfigValidationFieldRequiredError(path, "name") - } - return nil -} - -// DigitalInterruptConfig describes the configuration of digital interrupt for a board. -type DigitalInterruptConfig struct { - Name string `json:"name"` - Pin string `json:"pin"` -} - -// Validate ensures all parts of the config are valid. -func (config *DigitalInterruptConfig) Validate(path string) error { - if config.Name == "" { - return resource.NewConfigValidationFieldRequiredError(path, "name") - } - if config.Pin == "" { - return resource.NewConfigValidationFieldRequiredError(path, "pin") - } - return nil -} diff --git a/components/board/customlinux/README.md b/components/board/customlinux/README.md deleted file mode 100644 index 32eeaca280f..00000000000 --- a/components/board/customlinux/README.md +++ /dev/null @@ -1,18 +0,0 @@ -## [EXPERIMENTAL] Configuring a custom Linux board -This component supports a board running Linux and requires the user to provide a map of gpio pin names to the corresponding gpio chip and line number. The mappings should be provided in a json file in this format: -```json -{ - "pins": [ - { - "name": "string", - "device_name": "string", - "line_number": "int", - "pwm_chip_sysfs_dir": "string", - "pwm_id": "int" - } - ] -} -``` - -`pwm_chip_sysfs_dir` and `pwm_id` are optional fields. -To configure a new board with these mappings, set the `pin_config_file_path` attribute to the filepath to your json configuration file. diff --git a/components/board/customlinux/board.go b/components/board/customlinux/board.go deleted file mode 100644 index a6628dbfbd5..00000000000 --- a/components/board/customlinux/board.go +++ /dev/null @@ -1,102 +0,0 @@ -//go:build linux - -// Package customlinux implements a board running Linux. -// This is an Experimental package -package customlinux - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - - "go.uber.org/multierr" - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -const modelName = "customlinux" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - resource.RegisterComponent( - board.API, - resource.DefaultModelFamily.WithModel(modelName), - resource.Registration[board.Board, *Config]{ - Constructor: createNewBoard, - }) -} - -func createNewBoard( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (board.Board, error) { - return genericlinux.NewBoard(ctx, conf, pinDefsFromFile, logger) -} - -// This is a ConfigConverter which loads pin definitions from a file, assuming that the config -// passed in is a customlinux.Config underneath. -func pinDefsFromFile(conf resource.Config, logger logging.Logger) (*genericlinux.LinuxBoardConfig, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - pinDefs, err := parsePinConfig(newConf.BoardDefsFilePath, logger) - if err != nil { - return nil, err - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappingFromPinDefs(pinDefs) - if err != nil { - return nil, err - } - - return &genericlinux.LinuxBoardConfig{ - GpioMappings: gpioMappings, - }, nil -} - -func parsePinConfig(filePath string, logger logging.Logger) ([]genericlinux.PinDefinition, error) { - pinData, err := os.ReadFile(filepath.Clean(filePath)) - if err != nil { - return nil, err - } - - return parseRawPinData(pinData, filePath, logger) -} - -// This function is separate from parsePinConfig to make it testable without interacting with the -// file system. The filePath is passed in just for logging purposes. -func parseRawPinData(pinData []byte, filePath string, logger logging.Logger) ([]genericlinux.PinDefinition, error) { - var parsedPinData genericlinux.PinDefinitions - if err := json.Unmarshal(pinData, &parsedPinData); err != nil { - return nil, err - } - - var err error - for name, pin := range parsedPinData.Pins { - err = multierr.Combine(err, pin.Validate(filePath)) - - // Until we can reliably switch between gpio and pwm on lots of boards, pins that have - // hardware pwm enabled will be hardware pwm only. Disabling gpio functionality on these - // pins. - if parsedPinData.Pins[name].PwmChipSysfsDir != "" && parsedPinData.Pins[name].LineNumber >= 0 { - logger.Warnf("pin %s can be used for PWM only", parsedPinData.Pins[name].Name) - parsedPinData.Pins[name].LineNumber = -1 - } - } - if err != nil { - return nil, err - } - return parsedPinData.Pins, nil -} diff --git a/components/board/customlinux/board_nonlinux.go b/components/board/customlinux/board_nonlinux.go deleted file mode 100644 index ec65213a8b3..00000000000 --- a/components/board/customlinux/board_nonlinux.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !linux - -// Package customlinux implements a board running Linux. This file, however, is -// a placeholder for when you build the server in a non-Linux environment. -package customlinux diff --git a/components/board/customlinux/setup.go b/components/board/customlinux/setup.go deleted file mode 100644 index c1926d97427..00000000000 --- a/components/board/customlinux/setup.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build linux - -// Package customlinux implements a board running Linux -package customlinux - -import ( - "os" -) - -// A Config describes the configuration of a board and all of its connected parts. -type Config struct { - BoardDefsFilePath string `json:"board_defs_file_path"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - if _, err := os.Stat(conf.BoardDefsFilePath); err != nil { - return nil, err - } - // Should we read in and validate the board defs in here? - - return nil, nil -} diff --git a/components/board/customlinux/setup_test.go b/components/board/customlinux/setup_test.go deleted file mode 100644 index bb7f1e606ff..00000000000 --- a/components/board/customlinux/setup_test.go +++ /dev/null @@ -1,56 +0,0 @@ -//go:build linux - -// Package customlinux implements a board running linux -package customlinux - -import ( - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestConfigParse(t *testing.T) { - logger := logging.NewTestLogger(t) - emptyConfig := []byte(`{"pins": [{}]}`) - _, err := parseRawPinData(emptyConfig, "path", logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - - emptyPWMID := []byte(`{"pins": [{"name": "7", "device_name": "gpiochip1", "line_number": 71, "pwm_chip_sysfs_dir": "hi"}]}`) - _, err = parseRawPinData(emptyPWMID, "path", logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "must supply pwm_id for the pwm chip") - - invalidLineNumber := []byte(`{"pins": [{"name": "7", "device_name": "gpiochip1", "line_number": -2}]}`) - _, err = parseRawPinData(invalidLineNumber, "path", logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "line_number on gpio chip must be at least zero") - - validConfig := []byte(`{"pins": [{"name": "7", "device_name": "gpiochip1", "line_number": 80}]}`) - data, err := parseRawPinData(validConfig, "path", logger) - correctData := make([]genericlinux.PinDefinition, 1) - correctData[0] = genericlinux.PinDefinition{ - Name: "7", - DeviceName: "gpiochip1", - LineNumber: 80, - PwmID: -1, - } - test.That(t, err, test.ShouldBeNil) - test.That(t, data, test.ShouldResemble, correctData) -} - -func TestConfigValidate(t *testing.T) { - validConfig := Config{} - - _, err := validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "no such file or directory") - - validConfig.BoardDefsFilePath = "./" - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/board/digital_interrupts.go b/components/board/digital_interrupts.go deleted file mode 100644 index 1cf4f7c8a76..00000000000 --- a/components/board/digital_interrupts.go +++ /dev/null @@ -1,28 +0,0 @@ -package board - -import ( - "context" -) - -// Tick represents a signal received by an interrupt pin. This signal is communicated -// via registered channel to the various drivers. Depending on board implementation there may be a -// wraparound in timestamp values past 4294967295000 nanoseconds (~72 minutes) if the value -// was originally in microseconds as a 32-bit integer. The timestamp in nanoseconds of the -// tick SHOULD ONLY BE USED FOR CALCULATING THE TIME ELAPSED BETWEEN CONSECUTIVE TICKS AND NOT -// AS AN ABSOLUTE TIMESTAMP. -type Tick struct { - Name string - High bool - TimestampNanosec uint64 -} - -// A DigitalInterrupt represents a configured interrupt on the board that -// when interrupted, calls the added callbacks. -type DigitalInterrupt interface { - // Name returns the name of the interrupt. - Name() string - - // Value returns the current value of the interrupt which is - // based on the type of interrupt. - Value(ctx context.Context, extra map[string]interface{}) (int64, error) -} diff --git a/components/board/export_collectors_test.go b/components/board/export_collectors_test.go deleted file mode 100644 index 4b471980a2f..00000000000 --- a/components/board/export_collectors_test.go +++ /dev/null @@ -1,8 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package board - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var ( - NewAnalogCollector = newAnalogCollector - NewGPIOCollector = newGPIOCollector -) diff --git a/components/board/fake/board.go b/components/board/fake/board.go deleted file mode 100644 index 652c2df721b..00000000000 --- a/components/board/fake/board.go +++ /dev/null @@ -1,413 +0,0 @@ -// Package fake implements a fake board. -package fake - -import ( - "context" - "fmt" - "math/rand" - "reflect" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - pb "go.viam.com/api/component/board/v1" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// In order to maintain test functionality, testPin will always return the analog value it is set -// to (defaults to 0 before being set). To see changing fake analog values on a fake board, add an -// analog reader to any other pin. -var analogTestPin = "1" - -// In order to maintain test functionality, digital interrtups on any pin except nonZeroInterruptPin -// will always return a digital interrupt value of 0. To see non-zero fake interrupt values on a fake board, -// add an digital interrupt to pin 0. -var nonZeroInterruptPin = "0" - -// A Config describes the configuration of a fake board and all of its connected parts. -type Config struct { - AnalogReaders []board.AnalogReaderConfig `json:"analogs,omitempty"` - DigitalInterrupts []board.DigitalInterruptConfig `json:"digital_interrupts,omitempty"` - FailNew bool `json:"fail_new"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - for idx, conf := range conf.AnalogReaders { - if err := conf.Validate(fmt.Sprintf("%s.%s.%d", path, "analogs", idx)); err != nil { - return nil, err - } - } - for idx, conf := range conf.DigitalInterrupts { - if err := conf.Validate(fmt.Sprintf("%s.%s.%d", path, "digital_interrupts", idx)); err != nil { - return nil, err - } - } - - if conf.FailNew { - return nil, errors.New("whoops") - } - - return nil, nil -} - -var model = resource.DefaultModelFamily.WithModel("fake") - -func init() { - resource.RegisterComponent( - board.API, - model, - resource.Registration[board.Board, *Config]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - cfg resource.Config, - logger logging.Logger, - ) (board.Board, error) { - return NewBoard(ctx, cfg, logger) - }, - }) -} - -// NewBoard returns a new fake board. -func NewBoard(ctx context.Context, conf resource.Config, logger logging.Logger) (*Board, error) { - b := &Board{ - Named: conf.ResourceName().AsNamed(), - Analogs: map[string]*Analog{}, - Digitals: map[string]*DigitalInterrupt{}, - GPIOPins: map[string]*GPIOPin{}, - logger: logger, - } - - if err := b.processConfig(conf); err != nil { - return nil, err - } - - return b, nil -} - -func (b *Board) processConfig(conf resource.Config) error { - b.mu.Lock() - defer b.mu.Unlock() - - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - // TODO(RSDK-2684): we dont configure pins so we just unset them here. not really great behavior. - b.GPIOPins = map[string]*GPIOPin{} - - stillExists := map[string]struct{}{} - - for _, c := range newConf.AnalogReaders { - stillExists[c.Name] = struct{}{} - if curr, ok := b.Analogs[c.Name]; ok { - if curr.pin != c.Pin { - curr.reset(c.Pin) - } - continue - } - b.Analogs[c.Name] = newAnalogReader(c.Pin) - } - for name := range b.Analogs { - if _, ok := stillExists[name]; ok { - continue - } - delete(b.Analogs, name) - } - stillExists = map[string]struct{}{} - - var errs error - for _, c := range newConf.DigitalInterrupts { - stillExists[c.Name] = struct{}{} - if curr, ok := b.Digitals[c.Name]; ok { - if !reflect.DeepEqual(curr.conf, c) { - curr.reset(c) - } - continue - } - var err error - b.Digitals[c.Name], err = NewDigitalInterrupt(c) - if err != nil { - errs = multierr.Combine(errs, err) - } - } - for name := range b.Digitals { - if _, ok := stillExists[name]; ok { - continue - } - delete(b.Digitals, name) - } - - return nil -} - -// Reconfigure atomically reconfigures this board in place based on the new config. -func (b *Board) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - return b.processConfig(conf) -} - -// A Board provides dummy data from fake parts in order to implement a Board. -type Board struct { - resource.Named - - mu sync.RWMutex - Analogs map[string]*Analog - Digitals map[string]*DigitalInterrupt - GPIOPins map[string]*GPIOPin - logger logging.Logger - CloseCount int -} - -// AnalogByName returns the analog pin by the given name if it exists. -func (b *Board) AnalogByName(name string) (board.Analog, error) { - b.mu.RLock() - defer b.mu.RUnlock() - a, ok := b.Analogs[name] - if !ok { - return nil, errors.Errorf("can't find AnalogReader (%s)", name) - } - return a, nil -} - -// DigitalInterruptByName returns the interrupt by the given name if it exists. -func (b *Board) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { - b.mu.RLock() - defer b.mu.RUnlock() - d, ok := b.Digitals[name] - if !ok { - return nil, fmt.Errorf("cant find DigitalInterrupt (%s)", name) - } - return d, nil -} - -// GPIOPinByName returns the GPIO pin by the given name if it exists. -func (b *Board) GPIOPinByName(name string) (board.GPIOPin, error) { - b.mu.Lock() - defer b.mu.Unlock() - p, ok := b.GPIOPins[name] - if !ok { - pin := &GPIOPin{} - b.GPIOPins[name] = pin - return pin, nil - } - return p, nil -} - -// AnalogNames returns the names of all known analog pins. -func (b *Board) AnalogNames() []string { - b.mu.RLock() - defer b.mu.RUnlock() - names := []string{} - for k := range b.Analogs { - names = append(names, k) - } - return names -} - -// DigitalInterruptNames returns the names of all known digital interrupts. -func (b *Board) DigitalInterruptNames() []string { - b.mu.RLock() - defer b.mu.RUnlock() - names := []string{} - for k := range b.Digitals { - names = append(names, k) - } - return names -} - -// SetPowerMode sets the board to the given power mode. If provided, -// the board will exit the given power mode after the specified -// duration. -func (b *Board) SetPowerMode(ctx context.Context, mode pb.PowerMode, duration *time.Duration) error { - return grpc.UnimplementedError -} - -// StreamTicks starts a stream of digital interrupt ticks. -func (b *Board) StreamTicks(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, -) error { - for _, i := range interrupts { - name := i.Name() - d, ok := b.Digitals[name] - if !ok { - return fmt.Errorf("could not find digital interrupt: %s", name) - } - select { - case <-ctx.Done(): - return ctx.Err() - default: - // Keep going - } - // Get a random bool for the high tick value. - // linter complans about security but we don't care if someone - // can predict if the fake interrupts will be high or low. - //nolint:gosec - randBool := rand.Int()%2 == 0 - select { - case ch <- board.Tick{Name: d.conf.Name, High: randBool, TimestampNanosec: uint64(time.Now().Unix())}: - default: - // if nothing is listening to the channel just do nothing. - } - } - return nil -} - -// Close attempts to cleanly close each part of the board. -func (b *Board) Close(ctx context.Context) error { - b.mu.Lock() - defer b.mu.Unlock() - - b.CloseCount++ - - return nil -} - -// An Analog reads back the same set value. -type Analog struct { - pin string - Value int - CloseCount int - Mu sync.RWMutex - fakeValue int -} - -func newAnalogReader(pin string) *Analog { - return &Analog{pin: pin} -} - -func (a *Analog) reset(pin string) { - a.Mu.Lock() - a.pin = pin - a.Value = 0 - a.Mu.Unlock() -} - -func (a *Analog) Read(ctx context.Context, extra map[string]interface{}) (int, error) { - a.Mu.RLock() - defer a.Mu.RUnlock() - if a.pin != analogTestPin { - a.Value = a.fakeValue - a.fakeValue++ - } - return a.Value, nil -} - -func (a *Analog) Write(ctx context.Context, value int, extra map[string]interface{}) error { - a.Set(value) - return nil -} - -// Set is used to set the value of an Analog. -func (a *Analog) Set(value int) { - a.Mu.Lock() - defer a.Mu.Unlock() - a.Value = value -} - -// A GPIOPin reads back the same set values. -type GPIOPin struct { - high bool - pwm float64 - pwmFreq uint - - mu sync.Mutex -} - -// Set sets the pin to either low or high. -func (gp *GPIOPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - gp.mu.Lock() - defer gp.mu.Unlock() - - gp.high = high - gp.pwm = 0 - gp.pwmFreq = 0 - return nil -} - -// Get gets the high/low state of the pin. -func (gp *GPIOPin) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { - gp.mu.Lock() - defer gp.mu.Unlock() - - return gp.high, nil -} - -// PWM gets the pin's given duty cycle. -func (gp *GPIOPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - gp.mu.Lock() - defer gp.mu.Unlock() - - return gp.pwm, nil -} - -// SetPWM sets the pin to the given duty cycle. -func (gp *GPIOPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - gp.mu.Lock() - defer gp.mu.Unlock() - - gp.pwm = dutyCyclePct - return nil -} - -// PWMFreq gets the PWM frequency of the pin. -func (gp *GPIOPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - gp.mu.Lock() - defer gp.mu.Unlock() - - return gp.pwmFreq, nil -} - -// SetPWMFreq sets the given pin to the given PWM frequency. -func (gp *GPIOPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - gp.mu.Lock() - defer gp.mu.Unlock() - - gp.pwmFreq = freqHz - return nil -} - -// DigitalInterrupt is a fake digital interrupt. -type DigitalInterrupt struct { - mu sync.Mutex - conf board.DigitalInterruptConfig - value int64 -} - -// NewDigitalInterrupt returns a new fake digital interrupt. -func NewDigitalInterrupt(conf board.DigitalInterruptConfig) (*DigitalInterrupt, error) { - return &DigitalInterrupt{ - conf: conf, - }, nil -} - -func (s *DigitalInterrupt) reset(conf board.DigitalInterruptConfig) { - s.mu.Lock() - defer s.mu.Unlock() - s.conf = conf -} - -// Value returns the current value of the interrupt which is -// based on the type of interrupt. -func (s *DigitalInterrupt) Value(ctx context.Context, extra map[string]interface{}) (int64, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.conf.Pin == nonZeroInterruptPin { - s.value++ - return s.value, nil - } - return 0, nil -} - -// Name returns the name of the digital interrupt. -func (s *DigitalInterrupt) Name() string { - s.mu.Lock() - defer s.mu.Unlock() - return s.conf.Name -} diff --git a/components/board/fake/board_test.go b/components/board/fake/board_test.go deleted file mode 100644 index 38110ec8482..00000000000 --- a/components/board/fake/board_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package fake - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestFakeBoard(t *testing.T) { - logger := logging.NewTestLogger(t) - boardConfig := Config{ - AnalogReaders: []board.AnalogReaderConfig{ - {Name: "blue", Pin: analogTestPin}, - }, - DigitalInterrupts: []board.DigitalInterruptConfig{ - {Name: "i1", Pin: "35"}, - {Name: "i2", Pin: "31"}, - {Name: "a", Pin: "38"}, - {Name: "b", Pin: "40"}, - }, - } - - cfg := resource.Config{Name: "board1", ConvertedAttributes: &boardConfig} - b, err := NewBoard(context.Background(), cfg, logger) - test.That(t, err, test.ShouldBeNil) - - _, err = b.AnalogByName("blue") - test.That(t, err, test.ShouldBeNil) - - _, err = b.DigitalInterruptByName("i1") - test.That(t, err, test.ShouldBeNil) - _, err = b.DigitalInterruptByName("i2") - test.That(t, err, test.ShouldBeNil) - _, err = b.DigitalInterruptByName("a") - test.That(t, err, test.ShouldBeNil) - _, err = b.DigitalInterruptByName("b") - test.That(t, err, test.ShouldBeNil) -} - -func TestConfigValidate(t *testing.T) { - validConfig := Config{} - - validConfig.AnalogReaders = []board.AnalogReaderConfig{{}} - _, err := validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `path.analogs.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - - validConfig.AnalogReaders = []board.AnalogReaderConfig{{Name: "bar"}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldBeNil) - - validConfig.DigitalInterrupts = []board.DigitalInterruptConfig{{}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `path.digital_interrupts.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - - validConfig.DigitalInterrupts = []board.DigitalInterruptConfig{{Name: "bar"}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `path.digital_interrupts.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "pin") - - validConfig.DigitalInterrupts = []board.DigitalInterruptConfig{{Name: "bar", Pin: "3"}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/board/genericlinux/board.go b/components/board/genericlinux/board.go deleted file mode 100644 index bca89cd14d2..00000000000 --- a/components/board/genericlinux/board.go +++ /dev/null @@ -1,532 +0,0 @@ -//go:build linux - -// Package genericlinux implements a Linux-based board making heavy use of sysfs -// (https://en.wikipedia.org/wiki/Sysfs). This does not provide a board model itself but provides -// the underlying logic for any Linux/sysfs based board. -package genericlinux - -import ( - "context" - "fmt" - "strconv" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - pb "go.viam.com/api/component/board/v1" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/board/mcp3008helper" - "go.viam.com/rdk/components/board/pinwrappers" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// RegisterBoard registers a sysfs based board of the given model. -func RegisterBoard(modelName string, gpioMappings map[string]GPIOBoardMapping) { - resource.RegisterComponent( - board.API, - resource.DefaultModelFamily.WithModel(modelName), - resource.Registration[board.Board, *Config]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (board.Board, error) { - return NewBoard(ctx, conf, ConstPinDefs(gpioMappings), logger) - }, - }) -} - -// NewBoard is the constructor for a Board. -func NewBoard( - ctx context.Context, - conf resource.Config, - convertConfig ConfigConverter, - logger logging.Logger, -) (board.Board, error) { - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - - b := &Board{ - Named: conf.ResourceName().AsNamed(), - convertConfig: convertConfig, - - logger: logger, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - - analogReaders: map[string]*wrappedAnalogReader{}, - gpios: map[string]*gpioPin{}, - interrupts: map[string]*digitalInterrupt{}, - } - - if err := b.Reconfigure(ctx, nil, conf); err != nil { - return nil, err - } - return b, nil -} - -// Reconfigure reconfigures the board with interrupt pins, spi and i2c, and analogs. -func (b *Board) Reconfigure( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, -) error { - newConf, err := b.convertConfig(conf, b.logger) - if err != nil { - return err - } - - b.mu.Lock() - defer b.mu.Unlock() - - if err := b.reconfigureGpios(newConf); err != nil { - return err - } - if err := b.reconfigureAnalogReaders(ctx, newConf); err != nil { - return err - } - if err := b.reconfigureInterrupts(newConf); err != nil { - return err - } - return nil -} - -// This is a helper function used to reconfigure the GPIO pins. It looks for the key in the map -// whose value resembles the target pin definition. -func getMatchingPin(target GPIOBoardMapping, mapping map[string]GPIOBoardMapping) (string, bool) { - for name, def := range mapping { - if target == def { - return name, true - } - } - return "", false -} - -func (b *Board) reconfigureGpios(newConf *LinuxBoardConfig) error { - // First, find old pins that are no longer defined, and destroy them. - for oldName, mapping := range b.gpioMappings { - if _, ok := getMatchingPin(mapping, newConf.GpioMappings); ok { - continue // This pin is in the new mapping, so don't destroy it. - } - - // Otherwise, remove the pin because it's not in the new mapping. - if pin, ok := b.gpios[oldName]; ok { - if err := pin.Close(); err != nil { - return err - } - delete(b.gpios, oldName) - continue - } - - // If we get here, the old pin definition exists, but the old pin does not. Check if it's a - // digital interrupt. - if interrupt, ok := b.interrupts[oldName]; ok { - if err := closeInterrupt(interrupt); err != nil { - return err - } - delete(b.interrupts, oldName) - continue - } - - // If we get here, there is a logic bug somewhere. but failing to delete a nonexistent pin - // seemingly doesn't hurt anything, so just log the error and continue. - b.logger.Errorf("During reconfiguration, old pin '%s' should be destroyed, but "+ - "it doesn't exist!?", oldName) - } - - // Next, compare the new pin definitions to the old ones, to build up 2 sets: pins to rename, - // and new pins to create. Don't actually create any yet, in case you'd overwrite a pin that - // should be renamed out of the way first. - toRename := map[string]string{} // Maps old names for pins to new names - toCreate := map[string]GPIOBoardMapping{} - for newName, mapping := range newConf.GpioMappings { - if oldName, ok := getMatchingPin(mapping, b.gpioMappings); ok { - if oldName != newName { - toRename[oldName] = newName - } - } else { - toCreate[newName] = mapping - } - } - - // Rename the ones whose name changed. The ordering here is tricky: if B should be renamed to C - // while A should be renamed to B, we need to make sure we don't overwrite B with A and then - // rename it to C. To avoid this, move all the pins to rename into a temporary data structure, - // then move them all back again afterward. - tempGpios := map[string]*gpioPin{} - tempInterrupts := map[string]*digitalInterrupt{} - for oldName, newName := range toRename { - if pin, ok := b.gpios[oldName]; ok { - tempGpios[newName] = pin - delete(b.gpios, oldName) - continue - } - - // If we get here, again check if the missing pin is a digital interrupt. - if interrupt, ok := b.interrupts[oldName]; ok { - tempInterrupts[newName] = interrupt - delete(b.interrupts, oldName) - continue - } - - return fmt.Errorf("during reconfiguration, old pin '%s' should be renamed to '%s', but "+ - "it doesn't exist!?", oldName, newName) - } - - // Now move all the pins back from the temporary data structures. - for newName, pin := range tempGpios { - b.gpios[newName] = pin - } - for newName, interrupt := range tempInterrupts { - b.interrupts[newName] = interrupt - } - - // Finally, create the new pins. - for newName, mapping := range toCreate { - b.gpios[newName] = b.createGpioPin(mapping) - } - - b.gpioMappings = newConf.GpioMappings - return nil -} - -func (b *Board) reconfigureAnalogReaders(ctx context.Context, newConf *LinuxBoardConfig) error { - stillExists := map[string]struct{}{} - for _, c := range newConf.AnalogReaders { - channel, err := strconv.Atoi(c.Pin) - if err != nil { - return errors.Errorf("bad analog pin (%s)", c.Pin) - } - - bus := buses.NewSpiBus(c.SPIBus) - - stillExists[c.Name] = struct{}{} - if curr, ok := b.analogReaders[c.Name]; ok { - if curr.chipSelect != c.ChipSelect { - ar := &mcp3008helper.MCP3008AnalogReader{channel, bus, c.ChipSelect} - curr.reset(ctx, curr.chipSelect, - pinwrappers.SmoothAnalogReader(ar, board.AnalogReaderConfig{ - AverageOverMillis: c.AverageOverMillis, SamplesPerSecond: c.SamplesPerSecond, - }, b.logger)) - } - continue - } - ar := &mcp3008helper.MCP3008AnalogReader{channel, bus, c.ChipSelect} - b.analogReaders[c.Name] = newWrappedAnalogReader(ctx, c.ChipSelect, - pinwrappers.SmoothAnalogReader(ar, board.AnalogReaderConfig{ - AverageOverMillis: c.AverageOverMillis, SamplesPerSecond: c.SamplesPerSecond, - }, b.logger)) - } - - for name := range b.analogReaders { - if _, ok := stillExists[name]; ok { - continue - } - b.analogReaders[name].reset(ctx, "", nil) - delete(b.analogReaders, name) - } - return nil -} - -// This helper function is used while reconfiguring digital interrupts. It finds the new config (if -// any) for a pre-existing digital interrupt. -func findNewDigIntConfig( - interrupt *digitalInterrupt, confs []board.DigitalInterruptConfig, logger logging.Logger, -) *board.DigitalInterruptConfig { - for _, newConfig := range confs { - if newConfig.Pin == interrupt.config.Pin { - return &newConfig - } - } - if interrupt.config.Name == interrupt.config.Pin { - // This interrupt is named identically to its pin. It was probably created on the fly - // by some other component (an encoder?). Unless there's now some other config with the - // same name but on a different pin, keep it initialized as-is. - for _, intConfig := range confs { - if intConfig.Name == interrupt.config.Name { - // The name of this interrupt is defined in the new config, but on a different - // pin. This interrupt should be closed. - return nil - } - } - logger.Debugf( - "Keeping digital interrupt on pin %s even though it's not explicitly mentioned "+ - "in the new board config", - interrupt.config.Pin) - return interrupt.config - } - return nil -} - -func (b *Board) reconfigureInterrupts(newConf *LinuxBoardConfig) error { - // Any pin that already exists in the right configuration should just be copied over; closing - // and re-opening it risks losing its state. - newInterrupts := make(map[string]*digitalInterrupt, len(newConf.DigitalInterrupts)) - - // Reuse any old interrupts that have new configs - for _, oldInterrupt := range b.interrupts { - if newConfig := findNewDigIntConfig(oldInterrupt, newConf.DigitalInterrupts, b.logger); newConfig == nil { - // The old interrupt shouldn't exist any more, but it probably became a GPIO pin. - if err := closeInterrupt(oldInterrupt); err != nil { - return err // This should never happen, but the linter worries anyway. - } - if newGpioConfig, ok := b.gpioMappings[oldInterrupt.config.Pin]; ok { - // See gpio.go for createGpioPin. - b.gpios[oldInterrupt.config.Pin] = b.createGpioPin(newGpioConfig) - } else { - b.logger.Warnf("Old interrupt pin was on nonexistent GPIO pin '%s', ignoring", - oldInterrupt.config.Pin) - } - } else { // The old interrupt should stick around. - if err := oldInterrupt.interrupt.Reconfigure(*newConfig); err != nil { - return err - } - oldInterrupt.config = newConfig - newInterrupts[newConfig.Name] = oldInterrupt - } - } - oldInterrupts := b.interrupts - b.interrupts = newInterrupts - - // Add any new interrupts that should be freshly made. - for _, config := range newConf.DigitalInterrupts { - if interrupt, ok := b.interrupts[config.Name]; ok { - if interrupt.config.Pin == config.Pin { - continue // Already initialized; keep going - } - // If the interrupt's name matches but the pin does not, the interrupt we already have - // was implicitly created (e.g., its name is "38" so we created it on pin 38 even - // though it was not explicitly mentioned in the old board config), but the new config - // is explicit (e.g., its name is still "38" but it's been moved to pin 37). Close the - // old one and initialize it anew. - if err := closeInterrupt(interrupt); err != nil { - return err - } - // Although we delete the implicit interrupt from b.interrupts, it's still in - // oldInterrupts, so we haven't lost the channels it reports to and can still copy them - // over to the new struct. - delete(b.interrupts, config.Name) - } - - if oldPin, ok := b.gpios[config.Pin]; ok { - if err := oldPin.Close(); err != nil { - return err - } - delete(b.gpios, config.Pin) - } - - // If there was an old interrupt pin with this same name, reuse the part that holds its - // callbacks. Anything subscribed to the old pin will expect to still be subscribed to the - // new one. - var oldCallbackHolder pinwrappers.ReconfigurableDigitalInterrupt - if oldInterrupt, ok := oldInterrupts[config.Name]; ok { - oldCallbackHolder = oldInterrupt.interrupt - } - interrupt, err := b.createDigitalInterrupt( - b.cancelCtx, config, b.gpioMappings, oldCallbackHolder) - if err != nil { - return err - } - b.interrupts[config.Name] = interrupt - } - - return nil -} - -type wrappedAnalogReader struct { - mu sync.RWMutex - chipSelect string - reader *pinwrappers.AnalogSmoother -} - -func newWrappedAnalogReader(ctx context.Context, chipSelect string, reader *pinwrappers.AnalogSmoother) *wrappedAnalogReader { - var wrapped wrappedAnalogReader - wrapped.reset(ctx, chipSelect, reader) - return &wrapped -} - -func (a *wrappedAnalogReader) Read(ctx context.Context, extra map[string]interface{}) (int, error) { - a.mu.RLock() - defer a.mu.RUnlock() - if a.reader == nil { - return 0, errors.New("closed") - } - return a.reader.Read(ctx, extra) -} - -func (a *wrappedAnalogReader) Close(ctx context.Context) error { - return a.reader.Close(ctx) -} - -func (a *wrappedAnalogReader) reset(ctx context.Context, chipSelect string, reader *pinwrappers.AnalogSmoother) { - a.mu.Lock() - defer a.mu.Unlock() - if a.reader != nil { - utils.UncheckedError(a.reader.Close(ctx)) - } - a.reader = reader - a.chipSelect = chipSelect -} - -func (a *wrappedAnalogReader) Write(ctx context.Context, value int, extra map[string]interface{}) error { - return grpc.UnimplementedError -} - -// Board implements a component for a Linux machine. -type Board struct { - resource.Named - mu sync.RWMutex - convertConfig ConfigConverter - - gpioMappings map[string]GPIOBoardMapping - analogReaders map[string]*wrappedAnalogReader - logger logging.Logger - - gpios map[string]*gpioPin - interrupts map[string]*digitalInterrupt - - cancelCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup -} - -// AnalogByName returns the analog pin by the given name if it exists. -func (b *Board) AnalogByName(name string) (board.Analog, error) { - a, ok := b.analogReaders[name] - if !ok { - return nil, errors.Errorf("can't find AnalogReader (%s)", name) - } - return a, nil -} - -// DigitalInterruptByName returns the interrupt by the given name if it exists. -func (b *Board) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { - b.mu.Lock() - defer b.mu.Unlock() - - interrupt, ok := b.interrupts[name] - if ok { - return interrupt.interrupt, nil - } - - // Otherwise, the name is not something we recognize yet. If it appears to be a GPIO pin, we'll - // remove its GPIO capabilities and turn it into a digital interrupt. - gpio, ok := b.gpios[name] - if !ok { - return nil, fmt.Errorf("cant find GPIO (%s)", name) - } - if err := gpio.Close(); err != nil { - return nil, err - } - - defaultInterruptConfig := board.DigitalInterruptConfig{ - Name: name, - Pin: name, - } - interrupt, err := b.createDigitalInterrupt( - b.cancelCtx, defaultInterruptConfig, b.gpioMappings, nil) - if err != nil { - return nil, err - } - - delete(b.gpios, name) - b.interrupts[name] = interrupt - return interrupt.interrupt, nil -} - -// AnalogNames returns the names of all known analog pins. -func (b *Board) AnalogNames() []string { - names := []string{} - for k := range b.analogReaders { - names = append(names, k) - } - return names -} - -// DigitalInterruptNames returns the names of all known digital interrupts. -func (b *Board) DigitalInterruptNames() []string { - if b.interrupts == nil { - return nil - } - - names := []string{} - for name := range b.interrupts { - names = append(names, name) - } - return names -} - -// GPIOPinByName returns a GPIOPin by name. -func (b *Board) GPIOPinByName(pinName string) (board.GPIOPin, error) { - if pin, ok := b.gpios[pinName]; ok { - return pin, nil - } - - // Check if pin is a digital interrupt: those can still be used as inputs. - if interrupt, interruptOk := b.interrupts[pinName]; interruptOk { - return &gpioInterruptWrapperPin{*interrupt}, nil - } - - return nil, errors.Errorf("cannot find GPIO for unknown pin: %s", pinName) -} - -// SetPowerMode sets the board to the given power mode. If provided, -// the board will exit the given power mode after the specified -// duration. -func (b *Board) SetPowerMode( - ctx context.Context, - mode pb.PowerMode, - duration *time.Duration, -) error { - return grpc.UnimplementedError -} - -// StreamTicks starts a stream of digital interrupt ticks. -func (b *Board) StreamTicks(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, -) error { - for _, i := range interrupts { - pinwrappers.AddCallback(i.(*pinwrappers.BasicDigitalInterrupt), ch) - } - - b.activeBackgroundWorkers.Add(1) - - utils.ManagedGo(func() { - // Wait until it's time to shut down then remove callbacks. - select { - case <-ctx.Done(): - case <-b.cancelCtx.Done(): - } - for _, i := range interrupts { - pinwrappers.RemoveCallback(i.(*pinwrappers.BasicDigitalInterrupt), ch) - } - }, b.activeBackgroundWorkers.Done) - - return nil -} - -// Close attempts to cleanly close each part of the board. -func (b *Board) Close(ctx context.Context) error { - b.mu.Lock() - b.cancelFunc() - b.mu.Unlock() - b.activeBackgroundWorkers.Wait() - - var err error - for _, pin := range b.gpios { - err = multierr.Combine(err, pin.Close()) - } - for _, interrupt := range b.interrupts { - err = multierr.Combine(err, closeInterrupt(interrupt)) - } - for _, reader := range b.analogReaders { - err = multierr.Combine(err, reader.Close(ctx)) - } - return err -} diff --git a/components/board/genericlinux/board_nonlinux.go b/components/board/genericlinux/board_nonlinux.go deleted file mode 100644 index a69a9239f78..00000000000 --- a/components/board/genericlinux/board_nonlinux.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build !linux - -// Package genericlinux is for creating board components running Linux. This file, however, is a -// placeholder for when you build the server in a non-Linux environment. -package genericlinux - -import ( - "context" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// RegisterBoard would register a sysfs based board of the given model. However, this one never -// creates a board, and instead returns errors about making a Linux board on a non-Linux OS. -func RegisterBoard(modelName string, gpioMappings map[string]GPIOBoardMapping) { - resource.RegisterComponent( - board.API, - resource.DefaultModelFamily.WithModel(modelName), - resource.Registration[board.Board, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (board.Board, error) { - return nil, errors.New("linux boards are not supported on non-linux OSes") - }, - }) -} - -// GetGPIOBoardMappings attempts to find a compatible GPIOBoardMapping for the given board. -func GetGPIOBoardMappings(modelName string, boardInfoMappings map[string]BoardInformation) (map[string]GPIOBoardMapping, error) { - return nil, errors.New("linux boards are not supported on non-linux OSes") -} diff --git a/components/board/genericlinux/board_test.go b/components/board/genericlinux/board_test.go deleted file mode 100644 index 743d67b0fb6..00000000000 --- a/components/board/genericlinux/board_test.go +++ /dev/null @@ -1,146 +0,0 @@ -//go:build linux - -// These tests will only run on Linux! Viam's automated build system on Github uses Linux, though, -// so they should run on every PR. We made the tests Linux-only because this entire package is -// Linux-only, and building non-Linux support solely for the test meant that the code tested might -// not be the production code. -package genericlinux - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/mcp3008helper" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestGenericLinux(t *testing.T) { - ctx := context.Background() - - b := &Board{ - logger: logging.NewTestLogger(t), - } - - t.Run("test empty sysfs board", func(t *testing.T) { - _, err := b.GPIOPinByName("10") - test.That(t, err, test.ShouldNotBeNil) - }) - - b = &Board{ - Named: board.Named("foo").AsNamed(), - gpioMappings: nil, - analogReaders: map[string]*wrappedAnalogReader{"an": {}}, - logger: logging.NewTestLogger(t), - cancelCtx: ctx, - cancelFunc: func() { - }, - } - - t.Run("test analog-readers digital-interrupts and gpio names", func(t *testing.T) { - ans := b.AnalogNames() - test.That(t, ans, test.ShouldResemble, []string{"an"}) - - an1, err := b.AnalogByName("an") - test.That(t, an1, test.ShouldHaveSameTypeAs, &wrappedAnalogReader{}) - test.That(t, err, test.ShouldBeNil) - - an2, err := b.AnalogByName("missing") - test.That(t, an2, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - dns := b.DigitalInterruptNames() - test.That(t, dns, test.ShouldBeNil) - - dn1, err := b.DigitalInterruptByName("dn") - test.That(t, dn1, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - gn1, err := b.GPIOPinByName("10") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, gn1, test.ShouldBeNil) - }) -} - -func TestConfigValidate(t *testing.T) { - validConfig := Config{} - - validConfig.AnalogReaders = []mcp3008helper.MCP3008AnalogConfig{{}} - _, err := validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `path.analogs.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - - validConfig.AnalogReaders = []mcp3008helper.MCP3008AnalogConfig{{Name: "bar"}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldBeNil) - - validConfig.DigitalInterrupts = []board.DigitalInterruptConfig{{}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `path.digital_interrupts.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - - validConfig.DigitalInterrupts = []board.DigitalInterruptConfig{{Name: "bar"}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `path.digital_interrupts.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "pin") - - validConfig.DigitalInterrupts = []board.DigitalInterruptConfig{{Name: "bar", Pin: "3"}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldBeNil) -} - -func TestNewBoard(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - // Create a fake board mapping with two pins for testing. - testBoardMappings := make(map[string]GPIOBoardMapping, 2) - testBoardMappings["1"] = GPIOBoardMapping{ - GPIOChipDev: "gpiochip0", - GPIO: 1, - GPIOName: "1", - PWMSysFsDir: "", - PWMID: -1, - HWPWMSupported: false, - } - testBoardMappings["2"] = GPIOBoardMapping{ - GPIOChipDev: "gpiochip0", - GPIO: 2, - GPIOName: "2", - PWMSysFsDir: "pwm.00", - PWMID: 1, - HWPWMSupported: true, - } - - conf := &Config{} - conf.AnalogReaders = []mcp3008helper.MCP3008AnalogConfig{{Name: "an1", Pin: "1"}} - - config := resource.Config{ - Name: "board1", - ConvertedAttributes: conf, - } - b, err := NewBoard(ctx, config, ConstPinDefs(testBoardMappings), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, b, test.ShouldNotBeNil) - defer b.Close(ctx) - - ans := b.AnalogNames() - test.That(t, ans, test.ShouldResemble, []string{"an1"}) - - dis := b.DigitalInterruptNames() - test.That(t, dis, test.ShouldResemble, []string{}) - - gn1, err := b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - test.That(t, gn1, test.ShouldNotBeNil) - - gn2, err := b.GPIOPinByName("2") - test.That(t, err, test.ShouldBeNil) - test.That(t, gn2, test.ShouldNotBeNil) -} diff --git a/components/board/genericlinux/buses/i2c.go b/components/board/genericlinux/buses/i2c.go deleted file mode 100644 index ccf4284b981..00000000000 --- a/components/board/genericlinux/buses/i2c.go +++ /dev/null @@ -1,152 +0,0 @@ -//go:build linux - -// Package buses is for I2C and SPI boards that run Linux. This file is for I2C support on those -// boards. -package buses - -import ( - "context" - "sync" - - "periph.io/x/conn/v3/i2c" - "periph.io/x/conn/v3/i2c/i2creg" - "periph.io/x/host/v3" - - "go.viam.com/rdk/logging" -) - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } -} - -// I2cBus represents an I2C bus. You can use it to create handles for devices at specific -// addresses on the bus. Creating a handle locks the bus, and closing the handle unlocks the bus -// again, so that you can only communicate with 1 device on the bus at a time. -type i2cBus struct { - // Despite the type name BusCloser, this is the I2C bus itself (plus a way to close itself when - // it's done, though we never use that because we want to keep it open until the entire process - // exits)! - closeableBus i2c.BusCloser - mu sync.Mutex - deviceName string -} - -// NewI2cBus creates a new I2C (the public interface) object (implemented as the private i2cBus -// struct). -func NewI2cBus(deviceName string) (I2C, error) { - b := &i2cBus{} - if err := b.reset(deviceName); err != nil { - return nil, err - } - return b, nil -} - -func (bus *i2cBus) reset(deviceName string) error { - bus.mu.Lock() - defer bus.mu.Unlock() - - if bus.closeableBus != nil { // Close any old bus we used to have - if err := bus.closeableBus.Close(); err != nil { - return err - } - bus.closeableBus = nil - } - - bus.deviceName = deviceName - return nil -} - -// OpenHandle lets the i2cBus type implement the I2C interface. It returns a handle for -// communicating with a device at a specific I2C handle. Opening a handle locks the I2C bus so -// nothing else can use it, and closing the handle unlocks the bus again. -func (bus *i2cBus) OpenHandle(addr byte) (I2CHandle, error) { - bus.mu.Lock() // Lock the bus so no other handle can use it until this handle is closed. - - // If we haven't yet connected to the bus itself, do so now. - if bus.closeableBus == nil { - newBus, err := i2creg.Open(bus.deviceName) - if err != nil { - bus.mu.Unlock() // We never created a handle, so unlock the bus for next time. - return nil, err - } - bus.closeableBus = newBus - } - - return &I2cHandle{device: &i2c.Dev{Bus: bus.closeableBus, Addr: uint16(addr)}, parentBus: bus}, nil -} - -// I2cHandle represents a way to talk to a specific device on the I2C bus. Creating a handle locks -// the bus so nothing else can use it, and closing the handle unlocks it again. -type I2cHandle struct { // Implements the I2CHandle interface - device *i2c.Dev // Will become nil if we Close() the handle - parentBus *i2cBus -} - -// Write writes the given bytes to the handle. For I2C devices that organize their data into -// registers, prefer using WriteBlockData instead. -func (h *I2cHandle) Write(ctx context.Context, tx []byte) error { - return h.device.Tx(tx, nil) -} - -// Read reads the given number of bytes from the handle. For I2C devices that organize their data -// into registers, prefer using ReadBlockData instead. -func (h *I2cHandle) Read(ctx context.Context, count int) ([]byte, error) { - buffer := make([]byte, count) - err := h.device.Tx(nil, buffer) - if err != nil { - return nil, err - } - return buffer, nil -} - -// This is a private helper function, used to implement the rest of the I2CHandle interface. -func (h *I2cHandle) transactAtRegister(register byte, w, r []byte) error { - if w == nil { - w = []byte{} - } - fullW := make([]byte, len(w)+1) - fullW[0] = register - copy(fullW[1:], w) - return h.device.Tx(fullW, r) -} - -// ReadByteData reads a single byte from the given register on this I2C device. -func (h *I2cHandle) ReadByteData(ctx context.Context, register byte) (byte, error) { - result := make([]byte, 1) - err := h.transactAtRegister(register, nil, result) - if err != nil { - return 0, err - } - return result[0], nil -} - -// WriteByteData writes a single byte to the given register on this I2C device. -func (h *I2cHandle) WriteByteData(ctx context.Context, register, data byte) error { - return h.transactAtRegister(register, []byte{data}, nil) -} - -// ReadBlockData reads the given number of bytes from the I2C device, starting at the given -// register. -func (h *I2cHandle) ReadBlockData(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - result := make([]byte, numBytes) - err := h.transactAtRegister(register, nil, result) - if err != nil { - return nil, err - } - return result, nil -} - -// WriteBlockData writes the given bytes into the given register on the I2C device. -func (h *I2cHandle) WriteBlockData(ctx context.Context, register byte, data []byte) error { - return h.transactAtRegister(register, data, nil) -} - -// Close closes the handle to the device, and unlocks the I2C bus. -func (h *I2cHandle) Close() error { - defer h.parentBus.mu.Unlock() // Unlock the entire bus so someone else can use it - h.device = nil - // Don't close the bus itself: it should remain open for other handles to use - return nil -} diff --git a/components/board/genericlinux/buses/i2c_interfaces.go b/components/board/genericlinux/buses/i2c_interfaces.go deleted file mode 100644 index 46bc30920cc..00000000000 --- a/components/board/genericlinux/buses/i2c_interfaces.go +++ /dev/null @@ -1,44 +0,0 @@ -// Package buses offers SPI and I2C buses for generic Linux systems. -package buses - -import ( - "context" -) - -// I2C represents a shareable I2C bus on the board. -type I2C interface { - // OpenHandle locks returns a handle interface that MUST be closed when done. - // you cannot have 2 open for the same addr - OpenHandle(addr byte) (I2CHandle, error) -} - -// I2CHandle is similar to an io handle. It MUST be closed to release the bus. -type I2CHandle interface { - Write(ctx context.Context, tx []byte) error - Read(ctx context.Context, count int) ([]byte, error) - - ReadByteData(ctx context.Context, register byte) (byte, error) - WriteByteData(ctx context.Context, register, data byte) error - - ReadBlockData(ctx context.Context, register byte, numBytes uint8) ([]byte, error) - WriteBlockData(ctx context.Context, register byte, data []byte) error - - // Close closes the handle and releases the lock on the bus. - Close() error -} - -// An I2CRegister is a lightweight wrapper around a handle for a particular register. -type I2CRegister struct { - Handle I2CHandle - Register byte -} - -// ReadByteData reads a byte from the I2C channel register. -func (reg *I2CRegister) ReadByteData(ctx context.Context) (byte, error) { - return reg.Handle.ReadByteData(ctx, reg.Register) -} - -// WriteByteData writes a byte to the I2C channel register. -func (reg *I2CRegister) WriteByteData(ctx context.Context, data byte) error { - return reg.Handle.WriteByteData(ctx, reg.Register, data) -} diff --git a/components/board/genericlinux/buses/spi.go b/components/board/genericlinux/buses/spi.go deleted file mode 100644 index 5ca0dffc25e..00000000000 --- a/components/board/genericlinux/buses/spi.go +++ /dev/null @@ -1,81 +0,0 @@ -//go:build linux - -package buses - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "periph.io/x/conn/v3/physic" - "periph.io/x/conn/v3/spi" - "periph.io/x/conn/v3/spi/spireg" -) - -// NewSpiBus creates a new SPI bus. The name passed in should be the bus number, such as "0" or -// "1". We don't open this bus until you call spiHandle.Xfer(), so there are no errors to return -// immediately here. -func NewSpiBus(name string) SPI { - bus := spiBus{} - bus.reset(name) - return &bus -} - -type spiBus struct { - mu sync.Mutex - openHandle *spiHandle - bus atomic.Pointer[string] -} - -type spiHandle struct { - bus *spiBus - isClosed bool -} - -func (sb *spiBus) OpenHandle() (SPIHandle, error) { - sb.mu.Lock() - sb.openHandle = &spiHandle{bus: sb, isClosed: false} - return sb.openHandle, nil -} - -func (sb *spiBus) Close(ctx context.Context) error { - return nil -} - -func (sb *spiBus) reset(bus string) { - sb.bus.Store(&bus) -} - -func (sh *spiHandle) Xfer(ctx context.Context, baud uint, chipSelect string, mode uint, tx []byte) (rx []byte, err error) { - if sh.isClosed { - return nil, errors.New("can't use Xfer() on an already closed SPIHandle") - } - - busPtr := sh.bus.bus.Load() - if busPtr == nil { - return nil, errors.New("no bus selected") - } - - port, err := spireg.Open(fmt.Sprintf("SPI%s.%s", *busPtr, chipSelect)) - if err != nil { - return nil, err - } - defer func() { - err = multierr.Combine(err, port.Close()) - }() - conn, err := port.Connect(physic.Hertz*physic.Frequency(baud), spi.Mode(mode), 8) - if err != nil { - return nil, err - } - rx = make([]byte, len(tx)) - return rx, conn.Tx(tx, rx) -} - -func (sh *spiHandle) Close() error { - sh.isClosed = true - sh.bus.mu.Unlock() - return nil -} diff --git a/components/board/genericlinux/buses/spi_interfaces.go b/components/board/genericlinux/buses/spi_interfaces.go deleted file mode 100644 index b272cd0b5e4..00000000000 --- a/components/board/genericlinux/buses/spi_interfaces.go +++ /dev/null @@ -1,33 +0,0 @@ -package buses - -import ( - "context" -) - -// SPI represents a shareable SPI bus on a generic Linux board. -type SPI interface { - // OpenHandle locks the shared bus and returns a handle interface that MUST be closed when done. - OpenHandle() (SPIHandle, error) - Close(ctx context.Context) error -} - -// SPIHandle is similar to an io handle. It MUST be closed to release the bus. -type SPIHandle interface { - // Xfer performs a single SPI transfer, that is, the complete transaction from chipselect - // enable to chipselect disable. SPI transfers are synchronous, number of bytes received will - // be equal to the number of bytes sent. Write-only transfers can usually just discard the - // returned bytes. Read-only transfers usually transmit a request/address and continue with - // some number of null bytes to equal the expected size of the returning data. Large - // transmissions are usually broken up into multiple transfers. There are many different - // paradigms for most of the above, and implementation details are chip/device specific. - Xfer( - ctx context.Context, - baud uint, - chipSelect string, - mode uint, - tx []byte, - ) ([]byte, error) - - // Close closes the handle and releases the lock on the bus. - Close() error -} diff --git a/components/board/genericlinux/config.go b/components/board/genericlinux/config.go deleted file mode 100644 index 0e8d0acad53..00000000000 --- a/components/board/genericlinux/config.go +++ /dev/null @@ -1,69 +0,0 @@ -package genericlinux - -import ( - "fmt" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/mcp3008helper" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// A Config describes the configuration of a board and all of its connected parts. -type Config struct { - AnalogReaders []mcp3008helper.MCP3008AnalogConfig `json:"analogs,omitempty"` - DigitalInterrupts []board.DigitalInterruptConfig `json:"digital_interrupts,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - for idx, c := range conf.AnalogReaders { - if err := c.Validate(fmt.Sprintf("%s.%s.%d", path, "analogs", idx)); err != nil { - return nil, err - } - } - for idx, c := range conf.DigitalInterrupts { - if err := c.Validate(fmt.Sprintf("%s.%s.%d", path, "digital_interrupts", idx)); err != nil { - return nil, err - } - } - return nil, nil -} - -// LinuxBoardConfig is a struct containing absolutely everything a genericlinux board might need -// configured. It is a union of the configs for the customlinux boards and the genericlinux boards -// with static pin definitions, because those components all use the same underlying code but have -// different config types (e.g., only customlinux can change its pin definitions during -// reconfiguration). The LinuxBoardConfig struct is a unification of the two of them. Whenever we -// go through reconfiguration, we convert the provided config into a LinuxBoardConfig, and then -// reconfigure based on it. -type LinuxBoardConfig struct { - AnalogReaders []mcp3008helper.MCP3008AnalogConfig - DigitalInterrupts []board.DigitalInterruptConfig - GpioMappings map[string]GPIOBoardMapping -} - -// ConfigConverter is a type synonym for a function to turn whatever config we get during -// reconfiguration into a LinuxBoardConfig, so that we can reconfigure based on that. We return a -// pointer to a LinuxBoardConfig instead of the struct itself so that we can return nil if we -// encounter an error. -type ConfigConverter = func(resource.Config, logging.Logger) (*LinuxBoardConfig, error) - -// ConstPinDefs takes in a map from pin names to GPIOBoardMapping structs, and returns a -// ConfigConverter that will use these pin definitions in the underlying config. It is intended to -// be used for board components whose pin definitions are built into the RDK, such as the -// BeagleBone or Jetson boards. -func ConstPinDefs(gpioMappings map[string]GPIOBoardMapping) ConfigConverter { - return func(conf resource.Config, logger logging.Logger) (*LinuxBoardConfig, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - return &LinuxBoardConfig{ - AnalogReaders: newConf.AnalogReaders, - DigitalInterrupts: newConf.DigitalInterrupts, - GpioMappings: gpioMappings, - }, nil - } -} diff --git a/components/board/genericlinux/data.go b/components/board/genericlinux/data.go deleted file mode 100644 index 267398d95c3..00000000000 --- a/components/board/genericlinux/data.go +++ /dev/null @@ -1,176 +0,0 @@ -//go:build linux - -package genericlinux - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "go.viam.com/rdk/logging" - rdkutils "go.viam.com/rdk/utils" -) - -// adapted from https://github.com/NVIDIA/jetson-gpio (MIT License) - -func noBoardError(modelName string) error { - return fmt.Errorf("could not determine %q model", modelName) -} - -// pwmChipData is a struct used solely within GetGPIOBoardMappings and its sub-pieces. It -// describes a PWM chip within sysfs. -type pwmChipData struct { - Dir string // Absolute path to pseudofile within sysfs to interact with this chip - Npwm int // Taken from the /npwm pseudofile in sysfs: number of lines on the chip -} - -// GetGPIOBoardMappings attempts to find a compatible GPIOBoardMapping for the given board. -func GetGPIOBoardMappings(modelName string, boardInfoMappings map[string]BoardInformation) (map[string]GPIOBoardMapping, error) { - pinDefs, err := getCompatiblePinDefs(modelName, boardInfoMappings) - if err != nil { - return nil, err - } - - return GetGPIOBoardMappingFromPinDefs(pinDefs) -} - -// GetGPIOBoardMappingFromPinDefs attempts to find a compatible board-pin mapping using the pin definitions. -func GetGPIOBoardMappingFromPinDefs(pinDefs []PinDefinition) (map[string]GPIOBoardMapping, error) { - pwmChipsInfo, err := getPwmChipDefs(pinDefs) - if err != nil { - // Try continuing on without hardware PWM support. Many boards do not have it enabled by - // default, and perhaps this robot doesn't even use it. - logging.Global().Debugw("unable to find PWM chips, continuing without them", "error", err) - pwmChipsInfo = map[string]pwmChipData{} - } - - return getBoardMapping(pinDefs, pwmChipsInfo) -} - -// getCompatiblePinDefs returns a list of pin definitions, from the first BoardInformation struct -// that appears compatible with the machine we're running on. -func getCompatiblePinDefs(modelName string, boardInfoMappings map[string]BoardInformation) ([]PinDefinition, error) { - compatibles, err := rdkutils.GetDeviceInfo(modelName) - if err != nil { - return nil, fmt.Errorf("error while getting hardware info %w", err) - } - - var pinDefs []PinDefinition - for _, info := range boardInfoMappings { - for _, v := range info.Compats { - if _, ok := compatibles[v]; ok { - pinDefs = info.PinDefinitions - break - } - } - } - - if pinDefs == nil { - return nil, noBoardError(modelName) - } - return pinDefs, nil -} - -// A helper function: we read the contents of filePath and return its integer value. -func readIntFile(filePath string) (int, error) { - //nolint:gosec - contents, err := os.ReadFile(filePath) - if err != nil { - return -1, err - } - resultInt64, err := strconv.ParseInt(strings.TrimSpace(string(contents)), 10, 64) - return int(resultInt64), err -} - -func getPwmChipDefs(pinDefs []PinDefinition) (map[string]pwmChipData, error) { - // First, collect the names of all relevant PWM chips with duplicates removed. Go doesn't have - // native set objects, so we use a map whose values are ignored. - pwmChipNames := make(map[string]struct{}, len(pinDefs)) - for _, pinDef := range pinDefs { - if pinDef.PwmChipSysfsDir == "" { - continue - } - pwmChipNames[pinDef.PwmChipSysfsDir] = struct{}{} - } - - // Now, look for all chips whose names we found. - pwmChipsInfo := map[string]pwmChipData{} - const sysfsDir = "/sys/class/pwm" - files, err := os.ReadDir(sysfsDir) - if err != nil { - return nil, err - } - - for chipName := range pwmChipNames { - found := false - for _, file := range files { - if !strings.HasPrefix(file.Name(), "pwmchip") { - continue - } - - // look at symlinks to find the correct chip - symlink, err := os.Readlink(filepath.Join(sysfsDir, file.Name())) - if err != nil { - logging.Global().Errorw( - "file is not symlink", "file", file.Name(), "err:", err) - continue - } - - if strings.Contains(symlink, chipName) { - found = true - chipPath := filepath.Join(sysfsDir, file.Name()) - npwm, err := readIntFile(filepath.Join(chipPath, "npwm")) - if err != nil { - return nil, err - } - - pwmChipsInfo[chipName] = pwmChipData{Dir: chipPath, Npwm: npwm} - break - } - } - - if !found { - return nil, fmt.Errorf("unable to find PWM device %s", chipName) - } - } - return pwmChipsInfo, nil -} - -func getBoardMapping(pinDefs []PinDefinition, pwmChipsInfo map[string]pwmChipData, -) (map[string]GPIOBoardMapping, error) { - data := make(map[string]GPIOBoardMapping, len(pinDefs)) - - // For "use" on pins that don't have hardware PWMs - dummyPwmInfo := pwmChipData{Dir: "", Npwm: -1} - - for _, pinDef := range pinDefs { - pwmChipInfo, ok := pwmChipsInfo[pinDef.PwmChipSysfsDir] - if ok { - if pinDef.PwmID >= pwmChipInfo.Npwm { - return nil, fmt.Errorf("too high PWM ID %d for pin %s (npwm is %d for chip %s)", - pinDef.PwmID, pinDef.Name, pwmChipInfo.Npwm, pinDef.PwmChipSysfsDir) - } - } else { - if pinDef.PwmChipSysfsDir == "" { - // This pin isn't supposed to have hardware PWM support; all is well. - pwmChipInfo = dummyPwmInfo - } else { - logging.Global().Errorw( - "cannot find expected hardware PWM chip, continuing without it", "pin", pinDef.Name) - pwmChipInfo = dummyPwmInfo - } - } - - data[pinDef.Name] = GPIOBoardMapping{ - GPIOChipDev: pinDef.DeviceName, - GPIO: pinDef.LineNumber, - GPIOName: pinDef.Name, - PWMSysFsDir: pwmChipInfo.Dir, - PWMID: pinDef.PwmID, - HWPWMSupported: pinDef.PwmID != -1, - } - } - return data, nil -} diff --git a/components/board/genericlinux/digital_interrupts.go b/components/board/genericlinux/digital_interrupts.go deleted file mode 100644 index a3dd0caa5b4..00000000000 --- a/components/board/genericlinux/digital_interrupts.go +++ /dev/null @@ -1,146 +0,0 @@ -//go:build linux - -// Package genericlinux is for Linux boards, and this particular file is for digital interrupt pins -// using the ioctl interface, indirectly by way of mkch's gpio package. -package genericlinux - -import ( - "context" - "sync" - - "github.com/mkch/gpio" - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/pinwrappers" -) - -type digitalInterrupt struct { - boardWorkers *sync.WaitGroup - interrupt pinwrappers.ReconfigurableDigitalInterrupt - line *gpio.LineWithEvent - cancelCtx context.Context - cancelFunc func() - config *board.DigitalInterruptConfig -} - -func (b *Board) createDigitalInterrupt( - ctx context.Context, - config board.DigitalInterruptConfig, - gpioMappings map[string]GPIOBoardMapping, - // If we are reconfiguring a board, we might already have channels subscribed and listening for - // updates from an old interrupt that we're creating on a new pin. In that case, reuse the part - // that holds the callbacks. - oldCallbackHolder pinwrappers.ReconfigurableDigitalInterrupt, -) (*digitalInterrupt, error) { - mapping, ok := gpioMappings[config.Pin] - if !ok { - return nil, errors.Errorf("unknown interrupt pin %s", config.Pin) - } - - chip, err := gpio.OpenChip(mapping.GPIOChipDev) - if err != nil { - return nil, err - } - defer utils.UncheckedErrorFunc(chip.Close) - - line, err := chip.OpenLineWithEvents( - uint32(mapping.GPIO), gpio.Input, gpio.BothEdges, "viam-interrupt") - if err != nil { - return nil, err - } - - var interrupt pinwrappers.ReconfigurableDigitalInterrupt - if oldCallbackHolder == nil { - interrupt, err = pinwrappers.CreateDigitalInterrupt(config) - if err != nil { - return nil, multierr.Combine(err, line.Close()) - } - } else { - interrupt = oldCallbackHolder - if err := interrupt.Reconfigure(config); err != nil { - return nil, err // Should never have errors, but this makes the linter happy - } - } - - cancelCtx, cancelFunc := context.WithCancel(ctx) - result := digitalInterrupt{ - boardWorkers: &b.activeBackgroundWorkers, - interrupt: interrupt, - line: line, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - config: &config, - } - result.startMonitor() - return &result, nil -} - -func (di *digitalInterrupt) startMonitor() { - di.boardWorkers.Add(1) - utils.ManagedGo(func() { - for { - select { - case <-di.cancelCtx.Done(): - return - case event := <-di.line.Events(): - utils.UncheckedError(pinwrappers.Tick( - di.cancelCtx, di.interrupt.(*pinwrappers.BasicDigitalInterrupt), event.RisingEdge, uint64(event.Time.UnixNano()))) - } - } - }, di.boardWorkers.Done) -} - -func closeInterrupt(di *digitalInterrupt) error { - // We shut down the background goroutine that monitors this interrupt, but don't need to wait - // for it to finish shutting down because it doesn't use anything in the line itself (just a - // channel of events that the line generates). It will shut down sometime soon, and if that's - // after the line is closed, that's fine. - di.cancelFunc() - return di.line.Close() -} - -// struct implements board.GPIOPin to support reading current state of digital interrupt pins as GPIO inputs. -type gpioInterruptWrapperPin struct { - interrupt digitalInterrupt -} - -func (gp gpioInterruptWrapperPin) Set( - ctx context.Context, isHigh bool, extra map[string]interface{}, -) error { - return errors.New("cannot set value of a digital interrupt pin") -} - -func (gp gpioInterruptWrapperPin) Get(ctx context.Context, extra map[string]interface{}) (result bool, err error) { - value, err := gp.interrupt.line.Value() - if err != nil { - return false, err - } - - // We'd expect value to be either 0 or 1, but any non-zero value should be considered high. - return (value != 0), nil -} - -func (gp gpioInterruptWrapperPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, errors.New("cannot get PWM of a digital interrupt pin") -} - -func (gp gpioInterruptWrapperPin) SetPWM( - ctx context.Context, dutyCyclePct float64, extra map[string]interface{}, -) error { - return errors.New("cannot set PWM of a digital interrupt pin") -} - -func (gp gpioInterruptWrapperPin) PWMFreq( - ctx context.Context, extra map[string]interface{}, -) (uint, error) { - return 0, errors.New("cannot get PWM freq of a digital interrupt pin") -} - -func (gp gpioInterruptWrapperPin) SetPWMFreq( - ctx context.Context, freqHz uint, extra map[string]interface{}, -) error { - return errors.New("cannot set PWM freq of a digital interrupt pin") -} diff --git a/components/board/genericlinux/gpio.go b/components/board/genericlinux/gpio.go deleted file mode 100644 index 128c5172d46..00000000000 --- a/components/board/genericlinux/gpio.go +++ /dev/null @@ -1,386 +0,0 @@ -//go:build linux - -// Package genericlinux is for Linux boards, and this particular file is for GPIO pins using the -// ioctl interface, indirectly by way of mkch's gpio package. -package genericlinux - -import ( - "context" - "sync" - "time" - - "github.com/mkch/gpio" - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/logging" -) - -const noPin = 0xFFFFFFFF // noPin is the uint32 version of -1. A pin with this offset has no GPIO - -type gpioPin struct { - boardWorkers *sync.WaitGroup - - // These values should both be considered immutable. - devicePath string - offset uint32 - - // These values are mutable. Lock the mutex when interacting with them. - line *gpio.Line - isInput bool - hwPwm *pwmDevice // Defined in hw_pwm.go, will be nil for pins that don't support it. - pwmFreqHz uint - pwmDutyCyclePct float64 - - mu sync.Mutex - cancelCtx context.Context - logger logging.Logger - - swPwmCancel func() -} - -func (pin *gpioPin) wrapError(err error) error { - return errors.Wrapf(err, "from GPIO device %s line %d", pin.devicePath, pin.offset) -} - -// This is a private helper function that should only be called when the mutex is locked. It sets -// pin.line to a valid struct or returns an error. -func (pin *gpioPin) openGpioFd(isInput bool) error { - if isInput != pin.isInput { - // We're switching from an input pin to an output one or vice versa. Close the line and - // repoen in the other mode. - if err := pin.closeGpioFd(); err != nil { - return err // Already wrapped - } - pin.isInput = isInput - } - - if pin.line != nil { - return nil // The pin is already opened, don't re-open it. - } - - if pin.hwPwm != nil { - // If the pin is currently used by the hardware PWM chip, shut that down before we can open - // it for basic GPIO use. - if err := pin.hwPwm.Close(); err != nil { - return pin.wrapError(err) - } - } - - if pin.offset == noPin { - // This is not a GPIO pin. Now that we've turned off the PWM, return early. - return nil - } - - chip, err := gpio.OpenChip(pin.devicePath) - if err != nil { - return pin.wrapError(err) - } - defer utils.UncheckedErrorFunc(chip.Close) - - direction := gpio.Output - if pin.isInput { - direction = gpio.Input - } - - // The 0 just means the default output value for this pin is off. We'll set it to the intended - // value in Set(), below, if this is an output pin. - // NOTE: we could pass in extra flags to configure the pin to be open-source or open-drain, but - // we haven't done that yet, and we instead go with whatever the default on the board is. - line, err := chip.OpenLine(pin.offset, 0, direction, "viam-gpio") - if err != nil { - return pin.wrapError(err) - } - pin.line = line - return nil -} - -func (pin *gpioPin) closeGpioFd() error { - if pin.line == nil { - return nil // The pin is already closed. - } - if err := pin.line.Close(); err != nil { - return pin.wrapError(err) - } - pin.line = nil - return nil -} - -// This helps implement the board.GPIOPin interface for gpioPin. -func (pin *gpioPin) Set(ctx context.Context, isHigh bool, - extra map[string]interface{}, -) (err error) { - pin.mu.Lock() - defer pin.mu.Unlock() - - // Shut down any software PWM loop that might be running. - if pin.swPwmCancel != nil { - pin.swPwmCancel() - pin.swPwmCancel = nil - } - - return pin.setInternal(isHigh) -} - -// This function assumes you've already locked the mutex. It sets the value of a pin without -// changing whether the pin is part of a software PWM loop. -func (pin *gpioPin) setInternal(isHigh bool) (err error) { - var value byte - if isHigh { - value = 1 - } else { - value = 0 - } - - if err := pin.openGpioFd( /* isInput= */ false); err != nil { - return err - } - - if pin.offset == noPin { - if isHigh { - return errors.New("cannot set non-GPIO pin high") - } - // Otherwise, just return: we shut down any PWM stuff in openGpioFd. - return nil - } - - return pin.wrapError(pin.line.SetValue(value)) -} - -// This helps implement the board.GPIOPin interface for gpioPin. -func (pin *gpioPin) Get( - ctx context.Context, extra map[string]interface{}, -) (result bool, err error) { - pin.mu.Lock() - defer pin.mu.Unlock() - - if pin.offset == noPin { - return false, errors.New("cannot read from non-GPIO pin") - } - - if err := pin.openGpioFd( /* isInput= */ true); err != nil { - return false, err - } - - value, err := pin.line.Value() - if err != nil { - return false, pin.wrapError(err) - } - - // We'd expect value to be either 0 or 1, but any non-zero value should be considered high. - return (value != 0), nil -} - -// Lock the mutex before calling this! We'll spin up a background goroutine to create a PWM signal -// in software, if we're supposed to and one isn't already running. -func (pin *gpioPin) startSoftwarePWM() error { - if pin.pwmDutyCyclePct == 0 || pin.pwmFreqHz == 0 { - // We don't have both parameters set up. Stop any PWM loop we might have started previously. - if pin.swPwmCancel != nil { - pin.swPwmCancel() - pin.swPwmCancel = nil - } - if pin.hwPwm != nil { - return pin.hwPwm.Close() - } - // If we used to have a software PWM loop, we might have stopped the loop while the pin was - // on. Remember to turn it off! - return pin.setInternal(false) - } - - // Otherwise, we need to output a PWM signal. - if pin.hwPwm != nil { - if pin.pwmFreqHz > 1 { - if err := pin.closeGpioFd(); err != nil { - return err - } - // Shut down any software PWM loop that might be running. - if pin.swPwmCancel != nil { - pin.swPwmCancel() - pin.swPwmCancel = nil - } - return pin.hwPwm.SetPwm(pin.pwmFreqHz, pin.pwmDutyCyclePct) - } - // Although this pin has hardware PWM support, many PWM chips cannot output signals at - // frequencies this low. Stop any hardware PWM, and fall through to using a software PWM - // loop below. - if err := pin.hwPwm.Close(); err != nil { - return err - } - } - - // If we get here, we need a software loop to drive the PWM signal, either because this pin - // doesn't have hardware support or because we want to drive it at such a low frequency that - // the hardware chip can't do it. - if pin.swPwmCancel != nil { - // We already have a software PWM loop running. It will pick up the changes on its own. - return nil - } - - ctx, cancel := context.WithCancel(pin.cancelCtx) - pin.swPwmCancel = cancel - pin.boardWorkers.Add(1) - utils.ManagedGo(func() { pin.softwarePwmLoop(ctx) }, pin.boardWorkers.Done) - return nil -} - -// accurateSleep is intended to be a replacement for utils.SelectContextOrWait which wakes up -// closer to when it's supposed to. We return whether the context is still valid (not yet -// cancelled). -func accurateSleep(ctx context.Context, duration time.Duration) bool { - // If we use utils.SelectContextOrWait(), we will wake up sometime after when we're supposed - // to, which can be hundreds of microseconds later (because the process scheduler in the OS only - // schedules things every millisecond or two). For use cases like a web server responding to a - // query, that's fine. but when outputting a PWM signal, hundreds of microseconds can be a big - // deal. To avoid this, we sleep for less time than we're supposed to, and then busy-wait until - // the right time. Inspiration for this approach was taken from - // https://blog.bearcats.nl/accurate-sleep-function/ - // On a raspberry pi 4, naively calling utils.SelectContextOrWait tended to have an error of - // about 140-300 microseconds, while this version had an error of 0.3-0.6 microseconds. - startTime := time.Now() - maxBusyWaitTime := 1500 * time.Microsecond - if duration > maxBusyWaitTime { - shorterDuration := duration - maxBusyWaitTime - if !utils.SelectContextOrWait(ctx, shorterDuration) { - return false - } - } - - for time.Since(startTime) < duration { - if err := ctx.Err(); err != nil { - return false - } - // Otherwise, busy-wait some more - } - return true -} - -// We turn the pin either on or off, and then wait until it's time to turn it off or on again (or -// until we're supposed to shut down). We return whether we should continue the software PWM cycle. -func (pin *gpioPin) halfPwmCycle(ctx context.Context, shouldBeOn bool) bool { - // Make local copies of these, then release the mutex - var dutyCycle float64 - var freqHz uint - - // We encapsulate some of this code into its own function, to ensure that the mutex is unlocked - // at the appropriate time even if we return early. - shouldContinue := func() bool { - pin.mu.Lock() - defer pin.mu.Unlock() - // Before we modify the pin, check if we should stop running - if ctx.Err() != nil { - return false - } - - dutyCycle = pin.pwmDutyCyclePct - freqHz = pin.pwmFreqHz - - // If there's an error turning the pin on or off, don't stop the whole loop. Hopefully we - // can toggle it next time. However, log any errors so that we notice if there are a bunch - // of them. - utils.UncheckedErrorFunc(func() error { return pin.setInternal(shouldBeOn) }) - return true - }() - - if !shouldContinue { - return false - } - - if !shouldBeOn { - dutyCycle = 1 - dutyCycle - } - duration := time.Duration(float64(time.Second) * dutyCycle / float64(freqHz)) - - return accurateSleep(ctx, duration) -} - -func (pin *gpioPin) softwarePwmLoop(ctx context.Context) { - for { - if !pin.halfPwmCycle(ctx, true) { - return - } - if !pin.halfPwmCycle(ctx, false) { - return - } - } -} - -// This helps implement the board.GPIOPin interface for gpioPin. -func (pin *gpioPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - pin.mu.Lock() - defer pin.mu.Unlock() - - return pin.pwmDutyCyclePct, nil -} - -// This helps implement the board.GPIOPin interface for gpioPin. -func (pin *gpioPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - pin.mu.Lock() - defer pin.mu.Unlock() - - pin.pwmDutyCyclePct = dutyCyclePct - return pin.startSoftwarePWM() -} - -// This helps implement the board.GPIOPin interface for gpioPin. -func (pin *gpioPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - pin.mu.Lock() - defer pin.mu.Unlock() - - return pin.pwmFreqHz, nil -} - -// This helps implement the board.GPIOPin interface for gpioPin. -func (pin *gpioPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - pin.mu.Lock() - defer pin.mu.Unlock() - - pin.pwmFreqHz = freqHz - return pin.startSoftwarePWM() -} - -func (pin *gpioPin) Close() error { - // We keep the gpio.Line object open indefinitely, so it holds its state for as long as this - // struct is around. This function is a way to close it when we're about to go out of scope, so - // we don't leak file descriptors. - pin.mu.Lock() - defer pin.mu.Unlock() - - if pin.hwPwm != nil { - if err := pin.hwPwm.Close(); err != nil { - return err - } - } - - // If a pin has never been used, leave it alone. This is more important than you might expect: - // on some boards (e.g., the Beaglebone AI-64), turning off a GPIO pin tells the kernel that - // the pin is in use by the GPIO system and therefore it cannot be used for I2C or other - // functions. Make sure that closing a pin here doesn't disable I2C! - if pin.line != nil { - // If the entire server is shutting down, it's important to turn off all pins so they don't - // continue outputting signals we can no longer control. - if err := pin.setInternal(false); err != nil { // setInternal won't double-lock the mutex - return err - } - - if err := pin.closeGpioFd(); err != nil { - return err - } - } - - return nil -} - -func (b *Board) createGpioPin(mapping GPIOBoardMapping) *gpioPin { - pin := gpioPin{ - boardWorkers: &b.activeBackgroundWorkers, - devicePath: mapping.GPIOChipDev, - offset: uint32(mapping.GPIO), - cancelCtx: b.cancelCtx, - logger: b.logger, - } - if mapping.HWPWMSupported { - pin.hwPwm = newPwmDevice(mapping.PWMSysFsDir, mapping.PWMID, b.logger) - } - return &pin -} diff --git a/components/board/genericlinux/hw_pwm.go b/components/board/genericlinux/hw_pwm.go deleted file mode 100644 index 402b7f2a142..00000000000 --- a/components/board/genericlinux/hw_pwm.go +++ /dev/null @@ -1,209 +0,0 @@ -//go:build linux - -// Package genericlinux is for Linux boards. This particular file is for using sysfs to -// interact with PWM devices. All of these functions are idempotent: you can double-export a pin or -// double-close it with no problems. -package genericlinux - -import ( - "fmt" - "os" - "sync" - "time" - - "github.com/pkg/errors" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/logging" -) - -// There are times when we need to set the period to some value, any value. It must be a positive -// number of nanoseconds, but some boards (e.g., the Jetson Orin) cannot tolerate periods below 1 -// microsecond. We'll use 1 millisecond, for added confidence that all boards should support it. -const safePeriodNs = 1e6 - -type pwmDevice struct { - chipPath string - line int - - // We have no mutable state, but the mutex is used to write to multiple pseudofiles atomically. - mu sync.Mutex - logger logging.Logger -} - -func newPwmDevice(chipPath string, line int, logger logging.Logger) *pwmDevice { - return &pwmDevice{chipPath: chipPath, line: line, logger: logger} -} - -func writeValue(filepath string, value uint64, logger logging.Logger) error { - logger.Debugf("Writing %d to %s", value, filepath) - data := []byte(fmt.Sprintf("%d", value)) - // The file permissions (the third argument) aren't important: if the file needs to be created, - // something has gone horribly wrong! - err := os.WriteFile(filepath, data, 0o600) - // Some errors (e.g., trying to unexport an already-unexported pin) should get suppressed. If - // we're trying to debug something in here, log the error even if it will later be ignored. - if err != nil { - logger.Debugf("Encountered error writing to sysfs: %s", err) - } - return errors.Wrap(err, filepath) -} - -func (pwm *pwmDevice) writeChip(filename string, value uint64) error { - return writeValue(fmt.Sprintf("%s/%s", pwm.chipPath, filename), value, pwm.logger) -} - -func (pwm *pwmDevice) linePath() string { - return fmt.Sprintf("%s/pwm%d", pwm.chipPath, pwm.line) -} - -func (pwm *pwmDevice) writeLine(filename string, value uint64) error { - return writeValue(fmt.Sprintf("%s/%s", pwm.linePath(), filename), value, pwm.logger) -} - -// Export tells the OS that this pin is in use, and enables configuration via sysfs. -func (pwm *pwmDevice) export() error { - if _, err := os.Lstat(pwm.linePath()); err != nil { - if os.IsNotExist(err) { - // The pseudofile we're trying to export doesn't yet exist. Export it now. This is the - // happy path. - return pwm.writeChip("export", uint64(pwm.line)) - } - return err // Something unexpected has gone wrong. - } - // Otherwise, the line we're trying to export already exists. - pwm.logger.Debugf("Skipping re-export of already-exported line %d on HW PWM chip %s", - pwm.line, pwm.chipPath) - return nil -} - -// Unexport turns off any PWM signal the pin was providing, and tells the OS that this pin is no -// longer in use (so it can be reused as an input pin, etc.). -func (pwm *pwmDevice) unexport() error { - if _, err := os.Lstat(pwm.linePath()); err != nil { - if os.IsNotExist(err) { - pwm.logger.Debugf("Skipping unexport of already-unexported line %d on HW PWM chip %s", - pwm.line, pwm.chipPath) - return nil - } - return err // Something has gone wrong. - } - - // If we unexport the pin while it is enabled, it might continue outputting a PWM signal, - // causing trouble if you start using the pin for something else. So, we need to disable it. - // However, on certain boards (e.g., the Beaglebone AI64), disabling an already-disabled PWM - // device results in an error. We don't care if there's an error: it should be disabled no - // matter what. - goutils.UncheckedError(pwm.disable()) - - // On boards like the Odroid C4, there is a race condition in the kernel where, if you unexport - // the pin too quickly after changing something else about it (e.g., disabling it), the whole - // PWM system gets corrupted. Sleep for a small amount of time to avoid this. - time.Sleep(10 * time.Millisecond) - if err := pwm.writeChip("unexport", uint64(pwm.line)); err != nil { - return err - } - return nil -} - -// Enable tells an exported pin to output the PWM signal it has been configured with. -func (pwm *pwmDevice) enable() error { - // There is no harm in enabling an already-enabled pin; no errors will be returned if we try. - return pwm.writeLine("enable", 1) -} - -// Disable tells an exported pin to stop outputting its PWM signal, but it is still available for -// reconfiguring and re-enabling. -func (pwm *pwmDevice) disable() error { - // There is no harm in disabling an already-disabled pin; no errors will be returned if we try. - return pwm.writeLine("enable", 0) -} - -// Only call this from public functions, to avoid double-wrapping the errors. -func (pwm *pwmDevice) wrapError(err error) error { - // Note that if err is nil, errors.Wrap() will return nil, too. - return errors.Wrapf(err, "HW PWM chipPath %s, line %d", pwm.chipPath, pwm.line) -} - -// SetPwm configures an exported pin and enables its output signal. -// Warning: if this function returns a non-nil error, it could leave the pin in an indeterminate -// state. Maybe it's exported, maybe not. Maybe it's enabled, maybe not. The new frequency and duty -// cycle each might or might not be set. -func (pwm *pwmDevice) SetPwm(freqHz uint, dutyCycle float64) (err error) { - pwm.mu.Lock() - defer pwm.mu.Unlock() - - // If there is ever an error in here, annotate it with which sysfs device and line we're using. - defer func() { - err = pwm.wrapError(err) - }() - - // Every time this pin is used as a (non-PWM) GPIO input or output, it gets unexported on the - // PWM chip. Make sure to re-export it here. - if err := pwm.export(); err != nil { - return err - } - - // Intuitively, we should disable the pin, set the new parameters, and then enable it again. - // However, the BeagleBone AI64 has a weird quirk where you need to enable the pin *before* you - // set the parameters, because enabling it afterwards sets the pin constantly high until the - // period or duty cycle is modified again. So, enable the PWM signal first and *then* set it to - // the correct values. This shouldn't hurt anything on the other boards; it's just not the - // intuitive order. - if err := pwm.enable(); err != nil { - // If the board is newly booted up, the period (and everything else) might be initialized - // to 0, and enabling the pin with a period of 0 results in errors. Let's try making the - // period non-zero and enabling it again. - pwm.logger.Debugf("Cannot enable HW PWM device %s line %d, will try changing period: %s", - pwm.chipPath, pwm.line, err) - if err := pwm.writeLine("period", safePeriodNs); err != nil { - return err - } - // Now, try enabling the pin one more time before giving up. - if err := pwm.enable(); err != nil { - return err - } - } - - // Sysfs has a pseudofile named duty_cycle which contains the number of nanoseconds that the - // pin should be high within a period. It's not how the rest of the world defines a duty cycle, - // so we will refer to it here as the active duration. - periodNs := 1e9 / uint64(freqHz) - activeDurationNs := uint64(float64(periodNs) * dutyCycle) - - // If we ever try setting the active duration higher than the period (or the period lower than - // the active duration), we will get an error. So, make sure we never do that! - - // The BeagleBone has a weird quirk where, if you don't change the period or active duration - // after enabling the PWM line, it just goes high and stays there, rather than blinking at the - // intended rate. To avoid this, we first set the active duration to 0 and the period to 1 - // microsecond, and then set the period and active duration to their intended values. That way, - // if you turn the PWM signal off and on again, it still works because you've changed the - // values after (re-)enabling the line. - - // Setting the active duration to 0 should always work: this is guaranteed to be less than the - // period, unless the period in zero. In that case, just ignore the error. - goutils.UncheckedError(pwm.writeLine("duty_cycle", 0)) - - // Now that the active duration is 0, setting the period to any number should work. - if err := pwm.writeLine("period", safePeriodNs); err != nil { - return err - } - // Same thing here: the active duration is 0, so any value should work for the period. - if err := pwm.writeLine("period", periodNs); err != nil { - return err - } - // Now that the period is set to its intended value, there should be no trouble setting the - // active duration, which is guaranteed to be at most the period. - if err := pwm.writeLine("duty_cycle", activeDurationNs); err != nil { - return err - } - - return nil -} - -func (pwm *pwmDevice) Close() error { - pwm.mu.Lock() - defer pwm.mu.Unlock() - return pwm.wrapError(pwm.unexport()) -} diff --git a/components/board/genericlinux/pin_types.go b/components/board/genericlinux/pin_types.go deleted file mode 100644 index 87a78c86933..00000000000 --- a/components/board/genericlinux/pin_types.go +++ /dev/null @@ -1,88 +0,0 @@ -package genericlinux - -import ( - "encoding/json" - "fmt" - - "github.com/pkg/errors" - - "go.viam.com/rdk/resource" -) - -// GPIOBoardMapping represents a GPIO pin's location locally within a GPIO chip -// and globally within sysfs. -type GPIOBoardMapping struct { - GPIOChipDev string - GPIO int - GPIOName string - PWMSysFsDir string // Absolute path to the directory, empty string for none - PWMID int - HWPWMSupported bool -} - -// PinDefinition describes a gpio pin on a linux board. -type PinDefinition struct { - Name string `json:"name"` - DeviceName string `json:"device_name"` // name of the pin's chip's device, within /dev - LineNumber int `json:"line_number"` // relative line number on chip - PwmChipSysfsDir string `json:"pwm_chip_sysfs_dir,omitempty"` - PwmID int `json:"pwm_id,omitempty"` -} - -// PinDefinitions describes a list of pins on a linux board. -type PinDefinitions struct { - Pins []PinDefinition `json:"pins"` -} - -// UnmarshalJSON handles setting defaults for pin configs. -// Int values default to -1. -func (conf *PinDefinition) UnmarshalJSON(text []byte) error { - type TempPin PinDefinition // needed to prevent infinite recursive calls to UnmarshalJSON - aux := TempPin{ - LineNumber: -1, - PwmID: -1, - } - if err := json.Unmarshal(text, &aux); err != nil { - return err - } - *conf = PinDefinition(aux) - return nil -} - -// Validate ensures all parts of the config are valid. -func (conf *PinDefinition) Validate(path string) error { - if conf.Name == "" { - return resource.NewConfigValidationFieldRequiredError(path, "name") - } - - if conf.DeviceName == "" { - return resource.NewConfigValidationFieldRequiredError(path, "device_name") - } - - // We use -1 as a sentinel value indicating that this pin does not have GPIO capabilities. - // Besides that, the line number must be non-negative. - if conf.LineNumber < -1 { - return resource.NewConfigValidationError(path, errors.New("line_number on gpio chip must be at least zero")) - } - - if conf.PwmChipSysfsDir != "" && conf.PwmID == -1 { - return resource.NewConfigValidationError(path, errors.New("must supply pwm_id for the pwm chip")) - } - - return nil -} - -// BoardInformation details pin definitions and device compatibility for a particular board. -type BoardInformation struct { - PinDefinitions []PinDefinition - Compats []string -} - -// A NoBoardFoundError is returned when no compatible mapping is found for a board during GPIO board mapping. -type NoBoardFoundError struct { - modelName string -} - -func (err NoBoardFoundError) Error() string { - return fmt.Sprintf("could not determine %q model", err.modelName) -} diff --git a/components/board/gpio_pin.go b/components/board/gpio_pin.go deleted file mode 100644 index 49352c86f7e..00000000000 --- a/components/board/gpio_pin.go +++ /dev/null @@ -1,25 +0,0 @@ -package board - -import "context" - -// A GPIOPin represents an individual GPIO pin on a board. -type GPIOPin interface { - // Set sets the pin to either low or high. - Set(ctx context.Context, high bool, extra map[string]interface{}) error - - // Get gets the high/low state of the pin. - Get(ctx context.Context, extra map[string]interface{}) (bool, error) - - // PWM gets the pin's given duty cycle. - PWM(ctx context.Context, extra map[string]interface{}) (float64, error) - - // SetPWM sets the pin to the given duty cycle. - SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error - - // PWMFreq gets the PWM frequency of the pin. - PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) - - // SetPWMFreq sets the given pin to the given PWM frequency. For Raspberry Pis, - // 0 will use a default PWM frequency of 800. - SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error -} diff --git a/components/board/hat/pca9685/pca9685.go b/components/board/hat/pca9685/pca9685.go deleted file mode 100644 index bf0348baf52..00000000000 --- a/components/board/hat/pca9685/pca9685.go +++ /dev/null @@ -1,412 +0,0 @@ -//go:build linux - -// Package pca9685 implements a PCA9685 HAT. It's probably also a generic PCA9685 -// but that has not been verified yet. -package pca9685 - -import ( - "context" - "strconv" - "sync" - "time" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/board/v1" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("pca9685") - -var ( - _ = board.Board(&PCA9685{}) - _ = board.GPIOPin(&gpioPin{}) -) - -// Config describes a PCA9685 board attached to some other board via I2C. -type Config struct { - I2CBus string `json:"i2c_bus"` - I2CAddress *int `json:"i2c_address,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - if conf.I2CBus == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "i2c_bus") - } - - if conf.I2CAddress != nil && (*conf.I2CAddress < 0 || *conf.I2CAddress > 255) { - return nil, resource.NewConfigValidationError(path, errors.New("i2c_address must be an unsigned byte")) - } - return deps, nil -} - -func init() { - resource.RegisterComponent( - board.API, - model, - resource.Registration[board.Board, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (board.Board, error) { - return New(ctx, deps, conf, logger) - }, - }) -} - -// PCA9685 is a general purpose 16-channel 12-bit PWM controller. -type PCA9685 struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - - mu sync.RWMutex - address byte - referenceClockSpeed int - bus buses.I2C - gpioPins [16]gpioPin - logger logging.Logger -} - -const ( - defaultReferenceClockSpeed = 25000000 - - mode1Reg = 0x00 - prescaleReg = 0xFE -) - -// This should be considered const, except you cannot take the address of a const value. -var defaultAddr = 0x40 - -// New returns a new PCA9685 residing on the given bus and address. -func New(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (*PCA9685, error) { - pca := PCA9685{ - Named: conf.ResourceName().AsNamed(), - referenceClockSpeed: defaultReferenceClockSpeed, - logger: logger, - } - // each PWM combination spans 4 bytes - startAddr := byte(0x06) - - for chanIdx := 0; chanIdx < len(pca.gpioPins); chanIdx++ { - pca.gpioPins[chanIdx].pca = &pca - pca.gpioPins[chanIdx].startAddr = startAddr - startAddr += 4 - } - - if err := pca.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return &pca, nil -} - -// Reconfigure reconfigures the board atomically and in place. -func (pca *PCA9685) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - bus, err := buses.NewI2cBus(newConf.I2CBus) - if err != nil { - return err - } - - address := byte(defaultAddr) - if newConf.I2CAddress != nil { - address = byte(*newConf.I2CAddress) - } - - pca.mu.Lock() - defer pca.mu.Unlock() - - pca.bus = bus - pca.address = address - if err := pca.reset(ctx); err != nil { - return err - } - - return nil -} - -func (pca *PCA9685) parsePin(pin string) (int, error) { - pinInt, err := strconv.ParseInt(pin, 10, 32) - if err != nil { - return 0, err - } - if pinInt < 0 || int(pinInt) >= len(pca.gpioPins) { - return 0, errors.Errorf("channel number must be between [0, %d)", len(pca.gpioPins)) - } - return int(pinInt), nil -} - -// SetPowerMode sets the board to the given power mode. If provided, -// the board will exit the given power mode after the specified -// duration. -func (pca *PCA9685) SetPowerMode(ctx context.Context, mode pb.PowerMode, duration *time.Duration) error { - return grpc.UnimplementedError -} - -// GPIOPinByName returns a GPIOPin by name. -func (pca *PCA9685) GPIOPinByName(pin string) (board.GPIOPin, error) { - pinInt, err := pca.parsePin(pin) - if err != nil { - return nil, err - } - - if pinInt < 0 || pinInt >= len(pca.gpioPins) { - return nil, errors.New("pin name must be between [0, 16)") - } - return &pca.gpioPins[pinInt], nil -} - -func (pca *PCA9685) openHandle() (buses.I2CHandle, error) { - return pca.bus.OpenHandle(pca.address) -} - -func (pca *PCA9685) reset(ctx context.Context) error { - handle, err := pca.openHandle() - if err != nil { - return err - } - defer func() { - utils.UncheckedError(handle.Close()) - }() - return handle.WriteByteData(ctx, mode1Reg, 0x00) -} - -func (pca *PCA9685) frequency(ctx context.Context) (float64, error) { - handle, err := pca.openHandle() - if err != nil { - return 0, err - } - defer func() { - utils.UncheckedError(handle.Close()) - }() - - prescale, err := handle.ReadByteData(ctx, prescaleReg) - if err != nil { - return 0, err - } - return float64(pca.referenceClockSpeed) / 4096.0 / float64(prescale), nil -} - -// StreamTicks streams digital interrupt ticks. -// The pca9685 board does not have the systems hardware to implement a Tick counter. -func (pca *PCA9685) StreamTicks(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, -) error { - return grpc.UnimplementedError -} - -// SetFrequency sets the global PWM frequency for the pca. -func (pca *PCA9685) SetFrequency(ctx context.Context, frequency float64) error { - pca.mu.RLock() - defer pca.mu.RUnlock() - - prescale := byte((float64(pca.referenceClockSpeed) / 4096.0 / frequency) + 0.5) - if prescale < 3 { - return errors.New("invalid frequency") - } - - handle, err := pca.openHandle() - if err != nil { - return err - } - defer func() { - utils.UncheckedError(handle.Close()) - }() - - oldMode1, err := handle.ReadByteData(ctx, mode1Reg) - if err != nil { - return err - } - - if err := handle.WriteByteData(ctx, mode1Reg, (oldMode1&0x7F)|0x10); err != nil { - return err - } - if err := handle.WriteByteData(ctx, prescaleReg, prescale); err != nil { - return err - } - if err := handle.WriteByteData(ctx, mode1Reg, oldMode1); err != nil { - return err - } - time.Sleep(5 * time.Millisecond) - if err := handle.WriteByteData(ctx, mode1Reg, oldMode1|0xA0); err != nil { - return err - } - return nil -} - -// AnalogNames returns the names of all known analog pins. -func (pca *PCA9685) AnalogNames() []string { - return nil -} - -// DigitalInterruptNames returns the names of all known digital interrupts. -func (pca *PCA9685) DigitalInterruptNames() []string { - return nil -} - -// AnalogByName returns the analog pin by the given name if it exists. -func (pca *PCA9685) AnalogByName(name string) (board.Analog, error) { - return nil, nil -} - -// DigitalInterruptByName returns the interrupt by the given name if it exists. -func (pca *PCA9685) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { - return nil, grpc.UnimplementedError -} - -// A gpioPin in PCA9685 is the combination of a PWM's T_on and T_off -// represented as two 12-bit (4096 step) values. -type gpioPin struct { - pca *PCA9685 - startAddr byte -} - -func (gp *gpioPin) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { - dutyCycle, err := gp.PWM(ctx, extra) - if err != nil { - return false, err - } - return dutyCycle != 0, nil -} - -func (gp *gpioPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - var dutyCyclePct float64 - if high { - dutyCyclePct = 1 - } - - return gp.SetPWM(ctx, dutyCyclePct, extra) -} - -func (gp *gpioPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - gp.pca.mu.RLock() - defer gp.pca.mu.RUnlock() - - handle, err := gp.pca.openHandle() - if err != nil { - return 0, err - } - defer func() { - utils.UncheckedError(handle.Close()) - }() - - regOnLow := buses.I2CRegister{handle, gp.startAddr} - regOnHigh := buses.I2CRegister{handle, gp.startAddr + 1} - regOffLow := buses.I2CRegister{handle, gp.startAddr + 2} - regOffHigh := buses.I2CRegister{handle, gp.startAddr + 3} - - onLow, err := regOnLow.ReadByteData(ctx) - if err != nil { - return 0, err - } - onHigh, err := regOnHigh.ReadByteData(ctx) - if err != nil { - return 0, err - } - onVal := uint16(onLow) | (uint16(onHigh) << 8) - if onVal == 0x1000 { - return 1, nil - } - - // Off takes up zero steps - offLow, err := regOffLow.ReadByteData(ctx) - if err != nil { - return 0, err - } - offHigh, err := regOffHigh.ReadByteData(ctx) - if err != nil { - return 0, err - } - offVal := uint16(offLow) | (uint16(offHigh) << 8) - return float64(offVal<<4) / 0xffff, nil -} - -func (gp *gpioPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - gp.pca.mu.RLock() - defer gp.pca.mu.RUnlock() - - dutyCycle := uint16(dutyCyclePct * float64(0xffff)) - - handle, err := gp.pca.openHandle() - if err != nil { - return err - } - defer func() { - utils.UncheckedError(handle.Close()) - }() - - regOnLow := buses.I2CRegister{handle, gp.startAddr} - regOnHigh := buses.I2CRegister{handle, gp.startAddr + 1} - regOffLow := buses.I2CRegister{handle, gp.startAddr + 2} - regOffHigh := buses.I2CRegister{handle, gp.startAddr + 3} - - if dutyCycle == 0xffff { - // On takes up all steps - if err := regOnLow.WriteByteData(ctx, 0x00); err != nil { - return err - } - if err := regOnHigh.WriteByteData(ctx, 0x10); err != nil { - return err - } - - // Off takes up zero steps - if err := regOffLow.WriteByteData(ctx, 0x00); err != nil { - return err - } - if err := regOffHigh.WriteByteData(ctx, 0x00); err != nil { - return err - } - return nil - } - - // On takes up zero steps - if err := regOnLow.WriteByteData(ctx, 0x00); err != nil { - return err - } - if err := regOnHigh.WriteByteData(ctx, 0x00); err != nil { - return err - } - - // Off takes up "dutyCycle" steps - dutyCycle >>= 4 - - if err := regOffLow.WriteByteData(ctx, byte(dutyCycle&0xff)); err != nil { - return err - } - if err := regOffHigh.WriteByteData(ctx, byte(dutyCycle>>8)); err != nil { - return err - } - return nil -} - -func (gp *gpioPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - gp.pca.mu.RLock() - defer gp.pca.mu.RUnlock() - - freqHz, err := gp.pca.frequency(ctx) - if err != nil { - return 0, err - } - return uint(freqHz), nil -} - -func (gp *gpioPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - gp.pca.mu.RLock() - defer gp.pca.mu.RUnlock() - - return gp.pca.SetFrequency(ctx, float64(freqHz)) -} diff --git a/components/board/hat/pca9685/pca9685_nonlinux.go b/components/board/hat/pca9685/pca9685_nonlinux.go deleted file mode 100644 index 6c125f2d3a0..00000000000 --- a/components/board/hat/pca9685/pca9685_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pca9685 is unimplemented for Macs. -package pca9685 diff --git a/components/board/interrupt_stream.go b/components/board/interrupt_stream.go deleted file mode 100644 index df2905b2c73..00000000000 --- a/components/board/interrupt_stream.go +++ /dev/null @@ -1,118 +0,0 @@ -package board - -import ( - "context" - "sync" - - pb "go.viam.com/api/component/board/v1" - "go.viam.com/utils" - "google.golang.org/protobuf/types/known/structpb" -) - -type interruptStream struct { - *client - streamCancel context.CancelFunc - streamRunning bool - streamReady chan bool - streamMu sync.Mutex - - activeBackgroundWorkers sync.WaitGroup - extra *structpb.Struct -} - -func (s *interruptStream) startStream(ctx context.Context, interrupts []DigitalInterrupt, ch chan Tick) error { - s.streamMu.Lock() - defer s.streamMu.Unlock() - - if ctx.Err() != nil { - return ctx.Err() - } - - s.streamRunning = true - s.streamReady = make(chan bool) - s.activeBackgroundWorkers.Add(1) - ctx, cancel := context.WithCancel(ctx) - s.streamCancel = cancel - - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - names := []string{} - for _, i := range interrupts { - names = append(names, i.Name()) - } - - req := &pb.StreamTicksRequest{ - Name: s.client.info.name, - PinNames: names, - Extra: s.extra, - } - - // This call won't return any errors it had until the client tries to receive. - //nolint:errcheck - stream, _ := s.client.client.StreamTicks(ctx, req) - _, err := stream.Recv() - if err != nil { - s.client.logger.CError(ctx, err) - return err - } - - // Create a background go routine to receive from the server stream. - // We rely on calling the Done function here rather than in close stream - // since managed go calls that function when the routine exits. - utils.ManagedGo(func() { - s.recieveFromStream(ctx, stream, ch) - }, - s.activeBackgroundWorkers.Done) - - select { - case <-ctx.Done(): - return ctx.Err() - case <-s.streamReady: - return nil - } -} - -func (s *interruptStream) recieveFromStream(ctx context.Context, stream pb.BoardService_StreamTicksClient, ch chan Tick) { - defer func() { - s.streamMu.Lock() - defer s.streamMu.Unlock() - s.streamRunning = false - }() - // Close the stream ready channel so the above function returns. - if s.streamReady != nil { - close(s.streamReady) - } - s.streamReady = nil - defer s.closeStream() - - // repeatly receive from the stream - for { - select { - case <-ctx.Done(): - s.client.logger.Debug(ctx.Err()) - return - default: - } - streamResp, err := stream.Recv() - if err != nil { - // only debug log the context canceled error - s.client.logger.Debug(err) - return - } - // If there is a response, send to the tick channel. - tick := Tick{ - Name: streamResp.PinName, - High: streamResp.High, - TimestampNanosec: streamResp.Time, - } - ch <- tick - } -} - -func (s *interruptStream) closeStream() { - s.streamCancel() - s.client.removeStream(s) -} diff --git a/components/board/interrupt_streams_test.go b/components/board/interrupt_streams_test.go deleted file mode 100644 index 04c2dc7df06..00000000000 --- a/components/board/interrupt_streams_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package board - -import ( - "testing" - - "go.viam.com/test" -) - -func TestRemoveStream(t *testing.T) { - c := &client{} - - stream1 := &interruptStream{ - client: c, - } - stream2 := &interruptStream{ - client: c, - } - - stream3 := &interruptStream{ - client: c, - } - - testStreams := []*interruptStream{stream1, stream2, stream3} - c.interruptStreams = testStreams - expectedStreams := []*interruptStream{stream1, stream3} - c.removeStream(stream2) - test.That(t, c.interruptStreams, test.ShouldResemble, expectedStreams) -} diff --git a/components/board/jetson/README.md b/components/board/jetson/README.md deleted file mode 100644 index d9b3cc64b52..00000000000 --- a/components/board/jetson/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Jetson - -## Setup - -* Follow https://www.jetsonhacks.com/2020/05/16/nvidia-jetson-xavier-nx-developer-kit/ -* Also run `sudo modprobe spidev` or add as a start up script diff --git a/components/board/jetson/board.go b/components/board/jetson/board.go deleted file mode 100644 index 2f4e6f8f6b5..00000000000 --- a/components/board/jetson/board.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package jetson implements a jetson-based board. -package jetson - -import ( - "github.com/pkg/errors" - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" -) - -const modelName = "jetson" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappings(modelName, boardInfoMappings) - var noBoardErr genericlinux.NoBoardFoundError - if errors.As(err, &noBoardErr) { - logging.Global().Debugw("error getting jetson GPIO board mapping", "error", err) - } - - genericlinux.RegisterBoard(modelName, gpioMappings) -} diff --git a/components/board/jetson/data.go b/components/board/jetson/data.go deleted file mode 100644 index 5c2baaa1473..00000000000 --- a/components/board/jetson/data.go +++ /dev/null @@ -1,221 +0,0 @@ -package jetson - -import ( - "go.viam.com/rdk/components/board/genericlinux" -) - -const ( - jetsonTX2 = "jetson_tx2" - jetsonNano = "jetson_nano" - jetsonTX2NX = "jetson_tx2_NX" - jetsonOrinAGX = "jetson_orin_agx" - jetsonOrinNX = "jetson_orin_nx" - jetsonOrinNano = "jetson_orin_nano" -) - -//nolint:dupl // This is not actually a duplicate of jetsonNanoPins despite what the linter thinks -var jetsonTX2NXPins = []genericlinux.PinDefinition{ - {Name: "7", DeviceName: "gpiochip0", LineNumber: 76, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "11", DeviceName: "gpiochip1", LineNumber: 28, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip0", LineNumber: 72, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip1", LineNumber: 17, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip0", LineNumber: 18, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "16", DeviceName: "gpiochip0", LineNumber: 19, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip1", LineNumber: 20, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip0", LineNumber: 58, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip0", LineNumber: 57, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip1", LineNumber: 18, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip0", LineNumber: 56, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip0", LineNumber: 59, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip0", LineNumber: 163, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "29", DeviceName: "gpiochip0", LineNumber: 105, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip1", LineNumber: 50, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "32", DeviceName: "gpiochip1", LineNumber: 8, PwmChipSysfsDir: "3280000.pwm", PwmID: 0}, - {Name: "33", DeviceName: "gpiochip1", LineNumber: 13, PwmChipSysfsDir: "32a0000.pwm", PwmID: 0}, - {Name: "35", DeviceName: "gpiochip0", LineNumber: 75, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "36", DeviceName: "gpiochip1", LineNumber: 29, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip1", LineNumber: 19, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip0", LineNumber: 74, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip0", LineNumber: 73, PwmChipSysfsDir: "", PwmID: -1}, -} - -// NOTE: The Jetson TX2 (like many of the others defined in this file) has hardware PWM -// capabilities that we are not using. We can add that in, but the board is old enough that very -// few people are likely to use it, so don't bother until someone wants it. -var jetsonTX2Pins = []genericlinux.PinDefinition{ - // There exist other pins on the header (e.g., physical pins 3 and 5), but the spreadsheets at - // the bottom of https://jetsonhacks.com/nvidia-jetson-tx2-j21-header-pinout/ recommend against - // using them for GPIO. - {Name: "7", DeviceName: "gpiochip0", LineNumber: 76, PwmChipSysfsDir: "", PwmID: -1}, - // Output-only (due to base board) - {Name: "11", DeviceName: "gpiochip0", LineNumber: 146, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip0", LineNumber: 72, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip0", LineNumber: 77, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip2", LineNumber: 15, PwmChipSysfsDir: "", PwmID: -1}, - // Input-only (due to module): - {Name: "16", DeviceName: "gpiochip1", LineNumber: 40, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip0", LineNumber: 161, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip0", LineNumber: 109, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip0", LineNumber: 108, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip2", LineNumber: 14, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip0", LineNumber: 107, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip0", LineNumber: 110, PwmChipSysfsDir: "", PwmID: -1}, - // Board pin 26 is not available on this board - {Name: "29", DeviceName: "gpiochip0", LineNumber: 78, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip1", LineNumber: 42, PwmChipSysfsDir: "", PwmID: -1}, - // Output-only (due to module): - {Name: "32", DeviceName: "gpiochip1", LineNumber: 41, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "33", DeviceName: "gpiochip0", LineNumber: 69, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "35", DeviceName: "gpiochip0", LineNumber: 75, PwmChipSysfsDir: "", PwmID: -1}, - // Input-only (due to base board) IF NVIDIA debug card NOT plugged in - // Output-only (due to base board) IF NVIDIA debug card plugged in - {Name: "36", DeviceName: "gpiochip0", LineNumber: 147, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip0", LineNumber: 68, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip0", LineNumber: 74, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip0", LineNumber: 73, PwmChipSysfsDir: "", PwmID: -1}, -} - -//nolint:dupl // This is not actually a duplicate of jetsonTX2NXPins despite what the linter thinks -var jetsonNanoPins = []genericlinux.PinDefinition{ - {Name: "7", DeviceName: "gpiochip0", LineNumber: 216, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "11", DeviceName: "gpiochip0", LineNumber: 50, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip0", LineNumber: 79, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip0", LineNumber: 14, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip0", LineNumber: 194, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "16", DeviceName: "gpiochip0", LineNumber: 232, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip0", LineNumber: 15, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip0", LineNumber: 16, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip0", LineNumber: 17, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip0", LineNumber: 13, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip0", LineNumber: 18, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip0", LineNumber: 19, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip0", LineNumber: 20, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "29", DeviceName: "gpiochip0", LineNumber: 149, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip0", LineNumber: 200, PwmChipSysfsDir: "", PwmID: -1}, - // Older versions of L4T have a DT bug which instantiates a bogus device - // which prevents this library from using this PWM channel. - {Name: "32", DeviceName: "gpiochip0", LineNumber: 168, PwmChipSysfsDir: "7000a000.pwm", PwmID: 0}, - {Name: "33", DeviceName: "gpiochip0", LineNumber: 38, PwmChipSysfsDir: "7000a000.pwm", PwmID: 2}, - {Name: "35", DeviceName: "gpiochip0", LineNumber: 76, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "36", DeviceName: "gpiochip0", LineNumber: 51, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip0", LineNumber: 12, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip0", LineNumber: 77, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip0", LineNumber: 78, PwmChipSysfsDir: "", PwmID: -1}, -} - -// There are 6 pins whose Broadcom SOC channel is -1 (pins 3, 5, 8, 10, 27, and 28). We -// added these pin definitions ourselves because they're not present in -// https://github.com/NVIDIA/jetson-gpio/blob/master/lib/python/Jetson/GPIO/gpio_pin_data.py -// We were unable to find the broadcom channel numbers for these pins, but (as of April -// 2023) Viam doesn't use those values for anything anyway. -var jetsonOrinAGXPins = []genericlinux.PinDefinition{ - {Name: "3", DeviceName: "gpiochip1", LineNumber: 22, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "5", DeviceName: "gpiochip1", LineNumber: 21, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "7", DeviceName: "gpiochip0", LineNumber: 106, PwmChipSysfsDir: "", PwmID: -1}, - // Output-only (due to hardware limitation) - {Name: "8", DeviceName: "gpiochip0", LineNumber: 110, PwmChipSysfsDir: "", PwmID: -1}, - // Input-only (due to hardware limitation) - {Name: "10", DeviceName: "gpiochip0", LineNumber: 111, PwmChipSysfsDir: "", PwmID: -1}, - // Output-only (due to hardware limitation) - {Name: "11", DeviceName: "gpiochip0", LineNumber: 112, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip0", LineNumber: 50, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip0", LineNumber: 108, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip0", LineNumber: 85, PwmChipSysfsDir: "3280000.pwm", PwmID: 0}, - {Name: "16", DeviceName: "gpiochip1", LineNumber: 9, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip0", LineNumber: 43, PwmChipSysfsDir: "32c0000.pwm", PwmID: 0}, - {Name: "19", DeviceName: "gpiochip0", LineNumber: 135, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip0", LineNumber: 134, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip0", LineNumber: 96, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip0", LineNumber: 133, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip0", LineNumber: 136, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip0", LineNumber: 137, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "27", DeviceName: "gpiochip1", LineNumber: 20, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "28", DeviceName: "gpiochip1", LineNumber: 19, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "29", DeviceName: "gpiochip1", LineNumber: 1, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip1", LineNumber: 0, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "32", DeviceName: "gpiochip1", LineNumber: 8, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "33", DeviceName: "gpiochip1", LineNumber: 2, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "35", DeviceName: "gpiochip0", LineNumber: 53, PwmChipSysfsDir: "", PwmID: -1}, - // Input-only (due to hardware limitation) - {Name: "36", DeviceName: "gpiochip0", LineNumber: 113, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip1", LineNumber: 3, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip0", LineNumber: 52, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip0", LineNumber: 51, PwmChipSysfsDir: "", PwmID: -1}, -} - -// This pin mapping is used for both the Jetson Orin NX and the Jetson Orin Nano. -var jetsonOrinNXPins = []genericlinux.PinDefinition{ - {Name: "7", DeviceName: "gpiochip0", LineNumber: 144, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "11", DeviceName: "gpiochip0", LineNumber: 112, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip0", LineNumber: 50, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip0", LineNumber: 122, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip0", LineNumber: 85, PwmChipSysfsDir: "3280000.pwm", PwmID: 0}, - {Name: "16", DeviceName: "gpiochip0", LineNumber: 126, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip0", LineNumber: 125, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip0", LineNumber: 135, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip0", LineNumber: 134, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip0", LineNumber: 123, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip0", LineNumber: 133, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip0", LineNumber: 136, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip0", LineNumber: 137, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "29", DeviceName: "gpiochip0", LineNumber: 105, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip0", LineNumber: 106, PwmChipSysfsDir: "", PwmID: -1}, - // Pin 32 supposedly has hardware PWM support, but we've been unable to turn it on. - {Name: "32", DeviceName: "gpiochip0", LineNumber: 41, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "33", DeviceName: "gpiochip0", LineNumber: 43, PwmChipSysfsDir: "32c0000.pwm", PwmID: 0}, - {Name: "35", DeviceName: "gpiochip0", LineNumber: 53, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "36", DeviceName: "gpiochip0", LineNumber: 113, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip0", LineNumber: 124, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip0", LineNumber: 52, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip0", LineNumber: 51, PwmChipSysfsDir: "", PwmID: -1}, -} - -var boardInfoMappings = map[string]genericlinux.BoardInformation{ - jetsonTX2NX: { - jetsonTX2NXPins, - []string{ - "nvidia,p3509-0000+p3636-0001", - }, - }, - jetsonTX2: { - jetsonTX2Pins, - []string{ - "nvidia,p2771-0000", - "nvidia,p2771-0888", - "nvidia,p3489-0000", - "nvidia,lightning", - "nvidia,quill", - "nvidia,storm", - }, - }, - jetsonNano: { - jetsonNanoPins, - []string{ - "nvidia,p3450-0000", - "nvidia,p3450-0002", - "nvidia,jetson-nano", - }, - }, - jetsonOrinAGX: { - jetsonOrinAGXPins, - []string{ - "nvidia,p3737-0000+p3701-0000", - "nvidia,p3737-0000+p3701-0004", - }, - }, - jetsonOrinNX: { - jetsonOrinNXPins, - []string{ - "nvidia,p3509-0000+p3767-0000", - }, - }, - jetsonOrinNano: { - jetsonOrinNXPins, // The Jetson Orin Nano has the exact same pinout as the Jetson Orin NX. - []string{ - "nvidia,p3768-0000+p3767-0003", - "nvidia,p3768-0000+p3767-0005", - "nvidia,p3767-0003", - "nvidia,p3767-0005", - }, - }, -} diff --git a/components/board/mcp3008helper/mcp3008.go b/components/board/mcp3008helper/mcp3008.go deleted file mode 100644 index 198f256dff2..00000000000 --- a/components/board/mcp3008helper/mcp3008.go +++ /dev/null @@ -1,72 +0,0 @@ -// Package mcp3008helper is shared code for hooking an MCP3008 ADC up to a board. It is used in -// both the pi and genericlinux board implementations, but does not implement a board directly. -package mcp3008helper - -import ( - "context" - - "go.uber.org/multierr" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/resource" -) - -// MCP3008AnalogReader implements a board.AnalogReader using an MCP3008 ADC via SPI. -type MCP3008AnalogReader struct { - Channel int - Bus buses.SPI - Chip string -} - -// MCP3008AnalogConfig describes the configuration of a MCP3008 analog reader on a board. -type MCP3008AnalogConfig struct { - Name string `json:"name"` - Pin string `json:"pin"` // analog input pin on the ADC itself - SPIBus string `json:"spi_bus"` // name of the SPI bus (which is configured elsewhere in the config file) - ChipSelect string `json:"chip_select"` // the CS line for the ADC chip, typically a pin number on the board - AverageOverMillis int `json:"average_over_ms,omitempty"` - SamplesPerSecond int `json:"samples_per_sec,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (config *MCP3008AnalogConfig) Validate(path string) error { - if config.Name == "" { - return resource.NewConfigValidationFieldRequiredError(path, "name") - } - return nil -} - -func (mar *MCP3008AnalogReader) Read(ctx context.Context, extra map[string]interface{}) (value int, err error) { - var tx [3]byte - tx[0] = 1 // start bit - tx[1] = byte((8 + mar.Channel) << 4) // single-ended - tx[2] = 0 // extra clocks to receive full 10 bits of data - - bus, err := mar.Bus.OpenHandle() - if err != nil { - return 0, err - } - defer func() { - err = multierr.Combine(err, bus.Close()) - }() - - rx, err := bus.Xfer(ctx, 1000000, mar.Chip, 0, tx[:]) - if err != nil { - return 0, err - } - // Reassemble the 10-bit value. Do not include bits before the final 10, because they contain - // garbage and might be non-zero. - val := 0x03FF & ((int(rx[1]) << 8) | int(rx[2])) - - return val, nil -} - -// Close does nothing. -func (mar *MCP3008AnalogReader) Close(ctx context.Context) error { - return nil -} - -func (mar *MCP3008AnalogReader) Write(ctx context.Context, value int, extra map[string]interface{}) error { - return grpc.UnimplementedError -} diff --git a/components/board/numato/board.go b/components/board/numato/board.go deleted file mode 100644 index cd9d7ff3102..00000000000 --- a/components/board/numato/board.go +++ /dev/null @@ -1,424 +0,0 @@ -// Package numato is for numato IO boards. -package numato - -import ( - "bufio" - "context" - "encoding/hex" - "errors" - "fmt" - "io" - "math" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - goserial "github.com/jacobsa/go-serial/serial" - "go.uber.org/multierr" - pb "go.viam.com/api/component/board/v1" - "go.viam.com/utils" - "go.viam.com/utils/serial" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/pinwrappers" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - rdkutils "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("numato") - -var errNoBoard = errors.New("no numato boards found") - -// A Config describes the configuration of a board and all of its connected parts. -type Config struct { - Analogs []board.AnalogReaderConfig `json:"analogs,omitempty"` - Pins int `json:"pins"` - SerialPath string `json:"serial_path,omitempty"` -} - -func init() { - resource.RegisterComponent( - board.API, - model, - resource.Registration[board.Board, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (board.Board, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - return connect(ctx, conf.ResourceName(), newConf, logger) - }, - }) -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - if conf.Pins <= 0 { - return nil, utils.NewConfigValidationFieldRequiredError(path, "pins") - } - - for idx, conf := range conf.Analogs { - if err := conf.Validate(fmt.Sprintf("%s.%s.%d", path, "analogs", idx)); err != nil { - return nil, err - } - } - return nil, nil -} - -type mask []byte - -// numato uses a weird bit mask for setting some variables on the firmware. -func newMask(bits int) mask { - m := mask{} - for bits >= 8 { - m = append(m, byte(0)) - bits -= 8 - } - if bits != 0 { - panic(fmt.Errorf("bad number of bits %d", bits)) - } - return m -} - -func (m mask) hex() string { - return hex.EncodeToString(m) -} - -func (m *mask) set(bit int) { - idx := len(*m) - (bit / 8) - 1 - bitToSet := bit % 8 - - (*m)[idx] |= 1 << bitToSet -} - -type numatoBoard struct { - resource.Named - resource.AlwaysRebuild - pins int - analogs map[string]*pinwrappers.AnalogSmoother - - port io.ReadWriteCloser - closed int32 - logger logging.Logger - - lines chan string - mu sync.Mutex - - sent map[string]bool - sentMu sync.Mutex - workers rdkutils.StoppableWorkers -} - -func (b *numatoBoard) addToSent(msg string) { - b.sentMu.Lock() - defer b.sentMu.Unlock() - - if b.sent == nil { - b.sent = make(map[string]bool) - } - b.sent[msg] = true -} - -func (b *numatoBoard) wasSent(msg string) bool { - b.sentMu.Lock() - defer b.sentMu.Unlock() - - return b.sent[msg] -} - -func fixPin(bit int, pin string) string { - l := 1 - if bit >= 100 { - l = 3 - } else if bit >= 10 { - l = 2 - } - - for len(pin) < l { - pin = "0" + pin - } - - return pin -} - -func (b *numatoBoard) fixPin(pin string) string { - return fixPin(b.pins, pin) -} - -func (b *numatoBoard) doSendLocked(ctx context.Context, msg string) error { - _, err := b.port.Write(([]byte)(msg + "\n")) - - utils.SelectContextOrWait(ctx, 100*time.Microsecond) - return err -} - -func (b *numatoBoard) doSend(ctx context.Context, msg string) error { - b.addToSent(msg) - - b.mu.Lock() - defer b.mu.Unlock() - - return b.doSendLocked(ctx, msg) -} - -func (b *numatoBoard) doSendReceive(ctx context.Context, msg string) (string, error) { - b.addToSent(msg) - - b.mu.Lock() - defer b.mu.Unlock() - - err := b.doSendLocked(ctx, msg) - if err != nil { - return "", err - } - - select { - case <-ctx.Done(): - return "", errors.New("context ended") - case res := <-b.lines: - return res, nil - case <-time.After(1 * time.Second): - return "", multierr.Combine(errors.New("numato read timeout"), b.port.Close()) - } -} - -func (b *numatoBoard) readThread(_ context.Context) { - debug := true - - in := bufio.NewReader(b.port) - for { - if atomic.LoadInt32(&b.closed) == 1 { - close(b.lines) - return - } - line, err := in.ReadString('\n') - if err != nil { - if atomic.LoadInt32(&b.closed) == 1 { - close(b.lines) - return - } - b.logger.Warnw("error reading", "err", err) - break // TODO: restart connection - } - line = strings.TrimSpace(line) - - if debug { - b.logger.Debugf("got line %s", line) - } - - if len(line) == 0 || line[0] == '>' { - continue - } - - if b.wasSent(line) { - continue - } - - if debug { - b.logger.Debugf(" sending line %s", line) - } - b.lines <- line - } -} - -// StreamTicks streams digital interrupt ticks. -// The numato board does not have the systems hardware to implement a Tick counter. -func (b *numatoBoard) StreamTicks(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, -) error { - return grpc.UnimplementedError -} - -// AnalogByName returns an analog pin by name. -func (b *numatoBoard) AnalogByName(name string) (board.Analog, error) { - ar, ok := b.analogs[name] - if !ok { - return nil, fmt.Errorf("can't find AnalogReader (%s)", name) - } - return ar, nil -} - -// DigitalInterruptByName returns a digital interrupt by name. -func (b *numatoBoard) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { - return nil, grpc.UnimplementedError -} - -// AnalogNames returns the names of all known analog pins. -func (b *numatoBoard) AnalogNames() []string { - names := []string{} - for n := range b.analogs { - names = append(names, n) - } - return names -} - -// DigitalInterruptNames returns the names of all known digital interrupts. -func (b *numatoBoard) DigitalInterruptNames() []string { - return nil -} - -// GPIOPinByName returns the GPIO pin by the given name. -func (b *numatoBoard) GPIOPinByName(pin string) (board.GPIOPin, error) { - return &gpioPin{b, pin}, nil -} - -type gpioPin struct { - b *numatoBoard - pin string -} - -func (gp *gpioPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - fixedPin := gp.b.fixPin(gp.pin) - if high { - return gp.b.doSend(ctx, fmt.Sprintf("gpio set %s", fixedPin)) - } - return gp.b.doSend(ctx, fmt.Sprintf("gpio clear %s", fixedPin)) -} - -func (gp *gpioPin) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { - fixedPin := gp.b.fixPin(gp.pin) - res, err := gp.b.doSendReceive(ctx, fmt.Sprintf("gpio read %s", fixedPin)) - if err != nil { - return false, err - } - return res[len(res)-1] == '1', nil -} - -func (gp *gpioPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - return math.NaN(), errors.New("numato doesn't support PWM") -} - -func (gp *gpioPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - if dutyCyclePct == 1.0 { - return gp.Set(ctx, true, extra) - } - if dutyCyclePct == 0.0 { - return gp.Set(ctx, false, extra) - } - return errors.New("numato doesn't support pwm") -} - -func (gp *gpioPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - return 0, errors.New("numato doesn't support PWMFreq") -} - -func (gp *gpioPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - if freqHz == 0 { - return nil - } - return errors.New("numato doesn't support pwm") -} - -func (b *numatoBoard) SetPowerMode(ctx context.Context, mode pb.PowerMode, duration *time.Duration) error { - return grpc.UnimplementedError -} - -func (b *numatoBoard) Close(ctx context.Context) error { - atomic.AddInt32(&b.closed, 1) - - // Without this line, the coroutine gets stuck in the call to in.ReadString. - // Closing the device port will complete the call on some OSes, but on Mac attempting to close - // a serial device currently being read from will result in a deadlock. - // Send the board a command so we get a response back to read and complete the call so the coroutine can wake up - // and see it should exit. - _, err := b.doSendReceive(ctx, "ver") - if err != nil { - return err - } - if err := b.port.Close(); err != nil { - return err - } - - b.workers.Stop() - - for _, analog := range b.analogs { - if err := analog.Close(ctx); err != nil { - return err - } - } - return nil -} - -type analog struct { - b *numatoBoard - pin string -} - -func (a *analog) Read(ctx context.Context, extra map[string]interface{}) (int, error) { - res, err := a.b.doSendReceive(ctx, fmt.Sprintf("adc read %s", a.pin)) - if err != nil { - return 0, err - } - return strconv.Atoi(res) -} - -func (a *analog) Write(ctx context.Context, value int, extra map[string]interface{}) error { - return grpc.UnimplementedError -} - -func connect(ctx context.Context, name resource.Name, conf *Config, logger logging.Logger) (board.Board, error) { - pins := conf.Pins - var path string - if conf.SerialPath != "" { - path = conf.SerialPath - } else { - filter := serial.SearchFilter{Type: serial.TypeNumatoGPIO} - devs := serial.Search(filter) - if len(devs) == 0 { - return nil, errNoBoard - } - if len(devs) > 1 { - return nil, fmt.Errorf("found more than 1 numato board: %d", len(devs)) - } - - path = devs[0].Path - } - - options := goserial.OpenOptions{ - PortName: path, - BaudRate: 19200, - DataBits: 8, - StopBits: 1, - MinimumReadSize: 1, - } - - device, err := goserial.Open(options) - if err != nil { - return nil, err - } - - b := &numatoBoard{ - Named: name.AsNamed(), - pins: pins, - port: device, - logger: logger, - } - - b.analogs = map[string]*pinwrappers.AnalogSmoother{} - for _, c := range conf.Analogs { - r := &analog{b, c.Pin} - b.analogs[c.Name] = pinwrappers.SmoothAnalogReader(r, c, logger) - } - - b.lines = make(chan string) - - b.workers = rdkutils.NewStoppableWorkers(b.readThread) - - ver, err := b.doSendReceive(ctx, "ver") - if err != nil { - return nil, multierr.Combine(b.Close(ctx), err) - } - b.logger.CDebugw(ctx, "numato startup", "version", ver) - - return b, nil -} diff --git a/components/board/numato/board_test.go b/components/board/numato/board_test.go deleted file mode 100644 index 24eef77b09e..00000000000 --- a/components/board/numato/board_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package numato - -import ( - "context" - "errors" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestMask(t *testing.T) { - m := newMask(32) - test.That(t, len(m), test.ShouldEqual, 4) - test.That(t, m.hex(), test.ShouldEqual, "00000000") - - m.set(0) - test.That(t, m.hex(), test.ShouldEqual, "00000001") - - m.set(6) - m.set(7) - test.That(t, m.hex(), test.ShouldEqual, "000000c1") - - m.set(31) - test.That(t, m.hex(), test.ShouldEqual, "800000c1") -} - -func TestFixPins(t *testing.T) { - test.That(t, fixPin(128, "0"), test.ShouldEqual, "000") - test.That(t, fixPin(128, "00"), test.ShouldEqual, "000") - test.That(t, fixPin(128, "000"), test.ShouldEqual, "000") - - test.That(t, fixPin(128, "1"), test.ShouldEqual, "001") - test.That(t, fixPin(128, "01"), test.ShouldEqual, "001") - test.That(t, fixPin(128, "001"), test.ShouldEqual, "001") -} - -func TestNumato1(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - b, err := connect( - ctx, - board.Named("foo"), - &Config{ - Analogs: []board.AnalogReaderConfig{{Name: "foo", Pin: "01"}}, - Pins: 2, - }, - logger, - ) - if errors.Is(err, errNoBoard) { - t.Skip("no numato board connected") - } - test.That(t, err, test.ShouldBeNil) - defer b.Close(ctx) - - // For this to work 0 has be plugged into 1 - - zeroPin, err := b.GPIOPinByName("0") - test.That(t, err, test.ShouldBeNil) - onePin, err := b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - - // set to low - err = zeroPin.Set(context.Background(), false, nil) - test.That(t, err, test.ShouldBeNil) - - res, err := onePin.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, false) - - // set to high - err = zeroPin.Set(context.Background(), true, nil) - test.That(t, err, test.ShouldBeNil) - - res, err = onePin.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, true) - - // set back to low - err = zeroPin.Set(context.Background(), false, nil) - test.That(t, err, test.ShouldBeNil) - - res, err = onePin.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, false) - - // test analog - ar, err := b.AnalogByName("foo") - test.That(t, err, test.ShouldBeNil, true) - - res2, err := ar.Read(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldBeLessThan, 100) - - err = zeroPin.Set(context.Background(), true, nil) - test.That(t, err, test.ShouldBeNil) - - res2, err = ar.Read(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldBeGreaterThan, 1000) - - err = zeroPin.Set(context.Background(), false, nil) - test.That(t, err, test.ShouldBeNil) - - res2, err = ar.Read(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldBeLessThan, 100) -} - -func TestConfigValidate(t *testing.T) { - invalidConfig := Config{} - _, err := invalidConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `"pins" is required`) - - validConfig := Config{Pins: 128} - validConfig.Analogs = []board.AnalogReaderConfig{{}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `path.analogs.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - - validConfig.Analogs = []board.AnalogReaderConfig{{Name: "bar"}} - _, err = validConfig.Validate("path") - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/board/odroid/board.go b/components/board/odroid/board.go deleted file mode 100644 index e587f4ffe17..00000000000 --- a/components/board/odroid/board.go +++ /dev/null @@ -1,27 +0,0 @@ -// Package odroid implements a odroid based board. -package odroid - -import ( - "errors" - - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" -) - -const modelName = "odroid" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappings(modelName, boardInfoMappings) - var noBoardErr genericlinux.NoBoardFoundError - if errors.As(err, &noBoardErr) { - logging.Global().Debugw("error getting odroid GPIO board mapping", "error", err) - } - - genericlinux.RegisterBoard(modelName, gpioMappings) -} diff --git a/components/board/odroid/data.go b/components/board/odroid/data.go deleted file mode 100644 index 63628de4b2a..00000000000 --- a/components/board/odroid/data.go +++ /dev/null @@ -1,40 +0,0 @@ -package odroid - -import ( - "go.viam.com/rdk/components/board/genericlinux" -) - -const c4 = "ODROID-C4" - -var boardInfoMappings = map[string]genericlinux.BoardInformation{ - // pins 11, 15, and 35 are not included in the mapping beacause trying to use them at the same time as - // the pin on the same pwm chip (pins 7, 12, 33 respectively) causes errors. Only one pin from each - // pwm chip is included in the board mapping for simplicity. - c4: { - PinDefinitions: []genericlinux.PinDefinition{ - {Name: "3", DeviceName: "gpiochip1", LineNumber: 83, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "5", DeviceName: "gpiochip1", LineNumber: 84, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "7", DeviceName: "gpiochip1", LineNumber: 71, PwmChipSysfsDir: "ffd1a000.pwm", PwmID: 0}, - {Name: "8", DeviceName: "gpiochip1", LineNumber: 78, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "10", DeviceName: "gpiochip1", LineNumber: 79, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip1", LineNumber: 82, PwmChipSysfsDir: "ffd19000.pwm", PwmID: 0}, - {Name: "13", DeviceName: "gpiochip1", LineNumber: 70, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "16", DeviceName: "gpiochip1", LineNumber: 66, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip1", LineNumber: 67, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip1", LineNumber: 74, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip1", LineNumber: 75, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip1", LineNumber: 68, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip1", LineNumber: 77, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip1", LineNumber: 76, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip1", LineNumber: 23, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "27", DeviceName: "gpiochip1", LineNumber: 64, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "28", DeviceName: "gpiochip1", LineNumber: 65, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "29", DeviceName: "gpiochip1", LineNumber: 80, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip1", LineNumber: 81, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "32", DeviceName: "gpiochip1", LineNumber: 24, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "33", DeviceName: "gpiochip1", LineNumber: 72, PwmChipSysfsDir: "ffd1b000.pwm", PwmID: 0}, - {Name: "36", DeviceName: "gpiochip1", LineNumber: 22, PwmChipSysfsDir: "", PwmID: -1}, - }, - Compats: []string{"amlogic, g12a"}, - }, -} diff --git a/components/board/orangepi/board.go b/components/board/orangepi/board.go deleted file mode 100644 index 344043b4a8f..00000000000 --- a/components/board/orangepi/board.go +++ /dev/null @@ -1,27 +0,0 @@ -// Package orangepi implements a orangepi based board. -package orangepi - -import ( - "errors" - - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" -) - -const modelName = "orangepi" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappings(modelName, boardInfoMappings) - var noBoardErr genericlinux.NoBoardFoundError - if errors.As(err, &noBoardErr) { - logging.Global().Debugw("error getting orangepi GPIO board mapping", "error", err) - } - - genericlinux.RegisterBoard(modelName, gpioMappings) -} diff --git a/components/board/orangepi/data.go b/components/board/orangepi/data.go deleted file mode 100644 index 9514f4b3e11..00000000000 --- a/components/board/orangepi/data.go +++ /dev/null @@ -1,61 +0,0 @@ -package orangepi - -import "go.viam.com/rdk/components/board/genericlinux" - -const ( - opzero2 = "OrangePi Zero2" - op3lts = "OrangePi 3 LTS" -) - -var boardInfoMappings = map[string]genericlinux.BoardInformation{ - opzero2: { - // OP zero 2 user manual: https://drive.google.com/drive/folders/1ToDjWZQptABxfiRwaeYW1WzQILM5iwpb - // Gpio pins can be found on page 147. - PinDefinitions: []genericlinux.PinDefinition{ - {Name: "3", DeviceName: "gpiochip0", LineNumber: 229, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "5", DeviceName: "gpiochip0", LineNumber: 228, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "7", DeviceName: "gpiochip0", LineNumber: 73, PwmChipSysfsDir: "", PwmID: -1}, - // When we can switch between gpio and pwm, this would have line number 226. - {Name: "8", DeviceName: "gpiochip0", LineNumber: -1, PwmChipSysfsDir: "300a000.pwm", PwmID: 2}, - // When we can switch between gpio and pwm, this would have line number 227. - {Name: "10", DeviceName: "gpiochip0", LineNumber: -1, PwmChipSysfsDir: "300a000.pwm", PwmID: 1}, - {Name: "11", DeviceName: "gpiochip0", LineNumber: 70, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip0", LineNumber: 75, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip0", LineNumber: 69, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip0", LineNumber: 72, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "16", DeviceName: "gpiochip0", LineNumber: 79, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip0", LineNumber: 78, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip0", LineNumber: 231, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip0", LineNumber: 232, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip0", LineNumber: 71, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip0", LineNumber: 233, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip0", LineNumber: 74, PwmChipSysfsDir: "", PwmID: -1}, - }, - Compats: []string{"xunlong,orangepi-zero2", "allwinner,sun50i-h616"}, - }, - op3lts: { - // OP 3 LTS user manual: https://drive.google.com/file/d/1jka7avWnzNeTIQFkk78LoJdygWaGH2iu/view - // Gpio pins can be found on page 145. - PinDefinitions: []genericlinux.PinDefinition{ - {Name: "3", DeviceName: "gpiochip1", LineNumber: 122, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "5", DeviceName: "gpiochip1", LineNumber: 121, PwmChipSysfsDir: "", PwmID: -1}, - // When we can switch between gpio and pwm, the line number would be 118. - {Name: "7", DeviceName: "gpiochip1", LineNumber: -1, PwmChipSysfsDir: "300a000.pwm", PwmID: 0}, - {Name: "8", DeviceName: "gpiochip0", LineNumber: 2, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "10", DeviceName: "gpiochip0", LineNumber: 3, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "11", DeviceName: "gpiochip1", LineNumber: 120, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip1", LineNumber: 114, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip1", LineNumber: 119, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip0", LineNumber: 10, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "16", DeviceName: "gpiochip1", LineNumber: 111, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip1", LineNumber: 112, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip1", LineNumber: 229, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip1", LineNumber: 230, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip1", LineNumber: 117, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip1", LineNumber: 228, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip1", LineNumber: 227, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip0", LineNumber: 8, PwmChipSysfsDir: "", PwmID: -1}, - }, - Compats: []string{"xunlong,orangepi-3-lts", "allwinner,sun50i-h6"}, - }, -} diff --git a/components/board/pi/common/common.go b/components/board/pi/common/common.go deleted file mode 100644 index 3dab45aab9f..00000000000 --- a/components/board/pi/common/common.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package picommon contains shared information for supported and non-supported pi boards. -package picommon - -import ( - "go.viam.com/rdk/resource" -) - -// Model is the name used refer to any implementation of a pi based component. -var Model = resource.DefaultModelFamily.WithModel("pi") diff --git a/components/board/pi/common/config.go b/components/board/pi/common/config.go deleted file mode 100644 index 93dc6c778c7..00000000000 --- a/components/board/pi/common/config.go +++ /dev/null @@ -1,33 +0,0 @@ -package picommon - -import ( - "github.com/pkg/errors" - - "go.viam.com/rdk/resource" -) - -// ServoConfig is the config for a pi servo. -type ServoConfig struct { - Pin string `json:"pin"` - Min int `json:"min,omitempty"` - Max int `json:"max,omitempty"` // specifies a user inputted position limitation - StartPos *float64 `json:"starting_position_degs,omitempty"` - HoldPos *bool `json:"hold_position,omitempty"` // defaults True. False holds for 500 ms then disables servo - BoardName string `json:"board"` - MaxRotation int `json:"max_rotation_deg,omitempty"` // specifies a hardware position limitation. Defaults to 180 -} - -// Validate ensures all parts of the config are valid. -func (config *ServoConfig) Validate(path string) ([]string, error) { - var deps []string - if config.Pin == "" { - return nil, resource.NewConfigValidationError(path, - errors.New("need pin for pi servo")) - } - if config.BoardName == "" { - return nil, resource.NewConfigValidationError(path, - errors.New("need the name of the board")) - } - deps = append(deps, config.BoardName) - return deps, nil -} diff --git a/components/board/pi/common/errors.go b/components/board/pi/common/errors.go deleted file mode 100644 index 833e6068fe8..00000000000 --- a/components/board/pi/common/errors.go +++ /dev/null @@ -1,166 +0,0 @@ -package picommon - -import "github.com/pkg/errors" - -// PiGPIOErrorMap maps the error codes to the human readable error names. This can be found at the pigpio C interface. -var PiGPIOErrorMap = map[int]string{ - -1: "PI_INIT_FAILED: gpioInitialise failed", - -2: "PI_BAD_USER_GPIO: GPIO not 0-31", - -3: "PI_BAD_GPIO: GPIO not 0-53", - -4: "PI_BAD_MODE: mode not 0-7", - -5: "PI_BAD_LEVEL: level not 0-1", - -6: "PI_BAD_PUD: pud not 0-2", - -7: "PI_BAD_PULSEWIDTH: pulsewidth not 0 or 500-2500", - -8: "PI_BAD_DUTYCYCLE: dutycycle outside set range", - -9: "PI_BAD_TIMER: timer not 0-9", - -10: "PI_BAD_MS: ms not 10-60000", - -11: "PI_BAD_TIMETYPE: timetype not 0-1", - -12: "PI_BAD_SECONDS: seconds < 0", - -13: "PI_BAD_MICROS: micros not 0-999999", - -14: "PI_TIMER_FAILED: gpioSetTimerFunc failed", - -15: "PI_BAD_WDOG_TIMEOUT: timeout not 0-60000", - -16: "PI_NO_ALERT_FUNC: DEPRECATED", - -17: "PI_BAD_CLK_PERIPH: clock peripheral not 0-1", - -18: "PI_BAD_CLK_SOURCE: DEPRECATED", - -19: "PI_BAD_CLK_MICROS: clock micros not 1, 2, 4, 5, 8, or 10", - -20: "PI_BAD_BUF_MILLIS: buf millis not 100-10000", - -21: "PI_BAD_DUTYRANGE: dutycycle range not 25-40000", - -22: "PI_BAD_SIGNUM: signum not 0-63", - -23: "PI_BAD_PATHNAME: can't open pathname", - -24: "PI_NO_HANDLE: no handle available", - -25: "PI_BAD_HANDLE: unknown handle", - -26: "PI_BAD_IF_FLAGS: ifFlags > 4", - -27: "PI_BAD_CHANNEL_OR_PI_BAD_PRIM_CHANNEL: DMA channel not 0-15 OR DMA primary channel not 0-15", - -28: "PI_BAD_SOCKET_PORT: socket port not 1024-32000", - -29: "PI_BAD_FIFO_COMMAND: unrecognized fifo command", - -30: "PI_BAD_SECO_CHANNEL: DMA secondary channel not 0-15", - -31: "PI_NOT_INITIALISED: function called before gpioInitialise", - -32: "PI_INITIALISED: function called after gpioInitialise", - -33: "PI_BAD_WAVE_MODE: waveform mode not 0-3", - -34: "PI_BAD_CFG_INTERNAL: bad parameter in gpioCfgInternals call", - -35: "PI_BAD_WAVE_BAUD: baud rate not 50-250K(RX)/50-1M(TX)", - -36: "PI_TOO_MANY_PULSES: waveform has too many pulses", - -37: "PI_TOO_MANY_CHARS: waveform has too many chars", - -38: "PI_NOT_SERIAL_GPIO: no bit bang serial read on GPIO", - -39: "PI_BAD_SERIAL_STRUC: bad (null) serial structure parameter", - -40: "PI_BAD_SERIAL_BUF: bad (null) serial buf parameter", - -41: "PI_NOT_PERMITTED: GPIO operation not permitted", - -42: "PI_SOME_PERMITTED: one or more GPIO not permitted", - -43: "PI_BAD_WVSC_COMMND: bad WVSC subcommand", - -44: "PI_BAD_WVSM_COMMND: bad WVSM subcommand", - -45: "PI_BAD_WVSP_COMMND: bad WVSP subcommand", - -46: "PI_BAD_PULSELEN: trigger pulse length not 1-100", - -47: "PI_BAD_SCRIPT: invalid script", - -48: "PI_BAD_SCRIPT_ID: unknown script id", - -49: "PI_BAD_SER_OFFSET: add serial data offset > 30 minutes", - -50: "PI_GPIO_IN_USE: GPIO already in use", - -51: "PI_BAD_SERIAL_COUNT: must read at least a byte at a time", - -52: "PI_BAD_PARAM_NUM: script parameter id not 0-9", - -53: "PI_DUP_TAG: script has duplicate tag", - -54: "PI_TOO_MANY_TAGS: script has too many tags", - -55: "PI_BAD_SCRIPT_CMD: illegal script command", - -56: "PI_BAD_VAR_NUM: script variable id not 0-149", - -57: "PI_NO_SCRIPT_ROOM: no more room for scripts", - -58: "PI_NO_MEMORY: can't allocate temporary memory", - -59: "PI_SOCK_READ_FAILED: socket read failed", - -60: "PI_SOCK_WRIT_FAILED: socket write failed", - -61: "PI_TOO_MANY_PARAM: too many script parameters (> 10)", - -62: "PI_SCRIPT_NOT_READY: script initialising", - -63: "PI_BAD_TAG: script has unresolved tag", - -64: "PI_BAD_MICS_DELAY: bad MICS delay (too large)", - -65: "PI_BAD_MILS_DELAY: bad MILS delay (too large)", - -66: "PI_BAD_WAVE_ID: non existent wave id", - -67: "PI_TOO_MANY_CBS: No more CBs for waveform", - -68: "PI_TOO_MANY_OOL: No more OOL for waveform", - -69: "PI_EMPTY_WAVEFORM: attempt to create an empty waveform", - -70: "PI_NO_WAVEFORM_ID: no more waveforms", - -71: "PI_I2C_OPEN_FAILED: can't open I2C device", - -72: "PI_SER_OPEN_FAILED: can't open serial device", - -73: "PI_SPI_OPEN_FAILED: can't open SPI device", - -74: "PI_BAD_I2C_BUS: bad I2C bus", - -75: "PI_BAD_I2C_ADDR: bad I2C address", - -76: "PI_BAD_SPI_CHANNEL: bad SPI channel", - -77: "PI_BAD_FLAGS: bad i2c/spi/ser open flags", - -78: "PI_BAD_SPI_SPEED: bad SPI speed", - -79: "PI_BAD_SER_DEVICE: bad serial device name", - -80: "PI_BAD_SER_SPEED: bad serial baud rate", - -81: "PI_BAD_PARAM: bad i2c/spi/ser parameter", - -82: "PI_I2C_WRITE_FAILED: i2c write failed", - -83: "PI_I2C_READ_FAILED: i2c read failed", - -84: "PI_BAD_SPI_COUNT: bad SPI count", - -85: "PI_SER_WRITE_FAILED: ser write failed", - -86: "PI_SER_READ_FAILED: ser read failed", - -87: "PI_SER_READ_NO_DATA: ser read no data available", - -88: "PI_UNKNOWN_COMMAND: unknown command", - -89: "PI_SPI_XFER_FAILED: spi xfer/read/write failed", - -90: "PI_BAD_POINTER: bad (NULL) pointer", - -91: "PI_NO_AUX_SPI: no auxiliary SPI on Pi A or B", - -92: "PI_NOT_PWM_GPIO: GPIO is not in use for PWM", - -93: "PI_NOT_SERVO_GPI: GPIO is not in use for servo pulses", - -94: "PI_NOT_HCLK_GPIO: GPIO has no hardware clock", - -95: "PI_NOT_HPWM_GPIO: GPIO has no hardware PWM", - -96: "PI_BAD_HPWM_FREQ: invalid hardware PWM frequency", - -97: "PI_BAD_HPWM_DUTY: hardware PWM dutycycle not 0-1M", - -98: "PI_BAD_HCLK_FREQ: invalid hardware clock frequency", - -99: "PI_BAD_HCLK_PASS: need password to use hardware clock 1", - -100: "PI_HPWM_ILLEGAL: illegal, PWM in use for main clock", - -101: "PI_BAD_DATABITS: serial data bits not 1-32", - -102: "PI_BAD_STOPBITS: serial (half) stop bits not 2-8", - -103: "PI_MSG_TOOBIG: socket/pipe message too big", - -104: "PI_BAD_MALLOC_MODE: bad memory allocation mode", - -105: "PI_TOO_MANY_SEGS: too many I2C transaction segments", - -106: "PI_BAD_I2C_SEG: an I2C transaction segment failed", - -107: "PI_BAD_SMBUS_CMD: SMBus command not supported by driver", - -108: "PI_NOT_I2C_GPIO: no bit bang I2C in progress on GPIO", - -109: "PI_BAD_I2C_WLEN: bad I2C write length", - -110: "PI_BAD_I2C_RLEN: bad I2C read length", - -111: "PI_BAD_I2C_CMD: bad I2C command", - -112: "PI_BAD_I2C_BAUD: bad I2C baud rate, not 50-500k", - -113: "PI_CHAIN_LOOP_CNT: bad chain loop count", - -114: "PI_BAD_CHAIN_LOOP: empty chain loop", - -115: "PI_CHAIN_COUNTER: too many chain counters", - -116: "PI_BAD_CHAIN_CMD: bad chain command", - -117: "PI_BAD_CHAIN_DELAY: bad chain delay micros", - -118: "PI_CHAIN_NESTING: chain counters nested too deeply", - -119: "PI_CHAIN_TOO_BIG: chain is too long", - -120: "PI_DEPRECATED: deprecated function removed", - -121: "PI_BAD_SER_INVERT: bit bang serial invert not 0 or 1", - -122: "PI_BAD_EDGE: bad ISR edge value, not 0-2", - -123: "PI_BAD_ISR_INIT: bad ISR initialisation", - -124: "PI_BAD_FOREVER: loop forever must be last command", - -125: "PI_BAD_FILTER: bad filter parameter", - -126: "PI_BAD_PAD: bad pad number", - -127: "PI_BAD_STRENGTH: bad pad drive strength", - -128: "PI_FIL_OPEN_FAILED: file open failed", - -129: "PI_BAD_FILE_MODE: bad file mode", - -130: "PI_BAD_FILE_FLAG: bad file flag", - -131: "PI_BAD_FILE_READ: bad file read", - -132: "PI_BAD_FILE_WRITE: bad file write", - -133: "PI_FILE_NOT_ROPEN: file not open for read", - -134: "PI_FILE_NOT_WOPEN: file not open for write", - -135: "PI_BAD_FILE_SEEK: bad file seek", - -136: "PI_NO_FILE_MATCH: no files match pattern", - -137: "PI_NO_FILE_ACCESS: no permission to access file", - -138: "PI_FILE_IS_A_DIR: file is a directory", - -139: "PI_BAD_SHELL_STATUS: bad shell return status", - -140: "PI_BAD_SCRIPT_NAME: bad script name", - -141: "PI_BAD_SPI_BAUD: bad SPI baud rate, not 50-500k", - -142: "PI_NOT_SPI_GPIO: no bit bang SPI in progress on GPIO", - -143: "PI_BAD_EVENT_ID: bad event id", - -144: "PI_CMD_INTERRUPTED: Used by Python", - -145: "PI_NOT_ON_BCM2711: not available on BCM2711", - -146: "PI_ONLY_ON_BCM2711: only available on BCM271", - -2000: "PI_PIGIF_ERR_0", - -2099: "PI_PIGIF_ERR_99", - -3000: "PI_CUSTOM_ERR_0", - -3999: "PI_CUSTOM_ERR_999", -} - -// ConvertErrorCodeToMessage converts error code to a human-readable string. -func ConvertErrorCodeToMessage(errorCode int, message string) error { - errorMessage, exists := PiGPIOErrorMap[errorCode] - if exists { - return errors.Errorf("%s: %s", message, errorMessage) - } - return errors.Errorf("%s: %d", message, errorCode) -} diff --git a/components/board/pi/doc.go b/components/board/pi/doc.go deleted file mode 100644 index afe0448fd46..00000000000 --- a/components/board/pi/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pi implements a Board and its related interfaces for a Raspberry Pi. -package pi diff --git a/components/board/pi/impl/board.go b/components/board/pi/impl/board.go deleted file mode 100644 index 17ac918b29b..00000000000 --- a/components/board/pi/impl/board.go +++ /dev/null @@ -1,815 +0,0 @@ -//go:build linux && (arm64 || arm) && !no_pigpio && !no_cgo - -// Package piimpl contains the implementation of a supported Raspberry Pi board. -package piimpl - -/* - This driver contains various functionalities of raspberry pi board using the - pigpio library (https://abyz.me.uk/rpi/pigpio/pdif2.html). - NOTE: This driver only supports software PWM functionality of raspberry pi. - For software PWM, we currently support the default sample rate of - 5 microseconds, which supports the following 18 frequencies (Hz): - 8000 4000 2000 1600 1000 800 500 400 320 - 250 200 160 100 80 50 40 20 10 - Details on this can be found here -> https://abyz.me.uk/rpi/pigpio/pdif2.html#set_PWM_frequency -*/ - -// #include -// #include -// #include "pi.h" -// #cgo LDFLAGS: -lpigpio -import "C" - -import ( - "context" - "fmt" - "math" - "os" - "strconv" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - pb "go.viam.com/api/component/board/v1" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/board/mcp3008helper" - picommon "go.viam.com/rdk/components/board/pi/common" - "go.viam.com/rdk/components/board/pinwrappers" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - rdkutils "go.viam.com/rdk/utils" -) - -// init registers a pi board based on pigpio. -func init() { - resource.RegisterComponent( - board.API, - picommon.Model, - resource.Registration[board.Board, *Config]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (board.Board, error) { - return newPigpio(ctx, conf.ResourceName(), conf, logger) - }, - }) -} - -// A Config describes the configuration of a board and all of its connected parts. -type Config struct { - AnalogReaders []mcp3008helper.MCP3008AnalogConfig `json:"analogs,omitempty"` - DigitalInterrupts []DigitalInterruptConfig `json:"digital_interrupts,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - for idx, c := range conf.AnalogReaders { - if err := c.Validate(fmt.Sprintf("%s.%s.%d", path, "analogs", idx)); err != nil { - return nil, err - } - } - for idx, c := range conf.DigitalInterrupts { - if err := c.Validate(fmt.Sprintf("%s.%s.%d", path, "digital_interrupts", idx)); err != nil { - return nil, err - } - } - return nil, nil -} - -// piPigpio is an implementation of a board.Board of a Raspberry Pi -// accessed via pigpio. -type piPigpio struct { - resource.Named - // To prevent deadlocks, we must never lock this mutex while instanceMu, defined below, is - // locked. It's okay to lock instanceMu while this is locked, though. This invariant prevents - // deadlocks if both mutexes are locked by separate goroutines and are each waiting to lock the - // other as well. - mu sync.Mutex - cancelCtx context.Context - cancelFunc context.CancelFunc - duty int // added for mutex - gpioConfigSet map[int]bool - analogReaders map[string]*pinwrappers.AnalogSmoother - // `interrupts` maps interrupt names to the interrupts. `interruptsHW` maps broadcom addresses - // to these same values. The two should always have the same set of values. - interrupts map[string]ReconfigurableDigitalInterrupt - interruptsHW map[uint]ReconfigurableDigitalInterrupt - logger logging.Logger - isClosed bool - - activeBackgroundWorkers sync.WaitGroup -} - -var ( - pigpioInitialized bool - // To prevent deadlocks, we must never lock the mutex of a specific piPigpio struct, above, - // while this is locked. It is okay to lock this while one of those other mutexes is locked - // instead. - instanceMu sync.RWMutex - instances = map[*piPigpio]struct{}{} -) - -func initializePigpio() error { - instanceMu.Lock() - defer instanceMu.Unlock() - - if pigpioInitialized { - return nil - } - - resCode := C.gpioInitialise() - if resCode < 0 { - // failed to init, check for common causes - _, err := os.Stat("/sys/bus/platform/drivers/raspberrypi-firmware") - if err != nil { - return errors.New("not running on a pi") - } - if os.Getuid() != 0 { - return errors.New("not running as root, try sudo") - } - return picommon.ConvertErrorCodeToMessage(int(resCode), "error") - } - - pigpioInitialized = true - return nil -} - -// newPigpio makes a new pigpio based Board using the given config. -func newPigpio(ctx context.Context, name resource.Name, cfg resource.Config, logger logging.Logger) (board.Board, error) { - // this is so we can run it inside a daemon - internals := C.gpioCfgGetInternals() - internals |= C.PI_CFG_NOSIGHANDLER - resCode := C.gpioCfgSetInternals(internals) - if resCode < 0 { - return nil, picommon.ConvertErrorCodeToMessage(int(resCode), "gpioCfgSetInternals failed with code") - } - - if err := initializePigpio(); err != nil { - return nil, err - } - - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - piInstance := &piPigpio{ - Named: name.AsNamed(), - logger: logger, - isClosed: false, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - } - - if err := piInstance.Reconfigure(ctx, nil, cfg); err != nil { - // This has to happen outside of the lock to avoid a deadlock with interrupts. - C.gpioTerminate() - instanceMu.Lock() - pigpioInitialized = false - instanceMu.Unlock() - logger.CError(ctx, "Pi GPIO terminated due to failed init.") - return nil, err - } - return piInstance, nil -} - -func (pi *piPigpio) Reconfigure( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, -) error { - cfg, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - pi.mu.Lock() - defer pi.mu.Unlock() - - if err := pi.reconfigureAnalogReaders(ctx, cfg); err != nil { - return err - } - - // This is the only one that actually uses ctx, but we pass it to all previous helpers, too, to - // keep the interface consistent. - if err := pi.reconfigureInterrupts(ctx, cfg); err != nil { - return err - } - - instanceMu.Lock() - defer instanceMu.Unlock() - instances[pi] = struct{}{} - return nil -} - -// StreamTicks starts a stream of digital interrupt ticks. -func (pi *piPigpio) StreamTicks(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, -) error { - for _, i := range interrupts { - AddCallback(i.(*BasicDigitalInterrupt), ch) - } - - pi.activeBackgroundWorkers.Add(1) - - utils.ManagedGo(func() { - // Wait until it's time to shut down then remove callbacks. - select { - case <-ctx.Done(): - case <-pi.cancelCtx.Done(): - } - for _, i := range interrupts { - RemoveCallback(i.(*BasicDigitalInterrupt), ch) - } - }, pi.activeBackgroundWorkers.Done) - - return nil -} - -func (pi *piPigpio) reconfigureAnalogReaders(ctx context.Context, cfg *Config) error { - // No need to reconfigure the old analog readers; just throw them out and make new ones. - pi.analogReaders = map[string]*pinwrappers.AnalogSmoother{} - for _, ac := range cfg.AnalogReaders { - channel, err := strconv.Atoi(ac.Pin) - if err != nil { - return errors.Errorf("bad analog pin (%s)", ac.Pin) - } - - bus := &piPigpioSPI{pi: pi, busSelect: ac.SPIBus} - ar := &mcp3008helper.MCP3008AnalogReader{channel, bus, ac.ChipSelect} - - pi.analogReaders[ac.Name] = pinwrappers.SmoothAnalogReader(ar, board.AnalogReaderConfig{ - AverageOverMillis: ac.AverageOverMillis, SamplesPerSecond: ac.SamplesPerSecond, - }, pi.logger) - } - return nil -} - -// This is a helper function for digital interrupt reconfiguration. It finds the key in the map -// whose value is the given interrupt, and returns that key and whether we successfully found it. -func findInterruptName( - interrupt ReconfigurableDigitalInterrupt, - interrupts map[string]ReconfigurableDigitalInterrupt, -) (string, bool) { - for key, value := range interrupts { - if value == interrupt { - return key, true - } - } - return "", false -} - -// This is a very similar helper function, which does the same thing but for broadcom addresses. -func findInterruptBcom( - interrupt ReconfigurableDigitalInterrupt, - interruptsHW map[uint]ReconfigurableDigitalInterrupt, -) (uint, bool) { - for key, value := range interruptsHW { - if value == interrupt { - return key, true - } - } - return 0, false -} - -func (pi *piPigpio) reconfigureInterrupts(ctx context.Context, cfg *Config) error { - // We reuse the old interrupts when possible. - oldInterrupts := pi.interrupts - oldInterruptsHW := pi.interruptsHW - // Like with pi.interrupts and pi.interruptsHW, these two will have identical values, mapped to - // using different keys. - newInterrupts := map[string]ReconfigurableDigitalInterrupt{} - newInterruptsHW := map[uint]ReconfigurableDigitalInterrupt{} - - // This begins as a set of all interrupts, but we'll remove the ones we reuse. Then, we'll - // close whatever is left over. - interruptsToClose := make(map[ReconfigurableDigitalInterrupt]struct{}, len(oldInterrupts)) - for _, interrupt := range oldInterrupts { - interruptsToClose[interrupt] = struct{}{} - } - - reuseInterrupt := func( - interrupt ReconfigurableDigitalInterrupt, name string, bcom uint, - ) error { - newInterrupts[name] = interrupt - newInterruptsHW[bcom] = interrupt - delete(interruptsToClose, interrupt) - - // We also need to remove the reused interrupt from oldInterrupts and oldInterruptsHW, to - // avoid double-reuse (e.g., the old interrupt had name "foo" on pin 7, and the new config - // has name "foo" on pin 8 and name "bar" on pin 7). - if oldName, ok := findInterruptName(interrupt, oldInterrupts); ok { - delete(oldInterrupts, oldName) - } else { - // This should never happen. However, if it does, nothing is obviously broken, so we'll - // just log the weirdness and continue. - pi.logger.CErrorf(ctx, - "Tried reconfiguring old interrupt to new name %s and broadcom address %s, "+ - "but couldn't find its old name!?", name, bcom) - } - - if oldBcom, ok := findInterruptBcom(interrupt, oldInterruptsHW); ok { - delete(oldInterruptsHW, oldBcom) - if result := C.teardownInterrupt(C.int(oldBcom)); result != 0 { - return picommon.ConvertErrorCodeToMessage(int(result), "error") - } - } else { - // This should never happen, either, but is similarly not really a problem. - pi.logger.CErrorf(ctx, - "Tried reconfiguring old interrupt to new name %s and broadcom address %s, "+ - "but couldn't find its old bcom!?", name, bcom) - } - - if result := C.setupInterrupt(C.int(bcom)); result != 0 { - return picommon.ConvertErrorCodeToMessage(int(result), "error") - } - return nil - } - - for _, newConfig := range cfg.DigitalInterrupts { - bcom, ok := broadcomPinFromHardwareLabel(newConfig.Pin) - if !ok { - return errors.Errorf("no hw mapping for %s", newConfig.Pin) - } - - // Try reusing an interrupt with the same pin - if oldInterrupt, ok := oldInterruptsHW[bcom]; ok { - if err := reuseInterrupt(oldInterrupt, newConfig.Name, bcom); err != nil { - return err - } - continue - } - // If that didn't work, try reusing an interrupt with the same name - if oldInterrupt, ok := oldInterrupts[newConfig.Name]; ok { - if err := reuseInterrupt(oldInterrupt, newConfig.Name, bcom); err != nil { - return err - } - continue - } - - // Otherwise, create the new interrupt from scratch. - di, err := CreateDigitalInterrupt(newConfig) - if err != nil { - return err - } - newInterrupts[newConfig.Name] = di - newInterruptsHW[bcom] = di - if result := C.setupInterrupt(C.int(bcom)); result != 0 { - return picommon.ConvertErrorCodeToMessage(int(result), "error") - } - } - - // For the remaining interrupts, keep any that look implicitly created (interrupts whose name - // matches its broadcom address), and get rid of the rest. - for interrupt := range interruptsToClose { - name, ok := findInterruptName(interrupt, oldInterrupts) - if !ok { - // This should never happen - return errors.Errorf("Logic bug: found old interrupt %s without old name!?", interrupt) - } - - bcom, ok := findInterruptBcom(interrupt, oldInterruptsHW) - if !ok { - // This should never happen, either - return errors.Errorf("Logic bug: found old interrupt %s without old bcom!?", interrupt) - } - - if expectedBcom, ok := broadcomPinFromHardwareLabel(name); ok && bcom == expectedBcom { - // This digital interrupt looks like it was implicitly created. Keep it around! - newInterrupts[name] = interrupt - newInterruptsHW[bcom] = interrupt - } else { - // This digital interrupt is no longer used. - if result := C.teardownInterrupt(C.int(bcom)); result != 0 { - return picommon.ConvertErrorCodeToMessage(int(result), "error") - } - } - } - - pi.interrupts = newInterrupts - pi.interruptsHW = newInterruptsHW - return nil -} - -// GPIOPinByName returns a GPIOPin by name. -func (pi *piPigpio) GPIOPinByName(pin string) (board.GPIOPin, error) { - pi.mu.Lock() - defer pi.mu.Unlock() - bcom, have := broadcomPinFromHardwareLabel(pin) - if !have { - return nil, errors.Errorf("no hw pin for (%s)", pin) - } - return gpioPin{pi, int(bcom)}, nil -} - -type gpioPin struct { - pi *piPigpio - bcom int -} - -func (gp gpioPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - return gp.pi.SetGPIOBcom(gp.bcom, high) -} - -func (gp gpioPin) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { - return gp.pi.GetGPIOBcom(gp.bcom) -} - -func (gp gpioPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - return gp.pi.pwmBcom(gp.bcom) -} - -func (gp gpioPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - return gp.pi.SetPWMBcom(gp.bcom, dutyCyclePct) -} - -func (gp gpioPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - return gp.pi.pwmFreqBcom(gp.bcom) -} - -func (gp gpioPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - return gp.pi.SetPWMFreqBcom(gp.bcom, freqHz) -} - -// GetGPIOBcom gets the level of the given broadcom pin -func (pi *piPigpio) GetGPIOBcom(bcom int) (bool, error) { - pi.mu.Lock() - defer pi.mu.Unlock() - if !pi.gpioConfigSet[bcom] { - if pi.gpioConfigSet == nil { - pi.gpioConfigSet = map[int]bool{} - } - res := C.gpioSetMode(C.uint(bcom), C.PI_INPUT) - if res != 0 { - return false, picommon.ConvertErrorCodeToMessage(int(res), "failed to set mode") - } - pi.gpioConfigSet[bcom] = true - } - - // gpioRead retrns an int 1 or 0, we convert to a bool - return C.gpioRead(C.uint(bcom)) != 0, nil -} - -// SetGPIOBcom sets the given broadcom pin to high or low. -func (pi *piPigpio) SetGPIOBcom(bcom int, high bool) error { - pi.mu.Lock() - defer pi.mu.Unlock() - if !pi.gpioConfigSet[bcom] { - if pi.gpioConfigSet == nil { - pi.gpioConfigSet = map[int]bool{} - } - res := C.gpioSetMode(C.uint(bcom), C.PI_OUTPUT) - if res != 0 { - return picommon.ConvertErrorCodeToMessage(int(res), "failed to set mode") - } - pi.gpioConfigSet[bcom] = true - } - - v := 0 - if high { - v = 1 - } - C.gpioWrite(C.uint(bcom), C.uint(v)) - return nil -} - -func (pi *piPigpio) pwmBcom(bcom int) (float64, error) { - res := C.gpioGetPWMdutycycle(C.uint(bcom)) - return float64(res) / 255, nil -} - -// SetPWMBcom sets the given broadcom pin to the given PWM duty cycle. -func (pi *piPigpio) SetPWMBcom(bcom int, dutyCyclePct float64) error { - pi.mu.Lock() - defer pi.mu.Unlock() - dutyCycle := rdkutils.ScaleByPct(255, dutyCyclePct) - pi.duty = int(C.gpioPWM(C.uint(bcom), C.uint(dutyCycle))) - if pi.duty != 0 { - return errors.Errorf("pwm set fail %d", pi.duty) - } - return nil -} - -func (pi *piPigpio) pwmFreqBcom(bcom int) (uint, error) { - res := C.gpioGetPWMfrequency(C.uint(bcom)) - return uint(res), nil -} - -// SetPWMFreqBcom sets the given broadcom pin to the given PWM frequency. -func (pi *piPigpio) SetPWMFreqBcom(bcom int, freqHz uint) error { - pi.mu.Lock() - defer pi.mu.Unlock() - if freqHz == 0 { - freqHz = 800 // Original default from libpigpio - } - newRes := C.gpioSetPWMfrequency(C.uint(bcom), C.uint(freqHz)) - - if newRes == C.PI_BAD_USER_GPIO { - return picommon.ConvertErrorCodeToMessage(int(newRes), "pwm set freq failed") - } - - if newRes != C.int(freqHz) { - pi.logger.Infof("cannot set pwm freq to %d, setting to closest freq %d", freqHz, newRes) - } - return nil -} - -type piPigpioSPI struct { - pi *piPigpio - mu sync.Mutex - busSelect string - openHandle *piPigpioSPIHandle - nativeCSSeen bool - gpioCSSeen bool -} - -type piPigpioSPIHandle struct { - bus *piPigpioSPI - isClosed bool -} - -func (s *piPigpioSPIHandle) Xfer(ctx context.Context, baud uint, chipSelect string, mode uint, tx []byte) ([]byte, error) { - if s.isClosed { - return nil, errors.New("can't use Xfer() on an already closed SPIHandle") - } - - var spiFlags uint - var gpioCS bool - var nativeCS C.uint - - if s.bus.busSelect == "1" { - spiFlags |= 0x100 // Sets AUX SPI bus bit - if mode == 1 || mode == 3 { - return nil, errors.New("AUX SPI Bus doesn't support Mode 1 or Mode 3") - } - if chipSelect == "11" || chipSelect == "12" || chipSelect == "36" { - s.bus.nativeCSSeen = true - if chipSelect == "11" { - nativeCS = 1 - } else if chipSelect == "36" { - nativeCS = 2 - } - } else { - s.bus.gpioCSSeen = true - gpioCS = true - } - } else { - if chipSelect == "24" || chipSelect == "26" { - s.bus.nativeCSSeen = true - if chipSelect == "26" { - nativeCS = 1 - } - } else { - s.bus.gpioCSSeen = true - gpioCS = true - } - } - - // Libpigpio will always enable the native CS output on 24 & 26 (or 11, 12, & 36 for aux SPI) - // Thus you don't have anything using those pins even when we're directly controlling another (extended/gpio) CS line - // Use only the native CS pins OR don't use them at all - if s.bus.nativeCSSeen && s.bus.gpioCSSeen { - return nil, errors.New("pi SPI cannot use both native CS pins and extended/gpio CS pins at the same time") - } - - // Bitfields for mode - // Mode POL PHA - // 0 0 0 - // 1 0 1 - // 2 1 0 - // 3 1 1 - spiFlags |= mode - - count := len(tx) - rx := make([]byte, count) - rxPtr := C.CBytes(rx) - defer C.free(rxPtr) - txPtr := C.CBytes(tx) - defer C.free(txPtr) - - handle := C.spiOpen(nativeCS, (C.uint)(baud), (C.uint)(spiFlags)) - - if handle < 0 { - errMsg := fmt.Sprintf("error opening SPI Bus %s, flags were %X", s.bus.busSelect, spiFlags) - return nil, picommon.ConvertErrorCodeToMessage(int(handle), errMsg) - } - defer C.spiClose((C.uint)(handle)) - - if gpioCS { - // We're going to directly control chip select (not using CE0/CE1/CE2 from SPI controller.) - // This allows us to use a large number of chips on a single bus. - // Per "seen" checks above, cannot be mixed with the native CE0/CE1/CE2 - chipPin, err := s.bus.pi.GPIOPinByName(chipSelect) - if err != nil { - return nil, err - } - err = chipPin.Set(ctx, false, nil) - if err != nil { - return nil, err - } - } - - ret := C.spiXfer((C.uint)(handle), (*C.char)(txPtr), (*C.char)(rxPtr), (C.uint)(count)) - - if gpioCS { - chipPin, err := s.bus.pi.GPIOPinByName(chipSelect) - if err != nil { - return nil, err - } - err = chipPin.Set(ctx, true, nil) - if err != nil { - return nil, err - } - } - - if int(ret) != count { - return nil, errors.Errorf("error with spiXfer: Wanted %d bytes, got %d bytes", count, ret) - } - - return C.GoBytes(rxPtr, (C.int)(count)), nil -} - -func (s *piPigpioSPI) OpenHandle() (buses.SPIHandle, error) { - s.mu.Lock() - s.openHandle = &piPigpioSPIHandle{bus: s, isClosed: false} - return s.openHandle, nil -} - -func (s *piPigpioSPI) Close(ctx context.Context) error { - return nil -} - -func (s *piPigpioSPIHandle) Close() error { - s.isClosed = true - s.bus.mu.Unlock() - return nil -} - -// AnalogNames returns the names of all known analog pins. -func (pi *piPigpio) AnalogNames() []string { - pi.mu.Lock() - defer pi.mu.Unlock() - names := []string{} - for k := range pi.analogReaders { - names = append(names, k) - } - return names -} - -// DigitalInterruptNames returns the names of all known digital interrupts. -func (pi *piPigpio) DigitalInterruptNames() []string { - pi.mu.Lock() - defer pi.mu.Unlock() - names := []string{} - for k := range pi.interrupts { - names = append(names, k) - } - return names -} - -// AnalogByName returns an analog pin by name. -func (pi *piPigpio) AnalogByName(name string) (board.Analog, error) { - pi.mu.Lock() - defer pi.mu.Unlock() - a, ok := pi.analogReaders[name] - if !ok { - return nil, errors.Errorf("can't find Analog pin (%s)", name) - } - return a, nil -} - -// DigitalInterruptByName returns a digital interrupt by name. -// NOTE: During board setup, if a digital interrupt has not been created -// for a pin, then this function will attempt to create one with the pin -// number as the name. -func (pi *piPigpio) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { - pi.mu.Lock() - defer pi.mu.Unlock() - d, ok := pi.interrupts[name] - if !ok { - var err error - if bcom, have := broadcomPinFromHardwareLabel(name); have { - if d, ok := pi.interruptsHW[bcom]; ok { - return d, nil - } - d, err = CreateDigitalInterrupt(DigitalInterruptConfig{ - Name: name, - Pin: name, - Type: "basic", - }) - if err != nil { - return nil, err - } - if result := C.setupInterrupt(C.int(bcom)); result != 0 { - err := picommon.ConvertErrorCodeToMessage(int(result), "error") - return nil, errors.Errorf("Unable to set up interrupt on pin %s: %s", name, err) - } - - pi.interrupts[name] = d - pi.interruptsHW[bcom] = d - return d, nil - } - return d, fmt.Errorf("interrupt %s does not exist", name) - } - return d, nil -} - -func (pi *piPigpio) SetPowerMode(ctx context.Context, mode pb.PowerMode, duration *time.Duration) error { - return grpc.UnimplementedError -} - -// Close attempts to close all parts of the board cleanly. -func (pi *piPigpio) Close(ctx context.Context) error { - var terminate bool - // Prevent duplicate calls to Close a board as this may overlap with - // the reinitialization of the board - pi.mu.Lock() - defer pi.mu.Unlock() - if pi.isClosed { - pi.logger.Info("Duplicate call to close pi board detected, skipping") - return nil - } - pi.cancelFunc() - pi.activeBackgroundWorkers.Wait() - - var err error - for _, analog := range pi.analogReaders { - err = multierr.Combine(err, analog.Close(ctx)) - } - pi.analogReaders = map[string]*pinwrappers.AnalogSmoother{} - - for bcom := range pi.interruptsHW { - if result := C.teardownInterrupt(C.int(bcom)); result != 0 { - err = multierr.Combine(err, picommon.ConvertErrorCodeToMessage(int(result), "error")) - } - } - pi.interrupts = map[string]ReconfigurableDigitalInterrupt{} - pi.interruptsHW = map[uint]ReconfigurableDigitalInterrupt{} - - instanceMu.Lock() - if len(instances) == 1 { - terminate = true - } - delete(instances, pi) - - if terminate { - pigpioInitialized = false - instanceMu.Unlock() - // This has to happen outside of the lock to avoid a deadlock with interrupts. - C.gpioTerminate() - pi.logger.CDebug(ctx, "Pi GPIO terminated properly.") - } else { - instanceMu.Unlock() - } - - pi.isClosed = true - return err -} - -var ( - lastTick = uint32(0) - tickRollevers = 0 -) - -//export pigpioInterruptCallback -func pigpioInterruptCallback(gpio, level int, rawTick uint32) { - if rawTick < lastTick { - tickRollevers++ - } - lastTick = rawTick - - tick := (uint64(tickRollevers) * uint64(math.MaxUint32)) + uint64(rawTick) - - instanceMu.RLock() - defer instanceMu.RUnlock() - for instance := range instances { - i := instance.interruptsHW[uint(gpio)] - if i == nil { - logging.Global().Infof("no DigitalInterrupt configured for gpio %d", gpio) - continue - } - high := true - if level == 0 { - high = false - } - // this should *not* block for long otherwise the lock - // will be held - switch di := i.(type) { - case *BasicDigitalInterrupt: - err := Tick(instance.cancelCtx, di, high, tick*1000) - if err != nil { - instance.logger.Error(err) - } - case *ServoDigitalInterrupt: - err := ServoTick(instance.cancelCtx, di, high, tick*1000) - if err != nil { - instance.logger.Error(err) - } - default: - instance.logger.Error("unknown digital interrupt type") - } - } -} diff --git a/components/board/pi/impl/board_test.go b/components/board/pi/impl/board_test.go deleted file mode 100644 index 6a6b09d2066..00000000000 --- a/components/board/pi/impl/board_test.go +++ /dev/null @@ -1,321 +0,0 @@ -//go:build linux && (arm64 || arm) - -package piimpl - -import ( - "context" - "os" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - picommon "go.viam.com/rdk/components/board/pi/common" - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -func TestPiPigpio(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - cfg := Config{ - DigitalInterrupts: []DigitalInterruptConfig{ - {Name: "i1", Pin: "11"}, // bcom 17 - {Name: "servo-i", Pin: "22", Type: "servo"}, - }, - } - resourceConfig := resource.Config{ConvertedAttributes: &cfg} - - pp, err := newPigpio(ctx, board.Named("foo"), resourceConfig, logger) - if os.Getuid() != 0 || err != nil && err.Error() == "not running on a pi" { - t.Skip("not running as root on a pi") - return - } - test.That(t, err, test.ShouldBeNil) - - p := pp.(*piPigpio) - - defer func() { - err := p.Close(ctx) - test.That(t, err, test.ShouldBeNil) - }() - - t.Run("gpio and pwm", func(t *testing.T) { - pin, err := p.GPIOPinByName("29") - test.That(t, err, test.ShouldBeNil) - - // try to set high - err = pin.Set(ctx, true, nil) - test.That(t, err, test.ShouldBeNil) - - v, err := pin.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, v, test.ShouldEqual, true) - - // try to set low - err = pin.Set(ctx, false, nil) - test.That(t, err, test.ShouldBeNil) - - v, err = pin.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, v, test.ShouldEqual, false) - - // pwm 50% - err = pin.SetPWM(ctx, 0.5, nil) - test.That(t, err, test.ShouldBeNil) - - vF, err := pin.PWM(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, vF, test.ShouldAlmostEqual, 0.5, 0.01) - - // 4000 hz - err = pin.SetPWMFreq(ctx, 4000, nil) - test.That(t, err, test.ShouldBeNil) - - vI, err := pin.PWMFreq(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, vI, test.ShouldEqual, 4000) - - // 90% - err = pin.SetPWM(ctx, 0.9, nil) - test.That(t, err, test.ShouldBeNil) - - vF, err = pin.PWM(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, vF, test.ShouldAlmostEqual, 0.9, 0.01) - - // 8000hz - err = pin.SetPWMFreq(ctx, 8000, nil) - test.That(t, err, test.ShouldBeNil) - - vI, err = pin.PWMFreq(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, vI, test.ShouldEqual, 8000) - }) - - t.Run("basic interrupts", func(t *testing.T) { - err := p.SetGPIOBcom(17, false) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(5 * time.Millisecond) - - i1, err := p.DigitalInterruptByName("i1") - test.That(t, err, test.ShouldBeNil) - before, err := i1.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - err = p.SetGPIOBcom(17, true) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(5 * time.Millisecond) - - after, err := i1.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, after-before, test.ShouldEqual, int64(1)) - - err = p.SetGPIOBcom(27, false) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(5 * time.Millisecond) - _, err = p.DigitalInterruptByName("some") - test.That(t, err, test.ShouldNotBeNil) - i2, err := p.DigitalInterruptByName("13") - test.That(t, err, test.ShouldBeNil) - before, err = i2.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - err = p.SetGPIOBcom(27, true) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(5 * time.Millisecond) - - after, err = i2.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, after-before, test.ShouldEqual, int64(1)) - - _, err = p.DigitalInterruptByName("11") - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("servo in/out", func(t *testing.T) { - servoReg, ok := resource.LookupRegistration(servo.API, picommon.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, servoReg, test.ShouldNotBeNil) - servoInt, err := servoReg.Constructor( - ctx, - nil, - resource.Config{ - Name: "servo", - ConvertedAttributes: &picommon.ServoConfig{Pin: "22"}, - }, - logger, - ) - test.That(t, err, test.ShouldBeNil) - servo1 := servoInt.(servo.Servo) - - err = servo1.Move(ctx, 90, nil) - test.That(t, err, test.ShouldBeNil) - - err = servo1.Move(ctx, 190, nil) - test.That(t, err, test.ShouldNotBeNil) - - v, err := servo1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, int(v), test.ShouldEqual, 90) - - time.Sleep(300 * time.Millisecond) - - servoI, err := p.DigitalInterruptByName("servo-i") - test.That(t, err, test.ShouldBeNil) - val, err := servoI.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, val, test.ShouldAlmostEqual, int64(1500), 500) // this is a tad noisy - }) - - t.Run("servo initialize with pin error", func(t *testing.T) { - servoReg, ok := resource.LookupRegistration(servo.API, picommon.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, servoReg, test.ShouldNotBeNil) - _, err := servoReg.Constructor( - ctx, - nil, - resource.Config{ - Name: "servo", - ConvertedAttributes: &picommon.ServoConfig{Pin: ""}, - }, - logger, - ) - test.That(t, err.Error(), test.ShouldContainSubstring, "need pin for pi servo") - }) - - t.Run("check new servo defaults", func(t *testing.T) { - ctx := context.Background() - servoReg, ok := resource.LookupRegistration(servo.API, picommon.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, servoReg, test.ShouldNotBeNil) - servoInt, err := servoReg.Constructor( - ctx, - nil, - resource.Config{ - Name: "servo", - ConvertedAttributes: &picommon.ServoConfig{Pin: "22"}, - }, - logger, - ) - test.That(t, err, test.ShouldBeNil) - - servo1 := servoInt.(servo.Servo) - pos1, err := servo1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos1, test.ShouldEqual, 90) - }) - - t.Run("check set default position", func(t *testing.T) { - ctx := context.Background() - servoReg, ok := resource.LookupRegistration(servo.API, picommon.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, servoReg, test.ShouldNotBeNil) - - initPos := 33.0 - servoInt, err := servoReg.Constructor( - ctx, - nil, - resource.Config{ - Name: "servo", - ConvertedAttributes: &picommon.ServoConfig{Pin: "22", StartPos: &initPos}, - }, - logger, - ) - test.That(t, err, test.ShouldBeNil) - - servo1 := servoInt.(servo.Servo) - pos1, err := servo1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos1, test.ShouldEqual, 33) - - localServo := servo1.(*piPigpioServo) - test.That(t, localServo.holdPos, test.ShouldBeTrue) - }) -} - -func TestServoFunctions(t *testing.T) { - t.Run("check servo math", func(t *testing.T) { - pw := angleToPulseWidth(1, servoDefaultMaxRotation) - test.That(t, pw, test.ShouldEqual, 511) - pw = angleToPulseWidth(0, servoDefaultMaxRotation) - test.That(t, pw, test.ShouldEqual, 500) - pw = angleToPulseWidth(179, servoDefaultMaxRotation) - test.That(t, pw, test.ShouldEqual, 2488) - pw = angleToPulseWidth(180, servoDefaultMaxRotation) - test.That(t, pw, test.ShouldEqual, 2500) - pw = angleToPulseWidth(179, 270) - test.That(t, pw, test.ShouldEqual, 1825) - pw = angleToPulseWidth(180, 270) - test.That(t, pw, test.ShouldEqual, 1833) - a := pulseWidthToAngle(511, servoDefaultMaxRotation) - test.That(t, a, test.ShouldEqual, 1) - a = pulseWidthToAngle(500, servoDefaultMaxRotation) - test.That(t, a, test.ShouldEqual, 0) - a = pulseWidthToAngle(2500, servoDefaultMaxRotation) - test.That(t, a, test.ShouldEqual, 180) - a = pulseWidthToAngle(2488, servoDefaultMaxRotation) - test.That(t, a, test.ShouldEqual, 179) - a = pulseWidthToAngle(1825, 270) - test.That(t, a, test.ShouldEqual, 179) - a = pulseWidthToAngle(1833, 270) - test.That(t, a, test.ShouldEqual, 180) - }) - - t.Run(("check Move IsMoving ande pigpio errors"), func(t *testing.T) { - ctx := context.Background() - s := &piPigpioServo{pinname: "1", maxRotation: 180, opMgr: operation.NewSingleOperationManager()} - - s.res = -93 - err := s.pigpioErrors(int(s.res)) - test.That(t, err.Error(), test.ShouldContainSubstring, "pulsewidths") - moving, err := s.IsMoving(ctx) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, err, test.ShouldNotBeNil) - - s.res = -7 - err = s.pigpioErrors(int(s.res)) - test.That(t, err.Error(), test.ShouldContainSubstring, "range") - moving, err = s.IsMoving(ctx) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, err, test.ShouldNotBeNil) - - s.res = 0 - err = s.pigpioErrors(int(s.res)) - test.That(t, err, test.ShouldBeNil) - moving, err = s.IsMoving(ctx) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, err, test.ShouldBeNil) - - s.res = 1 - err = s.pigpioErrors(int(s.res)) - test.That(t, err, test.ShouldBeNil) - moving, err = s.IsMoving(ctx) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, err, test.ShouldBeNil) - - err = s.pigpioErrors(-4) - test.That(t, err.Error(), test.ShouldContainSubstring, "failed") - moving, err = s.IsMoving(ctx) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, err, test.ShouldBeNil) - - err = s.Move(ctx, 8, nil) - test.That(t, err, test.ShouldNotBeNil) - - err = s.Stop(ctx, nil) - test.That(t, err, test.ShouldNotBeNil) - - pos, err := s.Position(ctx, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, pos, test.ShouldEqual, 0) - }) -} diff --git a/components/board/pi/impl/digital_interrupts.go b/components/board/pi/impl/digital_interrupts.go deleted file mode 100644 index 0c27ab530db..00000000000 --- a/components/board/pi/impl/digital_interrupts.go +++ /dev/null @@ -1,199 +0,0 @@ -//go:build linux && (arm64 || arm) && !no_pigpio && !no_cgo - -package piimpl - -import ( - "context" - "sync" - "sync/atomic" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" -) - -// DigitalInterruptConfig describes the configuration of digital interrupt for the board. -type DigitalInterruptConfig struct { - Name string `json:"name"` - Pin string `json:"pin"` - Type string `json:"type,omitempty"` // e.g. basic, servo -} - -// Validate ensures all parts of the config are valid. -func (config *DigitalInterruptConfig) Validate(path string) error { - if config.Name == "" { - return resource.NewConfigValidationFieldRequiredError(path, "name") - } - if config.Pin == "" { - return resource.NewConfigValidationFieldRequiredError(path, "pin") - } - return nil -} - -// ServoRollingAverageWindow is how many entries to average over for -// servo ticks. -const ServoRollingAverageWindow = 10 - -// A ReconfigurableDigitalInterrupt is a simple reconfigurable digital interrupt that expects -// reconfiguration within the same type. -type ReconfigurableDigitalInterrupt interface { - board.DigitalInterrupt - Reconfigure(cfg DigitalInterruptConfig) error -} - -// CreateDigitalInterrupt is a factory method for creating a specific DigitalInterrupt based -// on the given config. If no type is specified, a BasicDigitalInterrupt is returned. -func CreateDigitalInterrupt(cfg DigitalInterruptConfig) (ReconfigurableDigitalInterrupt, error) { - if cfg.Type == "" { - cfg.Type = "basic" - } - - var i ReconfigurableDigitalInterrupt - switch cfg.Type { - case "basic": - i = &BasicDigitalInterrupt{} - case "servo": - i = &ServoDigitalInterrupt{ra: utils.NewRollingAverage(ServoRollingAverageWindow)} - default: - panic(errors.Errorf("unknown interrupt type (%s)", cfg.Type)) - } - - if err := i.Reconfigure(cfg); err != nil { - return nil, err - } - return i, nil -} - -// A BasicDigitalInterrupt records how many ticks/interrupts happen and can -// report when they happen to interested callbacks. -type BasicDigitalInterrupt struct { - count int64 - - callbacks []chan board.Tick - - mu sync.RWMutex - cfg DigitalInterruptConfig -} - -// Value returns the amount of ticks that have occurred. -func (i *BasicDigitalInterrupt) Value(ctx context.Context, extra map[string]interface{}) (int64, error) { - i.mu.RLock() - defer i.mu.RUnlock() - count := atomic.LoadInt64(&i.count) - return count, nil -} - -// Tick records an interrupt and notifies any interested callbacks. See comment on -// the DigitalInterrupt interface for caveats. -func Tick(ctx context.Context, i *BasicDigitalInterrupt, high bool, nanoseconds uint64) error { - if high { - atomic.AddInt64(&i.count, 1) - } - - i.mu.RLock() - defer i.mu.RUnlock() - for _, c := range i.callbacks { - select { - case <-ctx.Done(): - return errors.New("context cancelled") - case c <- board.Tick{Name: i.cfg.Name, High: high, TimestampNanosec: nanoseconds}: - } - } - return nil -} - -// AddCallback adds a listener for interrupts. -func AddCallback(i *BasicDigitalInterrupt, c chan board.Tick) { - i.mu.Lock() - defer i.mu.Unlock() - i.callbacks = append(i.callbacks, c) -} - -// RemoveCallback removes a listener for interrupts. -func RemoveCallback(i *BasicDigitalInterrupt, c chan board.Tick) { - i.mu.Lock() - defer i.mu.Unlock() - for id := range i.callbacks { - if i.callbacks[id] == c { - // To remove this item, we replace it with the last item in the list, then truncate the - // list by 1. - i.callbacks[id] = i.callbacks[len(i.callbacks)-1] - i.callbacks = i.callbacks[:len(i.callbacks)-1] - break - } - } -} - -// Name returns the name of the interrupt. -func (i *BasicDigitalInterrupt) Name() string { - i.mu.Lock() - defer i.mu.Unlock() - return i.cfg.Name -} - -// Reconfigure reconfigures this digital interrupt. -func (i *BasicDigitalInterrupt) Reconfigure(conf DigitalInterruptConfig) error { - i.mu.Lock() - defer i.mu.Unlock() - i.cfg = conf - return nil -} - -// A ServoDigitalInterrupt is an interrupt associated with a servo in order to -// track the amount of time that has passed between low signals (pulse width). Post processors -// make meaning of these widths. -type ServoDigitalInterrupt struct { - last uint64 - ra *utils.RollingAverage - - mu sync.RWMutex - cfg DigitalInterruptConfig -} - -// Value will return the window averaged value followed by its post processed -// result. -func (i *ServoDigitalInterrupt) Value(ctx context.Context, extra map[string]interface{}) (int64, error) { - i.mu.RLock() - defer i.mu.RUnlock() - v := int64(i.ra.Average()) - return v, nil -} - -// ServoTick records the time between two successive low signals (pulse width). How it is -// interpreted is based off the consumer of Value. -func ServoTick(ctx context.Context, i *ServoDigitalInterrupt, high bool, now uint64) error { - i.mu.RLock() - defer i.mu.RUnlock() - diff := now - i.last - i.last = now - - if i.last == 0 { - return nil - } - - if high { - // this is time between signals, ignore - return nil - } - - i.ra.Add(int(diff / 1000)) - return nil -} - -// Name returns the name of the interrupt. -func (i *ServoDigitalInterrupt) Name() string { - i.mu.Lock() - defer i.mu.Unlock() - return i.cfg.Name -} - -// Reconfigure reconfigures this digital interrupt. -func (i *ServoDigitalInterrupt) Reconfigure(conf DigitalInterruptConfig) error { - i.mu.Lock() - defer i.mu.Unlock() - - i.cfg = conf - return nil -} diff --git a/components/board/pi/impl/digital_interrupts_test.go b/components/board/pi/impl/digital_interrupts_test.go deleted file mode 100644 index 2dbd4b14531..00000000000 --- a/components/board/pi/impl/digital_interrupts_test.go +++ /dev/null @@ -1,203 +0,0 @@ -//go:build linux && (arm64 || arm) && !no_pigpio && !no_cgo - -package piimpl - -import ( - "context" - "sync" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" -) - -func nowNanosecondsTest() uint64 { - return uint64(time.Now().UnixNano()) -} - -func TestBasicDigitalInterrupt1(t *testing.T) { - config := DigitalInterruptConfig{ - Name: "i1", - } - - i, err := CreateDigitalInterrupt(config) - test.That(t, err, test.ShouldBeNil) - - basicInterrupt := i.(*BasicDigitalInterrupt) - - intVal, err := i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(0)) - test.That(t, Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1)) - test.That(t, Tick(context.Background(), basicInterrupt, false, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1)) - - c := make(chan board.Tick) - AddCallback(basicInterrupt, c) - - timeNanoSec := nowNanosecondsTest() - go func() { Tick(context.Background(), basicInterrupt, true, timeNanoSec) }() - time.Sleep(1 * time.Microsecond) - v := <-c - test.That(t, v.High, test.ShouldBeTrue) - test.That(t, v.TimestampNanosec, test.ShouldEqual, timeNanoSec) - - timeNanoSec = nowNanosecondsTest() - go func() { Tick(context.Background(), basicInterrupt, true, timeNanoSec) }() - v = <-c - test.That(t, v.High, test.ShouldBeTrue) - test.That(t, v.TimestampNanosec, test.ShouldEqual, timeNanoSec) - - RemoveCallback(basicInterrupt, c) - - c = make(chan board.Tick, 2) - AddCallback(basicInterrupt, c) - go func() { - Tick(context.Background(), basicInterrupt, true, uint64(1)) - Tick(context.Background(), basicInterrupt, true, uint64(4)) - }() - v = <-c - v1 := <-c - test.That(t, v.High, test.ShouldBeTrue) - test.That(t, v1.High, test.ShouldBeTrue) - test.That(t, v1.TimestampNanosec-v.TimestampNanosec, test.ShouldEqual, uint32(3)) -} - -func TestRemoveCallbackDigitalInterrupt(t *testing.T) { - config := DigitalInterruptConfig{ - Name: "d1", - } - i, err := CreateDigitalInterrupt(config) - test.That(t, err, test.ShouldBeNil) - basicInterrupt := i.(*BasicDigitalInterrupt) - intVal, err := i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(0)) - test.That(t, Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1)) - - c1 := make(chan board.Tick) - test.That(t, c1, test.ShouldNotBeNil) - AddCallback(basicInterrupt, c1) - var wg sync.WaitGroup - wg.Add(1) - ret := false - - go func() { - defer wg.Done() - select { - case <-context.Background().Done(): - return - default: - } - select { - case <-context.Background().Done(): - return - case tick := <-c1: - ret = tick.High - } - }() - test.That(t, Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(2)) - wg.Wait() - c2 := make(chan board.Tick) - test.That(t, c2, test.ShouldNotBeNil) - AddCallback(basicInterrupt, c2) - test.That(t, ret, test.ShouldBeTrue) - - RemoveCallback(basicInterrupt, c1) - RemoveCallback(basicInterrupt, c1) - - ret2 := false - result := make(chan bool, 1) - go func() { - defer wg.Done() - select { - case <-context.Background().Done(): - return - default: - } - select { - case <-context.Background().Done(): - return - case tick := <-c2: - ret2 = tick.High - } - }() - wg.Add(1) - go func() { - err := Tick(context.Background(), basicInterrupt, true, nowNanosecondsTest()) - if err != nil { - result <- true - } - result <- true - }() - select { - case <-time.After(1 * time.Second): - ret = false - case ret = <-result: - } - wg.Wait() - test.That(t, ret, test.ShouldBeTrue) - test.That(t, ret2, test.ShouldBeTrue) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(3)) -} - -func TestServoInterrupt(t *testing.T) { - config := DigitalInterruptConfig{ - Name: "s1", - Type: "servo", - } - - s, err := CreateDigitalInterrupt(config) - test.That(t, err, test.ShouldBeNil) - servoInterrupt := s.(*ServoDigitalInterrupt) - - now := uint64(0) - for i := 0; i < 20; i++ { - test.That(t, ServoTick(context.Background(), servoInterrupt, true, now), test.ShouldBeNil) - now += 1500 * 1000 // this is what we measure - test.That(t, ServoTick(context.Background(), servoInterrupt, false, now), test.ShouldBeNil) - now += 1000 * 1000 * 1000 // this is between measurements - } - - intVal, err := s.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1500)) -} - -func TestServoInterruptWithPP(t *testing.T) { - config := DigitalInterruptConfig{ - Name: "s1", - Type: "servo", - } - - s, err := CreateDigitalInterrupt(config) - test.That(t, err, test.ShouldBeNil) - servoInterrupt := s.(*ServoDigitalInterrupt) - - now := uint64(0) - for i := 0; i < 20; i++ { - test.That(t, ServoTick(context.Background(), servoInterrupt, true, now), test.ShouldBeNil) - now += 1500 * 1000 // this is what we measure - test.That(t, ServoTick(context.Background(), servoInterrupt, false, now), test.ShouldBeNil) - now += 1000 * 1000 * 1000 // this is between measurements - } - - intVal, err := s.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1500)) -} diff --git a/components/board/pi/impl/external_hardware_test.go b/components/board/pi/impl/external_hardware_test.go deleted file mode 100644 index 8d543ab1c8f..00000000000 --- a/components/board/pi/impl/external_hardware_test.go +++ /dev/null @@ -1,263 +0,0 @@ -//go:build linux && (arm64 || arm) - -package piimpl - -import ( - "context" - "os" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - picommon "go.viam.com/rdk/components/board/pi/common" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/components/encoder/incremental" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/motor/gpio" - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestPiHardware(t *testing.T) { - if testing.Short() { - t.Skip("skipping external pi hardware tests") - return - } - ctx := context.Background() - logger := logging.NewTestLogger(t) - - cfg := Config{ - DigitalInterrupts: []DigitalInterruptConfig{ - {Name: "i1", Pin: "11"}, // plug physical 12(18) into this (17) - {Name: "servo-i", Pin: "22", Type: "servo"}, // bcom-25 - {Name: "a", Pin: "33"}, // bcom 13 - {Name: "b", Pin: "37"}, // bcom 26 - }, - } - resourceConfig := resource.Config{ConvertedAttributes: &cfg} - - pp, err := newPigpio(ctx, board.Named("foo"), resourceConfig, logger) - if os.Getuid() != 0 || err != nil && err.Error() == "not running on a pi" { - t.Skip("not running as root on a pi") - return - } - test.That(t, err, test.ShouldBeNil) - - p := pp.(*piPigpio) - - defer func() { - err := p.Close(ctx) - test.That(t, err, test.ShouldBeNil) - }() - - t.Run("analog test", func(t *testing.T) { - reader, err := p.AnalogByName("blue") - test.That(t, err, test.ShouldBeNil) - if reader == nil { - t.Skip("no blue? analog") - return - } - - // try to set low - err = p.SetGPIOBcom(26, false) - test.That(t, err, test.ShouldBeNil) - - v, err := reader.Read(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, v, test.ShouldAlmostEqual, 0, 150) - - // try to set high - err = p.SetGPIOBcom(26, true) - test.That(t, err, test.ShouldBeNil) - - v, err = reader.Read(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, v, test.ShouldAlmostEqual, 1023, 150) - - // back to low - err = p.SetGPIOBcom(26, false) - test.That(t, err, test.ShouldBeNil) - - v, err = reader.Read(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, v, test.ShouldAlmostEqual, 0, 150) - }) - - t.Run("basic interrupts", func(t *testing.T) { - err = p.SetGPIOBcom(18, false) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(5 * time.Millisecond) - - i1, err := p.DigitalInterruptByName("i1") - test.That(t, err, test.ShouldBeNil) - before, err := i1.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - err = p.SetGPIOBcom(18, true) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(5 * time.Millisecond) - - after, err := i1.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, after-before, test.ShouldEqual, int64(1)) - }) - - t.Run("servo in/out", func(t *testing.T) { - servoReg, ok := resource.LookupRegistration(servo.API, picommon.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, servoReg, test.ShouldNotBeNil) - servoInt, err := servoReg.Constructor( - ctx, - nil, - resource.Config{ - Name: "servo", - ConvertedAttributes: &picommon.ServoConfig{Pin: "18"}, - }, - logger, - ) - test.That(t, err, test.ShouldBeNil) - servo1 := servoInt.(servo.Servo) - - err = servo1.Move(ctx, 90, nil) - test.That(t, err, test.ShouldBeNil) - - v, err := servo1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, int(v), test.ShouldEqual, 90) - - time.Sleep(300 * time.Millisecond) - - servoI, err := p.DigitalInterruptByName("servo-i") - test.That(t, err, test.ShouldBeNil) - val, err := servoI.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, val, test.ShouldAlmostEqual, int64(1500), 500) // this is a tad noisy - }) - - motorReg, ok := resource.LookupRegistration(motor.API, picommon.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - encoderReg, ok := resource.LookupRegistration(encoder.API, resource.DefaultModelFamily.WithModel("encoder")) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, encoderReg, test.ShouldNotBeNil) - - deps := make(resource.Dependencies) - _, err = encoderReg.Constructor(ctx, deps, resource.Config{ - Name: "encoder1", ConvertedAttributes: &incremental.Config{ - Pins: incremental.Pins{ - A: "a", - B: "b", - }, - BoardName: "test", - }, - }, logger) - test.That(t, err, test.ShouldBeNil) - - motorDeps := make([]string, 0) - motorDeps = append(motorDeps, "encoder1") - - motorInt, err := motorReg.Constructor(ctx, deps, resource.Config{ - Name: "motor1", ConvertedAttributes: &gpio.Config{ - Pins: gpio.PinConfig{ - A: "13", // bcom 27 - B: "40", // bcom 21 - PWM: "7", // bcom 4 - }, - BoardName: "test", - Encoder: "encoder1", - TicksPerRotation: 200, - }, - DependsOn: motorDeps, - }, logger) - test.That(t, err, test.ShouldBeNil) - motor1 := motorInt.(motor.Motor) - - t.Run("motor forward", func(t *testing.T) { - pos, err := motor1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldAlmostEqual, .0, 0o1) - - // 15 rpm is about what we can get from 5v. 2 rotations should take 8 seconds - err = motor1.GoFor(ctx, 15, 2, nil) - test.That(t, err, test.ShouldBeNil) - on, powerPct, err := motor1.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 1.0) - - encA, err := p.DigitalInterruptByName("a") - test.That(t, err, test.ShouldBeNil) - encB, err := p.DigitalInterruptByName("b") - test.That(t, err, test.ShouldBeNil) - - loops := 0 - for { - on, _, err := motor1.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - if !on { - break - } - - time.Sleep(100 * time.Millisecond) - - loops++ - if loops > 100 { - pos, err = motor1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - aVal, err := encA.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - bVal, err := encB.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - t.Fatalf("motor didn't move enough, a: %v b: %v pos: %v", - aVal, - bVal, - pos, - ) - } - } - }) - - t.Run("motor backward", func(t *testing.T) { - // 15 rpm is about what we can get from 5v. 2 rotations should take 8 seconds - err := motor1.GoFor(ctx, -15, 2, nil) - test.That(t, err, test.ShouldBeNil) - - on, powerPct, err := motor1.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 1.0) - - encA, err := p.DigitalInterruptByName("a") - test.That(t, err, test.ShouldBeNil) - encB, err := p.DigitalInterruptByName("b") - test.That(t, err, test.ShouldBeNil) - - loops := 0 - for { - on, _, err := motor1.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - if !on { - break - } - - time.Sleep(100 * time.Millisecond) - loops++ - aVal, err := encA.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - bVal, err := encB.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - if loops > 100 { - t.Fatalf("motor didn't move enough, a: %v b: %v", - aVal, - bVal, - ) - } - } - }) -} diff --git a/components/board/pi/impl/i2c.go b/components/board/pi/impl/i2c.go deleted file mode 100644 index af0ea8b6470..00000000000 --- a/components/board/pi/impl/i2c.go +++ /dev/null @@ -1,125 +0,0 @@ -//go:build linux && (arm64 || arm) && !no_pigpio && !no_cgo - -package piimpl - -// #include -// #include -// #include "pi.h" -// #cgo LDFLAGS: -lpigpio -import "C" - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board/genericlinux/buses" - picommon "go.viam.com/rdk/components/board/pi/common" -) - -type piPigpioI2C struct { - pi *piPigpio - id int -} - -type piPigpioI2CHandle struct { - bus *piPigpioI2C - i2cFlags C.uint - handle C.uint -} - -// Write will write the given slice of bytes to the given i2c address -func (s *piPigpioI2CHandle) Write(ctx context.Context, tx []byte) error { - txPtr := C.CBytes(tx) - defer C.free(txPtr) - - ret := int(C.i2cWriteDevice(s.handle, (*C.char)(txPtr), (C.uint)(len(tx)))) - - if ret != 0 { - return picommon.ConvertErrorCodeToMessage(ret, "error with i2c write") - } - - return nil -} - -// Read will read `count` bytes from the given address. -func (s *piPigpioI2CHandle) Read(ctx context.Context, count int) ([]byte, error) { - rx := make([]byte, count) - rxPtr := C.CBytes(rx) - defer C.free(rxPtr) - - ret := int(C.i2cReadDevice(s.handle, (*C.char)(rxPtr), (C.uint)(count))) - - if ret <= 0 { - return nil, picommon.ConvertErrorCodeToMessage(ret, "error with i2c read") - } - - return C.GoBytes(rxPtr, (C.int)(count)), nil -} - -func (s *piPigpioI2CHandle) ReadByteData(ctx context.Context, register byte) (byte, error) { - res := C.i2cReadByteData(s.handle, C.uint(register)) - if res < 0 { - return 0, picommon.ConvertErrorCodeToMessage(int(res), "error in ReadByteData") - } - return byte(res & 0xFF), nil -} - -func (s *piPigpioI2CHandle) WriteByteData(ctx context.Context, register, data byte) error { - res := C.i2cWriteByteData(s.handle, C.uint(register), C.uint(data)) - if res != 0 { - return picommon.ConvertErrorCodeToMessage(int(res), "error in WriteByteData") - } - return nil -} - -func (s *piPigpioI2CHandle) ReadBlockData(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - if numBytes > 32 { // A limitation from the underlying pigpio.h library - return nil, errors.New("Cannot read more than 32 bytes from I2C") - } - - data := make([]byte, numBytes) - response := C.i2cReadI2CBlockData( - s.handle, C.uint(register), (*C.char)(&data[0]), C.uint(numBytes)) - if response < 0 { - return nil, picommon.ConvertErrorCodeToMessage(int(response), "error in ReadBlockData") - } - return data, nil -} - -func (s *piPigpioI2CHandle) WriteBlockData(ctx context.Context, register byte, data []byte) error { - numBytes := len(data) - if numBytes > 32 { // A limitation from the underlying pigpio.h library - return errors.New("Cannot write more than 32 bytes from I2C") - } - - response := C.i2cWriteI2CBlockData( - s.handle, C.uint(register), (*C.char)(&data[0]), C.uint(numBytes)) - if response != 0 { - return picommon.ConvertErrorCodeToMessage(int(response), "error in WriteBlockData") - } - return nil -} - -func (s *piPigpioI2C) OpenHandle(addr byte) (buses.I2CHandle, error) { - handle := &piPigpioI2CHandle{bus: s} - - // Raspberry Pis are all on i2c bus 1 - // Exception being the very first model which is on 0 - bus := (C.uint)(s.id) - temp := C.i2cOpen(bus, (C.uint)(addr), handle.i2cFlags) - - if temp < 0 { - errMsg := fmt.Sprintf("error opening I2C Bus %d, flags were %X", bus, handle.i2cFlags) - return nil, picommon.ConvertErrorCodeToMessage(int(temp), errMsg) - } - handle.handle = C.uint(temp) - - return handle, nil -} - -func (s *piPigpioI2CHandle) Close() error { - C.i2cClose(s.handle) - return nil -} diff --git a/components/board/pi/impl/pi.c b/components/board/pi/impl/pi.c deleted file mode 100644 index 3c501a2a974..00000000000 --- a/components/board/pi/impl/pi.c +++ /dev/null @@ -1,32 +0,0 @@ -//go:build !no_pigpio -#include - -extern void pigpioInterruptCallback(int gpio, int level, uint32_t tick); - -// interruptCallback calls through to the go linked interrupt callback. -void interruptCallback(int gpio, int level, uint32_t tick) { - if (level == 2) { - // watchdog - return; - } - pigpioInterruptCallback(gpio, level, tick); -} - -int setupInterrupt(int gpio) { - int result = gpioSetMode(gpio, PI_INPUT); - if (result != 0) { - return result; - } - result = gpioSetPullUpDown(gpio, PI_PUD_UP); // should this be configurable? - if (result != 0) { - return result; - } - result = gpioSetAlertFunc(gpio, interruptCallback); - return result; -} - -int teardownInterrupt(int gpio) { - int result = gpioSetAlertFunc(gpio, NULL); - // Do we need to unset the pullup resistors? - return result; -} diff --git a/components/board/pi/impl/pi.h b/components/board/pi/impl/pi.h deleted file mode 100644 index 04bacb3daf4..00000000000 --- a/components/board/pi/impl/pi.h +++ /dev/null @@ -1,6 +0,0 @@ -//go:build !no_pigpio -#pragma once - -// interruptCallback calls through to the go linked interrupt callback. -int setupInterrupt(int gpio); -int teardownInterrupt(int gpio); diff --git a/components/board/pi/impl/servo.go b/components/board/pi/impl/servo.go deleted file mode 100644 index 8c5725d83df..00000000000 --- a/components/board/pi/impl/servo.go +++ /dev/null @@ -1,221 +0,0 @@ -//go:build linux && (arm64 || arm) && !no_pigpio && !no_cgo - -package piimpl - -// #include -// #include -// #cgo LDFLAGS: -lpigpio -// #include "pi.h" -import "C" - -import ( - "context" - "fmt" - "time" - - "github.com/pkg/errors" - "go.viam.com/utils" - - picommon "go.viam.com/rdk/components/board/pi/common" - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -var ( - holdTime = 250000000 // 250ms in nanoseconds - servoDefaultMaxRotation = 180 -) - -// init registers a pi servo based on pigpio. -func init() { - resource.RegisterComponent( - servo.API, - picommon.Model, - resource.Registration[servo.Servo, *picommon.ServoConfig]{ - Constructor: func( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (servo.Servo, error) { - newConf, err := resource.NativeConfig[*picommon.ServoConfig](conf) - if err != nil { - return nil, err - } - - if newConf.Pin == "" { - return nil, errors.New("need pin for pi servo") - } - - bcom, have := broadcomPinFromHardwareLabel(newConf.Pin) - if !have { - return nil, errors.Errorf("no hw mapping for %s", newConf.Pin) - } - - theServo := &piPigpioServo{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - pin: C.uint(bcom), - opMgr: operation.NewSingleOperationManager(), - } - if newConf.Min > 0 { - theServo.min = uint32(newConf.Min) - } - if newConf.Max > 0 { - theServo.max = uint32(newConf.Max) - } - theServo.maxRotation = uint32(newConf.MaxRotation) - if theServo.maxRotation == 0 { - theServo.maxRotation = uint32(servoDefaultMaxRotation) - } - if theServo.maxRotation < theServo.min { - return nil, errors.New("maxRotation is less than minimum") - } - if theServo.maxRotation < theServo.max { - return nil, errors.New("maxRotation is less than maximum") - } - - theServo.pinname = newConf.Pin - - if newConf.StartPos == nil { - setPos := C.gpioServo(theServo.pin, C.uint(1500)) // a 1500ms pulsewidth positions the servo at 90 degrees - errorCode := int(setPos) - if errorCode != 0 { - return nil, picommon.ConvertErrorCodeToMessage(errorCode, "gpioServo failed with") - } - } else { - setPos := C.gpioServo(theServo.pin, C.uint(angleToPulseWidth(int(*newConf.StartPos), int(theServo.maxRotation)))) - errorCode := int(setPos) - if errorCode != 0 { - return nil, picommon.ConvertErrorCodeToMessage(errorCode, "gpioServo failed with") - } - } - if newConf.HoldPos == nil || *newConf.HoldPos { - theServo.holdPos = true - } else { - theServo.res = C.gpioGetServoPulsewidth(theServo.pin) - theServo.holdPos = false - C.gpioServo(theServo.pin, C.uint(0)) // disables servo - } - - return theServo, nil - }, - }, - ) -} - -// piPigpioServo implements a servo.Servo using pigpio. -type piPigpioServo struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - logger logging.Logger - pin C.uint - pinname string - res C.int - min, max uint32 - opMgr *operation.SingleOperationManager - pulseWidth int // pulsewidth value, 500-2500us is 0-180 degrees, 0 is off - holdPos bool - maxRotation uint32 -} - -// Move moves the servo to the given angle (0-180 degrees) -// This will block until done or a new operation cancels this one -func (s *piPigpioServo) Move(ctx context.Context, angle uint32, extra map[string]interface{}) error { - ctx, done := s.opMgr.New(ctx) - defer done() - - if s.min > 0 && angle < s.min { - angle = s.min - } - if s.max > 0 && angle > s.max { - angle = s.max - } - pulseWidth := angleToPulseWidth(int(angle), int(s.maxRotation)) - res := C.gpioServo(s.pin, C.uint(pulseWidth)) - - s.pulseWidth = pulseWidth - - if res != 0 { - err := s.pigpioErrors(int(res)) - return err - } - - utils.SelectContextOrWait(ctx, time.Duration(pulseWidth)*time.Microsecond) // duration of pulswidth send on pin and servo moves - - if !s.holdPos { // the following logic disables a servo once it has reached a position or after a certain amount of time has been reached - time.Sleep(time.Duration(holdTime)) // time before a stop is sent - setPos := C.gpioServo(s.pin, C.uint(0)) - if setPos < 0 { - return errors.Errorf("servo on pin %s failed with code %d", s.pinname, setPos) - } - } - return nil -} - -// returns piGPIO specific errors to user -func (s *piPigpioServo) pigpioErrors(res int) error { - switch { - case res == C.PI_NOT_SERVO_GPIO: - return errors.Errorf("gpioservo pin %s is not set up to send and receive pulsewidths", s.pinname) - case res == C.PI_BAD_PULSEWIDTH: - return errors.Errorf("gpioservo on pin %s trying to reach out of range position", s.pinname) - case res == 0: - return nil - case res < 0 && res != C.PI_BAD_PULSEWIDTH && res != C.PI_NOT_SERVO_GPIO: - errMsg := fmt.Sprintf("gpioServo on pin %s failed", s.pinname) - return picommon.ConvertErrorCodeToMessage(res, errMsg) - default: - return nil - } -} - -// Position returns the current set angle (degrees) of the servo. -func (s *piPigpioServo) Position(ctx context.Context, extra map[string]interface{}) (uint32, error) { - res := C.gpioGetServoPulsewidth(s.pin) - err := s.pigpioErrors(int(res)) - if int(res) != 0 { - s.res = res - } - if err != nil { - return 0, err - } - return uint32(pulseWidthToAngle(int(s.res), int(s.maxRotation))), nil -} - -// angleToPulseWidth changes the input angle in degrees -// into the corresponding pulsewidth value in microsecond -func angleToPulseWidth(angle, maxRotation int) int { - pulseWidth := 500 + (2000 * angle / maxRotation) - return pulseWidth -} - -// pulseWidthToAngle changes the pulsewidth value in microsecond -// to the corresponding angle in degrees -func pulseWidthToAngle(pulseWidth, maxRotation int) int { - angle := maxRotation * (pulseWidth + 1 - 500) / 2000 - return angle -} - -// Stop stops the servo. It is assumed the servo stops immediately. -func (s *piPigpioServo) Stop(ctx context.Context, extra map[string]interface{}) error { - _, done := s.opMgr.New(ctx) - defer done() - getPos := C.gpioServo(s.pin, C.uint(0)) - errorCode := int(getPos) - if errorCode != 0 { - return picommon.ConvertErrorCodeToMessage(errorCode, "gpioServo failed with") - } - return nil -} - -func (s *piPigpioServo) IsMoving(ctx context.Context) (bool, error) { - err := s.pigpioErrors(int(s.res)) - if err != nil { - return false, err - } - if int(s.res) == 0 { - return false, nil - } - return s.opMgr.OpRunning(), nil -} diff --git a/components/board/pi/impl/utils.go b/components/board/pi/impl/utils.go deleted file mode 100644 index b8b231a17bf..00000000000 --- a/components/board/pi/impl/utils.go +++ /dev/null @@ -1,80 +0,0 @@ -//go:build linux && (arm64 || arm) && !no_pigpio - -package piimpl - -import "fmt" - -// piHWPinToBroadcom maps the hardware inscribed pin number to -// its Broadcom pin. For the sake of programming, a user typically -// knows the hardware pin since they have the board on hand but does -// not know the corresponding Broadcom pin. -var piHWPinToBroadcom = map[string]uint{ - // 1 -> 3v3 - // 2 -> 5v - "3": 2, - "sda": 2, - // 4 -> 5v - "5": 3, - "scl": 3, - // 6 -> GND - "7": 4, - "8": 14, - // 9 -> GND - "10": 15, - "11": 17, - "12": 18, - "clk": 18, - "13": 27, - // 14 -> GND - "15": 22, - "16": 23, - // 17 -> 3v3 - "18": 24, - "19": 10, - "mosi": 10, - // 20 -> GND - "21": 9, - "miso": 9, - "22": 25, - "23": 11, - "sclk": 11, - "24": 8, - "ce0": 8, - // 25 -> GND - "26": 7, - "ce1": 7, - "27": 0, - "28": 1, - "29": 5, - // 30 -> GND - "31": 6, - "32": 12, - "33": 13, - // 34 -> GND - "35": 19, - "36": 16, - "37": 26, - "38": 20, - // 39 -> GND - "40": 21, -} - -// TODO: we should agree on one config standard for pin definitions -// instead of doing this. Maybe just use the actual pin number? -// It might be reasonable to force users to look up the associations -// online - GV - -// broadcomPinFromHardwareLabel returns a Raspberry Pi pin number given -// a hardware label for the pin passed from a config. -func broadcomPinFromHardwareLabel(hwPin string) (uint, bool) { - pin, ok := piHWPinToBroadcom[hwPin] - if ok { - return pin, true - } - for _, existingVal := range piHWPinToBroadcom { - if hwPin == fmt.Sprintf("io%d", existingVal) { - return existingVal, true - } - } - return 1000, false -} diff --git a/components/board/pi/impl/verify_main_test.go b/components/board/pi/impl/verify_main_test.go deleted file mode 100644 index 4bfea81a146..00000000000 --- a/components/board/pi/impl/verify_main_test.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build linux && (arm64 || arm) - -package piimpl - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/board/pi/pi_notsupported.go b/components/board/pi/pi_notsupported.go deleted file mode 100644 index b9f991836fb..00000000000 --- a/components/board/pi/pi_notsupported.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build !(linux && (arm64 || arm) && !no_pigpio) - -package pi - -import ( - "context" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux" - picommon "go.viam.com/rdk/components/board/pi/common" - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// init registers a failing pi board since this can only be compiled on non-pi systems. -func init() { - resource.RegisterComponent( - board.API, - picommon.Model, - resource.Registration[board.Board, *genericlinux.Config]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (board.Board, error) { - return nil, errors.New("not running on a pi") - }}) - resource.RegisterComponent( - servo.API, - picommon.Model, - resource.Registration[servo.Servo, *picommon.ServoConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (servo.Servo, error) { - return nil, errors.New("not running on a pi") - }, - }, - ) -} diff --git a/components/board/pi/pi_supported.go b/components/board/pi/pi_supported.go deleted file mode 100644 index ac3bf5c1618..00000000000 --- a/components/board/pi/pi_supported.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build linux && (arm64 || arm) && !no_pigpio - -package pi - -import ( - // for easily importing implementation. - _ "go.viam.com/rdk/components/board/pi/impl" -) diff --git a/components/board/pi/verify_main_test.go b/components/board/pi/verify_main_test.go deleted file mode 100644 index a91ebdc21d4..00000000000 --- a/components/board/pi/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package pi - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/board/pi5/board.go b/components/board/pi5/board.go deleted file mode 100644 index e00a3f54e54..00000000000 --- a/components/board/pi5/board.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package pi5 implements a raspberry pi 5 board. -package pi5 - -import ( - "github.com/pkg/errors" - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" -) - -const modelName = "pi5" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappings(modelName, boardInfoMappings) - var noBoardErr genericlinux.NoBoardFoundError - if errors.As(err, &noBoardErr) { - logging.Global().Debugw("error getting pi5 GPIO board mapping", "error", err) - } - - genericlinux.RegisterBoard(modelName, gpioMappings) -} diff --git a/components/board/pi5/data.go b/components/board/pi5/data.go deleted file mode 100644 index 68a8ce2fe80..00000000000 --- a/components/board/pi5/data.go +++ /dev/null @@ -1,46 +0,0 @@ -package pi5 - -import "go.viam.com/rdk/components/board/genericlinux" - -// Thanks to "Dan Makes Things" at https://www.makerforge.tech/posts/viam-custom-board-pi5/ for -// collaborating on setting this up! -var boardInfoMappings = map[string]genericlinux.BoardInformation{ - "pi5": { - PinDefinitions: []genericlinux.PinDefinition{ - {Name: "3", DeviceName: "gpiochip4", LineNumber: 2, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "5", DeviceName: "gpiochip4", LineNumber: 3, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "7", DeviceName: "gpiochip4", LineNumber: 4, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "8", DeviceName: "gpiochip4", LineNumber: 14, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "10", DeviceName: "gpiochip4", LineNumber: 15, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "11", DeviceName: "gpiochip4", LineNumber: 17, PwmChipSysfsDir: "", PwmID: -1}, - // When we can do GPIO and hardware PWM on the same pin, this will have line number 18. - {Name: "12", DeviceName: "gpiochip4", LineNumber: -1, PwmChipSysfsDir: "1f00098000.pwm", PwmID: 2}, - {Name: "13", DeviceName: "gpiochip4", LineNumber: 27, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip4", LineNumber: 22, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "16", DeviceName: "gpiochip4", LineNumber: 23, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip4", LineNumber: 24, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip4", LineNumber: 10, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip4", LineNumber: 9, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip4", LineNumber: 25, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip4", LineNumber: 11, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip4", LineNumber: 8, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip4", LineNumber: 7, PwmChipSysfsDir: "", PwmID: -1}, - // Per https://www.raspberrypi.com/documentation/computers/images/GPIO-duplicate.png - // Physical pins 27 and 28 (shown in white in that diagram) should not be used for - // normal GPIO stuff. - {Name: "29", DeviceName: "gpiochip4", LineNumber: 5, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip4", LineNumber: 6, PwmChipSysfsDir: "", PwmID: -1}, - // We'd expect pins 32 and 33 to have hardware PWM support, too, but we haven't gotten - // that to work yet. - {Name: "32", DeviceName: "gpiochip4", LineNumber: 12, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "33", DeviceName: "gpiochip4", LineNumber: 13, PwmChipSysfsDir: "", PwmID: -1}, - // When we can do GPIO and hardware PWM on the same pin, this will have line number 19. - {Name: "35", DeviceName: "gpiochip4", LineNumber: -1, PwmChipSysfsDir: "1f00098000.pwm", PwmID: 3}, - {Name: "36", DeviceName: "gpiochip4", LineNumber: 16, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip4", LineNumber: 26, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip4", LineNumber: 20, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip4", LineNumber: 21, PwmChipSysfsDir: "", PwmID: -1}, - }, - Compats: []string{"raspberrypi,5-model-b", "brcm,bcm2712"}, - }, -} diff --git a/components/board/pinwrappers/analog_smoother_test.go b/components/board/pinwrappers/analog_smoother_test.go deleted file mode 100644 index 0f5d3b9ae53..00000000000 --- a/components/board/pinwrappers/analog_smoother_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package pinwrappers - -import ( - "context" - "math/rand" - "sync" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" -) - -type testAnalog struct { - mu sync.Mutex - r *rand.Rand - n int64 - lim int64 - stop bool -} - -func (t *testAnalog) Read(ctx context.Context, extra map[string]interface{}) (int, error) { - t.mu.Lock() - defer t.mu.Unlock() - if t.stop || t.n >= t.lim { - return 0, errStopReading - } - t.n++ - return t.r.Intn(100), nil -} - -func (t *testAnalog) Write(ctx context.Context, value int, extra map[string]interface{}) error { - return grpc.UnimplementedError -} - -func (t *testAnalog) Close(ctx context.Context) error { - return nil -} - -func TestAnalogSmoother1(t *testing.T) { - testReader := testAnalog{ - r: rand.New(rand.NewSource(11)), - lim: 200, - } - defer func() { - testReader.mu.Lock() - defer testReader.mu.Unlock() - testReader.stop = true - }() - - logger := logging.NewTestLogger(t) - as := SmoothAnalogReader(&testReader, board.AnalogReaderConfig{ - AverageOverMillis: 10, - SamplesPerSecond: 10000, - }, logger) - - testutils.WaitForAssertionWithSleep(t, 10*time.Millisecond, 200, func(tb testing.TB) { - tb.Helper() - v, err := as.Read(context.Background(), nil) - test.That(tb, err, test.ShouldEqual, errStopReading) - test.That(tb, v, test.ShouldEqual, 52) - - // need lock to access testReader.n - testReader.mu.Lock() - defer testReader.mu.Unlock() - test.That(tb, testReader.n, test.ShouldEqual, testReader.lim) - }) - - test.That(t, as.Close(context.Background()), test.ShouldBeNil) -} diff --git a/components/board/pinwrappers/analogs.go b/components/board/pinwrappers/analogs.go deleted file mode 100644 index 4baf5647777..00000000000 --- a/components/board/pinwrappers/analogs.go +++ /dev/null @@ -1,140 +0,0 @@ -package pinwrappers - -import ( - "context" - "sync/atomic" - "time" - - "github.com/pkg/errors" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -var errStopReading = errors.New("stop reading") - -// An AnalogSmoother smooths the readings out from an underlying reader. -type AnalogSmoother struct { - Raw board.Analog - AverageOverMillis int - SamplesPerSecond int - data *utils.RollingAverage - lastData int - lastError atomic.Pointer[errValue] - logger logging.Logger - workers utils.StoppableWorkers -} - -// SmoothAnalogReader wraps the given reader in a smoother. -func SmoothAnalogReader(r board.Analog, c board.AnalogReaderConfig, logger logging.Logger) *AnalogSmoother { - smoother := &AnalogSmoother{ - Raw: r, - AverageOverMillis: c.AverageOverMillis, - SamplesPerSecond: c.SamplesPerSecond, - logger: logger, - } - if smoother.SamplesPerSecond <= 0 { - logger.Debug("Can't read nonpositive samples per second; defaulting to 1 instead") - smoother.SamplesPerSecond = 1 - } - smoother.Start() - return smoother -} - -// An errValue is used to atomically store an error. -type errValue struct { - present bool - err error -} - -// Close stops the smoothing routine. -func (as *AnalogSmoother) Close(ctx context.Context) error { - as.workers.Stop() - return nil -} - -// Read returns the smoothed out reading. -func (as *AnalogSmoother) Read(ctx context.Context, extra map[string]interface{}) (int, error) { - if as.data == nil { // We're using raw data, and not averaging - return as.lastData, nil - } - - avg := as.data.Average() - lastErr := as.lastError.Load() - if lastErr == nil { - return avg, nil - } - //nolint:forcetypeassert - if lastErr.present { - return avg, lastErr.err - } - return avg, nil -} - -// Start begins the smoothing routine that reads from the underlying -// analog reader. -func (as *AnalogSmoother) Start() { - // examples 1 - // AverageOverMillis 10 - // SamplesPerSecond 1000 - // numSamples 10 - - // examples 2 - // AverageOverMillis 10 - // SamplesPerSecond 10000 - // numSamples 100 - - // examples 3 - // AverageOverMillis 2000 - // SamplesPerSecond 2 - // numSamples 4 - - numSamples := (as.SamplesPerSecond * as.AverageOverMillis) / 1000 - var nanosBetween int - if numSamples >= 1 { - as.data = utils.NewRollingAverage(numSamples) - nanosBetween = 1e9 / as.SamplesPerSecond - } else { - as.logger.Debug("Too few samples to smooth over; defaulting to raw data.") - as.data = nil - nanosBetween = as.AverageOverMillis * 1e6 - } - - as.workers = utils.NewStoppableWorkers(func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - default: - } - start := time.Now() - reading, err := as.Raw.Read(ctx, nil) - as.lastError.Store(&errValue{err != nil, err}) - if err == nil { - as.lastData = reading - if as.data != nil { - as.data.Add(reading) - } - } else { // Non-nil error - if errors.Is(err, errStopReading) { - break - } - as.logger.CInfow(ctx, "error reading analog", "error", err) - } - - end := time.Now() - - toSleep := int64(nanosBetween) - (end.UnixNano() - start.UnixNano()) - if !goutils.SelectContextOrWait(ctx, time.Duration(toSleep)) { - return - } - } - }) -} - -func (as *AnalogSmoother) Write(ctx context.Context, value int, extra map[string]interface{}) error { - return grpc.UnimplementedError -} diff --git a/components/board/pinwrappers/digital_interrupts.go b/components/board/pinwrappers/digital_interrupts.go deleted file mode 100644 index 28408952fcc..00000000000 --- a/components/board/pinwrappers/digital_interrupts.go +++ /dev/null @@ -1,106 +0,0 @@ -// Package pinwrappers implements interfaces that wrap the basic board interface and return types, and expands them with new -// methods and interfaces for the built in board models. Current expands analog reader and digital interrupt. -package pinwrappers - -import ( - "context" - "errors" - "sync" - "sync/atomic" - - "go.viam.com/rdk/components/board" -) - -// A ReconfigurableDigitalInterrupt is a simple reconfigurable digital interrupt that expects -// reconfiguration within the same type. -type ReconfigurableDigitalInterrupt interface { - board.DigitalInterrupt - Reconfigure(cfg board.DigitalInterruptConfig) error -} - -// CreateDigitalInterrupt is a factory method for creating a specific DigitalInterrupt based -// on the given config. If no type is specified, a BasicDigitalInterrupt is returned. -func CreateDigitalInterrupt(cfg board.DigitalInterruptConfig) (ReconfigurableDigitalInterrupt, error) { - i := &BasicDigitalInterrupt{} - - if err := i.Reconfigure(cfg); err != nil { - return nil, err - } - return i, nil -} - -// A BasicDigitalInterrupt records how many ticks/interrupts happen and can -// report when they happen to interested callbacks. -type BasicDigitalInterrupt struct { - count int64 - - callbacks []chan board.Tick - - mu sync.RWMutex - cfg board.DigitalInterruptConfig -} - -// Value returns the amount of ticks that have occurred. -func (i *BasicDigitalInterrupt) Value(ctx context.Context, extra map[string]interface{}) (int64, error) { - i.mu.RLock() - defer i.mu.RUnlock() - count := atomic.LoadInt64(&i.count) - return count, nil -} - -// Tick records an interrupt and notifies any interested callbacks. See comment on -// the DigitalInterrupt interface for caveats. -func Tick(ctx context.Context, i *BasicDigitalInterrupt, high bool, nanoseconds uint64) error { - if high { - atomic.AddInt64(&i.count, 1) - } - i.mu.RLock() - defer i.mu.RUnlock() - for _, c := range i.callbacks { - select { - case <-ctx.Done(): - return errors.New("context cancelled") - case c <- board.Tick{Name: i.cfg.Name, High: high, TimestampNanosec: nanoseconds}: - } - } - return nil -} - -// AddCallback adds a listener for interrupts. -func AddCallback(i *BasicDigitalInterrupt, c chan board.Tick) { - i.mu.Lock() - defer i.mu.Unlock() - i.callbacks = append(i.callbacks, c) -} - -// RemoveCallback removes a listener for interrupts. -func RemoveCallback(i *BasicDigitalInterrupt, c chan board.Tick) { - i.mu.Lock() - defer i.mu.Unlock() - - for id := range i.callbacks { - if i.callbacks[id] == c { - // To remove this item, we replace it with the last item in the list, then truncate the - // list by 1. - i.callbacks[id] = i.callbacks[len(i.callbacks)-1] - i.callbacks = i.callbacks[:len(i.callbacks)-1] - break - } - } -} - -// Name returns the name of the digital interrupt. -func (i *BasicDigitalInterrupt) Name() string { - i.mu.Lock() - defer i.mu.Unlock() - return i.cfg.Name -} - -// Reconfigure reconfigures this digital interrupt with a new formula. -func (i *BasicDigitalInterrupt) Reconfigure(conf board.DigitalInterruptConfig) error { - i.mu.Lock() - defer i.mu.Unlock() - - i.cfg = conf - return nil -} diff --git a/components/board/pinwrappers/digital_interrupts_test.go b/components/board/pinwrappers/digital_interrupts_test.go deleted file mode 100644 index 8d9162c8f9b..00000000000 --- a/components/board/pinwrappers/digital_interrupts_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package pinwrappers - -import ( - "context" - "sync" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" -) - -func nowNanosecondsTest() uint64 { - return uint64(time.Now().UnixNano()) -} - -func TestBasicDigitalInterrupt1(t *testing.T) { - config := board.DigitalInterruptConfig{ - Name: "i1", - } - - i, err := CreateDigitalInterrupt(config) - test.That(t, err, test.ShouldBeNil) - - di := i.(*BasicDigitalInterrupt) - - intVal, err := i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(0)) - test.That(t, Tick(context.Background(), i.(*BasicDigitalInterrupt), true, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1)) - test.That(t, Tick(context.Background(), i.(*BasicDigitalInterrupt), false, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1)) - - c := make(chan board.Tick) - AddCallback(di, c) - - timeNanoSec := nowNanosecondsTest() - go func() { Tick(context.Background(), di, true, timeNanoSec) }() - time.Sleep(1 * time.Microsecond) - v := <-c - test.That(t, v.High, test.ShouldBeTrue) - test.That(t, v.TimestampNanosec, test.ShouldEqual, timeNanoSec) - - timeNanoSec = nowNanosecondsTest() - go func() { Tick(context.Background(), di, true, timeNanoSec) }() - v = <-c - test.That(t, v.High, test.ShouldBeTrue) - test.That(t, v.TimestampNanosec, test.ShouldEqual, timeNanoSec) - - RemoveCallback(di, c) - - c = make(chan board.Tick, 2) - AddCallback(di, c) - go func() { - Tick(context.Background(), di, true, uint64(1)) - Tick(context.Background(), di, true, uint64(4)) - }() - v = <-c - v1 := <-c - test.That(t, v.High, test.ShouldBeTrue) - test.That(t, v1.High, test.ShouldBeTrue) - test.That(t, v1.TimestampNanosec-v.TimestampNanosec, test.ShouldEqual, uint32(3)) -} - -func TestRemoveCallbackDigitalInterrupt(t *testing.T) { - config := board.DigitalInterruptConfig{ - Name: "d1", - } - i, err := CreateDigitalInterrupt(config) - test.That(t, err, test.ShouldBeNil) - di := i.(*BasicDigitalInterrupt) - intVal, err := i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(0)) - test.That(t, Tick(context.Background(), di, true, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(1)) - - c1 := make(chan board.Tick) - test.That(t, c1, test.ShouldNotBeNil) - AddCallback(di, c1) - var wg sync.WaitGroup - wg.Add(1) - ret := false - - go func() { - defer wg.Done() - select { - case <-context.Background().Done(): - return - default: - } - select { - case <-context.Background().Done(): - return - case tick := <-c1: - ret = tick.High - } - }() - test.That(t, Tick(context.Background(), di, true, nowNanosecondsTest()), test.ShouldBeNil) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(2)) - wg.Wait() - c2 := make(chan board.Tick) - test.That(t, c2, test.ShouldNotBeNil) - AddCallback(di, c2) - test.That(t, ret, test.ShouldBeTrue) - - RemoveCallback(di, c1) - - ret2 := false - result := make(chan bool, 1) - go func() { - defer wg.Done() - select { - case <-context.Background().Done(): - return - default: - } - select { - case <-context.Background().Done(): - return - case tick := <-c2: - ret2 = tick.High - } - }() - wg.Add(1) - go func() { - err := Tick(context.Background(), di, true, nowNanosecondsTest()) - if err != nil { - result <- true - } - result <- true - }() - select { - case <-time.After(1 * time.Second): - ret = false - case ret = <-result: - } - wg.Wait() - test.That(t, ret, test.ShouldBeTrue) - test.That(t, ret2, test.ShouldBeTrue) - intVal, err = i.Value(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, intVal, test.ShouldEqual, int64(3)) -} diff --git a/components/board/register/register.go b/components/board/register/register.go deleted file mode 100644 index 7e9cfc55191..00000000000 --- a/components/board/register/register.go +++ /dev/null @@ -1,17 +0,0 @@ -// Package register registers all relevant Boards and also API specific functions -package register - -import ( - // for boards. - _ "go.viam.com/rdk/components/board/beaglebone" - _ "go.viam.com/rdk/components/board/customlinux" - _ "go.viam.com/rdk/components/board/fake" - _ "go.viam.com/rdk/components/board/hat/pca9685" - _ "go.viam.com/rdk/components/board/jetson" - _ "go.viam.com/rdk/components/board/numato" - _ "go.viam.com/rdk/components/board/odroid" - _ "go.viam.com/rdk/components/board/orangepi" - _ "go.viam.com/rdk/components/board/pi5" - _ "go.viam.com/rdk/components/board/ti" - _ "go.viam.com/rdk/components/board/upboard" -) diff --git a/components/board/register/register_cgo.go b/components/board/register/register_cgo.go deleted file mode 100644 index fa331d3aa11..00000000000 --- a/components/board/register/register_cgo.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !no_cgo - -package register - -import ( - // blank import registration pattern. - _ "go.viam.com/rdk/components/board/pi" -) diff --git a/components/board/server.go b/components/board/server.go deleted file mode 100644 index 08fcb276d0b..00000000000 --- a/components/board/server.go +++ /dev/null @@ -1,282 +0,0 @@ -// Package board contains a gRPC based Board service server. -package board - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/board/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// serviceServer implements the BoardService from board.proto. -type serviceServer struct { - pb.UnimplementedBoardServiceServer - coll resource.APIResourceCollection[Board] -} - -// NewRPCServiceServer constructs an board gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Board]) interface{} { - return &serviceServer{coll: coll} -} - -// SetGPIO sets a given pin of a board of the underlying robot to either low or high. -func (s *serviceServer) SetGPIO(ctx context.Context, req *pb.SetGPIORequest) (*pb.SetGPIOResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - p, err := b.GPIOPinByName(req.Pin) - if err != nil { - return nil, err - } - - return &pb.SetGPIOResponse{}, p.Set(ctx, req.High, req.Extra.AsMap()) -} - -// GetGPIO gets the high/low state of a given pin of a board of the underlying robot. -func (s *serviceServer) GetGPIO(ctx context.Context, req *pb.GetGPIORequest) (*pb.GetGPIOResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - p, err := b.GPIOPinByName(req.Pin) - if err != nil { - return nil, err - } - - high, err := p.Get(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetGPIOResponse{High: high}, nil -} - -// PWM gets the duty cycle of the given pin of a board of the underlying robot. -func (s *serviceServer) PWM(ctx context.Context, req *pb.PWMRequest) (*pb.PWMResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - p, err := b.GPIOPinByName(req.Pin) - if err != nil { - return nil, err - } - - pwm, err := p.PWM(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.PWMResponse{DutyCyclePct: pwm}, nil -} - -// SetPWM sets a given pin of the underlying robot to the given duty cycle. -func (s *serviceServer) SetPWM(ctx context.Context, req *pb.SetPWMRequest) (*pb.SetPWMResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - p, err := b.GPIOPinByName(req.Pin) - if err != nil { - return nil, err - } - - return &pb.SetPWMResponse{}, p.SetPWM(ctx, req.DutyCyclePct, req.Extra.AsMap()) -} - -// PWMFrequency gets the PWM frequency of the given pin of a board of the underlying robot. -func (s *serviceServer) PWMFrequency(ctx context.Context, req *pb.PWMFrequencyRequest) (*pb.PWMFrequencyResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - p, err := b.GPIOPinByName(req.Pin) - if err != nil { - return nil, err - } - - freq, err := p.PWMFreq(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.PWMFrequencyResponse{FrequencyHz: uint64(freq)}, nil -} - -// SetPWMFrequency sets a given pin of a board of the underlying robot to the given PWM frequency. -// For Raspberry Pis, 0 will use a default PWM frequency of 800. -func (s *serviceServer) SetPWMFrequency( - ctx context.Context, - req *pb.SetPWMFrequencyRequest, -) (*pb.SetPWMFrequencyResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - p, err := b.GPIOPinByName(req.Pin) - if err != nil { - return nil, err - } - - return &pb.SetPWMFrequencyResponse{}, p.SetPWMFreq(ctx, uint(req.FrequencyHz), req.Extra.AsMap()) -} - -// ReadAnalogReader reads off the current value of an analog reader of a board of the underlying robot. -func (s *serviceServer) ReadAnalogReader( - ctx context.Context, - req *pb.ReadAnalogReaderRequest, -) (*pb.ReadAnalogReaderResponse, error) { - b, err := s.coll.Resource(req.BoardName) - if err != nil { - return nil, err - } - - theReader, err := b.AnalogByName(req.AnalogReaderName) - if err != nil { - return nil, err - } - - val, err := theReader.Read(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.ReadAnalogReaderResponse{Value: int32(val)}, nil -} - -// WriteAnalog writes the analog value to the analog writer pin of the underlying robot. -func (s *serviceServer) WriteAnalog( - ctx context.Context, - req *pb.WriteAnalogRequest, -) (*pb.WriteAnalogResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - analog, err := b.AnalogByName(req.Pin) - if err != nil { - return nil, err - } - - err = analog.Write(ctx, int(req.Value), req.Extra.AsMap()) - if err != nil { - return nil, err - } - - return &pb.WriteAnalogResponse{}, nil -} - -// GetDigitalInterruptValue returns the current value of the interrupt which is based on the type of interrupt. -func (s *serviceServer) GetDigitalInterruptValue( - ctx context.Context, - req *pb.GetDigitalInterruptValueRequest, -) (*pb.GetDigitalInterruptValueResponse, error) { - b, err := s.coll.Resource(req.BoardName) - if err != nil { - return nil, err - } - - interrupt, err := b.DigitalInterruptByName(req.DigitalInterruptName) - if err != nil { - return nil, err - } - - val, err := interrupt.Value(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetDigitalInterruptValueResponse{Value: val}, nil -} - -func (s *serviceServer) StreamTicks( - req *pb.StreamTicksRequest, - server pb.BoardService_StreamTicksServer, -) error { - b, err := s.coll.Resource(req.Name) - if err != nil { - return err - } - - ticksChan := make(chan Tick) - interrupts := []DigitalInterrupt{} - - for _, name := range req.PinNames { - di, err := b.DigitalInterruptByName(name) - if err != nil { - return err - } - interrupts = append(interrupts, di) - } - err = b.StreamTicks(server.Context(), interrupts, ticksChan, req.Extra.AsMap()) - if err != nil { - return err - } - - // Send an empty response first so the client doesn't block while checking for errors. - err = server.Send(&pb.StreamTicksResponse{}) - if err != nil { - return err - } - - for { - select { - case <-server.Context().Done(): - return server.Context().Err() - default: - } - - select { - case <-server.Context().Done(): - return server.Context().Err() - case msg := <-ticksChan: - err := server.Send(&pb.StreamTicksResponse{PinName: msg.Name, High: msg.High, Time: msg.TimestampNanosec}) - if err != nil { - return err - } - } - } -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - b, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, b, req) -} - -func (s *serviceServer) SetPowerMode(ctx context.Context, - req *pb.SetPowerModeRequest, -) (*pb.SetPowerModeResponse, error) { - b, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - if req.Duration == nil { - err = b.SetPowerMode(ctx, req.PowerMode, nil) - } else { - if err := req.Duration.CheckValid(); err != nil { - return nil, err - } - duration := req.Duration.AsDuration() - err = b.SetPowerMode(ctx, req.PowerMode, &duration) - } - - if err != nil { - return nil, err - } - - return &pb.SetPowerModeResponse{}, nil -} diff --git a/components/board/server_test.go b/components/board/server_test.go deleted file mode 100644 index 678b9c9dda2..00000000000 --- a/components/board/server_test.go +++ /dev/null @@ -1,879 +0,0 @@ -package board_test - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/board/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/grpc" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errFoo = errors.New("whoops") - errNotFound = errors.New("not found") - errSendFailed = errors.New("send fail") - errAnalog = errors.New("unknown analog error") - errDigital = errors.New("unknown digital interrupt error") -) - -func newServer() (pb.BoardServiceServer, *inject.Board, error) { - injectBoard := &inject.Board{} - boards := map[resource.Name]board.Board{ - board.Named(testBoardName): injectBoard, - } - boardSvc, err := resource.NewAPIResourceCollection(board.API, boards) - if err != nil { - return nil, nil, err - } - return board.NewRPCServiceServer(boardSvc).(pb.BoardServiceServer), injectBoard, nil -} - -func TestServerSetGPIO(t *testing.T) { - type request = pb.SetGPIORequest - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectErr error - req *request - expCapArgs []interface{} - expRespErr string - }{ - { - injectErr: nil, - req: &request{Name: missingBoardName}, - expCapArgs: []interface{}(nil), - expRespErr: errNotFound.Error(), - }, - { - injectErr: errFoo, - req: &request{Name: testBoardName, Pin: "one", High: true}, - expCapArgs: []interface{}{ctx, true}, - expRespErr: errFoo.Error(), - }, - { - injectErr: nil, - req: &request{Name: testBoardName, Pin: "one", High: true, Extra: pbExpectedExtra}, - expCapArgs: []interface{}{ctx, true}, - expRespErr: "", - }, - } - - //nolint:dupl - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectGPIOPin := &inject.GPIOPin{} - injectBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return injectGPIOPin, nil - } - - injectGPIOPin.SetFunc = func(ctx context.Context, high bool, extra map[string]interface{}) error { - actualExtra = extra - return tc.injectErr - } - - _, err = server.SetGPIO(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - test.That(t, injectGPIOPin.SetCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -func TestServerGetGPIO(t *testing.T) { - type request = pb.GetGPIORequest - type response = pb.GetGPIOResponse - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectResult bool - injectErr error - req *request - expCapArgs []interface{} - expResp *response - expRespErr string - }{ - { - injectResult: false, - injectErr: nil, - req: &request{Name: missingBoardName}, - expCapArgs: []interface{}(nil), - expResp: nil, - expRespErr: errNotFound.Error(), - }, - { - injectResult: false, - injectErr: errFoo, - req: &request{Name: testBoardName, Pin: "one"}, - expCapArgs: []interface{}{ctx}, - expResp: nil, - expRespErr: errFoo.Error(), - }, - { - injectResult: true, - injectErr: nil, - req: &request{Name: testBoardName, Pin: "one", Extra: pbExpectedExtra}, - expCapArgs: []interface{}{ctx}, - expResp: &response{High: true}, - expRespErr: "", - }, - } - - //nolint:dupl - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectGPIOPin := &inject.GPIOPin{} - injectBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return injectGPIOPin, nil - } - - injectGPIOPin.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - actualExtra = extra - return tc.injectResult, tc.injectErr - } - - resp, err := server.GetGPIO(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, tc.expResp) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - test.That(t, injectGPIOPin.GetCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -//nolint:dupl -func TestServerPWM(t *testing.T) { - type request = pb.PWMRequest - type response = pb.PWMResponse - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectResult float64 - injectErr error - req *request - expCapArgs []interface{} - expResp *response - expRespErr string - }{ - { - injectResult: 0, - injectErr: nil, - req: &request{Name: missingBoardName}, - expCapArgs: []interface{}(nil), - expResp: nil, - expRespErr: errNotFound.Error(), - }, - { - injectResult: 0, - injectErr: errFoo, - req: &request{Name: testBoardName, Pin: "one"}, - expCapArgs: []interface{}{ctx}, - expResp: nil, - expRespErr: errFoo.Error(), - }, - { - injectResult: 0.1, - injectErr: nil, - req: &request{Name: testBoardName, Pin: "one", Extra: pbExpectedExtra}, - expCapArgs: []interface{}{ctx}, - expResp: &response{DutyCyclePct: 0.1}, - expRespErr: "", - }, - } - - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectGPIOPin := &inject.GPIOPin{} - injectBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return injectGPIOPin, nil - } - - injectGPIOPin.PWMFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - actualExtra = extra - return tc.injectResult, tc.injectErr - } - - resp, err := server.PWM(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, tc.expResp) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - test.That(t, injectGPIOPin.PWMCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -func TestServerSetPWM(t *testing.T) { - type request = pb.SetPWMRequest - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectErr error - req *request - expCapArgs []interface{} - expRespErr string - }{ - { - injectErr: nil, - req: &request{Name: missingBoardName}, - expCapArgs: []interface{}(nil), - expRespErr: errNotFound.Error(), - }, - { - injectErr: errFoo, - req: &request{Name: testBoardName, Pin: "one", DutyCyclePct: 0.03}, - expCapArgs: []interface{}{ctx, 0.03}, - expRespErr: errFoo.Error(), - }, - { - injectErr: nil, - req: &request{Name: testBoardName, Pin: "one", DutyCyclePct: 0.03, Extra: pbExpectedExtra}, - expCapArgs: []interface{}{ctx, 0.03}, - expRespErr: "", - }, - } - - //nolint:dupl - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectGPIOPin := &inject.GPIOPin{} - injectBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return injectGPIOPin, nil - } - - injectGPIOPin.SetPWMFunc = func(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - actualExtra = extra - return tc.injectErr - } - - _, err = server.SetPWM(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - test.That(t, injectGPIOPin.SetPWMCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -//nolint:dupl -func TestServerPWMFrequency(t *testing.T) { - type request = pb.PWMFrequencyRequest - type response = pb.PWMFrequencyResponse - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectResult uint - injectErr error - req *request - expCapArgs []interface{} - expResp *response - expRespErr string - }{ - { - injectResult: 0, - injectErr: nil, - req: &request{Name: missingBoardName}, - expCapArgs: []interface{}(nil), - expResp: nil, - expRespErr: errNotFound.Error(), - }, - { - injectResult: 0, - injectErr: errFoo, - req: &request{Name: testBoardName, Pin: "one"}, - expCapArgs: []interface{}{ctx}, - expResp: nil, - expRespErr: errFoo.Error(), - }, - { - injectResult: 1, - injectErr: nil, - req: &request{Name: testBoardName, Pin: "one", Extra: pbExpectedExtra}, - expCapArgs: []interface{}{ctx}, - expResp: &response{FrequencyHz: 1}, - expRespErr: "", - }, - } - - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectGPIOPin := &inject.GPIOPin{} - injectBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return injectGPIOPin, nil - } - - injectGPIOPin.PWMFreqFunc = func(ctx context.Context, extra map[string]interface{}) (uint, error) { - actualExtra = extra - return tc.injectResult, tc.injectErr - } - - resp, err := server.PWMFrequency(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, tc.expResp) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - test.That(t, injectGPIOPin.PWMFreqCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -func TestServerSetPWMFrequency(t *testing.T) { - type request = pb.SetPWMFrequencyRequest - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectErr error - req *request - expCapArgs []interface{} - expRespErr string - }{ - { - injectErr: nil, - req: &request{Name: missingBoardName}, - expCapArgs: []interface{}(nil), - expRespErr: errNotFound.Error(), - }, - { - injectErr: errFoo, - req: &request{Name: testBoardName, Pin: "one", FrequencyHz: 123123}, - expCapArgs: []interface{}{ctx, uint(123123)}, - expRespErr: errFoo.Error(), - }, - { - injectErr: nil, - req: &request{Name: testBoardName, Pin: "one", FrequencyHz: 123123, Extra: pbExpectedExtra}, - expCapArgs: []interface{}{ctx, uint(123123)}, - expRespErr: "", - }, - } - - //nolint:dupl - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectGPIOPin := &inject.GPIOPin{} - injectBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return injectGPIOPin, nil - } - - injectGPIOPin.SetPWMFreqFunc = func(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - actualExtra = extra - return tc.injectErr - } - - _, err = server.SetPWMFrequency(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - test.That(t, injectGPIOPin.SetPWMFreqCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -//nolint:dupl -func TestServerReadAnalogReader(t *testing.T) { - type request = pb.ReadAnalogReaderRequest - type response = pb.ReadAnalogReaderResponse - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectAnalog *inject.Analog - injectAnalogErr error - injectResult int - injectErr error - req *request - expCapAnalogArgs []interface{} - expCapArgs []interface{} - expResp *response - expRespErr string - }{ - { - injectAnalog: nil, - injectAnalogErr: errAnalog, - injectResult: 0, - injectErr: nil, - req: &request{BoardName: missingBoardName}, - expCapAnalogArgs: []interface{}(nil), - expCapArgs: []interface{}(nil), - expResp: nil, - expRespErr: errNotFound.Error(), - }, - { - injectAnalog: nil, - injectAnalogErr: errAnalog, - injectResult: 0, - injectErr: nil, - req: &request{BoardName: testBoardName, AnalogReaderName: "analog1"}, - expCapAnalogArgs: []interface{}{"analog1"}, - expCapArgs: []interface{}(nil), - expResp: nil, - expRespErr: "unknown analog error", - }, - { - injectAnalog: &inject.Analog{}, - injectAnalogErr: nil, - injectResult: 0, - injectErr: errFoo, - req: &request{BoardName: testBoardName, AnalogReaderName: "analog1"}, - expCapAnalogArgs: []interface{}{"analog1"}, - expCapArgs: []interface{}{ctx}, - expResp: nil, - expRespErr: errFoo.Error(), - }, - { - injectAnalog: &inject.Analog{}, - injectAnalogErr: nil, - injectResult: 8, - injectErr: nil, - req: &request{BoardName: testBoardName, AnalogReaderName: "analog1", Extra: pbExpectedExtra}, - expCapAnalogArgs: []interface{}{"analog1"}, - expCapArgs: []interface{}{ctx}, - expResp: &response{Value: 8}, - expRespErr: "", - }, - } - - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectBoard.AnalogByNameFunc = func(name string) (board.Analog, error) { - return tc.injectAnalog, tc.injectAnalogErr - } - - if tc.injectAnalog != nil { - tc.injectAnalog.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { - actualExtra = extra - return tc.injectResult, tc.injectErr - } - } - - resp, err := server.ReadAnalogReader(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, tc.expResp) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - test.That(t, injectBoard.AnalogByNameCap(), test.ShouldResemble, tc.expCapAnalogArgs) - test.That(t, tc.injectAnalog.ReadCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -func TestServerWriteAnalog(t *testing.T) { - type request = pb.WriteAnalogRequest - type response = pb.WriteAnalogResponse - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - name string - injectErr error - req *request - expCaptureArgs []interface{} - expResp *response - expRespErr string - }{ - { - name: "Successful analog write", - injectErr: nil, - req: &request{Name: testBoardName, Pin: "analogwriter1", Value: 1, Extra: pbExpectedExtra}, - expResp: &response{}, - expRespErr: "", - }, - { - name: "Analog write called on a board that does not exist should return not found error", - injectErr: nil, - req: &request{Name: missingBoardName}, - expResp: nil, - expRespErr: errNotFound.Error(), - }, - { - name: "An error on the analog writer write should be returned", - injectErr: errFoo, - req: &request{Name: testBoardName, Pin: "analogwriter1", Value: 3}, - - expResp: nil, - expRespErr: errFoo.Error(), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectAnalog := inject.Analog{} - injectAnalog.WriteFunc = func(ctx context.Context, value int, extra map[string]interface{}) error { - actualExtra = extra - return tc.injectErr - } - injectBoard.AnalogByNameFunc = func(pin string) (board.Analog, error) { - return &injectAnalog, nil - } - - resp, err := server.WriteAnalog(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, tc.expResp) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - }) - } -} - -//nolint:dupl -func TestServerGetDigitalInterruptValue(t *testing.T) { - type request = pb.GetDigitalInterruptValueRequest - type response = pb.GetDigitalInterruptValueResponse - ctx := context.Background() - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - injectDigitalInterrupt *inject.DigitalInterrupt - injectDigitalInterruptErr error - injectResult int64 - injectErr error - req *request - expCapDigitalInterruptArgs []interface{} - expCapArgs []interface{} - expResp *response - expRespErr string - }{ - { - injectDigitalInterrupt: nil, - injectDigitalInterruptErr: errDigital, - injectResult: 0, - injectErr: nil, - req: &request{BoardName: missingBoardName}, - expCapDigitalInterruptArgs: []interface{}(nil), - expCapArgs: []interface{}(nil), - expResp: nil, - expRespErr: errNotFound.Error(), - }, - { - injectDigitalInterrupt: nil, - injectDigitalInterruptErr: errDigital, - injectResult: 0, - injectErr: nil, - req: &request{BoardName: testBoardName, DigitalInterruptName: "digital1"}, - expCapDigitalInterruptArgs: []interface{}{"digital1"}, - expCapArgs: []interface{}(nil), - expResp: nil, - expRespErr: "unknown digital interrupt error", - }, - { - injectDigitalInterrupt: &inject.DigitalInterrupt{}, - injectDigitalInterruptErr: nil, - injectResult: 0, - injectErr: errFoo, - req: &request{BoardName: testBoardName, DigitalInterruptName: "digital1"}, - expCapDigitalInterruptArgs: []interface{}{"digital1"}, - expCapArgs: []interface{}{ctx}, - expResp: nil, - expRespErr: errFoo.Error(), - }, - { - injectDigitalInterrupt: &inject.DigitalInterrupt{}, - injectDigitalInterruptErr: nil, - injectResult: 42, - injectErr: nil, - req: &request{BoardName: testBoardName, DigitalInterruptName: "digital1", Extra: pbExpectedExtra}, - expCapDigitalInterruptArgs: []interface{}{"digital1"}, - expCapArgs: []interface{}{ctx}, - expResp: &response{Value: 42}, - expRespErr: "", - }, - } - - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - - injectBoard.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - return tc.injectDigitalInterrupt, tc.injectDigitalInterruptErr - } - - if tc.injectDigitalInterrupt != nil { - tc.injectDigitalInterrupt.ValueFunc = func(ctx context.Context, extra map[string]interface{}) (int64, error) { - actualExtra = extra - return tc.injectResult, tc.injectErr - } - } - - resp, err := server.GetDigitalInterruptValue(ctx, tc.req) - if tc.expRespErr == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, tc.expResp) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - - test.That(t, injectBoard.DigitalInterruptByNameCap(), test.ShouldResemble, tc.expCapDigitalInterruptArgs) - test.That(t, tc.injectDigitalInterrupt.ValueCap(), test.ShouldResemble, tc.expCapArgs) - }) - } -} - -type streamTicksServer struct { - grpc.ServerStream - ctx context.Context - ticksChan chan *pb.StreamTicksResponse - fail bool -} - -func (x *streamTicksServer) Context() context.Context { - return x.ctx -} - -func (x *streamTicksServer) Send(m *pb.StreamTicksResponse) error { - if x.fail { - return errSendFailed - } - if x.ticksChan == nil { - return nil - } - x.ticksChan <- m - return nil -} - -func TestStreamTicks(t *testing.T) { - type request = pb.StreamTicksRequest - type response = pb.StreamTicksResponse - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - pbExpectedExtra, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - tests := []struct { - name string - injectDigitalInterrupts []*inject.DigitalInterrupt - injectDigitalInterruptErr error - streamTicksErr error - req *request - expResp *response - expRespErr string - sendFail bool - }{ - { - name: "successful stream with multiple interrupts", - injectDigitalInterrupts: []*inject.DigitalInterrupt{{}, {}}, - injectDigitalInterruptErr: nil, - streamTicksErr: nil, - req: &request{Name: testBoardName, PinNames: []string{"digital1", "digital2"}, Extra: pbExpectedExtra}, - expResp: &response{PinName: "digital1", Time: uint64(time.Nanosecond), High: true}, - sendFail: false, - }, - { - name: "successful stream with one interrupt", - injectDigitalInterrupts: []*inject.DigitalInterrupt{{}}, - injectDigitalInterruptErr: nil, - streamTicksErr: nil, - req: &request{Name: testBoardName, PinNames: []string{"digital1"}, Extra: pbExpectedExtra}, - expResp: &response{PinName: "digital1", Time: uint64(time.Nanosecond), High: true}, - sendFail: false, - }, - { - name: "missing board name should return error", - streamTicksErr: nil, - req: &request{Name: missingBoardName, PinNames: []string{"pin1"}}, - expResp: nil, - expRespErr: errNotFound.Error(), - }, - { - name: "unknown digital interrupt should return error", - injectDigitalInterrupts: nil, - injectDigitalInterruptErr: errDigital, - streamTicksErr: errors.New("unknown digital interrupt: digital3"), - req: &request{Name: testBoardName, PinNames: []string{"digital3"}}, - expResp: nil, - expRespErr: "unknown digital interrupt: digital3", - sendFail: false, - }, - { - name: "failing to send tick should return error", - injectDigitalInterrupts: []*inject.DigitalInterrupt{{}}, - injectDigitalInterruptErr: errSendFailed, - streamTicksErr: nil, - req: &request{Name: testBoardName, PinNames: []string{"digital1"}, Extra: pbExpectedExtra}, - expResp: &response{PinName: "digital1", Time: uint64(time.Nanosecond), High: true}, - expRespErr: "send fail", - sendFail: true, - }, - } - - for _, tc := range tests { - t.Run("", func(t *testing.T) { - server, injectBoard, err := newServer() - test.That(t, err, test.ShouldBeNil) - var actualExtra map[string]interface{} - callbacks := []chan board.Tick{} - - injectBoard.StreamTicksFunc = func( - ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, - ) error { - actualExtra = extra - callbacks = append(callbacks, ch) - return tc.streamTicksErr - } - - injectBoard.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - if name == "digital1" { - return tc.injectDigitalInterrupts[0], tc.injectDigitalInterruptErr - } else if name == "digital2" { - return tc.injectDigitalInterrupts[1], tc.injectDigitalInterruptErr - } - return nil, nil - } - - cancelCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - ch := make(chan *pb.StreamTicksResponse) - s := &streamTicksServer{ - ctx: cancelCtx, - ticksChan: ch, - fail: tc.sendFail, - } - - sendTick := func() { - for _, ch := range callbacks { - ch <- board.Tick{Name: "digital1", High: true, TimestampNanosec: uint64(time.Nanosecond)} - } - } - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - err = server.StreamTicks(tc.req, s) - }() - - if tc.expRespErr == "" { - // First resp will be blank - <-s.ticksChan - - sendTick() - resp := <-s.ticksChan - - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, expectedExtra) - test.That(t, resp.High, test.ShouldEqual, true) - test.That(t, resp.PinName, test.ShouldEqual, "digital1") - test.That(t, resp.Time, test.ShouldEqual, uint64(time.Nanosecond)) - - cancel() - wg.Wait() - } else { - // Canceling the stream before checking the error to avoid a data race. - cancel() - wg.Wait() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.expRespErr) - } - }) - } -} diff --git a/components/board/status.go b/components/board/status.go deleted file mode 100644 index 003d289f096..00000000000 --- a/components/board/status.go +++ /dev/null @@ -1,47 +0,0 @@ -package board - -import ( - "context" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/board/v1" -) - -// CreateStatus constructs a new up to date status from the given board. -// The operation can take time and be expensive, so it can be cancelled by the -// given context. -func CreateStatus(ctx context.Context, b Board) (*pb.Status, error) { - var status pb.Status - - if names := b.AnalogNames(); len(names) != 0 { - status.Analogs = make(map[string]int32, len(names)) - for _, name := range names { - x, err := b.AnalogByName(name) - if err != nil { - return nil, err - } - val, err := x.Read(ctx, nil) - if err != nil { - return nil, errors.Wrapf(err, "couldn't read analog (%s)", name) - } - status.Analogs[name] = int32(val) - } - } - - if names := b.DigitalInterruptNames(); len(names) != 0 { - status.DigitalInterrupts = make(map[string]int64, len(names)) - for _, name := range names { - x, err := b.DigitalInterruptByName(name) - if err != nil { - return nil, err - } - intVal, err := x.Value(ctx, nil) - if err != nil { - return nil, err - } - status.DigitalInterrupts[name] = intVal - } - } - - return &status, nil -} diff --git a/components/board/ti/board.go b/components/board/ti/board.go deleted file mode 100644 index 0061d8f3c62..00000000000 --- a/components/board/ti/board.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package ti implements a ti based board. -package ti - -import ( - "github.com/pkg/errors" - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" -) - -const modelName = "ti" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappings(modelName, boardInfoMappings) - var noBoardErr genericlinux.NoBoardFoundError - if errors.As(err, &noBoardErr) { - logging.Global().Debugw("error getting ti GPIO board mapping", "error", err) - } - - genericlinux.RegisterBoard(modelName, gpioMappings) -} diff --git a/components/board/ti/data.go b/components/board/ti/data.go deleted file mode 100644 index 70bb7e3ee44..00000000000 --- a/components/board/ti/data.go +++ /dev/null @@ -1,42 +0,0 @@ -package ti - -import "go.viam.com/rdk/components/board/genericlinux" - -const tiTDA4VM = "ti_tda4vm" - -var boardInfoMappings = map[string]genericlinux.BoardInformation{ - tiTDA4VM: { - []genericlinux.PinDefinition{ - // Pins 3 and 5 don't work as GPIO by default; you might need to disable the I2C bus to - // use them. - {Name: "3", DeviceName: "gpiochip1", LineNumber: 84, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "5", DeviceName: "gpiochip1", LineNumber: 83, PwmChipSysfsDir: "", PwmID: -1}, - // Pin 7 appears to be input-only, due to some sort of hardware limitation. - {Name: "7", DeviceName: "gpiochip1", LineNumber: 7, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "8", DeviceName: "gpiochip1", LineNumber: 70, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "10", DeviceName: "gpiochip1", LineNumber: 81, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "11", DeviceName: "gpiochip1", LineNumber: 71, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip1", LineNumber: 1, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip1", LineNumber: 82, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip1", LineNumber: 11, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "16", DeviceName: "gpiochip1", LineNumber: 5, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip2", LineNumber: 12, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip1", LineNumber: 101, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip1", LineNumber: 107, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip1", LineNumber: 8, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip1", LineNumber: 103, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip1", LineNumber: 102, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip1", LineNumber: 108, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "29", DeviceName: "gpiochip1", LineNumber: 93, PwmChipSysfsDir: "3020000.pwm", PwmID: 0}, - {Name: "31", DeviceName: "gpiochip1", LineNumber: 94, PwmChipSysfsDir: "3020000.pwm", PwmID: 1}, - {Name: "32", DeviceName: "gpiochip1", LineNumber: 98, PwmChipSysfsDir: "3030000.pwm", PwmID: 0}, - {Name: "33", DeviceName: "gpiochip1", LineNumber: 99, PwmChipSysfsDir: "3030000.pwm", PwmID: 1}, - {Name: "35", DeviceName: "gpiochip1", LineNumber: 2, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "36", DeviceName: "gpiochip1", LineNumber: 97, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip1", LineNumber: 115, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip1", LineNumber: 3, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip1", LineNumber: 4, PwmChipSysfsDir: "", PwmID: -1}, - }, - []string{"ti,j721e-sk", "ti,j721e"}, - }, -} diff --git a/components/board/upboard/board.go b/components/board/upboard/board.go deleted file mode 100644 index 4af1e55309c..00000000000 --- a/components/board/upboard/board.go +++ /dev/null @@ -1,32 +0,0 @@ -// Package upboard implements an Intel based board. -package upboard - -// This is experimental -/* - Datasheet: https://github.com/up-board/up-community/wiki/Pinout_UP4000 - Supported board: UP4000 -*/ - -import ( - "github.com/pkg/errors" - "periph.io/x/host/v3" - - "go.viam.com/rdk/components/board/genericlinux" - "go.viam.com/rdk/logging" -) - -const modelName = "upboard" - -func init() { - if _, err := host.Init(); err != nil { - logging.Global().Debugw("error initializing host", "error", err) - } - - gpioMappings, err := genericlinux.GetGPIOBoardMappings(modelName, boardInfoMappings) - var noBoardErr genericlinux.NoBoardFoundError - if errors.As(err, &noBoardErr) { - logging.Global().Debugw("error getting up board GPIO board mapping", "error", err) - } - - genericlinux.RegisterBoard(modelName, gpioMappings) -} diff --git a/components/board/upboard/data.go b/components/board/upboard/data.go deleted file mode 100644 index d69b98a3966..00000000000 --- a/components/board/upboard/data.go +++ /dev/null @@ -1,49 +0,0 @@ -package upboard - -// This is experimental. - -import "go.viam.com/rdk/components/board/genericlinux" - -const upboard = "up_4000" - -var boardInfoMappings = map[string]genericlinux.BoardInformation{ - upboard: { - []genericlinux.PinDefinition{ - /* - pinout for up4000: https://github.com/up-board/up-community/wiki/Pinout_UP4000 - GPIOChipSysFSDir: path to the directory of a chip. Can be found from the output of gpiodetect - */ - // GPIO pin definition - {Name: "3", DeviceName: "gpiochip4", LineNumber: 2, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "5", DeviceName: "gpiochip4", LineNumber: 3, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "7", DeviceName: "gpiochip4", LineNumber: 4, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "8", DeviceName: "gpiochip4", LineNumber: 14, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "10", DeviceName: "gpiochip4", LineNumber: 15, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "11", DeviceName: "gpiochip4", LineNumber: 17, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "12", DeviceName: "gpiochip4", LineNumber: 18, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "13", DeviceName: "gpiochip4", LineNumber: 27, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "15", DeviceName: "gpiochip4", LineNumber: 22, PwmChipSysfsDir: "", PwmID: -1}, - // Pin 16 supposedly has hardware PWM from pwmID 3, but we haven't gotten it to work. - {Name: "16", DeviceName: "gpiochip4", LineNumber: 23, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "18", DeviceName: "gpiochip4", LineNumber: 24, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "19", DeviceName: "gpiochip4", LineNumber: 10, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "21", DeviceName: "gpiochip4", LineNumber: 9, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "22", DeviceName: "gpiochip4", LineNumber: 25, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "23", DeviceName: "gpiochip4", LineNumber: 11, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "24", DeviceName: "gpiochip4", LineNumber: 8, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "26", DeviceName: "gpiochip4", LineNumber: 7, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "27", DeviceName: "gpiochip4", LineNumber: 0, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "28", DeviceName: "gpiochip4", LineNumber: 1, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "29", DeviceName: "gpiochip4", LineNumber: 5, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "31", DeviceName: "gpiochip4", LineNumber: 6, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "32", DeviceName: "gpiochip4", LineNumber: 12, PwmChipSysfsDir: "0000:00:1a.0", PwmID: 0}, - {Name: "33", DeviceName: "gpiochip4", LineNumber: 13, PwmChipSysfsDir: "0000:00:1a.0", PwmID: 1}, - {Name: "35", DeviceName: "gpiochip4", LineNumber: 19, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "36", DeviceName: "gpiochip4", LineNumber: 16, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "37", DeviceName: "gpiochip4", LineNumber: 26, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "38", DeviceName: "gpiochip4", LineNumber: 20, PwmChipSysfsDir: "", PwmID: -1}, - {Name: "40", DeviceName: "gpiochip4", LineNumber: 21, PwmChipSysfsDir: "", PwmID: -1}, - }, - []string{"UP-APL03"}, - }, -} diff --git a/components/board/verify_main_test.go b/components/board/verify_main_test.go deleted file mode 100644 index 8d1ed1ef75d..00000000000 --- a/components/board/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package board - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/camera/align/data/join_cam.json b/components/camera/align/data/join_cam.json deleted file mode 100644 index 9a256d97785..00000000000 --- a/components/camera/align/data/join_cam.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "components": [ - { - "name": "join_cam", - "type": "camera", - "model": "join_color_depth", - "attributes": { - "debug": false, - "color_camera_name": "color", - "depth_camera_name": "depth", - "output_image_type": "depth", - "intrinsic_parameters": { - "height_px": 768, - "width_px": 1024, - "fx": 821.32642889, - "fy": 821.68607359, - "ppx": 494.95941428, - "ppy": 370.70529534 - }, - "distortion_parameters": { - "rk1": 0.11297234, - "rk2": -0.21375332, - "rk3": -0.01584774, - "tp1": -0.00302002, - "tp2": 0.19969297 - } - } - } - ] -} diff --git a/components/camera/align/join.go b/components/camera/align/join.go deleted file mode 100644 index 4fe285c5c4c..00000000000 --- a/components/camera/align/join.go +++ /dev/null @@ -1,205 +0,0 @@ -// Package align defines the camera models that are used to align a color camera's output with a depth camera's output, -// in order to make point clouds. -package align - -import ( - "context" - "fmt" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - "go.uber.org/multierr" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" -) - -var joinModel = resource.DefaultModelFamily.WithModel("join_color_depth") - -func init() { - resource.RegisterComponent(camera.API, joinModel, - resource.Registration[camera.Camera, *joinConfig]{ - Constructor: func(ctx context.Context, deps resource.Dependencies, - conf resource.Config, logger logging.Logger, - ) (camera.Camera, error) { - newConf, err := resource.NativeConfig[*joinConfig](conf) - if err != nil { - return nil, err - } - colorName := newConf.Color - color, err := camera.FromDependencies(deps, colorName) - if err != nil { - return nil, fmt.Errorf("no color camera (%s): %w", colorName, err) - } - - depthName := newConf.Depth - depth, err := camera.FromDependencies(deps, depthName) - if err != nil { - return nil, fmt.Errorf("no depth camera (%s): %w", depthName, err) - } - src, err := newJoinColorDepth(ctx, color, depth, newConf, logger) - if err != nil { - return nil, err - } - return camera.FromVideoSource(conf.ResourceName(), src, logger), nil - }, - }) -} - -// joinConfig is the attribute struct for aligning. -type joinConfig struct { - ImageType string `json:"output_image_type"` - Color string `json:"color_camera_name"` - Depth string `json:"depth_camera_name"` - CameraParameters *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters,omitempty"` - Debug bool `json:"debug,omitempty"` - DistortionParameters *transform.BrownConrady `json:"distortion_parameters,omitempty"` -} - -func (cfg *joinConfig) Validate(path string) ([]string, error) { - var deps []string - if cfg.Color == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "color_camera_name") - } - deps = append(deps, cfg.Color) - if cfg.Depth == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "depth_camera_name") - } - - if cfg.CameraParameters != nil { - if cfg.CameraParameters.Height < 0 || cfg.CameraParameters.Width < 0 { - return nil, fmt.Errorf( - "got illegal negative dimensions for width_px and height_px (%d, %d) fields set in intrinsic_parameters"+ - " for join_color_depth camera", - cfg.CameraParameters.Width, cfg.CameraParameters.Height) - } - } - deps = append(deps, cfg.Depth) - return deps, nil -} - -// joinColorDepth takes a color and depth image source and aligns them together. -type joinColorDepth struct { - color, depth gostream.VideoStream - colorName, depthName string - underlyingCamera camera.VideoSource - projector transform.Projector - imageType camera.ImageType - debug bool - logger logging.Logger -} - -// newJoinColorDepth creates a gostream.VideoSource that aligned color and depth channels. -func newJoinColorDepth(ctx context.Context, color, depth camera.VideoSource, conf *joinConfig, logger logging.Logger, -) (camera.VideoSource, error) { - imgType := camera.ImageType(conf.ImageType) - // get intrinsic parameters from config, or from the underlying camera - var camParams *transform.PinholeCameraIntrinsics - if conf.CameraParameters == nil { - if imgType == camera.DepthStream { - props, err := depth.Properties(ctx) - if err == nil { - camParams = props.IntrinsicParams - } - } else { - props, err := color.Properties(ctx) - if err == nil { - camParams = props.IntrinsicParams - } - } - } else { - camParams = conf.CameraParameters - } - err := camParams.CheckValid() - if err != nil { - if conf.CameraParameters != nil { - return nil, errors.Wrap(err, "error in the intrinsic_parameters field of the attributes") - } - return nil, errors.Wrap(err, "error in the intrinsic parameters of the underlying camera") - } - videoSrc := &joinColorDepth{ - colorName: conf.Color, - depthName: conf.Depth, - color: gostream.NewEmbeddedVideoStream(color), - depth: gostream.NewEmbeddedVideoStream(depth), - projector: camParams, - imageType: imgType, - debug: conf.Debug, - logger: logger, - } - if conf.Color == conf.Depth { // store the underlying VideoSource for an Images call - videoSrc.underlyingCamera = color - } - cameraModel := camera.NewPinholeModelWithBrownConradyDistortion(conf.CameraParameters, conf.DistortionParameters) - return camera.NewVideoSourceFromReader( - ctx, - videoSrc, - &cameraModel, - imgType, - ) -} - -// Read returns the next image from either the color or depth camera.. -// imageType parameter will determine which channel gets streamed. -func (jcd *joinColorDepth) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "align::joinColorDepth::Read") - defer span.End() - switch jcd.imageType { - case camera.ColorStream, camera.UnspecifiedStream: - return jcd.color.Next(ctx) - case camera.DepthStream: - return jcd.depth.Next(ctx) - default: - return nil, nil, camera.NewUnsupportedImageTypeError(jcd.imageType) - } -} - -func (jcd *joinColorDepth) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "align::joinColorDepth::NextPointCloud") - defer span.End() - if jcd.projector == nil { - return nil, transform.NewNoIntrinsicsError("no intrinsic_parameters in camera attributes") - } - if jcd.colorName == jcd.depthName { - return jcd.nextPointCloudFromImages(ctx) - } - col, dm := camera.SimultaneousColorDepthNext(ctx, jcd.color, jcd.depth) - if col == nil { - return nil, errors.Errorf("could not get color image from source camera %q for join_color_depth camera", jcd.colorName) - } - if dm == nil { - return nil, errors.Errorf("could not get depth image from source camera %q for join_color_depth camera", jcd.depthName) - } - return jcd.projector.RGBDToPointCloud(rimage.ConvertImage(col), dm) -} - -func (jcd *joinColorDepth) nextPointCloudFromImages(ctx context.Context) (pointcloud.PointCloud, error) { - imgs, _, err := jcd.underlyingCamera.Images(ctx) - if err != nil { - return nil, errors.Wrapf(err, "could not call Images on underlying camera %q", jcd.colorName) - } - var col *rimage.Image - var dm *rimage.DepthMap - for _, img := range imgs { - if img.SourceName == "color" { - col = rimage.ConvertImage(img.Image) - } - if img.SourceName == "depth" { - dm, err = rimage.ConvertImageToDepthMap(ctx, img.Image) - if err != nil { - return nil, errors.Wrap(err, "image called 'depth' from Images not actually a depth map") - } - } - } - return jcd.projector.RGBDToPointCloud(col, dm) -} - -func (jcd *joinColorDepth) Close(ctx context.Context) error { - return multierr.Combine(jcd.color.Close(ctx), jcd.depth.Close(ctx)) -} diff --git a/components/camera/align/join_test.go b/components/camera/align/join_test.go deleted file mode 100644 index 4b6685871ed..00000000000 --- a/components/camera/align/join_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package align - -import ( - "context" - "errors" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -func TestJoinWithImages(t *testing.T) { - logger := logging.NewTestLogger(t) - cam := inject.NewCamera("cam") - params := &transform.PinholeCameraIntrinsics{ // D435 intrinsics for 424x240 - Width: 424, - Height: 240, - Fx: 304.1299133300781, - Fy: 304.2772216796875, - Ppx: 213.47967529296875, - Ppy: 124.63351440429688, - } - img, err := rimage.NewImageFromFile(artifact.MustPath("pointcloud/the_color_image_intel_424.jpg")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("pointcloud/the_depth_image_intel_424.png")) - test.That(t, err, test.ShouldBeNil) - cam.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{ - IntrinsicParams: params, - }, nil - } - cam.ImagesFunc = func(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - imgs := []camera.NamedImage{{img, "color"}, {dm, "depth"}} - return imgs, resource.ResponseMetadata{}, nil - } - cfg := &joinConfig{ - ImageType: "color", - Color: "intel", - Depth: "intel", - } - joinCam, err := newJoinColorDepth(context.Background(), cam, cam, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - alignedPointCloud, err := joinCam.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, alignedPointCloud, test.ShouldNotBeNil) - tempPCD, err := os.CreateTemp(t.TempDir(), "*.pcd") - test.That(t, err, test.ShouldBeNil) - defer os.Remove(tempPCD.Name()) - err = pointcloud.ToPCD(alignedPointCloud, tempPCD, pointcloud.PCDBinary) - test.That(t, err, test.ShouldBeNil) - - test.That(t, joinCam.Close(context.Background()), test.ShouldBeNil) -} - -func TestJoin(t *testing.T) { - logger := logging.NewTestLogger(t) - conf, err := config.Read(context.Background(), utils.ResolveFile("components/camera/align/data/join_cam.json"), logger) - test.That(t, err, test.ShouldBeNil) - - c := conf.FindComponent("join_cam") - test.That(t, c, test.ShouldNotBeNil) - - joinConf, ok := c.ConvertedAttributes.(*joinConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, joinConf, test.ShouldNotBeNil) - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board2.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - // create the source cameras - colorSrc := &videosource.StaticSource{ColorImg: img} - colorVideoSrc, err := camera.NewVideoSourceFromReader(context.Background(), colorSrc, nil, camera.ColorStream) - test.That(t, err, test.ShouldBeNil) - depthSrc := &videosource.StaticSource{DepthImg: dm} - depthVideoSrc, err := camera.NewVideoSourceFromReader(context.Background(), depthSrc, nil, camera.DepthStream) - test.That(t, err, test.ShouldBeNil) - - // create the join camera - is, err := newJoinColorDepth(context.Background(), colorVideoSrc, depthVideoSrc, joinConf, logger) - test.That(t, err, test.ShouldBeNil) - // get images and point clouds - alignedPointCloud, err := is.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, alignedPointCloud, test.ShouldNotBeNil) - outImage, _, err := camera.ReadImage(context.Background(), is) - test.That(t, err, test.ShouldBeNil) - outDepth, ok := outImage.(*rimage.DepthMap) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, outDepth, test.ShouldNotBeNil) - - test.That(t, colorVideoSrc.Close(context.Background()), test.ShouldBeNil) - test.That(t, depthVideoSrc.Close(context.Background()), test.ShouldBeNil) - test.That(t, is.Close(context.Background()), test.ShouldBeNil) - // set necessary fields to nil, expect errors - joinConf.CameraParameters = nil - _, err = newJoinColorDepth(context.Background(), colorVideoSrc, depthVideoSrc, joinConf, logger) - test.That(t, errors.Is(err, transform.ErrNoIntrinsics), test.ShouldBeTrue) -} diff --git a/components/camera/camera.go b/components/camera/camera.go deleted file mode 100644 index 9fcb7aa0684..00000000000 --- a/components/camera/camera.go +++ /dev/null @@ -1,463 +0,0 @@ -// Package camera defines an image capturing device. -package camera - -import ( - "context" - "image" - "sync" - "time" - - "github.com/pion/mediadevices/pkg/prop" - "github.com/pkg/errors" - "go.opencensus.io/trace" - "go.uber.org/multierr" - pb "go.viam.com/api/component/camera/v1" - viamutils "go.viam.com/utils" - - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/data" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/depthadapter" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Camera]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterCameraServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.CameraService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: nextPointCloud.String(), - }, newNextPointCloudCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: readImage.String(), - }, newReadImageCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: getImages.String(), - }, newGetImagesCollector) -} - -// SubtypeName is a constant that identifies the camera resource subtype string. -const SubtypeName = "camera" - -// API is a variable that identifies the camera resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named camera's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// Properties is a lookup for a camera's features and settings. -type Properties struct { - // SupportsPCD indicates that the Camera supports a valid - // implementation of NextPointCloud - SupportsPCD bool - ImageType ImageType - IntrinsicParams *transform.PinholeCameraIntrinsics - DistortionParams transform.Distorter - MimeTypes []string -} - -// NamedImage is a struct that associates the source from where the image came from to the Image. -type NamedImage struct { - Image image.Image - SourceName string -} - -// A Camera is a resource that can capture frames. -type Camera interface { - resource.Resource - VideoSource -} - -// A VideoSource represents anything that can capture frames. -type VideoSource interface { - projectorProvider - // Images is used for getting simultaneous images from different imagers, - // along with associated metadata (just timestamp for now). It's not for getting a time series of images from the same imager. - // - // myCamera, err := camera.FromRobot(machine, "my_camera") - // - // images, metadata, err := myCamera.Images(context.Background()) - Images(ctx context.Context) ([]NamedImage, resource.ResponseMetadata, error) - // Stream returns a stream that makes a best effort to return consecutive images - // that may have a MIME type hint dictated in the context via gostream.WithMIMETypeHint. - // - // myCamera, err := camera.FromRobot(machine, "my_camera") - // - // // gets the stream from a camera - // stream, err := myCamera.Stream(context.Background()) - // - // // gets an image from the camera stream - // img, release, err := stream.Next(context.Background()) - // defer release() - Stream(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) - - // NextPointCloud returns the next immediately available point cloud, not necessarily one - // a part of a sequence. In the future, there could be streaming of point clouds. - // - // myCamera, err := camera.FromRobot(machine, "my_camera") - // - // pointCloud, err := myCamera.NextPointCloud(context.Background()) - NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) - // Properties returns properties that are intrinsic to the particular - // implementation of a camera - // - // myCamera, err := camera.FromRobot(machine, "my_camera") - // - // // gets the properties from a camera - // properties, err := myCamera.Properties(context.Background()) - Properties(ctx context.Context) (Properties, error) - - // Close shuts down the resource and prevents further use - // - // myCamera, err := camera.FromRobot(machine, "my_camera") - // - // err = myCamera.Close(ctx) - Close(ctx context.Context) error -} - -// ReadImage reads an image from the given source that is immediately available. -func ReadImage(ctx context.Context, src gostream.VideoSource) (image.Image, func(), error) { - return gostream.ReadImage(ctx, src) -} - -type projectorProvider interface { - Projector(ctx context.Context) (transform.Projector, error) -} - -// A PointCloudSource is a source that can generate pointclouds. -type PointCloudSource interface { - NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) -} - -// A ImagesSource is a source that can return a list of images with timestamp. -type ImagesSource interface { - Images(ctx context.Context) ([]NamedImage, resource.ResponseMetadata, error) -} - -// FromVideoSource creates a Camera resource from a VideoSource. -// Note: this strips away Reconfiguration and DoCommand abilities. -// If needed, implement the Camera another way. For example, a webcam -// implements a Camera manually so that it can atomically reconfigure itself. -func FromVideoSource(name resource.Name, src VideoSource, logger logging.Logger) Camera { - var rtpPassthroughSource rtppassthrough.Source - if ps, ok := src.(rtppassthrough.Source); ok { - rtpPassthroughSource = ps - } - return &sourceBasedCamera{ - rtpPassthroughSource: rtpPassthroughSource, - Named: name.AsNamed(), - VideoSource: src, - Logger: logger, - } -} - -type sourceBasedCamera struct { - resource.Named - resource.AlwaysRebuild - VideoSource - rtpPassthroughSource rtppassthrough.Source - logging.Logger -} - -func (vs *sourceBasedCamera) SubscribeRTP( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, -) (rtppassthrough.Subscription, error) { - if vs.rtpPassthroughSource != nil { - return vs.rtpPassthroughSource.SubscribeRTP(ctx, bufferSize, packetsCB) - } - return rtppassthrough.NilSubscription, errors.New("SubscribeRTP unimplemented") -} - -func (vs *sourceBasedCamera) Unsubscribe(ctx context.Context, id rtppassthrough.SubscriptionID) error { - if vs.rtpPassthroughSource != nil { - return vs.rtpPassthroughSource.Unsubscribe(ctx, id) - } - return errors.New("Unsubscribe unimplemented") -} - -// NewVideoSourceFromReader creates a VideoSource either with or without a projector. The stream type -// argument is for detecting whether or not the resulting camera supports return -// of pointcloud data in the absence of an implemented NextPointCloud function. -// If this is unknown or not applicable, a value of camera.Unspecified stream can be supplied. -func NewVideoSourceFromReader( - ctx context.Context, - reader gostream.VideoReader, - syst *transform.PinholeCameraModel, imageType ImageType, -) (VideoSource, error) { - if reader == nil { - return nil, errors.New("cannot have a nil reader") - } - var rtpPassthroughSource rtppassthrough.Source - passthrough, isRTPPassthrough := reader.(rtppassthrough.Source) - if isRTPPassthrough { - rtpPassthroughSource = passthrough - } - vs := gostream.NewVideoSource(reader, prop.Video{}) - actualSystem := syst - if actualSystem == nil { - srcCam, ok := reader.(VideoSource) - if ok { - props, err := srcCam.Properties(ctx) - if err != nil { - return nil, NewPropertiesError("source camera") - } - - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - actualSystem = &cameraModel - } - } - return &videoSource{ - rtpPassthroughSource: rtpPassthroughSource, - system: actualSystem, - videoSource: vs, - videoStream: gostream.NewEmbeddedVideoStream(vs), - actualSource: reader, - imageType: imageType, - }, nil -} - -func (vs *videoSource) SubscribeRTP( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, -) (rtppassthrough.Subscription, error) { - if vs.rtpPassthroughSource != nil { - return vs.rtpPassthroughSource.SubscribeRTP(ctx, bufferSize, packetsCB) - } - return rtppassthrough.NilSubscription, errors.New("SubscribeRTP unimplemented") -} - -func (vs *videoSource) Unsubscribe(ctx context.Context, id rtppassthrough.SubscriptionID) error { - if vs.rtpPassthroughSource != nil { - return vs.rtpPassthroughSource.Unsubscribe(ctx, id) - } - return errors.New("Unsubscribe unimplemented") -} - -// NewPinholeModelWithBrownConradyDistortion creates a transform.PinholeCameraModel from -// a *transform.PinholeCameraIntrinsics and a *transform.BrownConrady. -// If *transform.BrownConrady is `nil`, transform.PinholeCameraModel.Distortion -// is not set & remains nil, to prevent https://go.dev/doc/faq#nil_error. -func NewPinholeModelWithBrownConradyDistortion(pinholeCameraIntrinsics *transform.PinholeCameraIntrinsics, - distortion *transform.BrownConrady, -) transform.PinholeCameraModel { - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = pinholeCameraIntrinsics - - if distortion != nil { - cameraModel.Distortion = distortion - } - return cameraModel -} - -// NewPropertiesError returns an error specific to a failure in Properties. -func NewPropertiesError(cameraIdentifier string) error { - return errors.Errorf("failed to get properties from %s", cameraIdentifier) -} - -// WrapVideoSourceWithProjector creates a Camera either with or without a projector. The stream type -// argument is for detecting whether or not the resulting camera supports return -// of pointcloud data in the absence of an implemented NextPointCloud function. -// If this is unknown or not applicable, a value of camera.Unspecified stream can be supplied. -func WrapVideoSourceWithProjector( - ctx context.Context, - source gostream.VideoSource, - syst *transform.PinholeCameraModel, imageType ImageType, -) (VideoSource, error) { - if source == nil { - return nil, errors.New("cannot have a nil source") - } - actualSystem := syst - if actualSystem == nil { - //nolint:staticcheck - srcCam, ok := source.(Camera) - if ok { - props, err := srcCam.Properties(ctx) - if err != nil { - return nil, NewPropertiesError("source camera") - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - - actualSystem = &cameraModel - } - } - return &videoSource{ - system: actualSystem, - videoSource: source, - videoStream: gostream.NewEmbeddedVideoStream(source), - actualSource: source, - imageType: imageType, - }, nil -} - -// videoSource implements a Camera with a gostream.VideoSource. -type videoSource struct { - rtpPassthroughSource rtppassthrough.Source - videoSource gostream.VideoSource - videoStream gostream.VideoStream - actualSource interface{} - system *transform.PinholeCameraModel - imageType ImageType -} - -func (vs *videoSource) Stream(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return vs.videoSource.Stream(ctx, errHandlers...) -} - -// Images is for getting simultaneous images from different sensors -// If the underlying source did not specify an Images function, a default is applied. -// The default returns a list of 1 image from ReadImage, and the current time. -func (vs *videoSource) Images(ctx context.Context) ([]NamedImage, resource.ResponseMetadata, error) { - ctx, span := trace.StartSpan(ctx, "camera::videoSource::Images") - defer span.End() - if c, ok := vs.actualSource.(ImagesSource); ok { - return c.Images(ctx) - } - img, release, err := ReadImage(ctx, vs.videoSource) - if err != nil { - return nil, resource.ResponseMetadata{}, errors.Wrap(err, "videoSource: call to get Images failed") - } - defer func() { - if release != nil { - release() - } - }() - ts := time.Now() - return []NamedImage{{img, ""}}, resource.ResponseMetadata{CapturedAt: ts}, nil -} - -// NextPointCloud returns the next PointCloud from the camera, or will error if not supported. -func (vs *videoSource) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "camera::videoSource::NextPointCloud") - defer span.End() - if c, ok := vs.actualSource.(PointCloudSource); ok { - return c.NextPointCloud(ctx) - } - if vs.system == nil || vs.system.PinholeCameraIntrinsics == nil { - return nil, transform.NewNoIntrinsicsError("cannot do a projection to a point cloud") - } - img, release, err := vs.videoStream.Next(ctx) - defer release() - if err != nil { - return nil, err - } - dm, err := rimage.ConvertImageToDepthMap(ctx, img) - if err != nil { - return nil, errors.Wrapf(err, "cannot project to a point cloud") - } - return depthadapter.ToPointCloud(dm, vs.system.PinholeCameraIntrinsics), nil -} - -func (vs *videoSource) Projector(ctx context.Context) (transform.Projector, error) { - if vs.system == nil || vs.system.PinholeCameraIntrinsics == nil { - return nil, transform.NewNoIntrinsicsError("No features in config") - } - return vs.system.PinholeCameraIntrinsics, nil -} - -func (vs *videoSource) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if res, ok := vs.videoSource.(resource.Resource); ok { - return res.DoCommand(ctx, cmd) - } - return nil, resource.ErrDoUnimplemented -} - -func (vs *videoSource) Properties(ctx context.Context) (Properties, error) { - _, supportsPCD := vs.actualSource.(PointCloudSource) - result := Properties{ - SupportsPCD: supportsPCD, - } - if vs.system == nil { - return result, nil - } - if (vs.system.PinholeCameraIntrinsics != nil) && (vs.imageType == DepthStream) { - result.SupportsPCD = true - } - result.ImageType = vs.imageType - result.IntrinsicParams = vs.system.PinholeCameraIntrinsics - - if vs.system.Distortion != nil { - result.DistortionParams = vs.system.Distortion - } - - return result, nil -} - -func (vs *videoSource) Close(ctx context.Context) error { - return multierr.Combine(vs.videoStream.Close(ctx), vs.videoSource.Close(ctx)) -} - -// FromDependencies is a helper for getting the named camera from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Camera, error) { - return resource.FromDependencies[Camera](deps, Named(name)) -} - -// FromRobot is a helper for getting the named Camera from the given Robot. -func FromRobot(r robot.Robot, name string) (Camera, error) { - return robot.ResourceFromRobot[Camera](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all camera names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// SimultaneousColorDepthNext will call Next on both the color and depth camera as simultaneously as possible. -func SimultaneousColorDepthNext(ctx context.Context, color, depth gostream.VideoStream) (image.Image, *rimage.DepthMap) { - var wg sync.WaitGroup - var col image.Image - var dm *rimage.DepthMap - // do a parallel request for the color and depth image - // get color image - wg.Add(1) - viamutils.PanicCapturingGo(func() { - defer wg.Done() - var err error - col, _, err = color.Next(ctx) - if err != nil { - panic(err) - } - }) - // get depth image - wg.Add(1) - viamutils.PanicCapturingGo(func() { - defer wg.Done() - d, _, err := depth.Next(ctx) - if err != nil { - panic(err) - } - dm, err = rimage.ConvertImageToDepthMap(ctx, d) - if err != nil { - panic(err) - } - }) - wg.Wait() - return col, dm -} diff --git a/components/camera/camera_test.go b/components/camera/camera_test.go deleted file mode 100644 index 4871d512223..00000000000 --- a/components/camera/camera_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package camera_test - -import ( - "context" - "image" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - rutils "go.viam.com/rdk/utils" -) - -const ( - testCameraName = "camera1" - depthCameraName = "camera_depth" - failCameraName = "camera2" - missingCameraName = "camera3" -) - -type simpleSource struct { - filePath string -} - -func (s *simpleSource) Read(ctx context.Context) (image.Image, func(), error) { - img, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath(s.filePath+".dat.gz")) - return img, func() {}, err -} - -func (s *simpleSource) Close(ctx context.Context) error { - return nil -} - -type simpleSourceWithPCD struct { - filePath string -} - -func (s *simpleSourceWithPCD) Read(ctx context.Context) (image.Image, func(), error) { - img, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath(s.filePath+".dat.gz")) - return img, func() {}, err -} - -func (s *simpleSourceWithPCD) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - return nil, nil -} - -func (s *simpleSourceWithPCD) Close(ctx context.Context) error { - return nil -} - -func TestNewPinholeModelWithBrownConradyDistortion(t *testing.T) { - intrinsics := &transform.PinholeCameraIntrinsics{ - Width: 10, - Height: 10, - Fx: 1.0, - Fy: 2.0, - Ppx: 3.0, - Ppy: 4.0, - } - distortion := &transform.BrownConrady{} - - expected1 := transform.PinholeCameraModel{PinholeCameraIntrinsics: intrinsics, Distortion: distortion} - pinholeCameraModel1 := camera.NewPinholeModelWithBrownConradyDistortion(intrinsics, distortion) - test.That(t, pinholeCameraModel1, test.ShouldResemble, expected1) - - expected2 := transform.PinholeCameraModel{PinholeCameraIntrinsics: intrinsics} - pinholeCameraModel2 := camera.NewPinholeModelWithBrownConradyDistortion(intrinsics, nil) - test.That(t, pinholeCameraModel2, test.ShouldResemble, expected2) - test.That(t, pinholeCameraModel2.Distortion, test.ShouldBeNil) - - expected3 := transform.PinholeCameraModel{Distortion: distortion} - pinholeCameraModel3 := camera.NewPinholeModelWithBrownConradyDistortion(nil, distortion) - test.That(t, pinholeCameraModel3, test.ShouldResemble, expected3) - - expected4 := transform.PinholeCameraModel{} - pinholeCameraModel4 := camera.NewPinholeModelWithBrownConradyDistortion(nil, nil) - test.That(t, pinholeCameraModel4, test.ShouldResemble, expected4) - test.That(t, pinholeCameraModel4.Distortion, test.ShouldBeNil) -} - -func TestNewCamera(t *testing.T) { - intrinsics1 := &transform.PinholeCameraIntrinsics{Width: 128, Height: 72} - intrinsics2 := &transform.PinholeCameraIntrinsics{Width: 100, Height: 100} - videoSrc := &simpleSource{"rimage/board1_small"} - videoSrcPCD := &simpleSourceWithPCD{"rimage/board1_small"} - - // no camera - _, err := camera.NewVideoSourceFromReader(context.Background(), nil, nil, camera.UnspecifiedStream) - test.That(t, err, test.ShouldBeError, errors.New("cannot have a nil reader")) - - // camera with no camera parameters - cam1, err := camera.NewVideoSourceFromReader(context.Background(), videoSrc, nil, camera.UnspecifiedStream) - test.That(t, err, test.ShouldBeNil) - props, err := cam1.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, props.SupportsPCD, test.ShouldBeFalse) - test.That(t, props.IntrinsicParams, test.ShouldBeNil) - cam1, err = camera.NewVideoSourceFromReader(context.Background(), videoSrcPCD, nil, camera.UnspecifiedStream) - test.That(t, err, test.ShouldBeNil) - props, err = cam1.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, props.SupportsPCD, test.ShouldBeTrue) - test.That(t, props.IntrinsicParams, test.ShouldBeNil) - - // camera with camera parameters - cam2, err := camera.NewVideoSourceFromReader( - context.Background(), - videoSrc, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: intrinsics1}, - camera.DepthStream, - ) - test.That(t, err, test.ShouldBeNil) - props, err = cam2.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, *(props.IntrinsicParams), test.ShouldResemble, *intrinsics1) - - // camera with camera parameters inherited from other camera - cam2props, err := cam2.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - cam3, err := camera.NewVideoSourceFromReader( - context.Background(), - videoSrc, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: cam2props.IntrinsicParams}, - camera.DepthStream, - ) - test.That(t, err, test.ShouldBeNil) - cam3props, err := cam3.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, *(cam3props.IntrinsicParams), test.ShouldResemble, *(cam2props.IntrinsicParams)) - - // camera with different camera parameters, will not inherit - cam4, err := camera.NewVideoSourceFromReader( - context.Background(), - videoSrc, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: intrinsics2}, - camera.DepthStream, - ) - test.That(t, err, test.ShouldBeNil) - cam4props, err := cam4.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, cam4props.IntrinsicParams, test.ShouldNotBeNil) - test.That(t, *(cam4props.IntrinsicParams), test.ShouldNotResemble, *(cam2props.IntrinsicParams)) -} - -type cloudSource struct { - resource.Named - resource.AlwaysRebuild - *simpleSource -} - -func (cs *cloudSource) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - p := pointcloud.New() - return p, p.Set(pointcloud.NewVector(0, 0, 0), nil) -} - -func TestCameraWithNoProjector(t *testing.T) { - videoSrc := &simpleSource{"rimage/board1"} - noProj, err := camera.NewVideoSourceFromReader(context.Background(), videoSrc, nil, camera.DepthStream) - test.That(t, err, test.ShouldBeNil) - _, err = noProj.NextPointCloud(context.Background()) - test.That(t, errors.Is(err, transform.ErrNoIntrinsics), test.ShouldBeTrue) - _, err = noProj.Projector(context.Background()) - test.That(t, errors.Is(err, transform.ErrNoIntrinsics), test.ShouldBeTrue) - - // make a camera with a NextPointCloudFunction - videoSrc2 := &cloudSource{Named: camera.Named("foo").AsNamed(), simpleSource: videoSrc} - noProj2, err := camera.NewVideoSourceFromReader(context.Background(), videoSrc2, nil, camera.DepthStream) - test.That(t, err, test.ShouldBeNil) - pc, err := noProj2.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - _, got := pc.At(0, 0, 0) - test.That(t, got, test.ShouldBeTrue) - - img, _, err := camera.ReadImage( - gostream.WithMIMETypeHint(context.Background(), rutils.WithLazyMIMEType(rutils.MimeTypePNG)), - noProj2) - test.That(t, err, test.ShouldBeNil) - - depthImg := img.(*rimage.DepthMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, depthImg.Bounds().Dx(), test.ShouldEqual, 1280) - test.That(t, depthImg.Bounds().Dy(), test.ShouldEqual, 720) - - test.That(t, noProj2.Close(context.Background()), test.ShouldBeNil) -} - -func TestCameraWithProjector(t *testing.T) { - videoSrc := &simpleSource{"rimage/board1"} - params1 := &transform.PinholeCameraIntrinsics{ // not the real camera parameters -- fake for test - Width: 1280, - Height: 720, - Fx: 200, - Fy: 200, - Ppx: 100, - Ppy: 100, - } - src, err := camera.NewVideoSourceFromReader( - context.Background(), - videoSrc, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: params1}, - camera.DepthStream, - ) - test.That(t, err, test.ShouldBeNil) - pc, err := src.NextPointCloud(context.Background()) - test.That(t, pc.Size(), test.ShouldEqual, 921600) - test.That(t, err, test.ShouldBeNil) - proj, err := src.Projector(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, proj, test.ShouldNotBeNil) - test.That(t, src.Close(context.Background()), test.ShouldBeNil) - - // camera with a point cloud function - videoSrc2 := &cloudSource{Named: camera.Named("foo").AsNamed(), simpleSource: videoSrc} - props, err := src.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - cam2, err := camera.NewVideoSourceFromReader( - context.Background(), - videoSrc2, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: props.IntrinsicParams}, - camera.DepthStream, - ) - test.That(t, err, test.ShouldBeNil) - pc, err = cam2.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - _, got := pc.At(0, 0, 0) - test.That(t, got, test.ShouldBeTrue) - - img, _, err := camera.ReadImage( - gostream.WithMIMETypeHint(context.Background(), rutils.MimeTypePNG), - cam2) - test.That(t, err, test.ShouldBeNil) - - depthImg := img.(*rimage.DepthMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, depthImg.Bounds().Dx(), test.ShouldEqual, 1280) - test.That(t, depthImg.Bounds().Dy(), test.ShouldEqual, 720) - // cam2 should implement a default GetImages, that just returns the one image - images, _, err := cam2.Images(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(images), test.ShouldEqual, 1) - test.That(t, images[0].Image, test.ShouldHaveSameTypeAs, &rimage.DepthMap{}) - test.That(t, images[0].Image.Bounds().Dx(), test.ShouldEqual, 1280) - test.That(t, images[0].Image.Bounds().Dy(), test.ShouldEqual, 720) - - test.That(t, cam2.Close(context.Background()), test.ShouldBeNil) -} diff --git a/components/camera/client.go b/components/camera/client.go deleted file mode 100644 index cfb8b89470a..00000000000 --- a/components/camera/client.go +++ /dev/null @@ -1,672 +0,0 @@ -package camera - -import ( - "bytes" - "context" - "fmt" - "image" - "io" - "slices" - "sync" - - "github.com/pion/rtp" - "github.com/pion/webrtc/v3" - "github.com/pkg/errors" - "go.opencensus.io/trace" - pb "go.viam.com/api/component/camera/v1" - streampb "go.viam.com/api/stream/v1" - goutils "go.viam.com/utils" - goprotoutils "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/data" - "go.viam.com/rdk/gostream" - rdkgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -var ( - // ErrNoPeerConnection indicates there was no peer connection. - ErrNoPeerConnection = errors.New("No PeerConnection") - // ErrNoSharedPeerConnection indicates there was no shared peer connection. - ErrNoSharedPeerConnection = errors.New("No Shared PeerConnection") - // ErrUnknownSubscriptionID indicates that a SubscriptionID is unknown. - ErrUnknownSubscriptionID = errors.New("SubscriptionID Unknown") -) - -type ( - singlePacketCallback func(*rtp.Packet) - bufAndCB struct { - cb singlePacketCallback - buf *rtppassthrough.Buffer - } - bufAndCBByID map[rtppassthrough.SubscriptionID]bufAndCB -) - -// client implements CameraServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - ctx context.Context - cancelFn context.CancelFunc - name string - conn rpc.ClientConn - client pb.CameraServiceClient - streamClient streampb.StreamServiceClient - logger logging.Logger - activeBackgroundWorkers sync.WaitGroup - - mu sync.Mutex - healthyClientCh chan struct{} - bufAndCBByID bufAndCBByID - currentSubParentID rtppassthrough.SubscriptionID - subParentToChildren map[rtppassthrough.SubscriptionID][]rtppassthrough.SubscriptionID - trackClosed <-chan struct{} -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Camera, error) { - c := pb.NewCameraServiceClient(conn) - streamClient := streampb.NewStreamServiceClient(conn) - trackClosed := make(chan struct{}) - close(trackClosed) - closeCtx, cancelFn := context.WithCancel(context.Background()) - return &client{ - ctx: closeCtx, - cancelFn: cancelFn, - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - conn: conn, - streamClient: streamClient, - client: c, - bufAndCBByID: map[rtppassthrough.SubscriptionID]bufAndCB{}, - trackClosed: trackClosed, - subParentToChildren: map[rtppassthrough.SubscriptionID][]rtppassthrough.SubscriptionID{}, - logger: logger, - }, nil -} - -func getExtra(ctx context.Context) (*structpb.Struct, error) { - ext := &structpb.Struct{} - if extra, ok := FromContext(ctx); ok { - var err error - if ext, err = goprotoutils.StructToStructPb(extra); err != nil { - return nil, err - } - } - - dataExt, err := data.GetExtraFromContext(ctx) - if err != nil { - return nil, err - } - - proto.Merge(ext, dataExt) - return ext, nil -} - -func (c *client) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::client::Read") - defer span.End() - mimeType := gostream.MIMETypeHint(ctx, "") - expectedType, _ := utils.CheckLazyMIMEType(mimeType) - - ext, err := getExtra(ctx) - if err != nil { - return nil, nil, err - } - - resp, err := c.client.GetImage(ctx, &pb.GetImageRequest{ - Name: c.name, - MimeType: expectedType, - Extra: ext, - }) - if err != nil { - return nil, nil, err - } - - if expectedType != "" && resp.MimeType != expectedType { - c.logger.CDebugw(ctx, "got different MIME type than what was asked for", "sent", expectedType, "received", resp.MimeType) - } else { - resp.MimeType = mimeType - } - - resp.MimeType = utils.WithLazyMIMEType(resp.MimeType) - img, err := rimage.DecodeImage(ctx, resp.Image, resp.MimeType) - if err != nil { - return nil, nil, err - } - return img, func() {}, nil -} - -func (c *client) Stream( - ctx context.Context, - errHandlers ...gostream.ErrorHandler, -) (gostream.VideoStream, error) { - ctx, span := trace.StartSpan(ctx, "camera::client::Stream") - - // RSDK-6340: The resource manager closes remote resources when the underlying - // connection goes bad. However, when the connection is re-established, the client - // objects these resources represent are not re-initialized/marked "healthy". - // `healthyClientCh` helps track these transitions between healthy and unhealthy - // states. - // - // When a new `client.Stream()` is created we will either use the existing - // `healthyClientCh` or create a new one. - // - // The goroutine a `Stream()` method spins off will listen to its version of the - // `healthyClientCh` to be notified when the connection has died so it can gracefully - // terminate. - // - // When a connection becomes unhealthy, the resource manager will call `Close` on the - // camera client object. Closing the client will: - // 1. close its `client.healthyClientCh` channel - // 2. wait for existing "stream" goroutines to drain - // 3. nil out the `client.healthyClientCh` member variable - // - // New streams concurrent with closing cannot start until this drain completes. There - // will never be stream goroutines from the old "generation" running concurrently - // with those from the new "generation". - c.mu.Lock() - if c.healthyClientCh == nil { - c.healthyClientCh = make(chan struct{}) - } - healthyClientCh := c.healthyClientCh - c.mu.Unlock() - - ctxWithMIME := gostream.WithMIMETypeHint(context.Background(), gostream.MIMETypeHint(ctx, "")) - streamCtx, stream, frameCh := gostream.NewMediaStreamForChannel[image.Image](ctxWithMIME) - - c.activeBackgroundWorkers.Add(1) - - goutils.PanicCapturingGo(func() { - streamCtx = trace.NewContext(streamCtx, span) - defer span.End() - - defer c.activeBackgroundWorkers.Done() - defer close(frameCh) - - for { - if streamCtx.Err() != nil { - return - } - - frame, release, err := c.Read(streamCtx) - if err != nil { - for _, handler := range errHandlers { - handler(streamCtx, err) - } - } - - select { - case <-streamCtx.Done(): - return - case <-healthyClientCh: - if err := stream.Close(ctxWithMIME); err != nil { - c.logger.CWarnw(ctx, "error closing stream", "err", err) - } - return - case frameCh <- gostream.MediaReleasePairWithError[image.Image]{ - Media: frame, - Release: release, - Err: err, - }: - } - } - }) - - return stream, nil -} - -func (c *client) Images(ctx context.Context) ([]NamedImage, resource.ResponseMetadata, error) { - ctx, span := trace.StartSpan(ctx, "camera::client::Images") - defer span.End() - - resp, err := c.client.GetImages(ctx, &pb.GetImagesRequest{ - Name: c.name, - }) - if err != nil { - return nil, resource.ResponseMetadata{}, errors.Wrap(err, "camera client: could not gets images from the camera") - } - - images := make([]NamedImage, 0, len(resp.Images)) - // keep everything lazy encoded by default, if type is unknown, attempt to decode it - for _, img := range resp.Images { - var rdkImage image.Image - switch img.Format { - case pb.Format_FORMAT_RAW_RGBA: - rdkImage = rimage.NewLazyEncodedImage(img.Image, utils.MimeTypeRawRGBA) - case pb.Format_FORMAT_RAW_DEPTH: - rdkImage = rimage.NewLazyEncodedImage(img.Image, utils.MimeTypeRawDepth) - case pb.Format_FORMAT_JPEG: - rdkImage = rimage.NewLazyEncodedImage(img.Image, utils.MimeTypeJPEG) - case pb.Format_FORMAT_PNG: - rdkImage = rimage.NewLazyEncodedImage(img.Image, utils.MimeTypePNG) - case pb.Format_FORMAT_UNSPECIFIED: - rdkImage, _, err = image.Decode(bytes.NewReader(img.Image)) - if err != nil { - return nil, resource.ResponseMetadata{}, err - } - } - images = append(images, NamedImage{rdkImage, img.SourceName}) - } - return images, resource.ResponseMetadataFromProto(resp.ResponseMetadata), nil -} - -func (c *client) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "camera::client::NextPointCloud") - defer span.End() - - ctx, getPcdSpan := trace.StartSpan(ctx, "camera::client::NextPointCloud::GetPointCloud") - - ext, err := data.GetExtraFromContext(ctx) - if err != nil { - return nil, err - } - - resp, err := c.client.GetPointCloud(ctx, &pb.GetPointCloudRequest{ - Name: c.name, - MimeType: utils.MimeTypePCD, - Extra: ext, - }) - getPcdSpan.End() - if err != nil { - return nil, err - } - - if resp.MimeType != utils.MimeTypePCD { - return nil, fmt.Errorf("unknown pc mime type %s", resp.MimeType) - } - - return func() (pointcloud.PointCloud, error) { - _, span := trace.StartSpan(ctx, "camera::client::NextPointCloud::ReadPCD") - defer span.End() - - return pointcloud.ReadPCD(bytes.NewReader(resp.PointCloud)) - }() -} - -func (c *client) Projector(ctx context.Context) (transform.Projector, error) { - var proj transform.Projector - props, err := c.Properties(ctx) - if err != nil { - return nil, err - } - intrinsics := props.IntrinsicParams - err = intrinsics.CheckValid() - if err != nil { - return nil, err - } - proj = intrinsics - return proj, nil -} - -func (c *client) Properties(ctx context.Context) (Properties, error) { - result := Properties{} - resp, err := c.client.GetProperties(ctx, &pb.GetPropertiesRequest{ - Name: c.name, - }) - if err != nil { - return Properties{}, err - } - if intrinsics := resp.IntrinsicParameters; intrinsics != nil { - result.IntrinsicParams = &transform.PinholeCameraIntrinsics{ - Width: int(intrinsics.WidthPx), - Height: int(intrinsics.HeightPx), - Fx: intrinsics.FocalXPx, - Fy: intrinsics.FocalYPx, - Ppx: intrinsics.CenterXPx, - Ppy: intrinsics.CenterYPx, - } - } - result.MimeTypes = resp.MimeTypes - result.SupportsPCD = resp.SupportsPcd - // if no distortion model present, return result with no model - if resp.DistortionParameters == nil { - return result, nil - } - if resp.DistortionParameters.Model == "" { // same as if nil - return result, nil - } - // switch distortion model based on model name - model := transform.DistortionType(resp.DistortionParameters.Model) - distorter, err := transform.NewDistorter(model, resp.DistortionParameters.Parameters) - if err != nil { - return Properties{}, err - } - result.DistortionParams = distorter - return result, nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return protoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} - -// TODO(RSDK-6433): This method can be called more than once during a client's lifecycle. -// For example, consider a case where a remote camera goes offline and then back online. -// We will call `Close` on the camera client when we detect the disconnection to remove -// active streams but then reuse the client when the connection is re-established. -func (c *client) Close(ctx context.Context) error { - _, span := trace.StartSpan(ctx, "camera::client::Close") - defer span.End() - - c.cancelFn() - c.mu.Lock() - - if c.healthyClientCh != nil { - close(c.healthyClientCh) - } - c.healthyClientCh = nil - // unsubscribe from all video streams that have been established with modular cameras - c.unsubscribeAll() - - // NOTE: (Nick S) we are intentionally releasing the lock before we wait for - // background goroutines to terminate as some of them need to be able - // to take the lock to terminate - c.mu.Unlock() - c.activeBackgroundWorkers.Wait() - return nil -} - -// SubscribeRTP begins a subscription to receive RTP packets. -// When the Subscription terminates the context in the returned Subscription -// is cancelled. -// It maintains the invariant that there is at most a single track between -// the client and WebRTC peer. -// It is strongly recommended to set a timeout on ctx as the underlying -// WebRTC peer connection's Track can be removed before sending an RTP packet which -// results in SubscribeRTP blocking until the provided context is cancelled or the client -// is closed. -// This rare condition may happen in cases like reconfiguring concurrently with -// a SubscribeRTP call. -func (c *client) SubscribeRTP( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, -) (rtppassthrough.Subscription, error) { - ctx, span := trace.StartSpan(ctx, "camera::client::SubscribeRTP") - defer span.End() - c.mu.Lock() - defer c.mu.Unlock() - sub, buf, err := rtppassthrough.NewSubscription(bufferSize) - if err != nil { - return sub, err - } - g := utils.NewGuard(func() { - buf.Close() - }) - defer g.OnFail() - - if c.conn.PeerConn() == nil { - return rtppassthrough.NilSubscription, ErrNoPeerConnection - } - - // check if we have established a connection that can be shared by multiple clients asking for cameras streams from viam server. - sc, ok := c.conn.(*rdkgrpc.SharedConn) - if !ok { - return rtppassthrough.NilSubscription, ErrNoSharedPeerConnection - } - - c.logger.CDebugw(ctx, "SubscribeRTP", "subID", sub.ID.String(), "name", c.Name(), "bufAndCBByID", c.bufAndCBByID.String()) - defer func() { - c.logger.CDebugw(ctx, "SubscribeRTP after", "subID", sub.ID.String(), - "name", c.Name(), "bufAndCBByID", c.bufAndCBByID.String()) - }() - // B/c there is only ever either 0 or 1 peer connections between a module & a viam-server - // once AddStream is called on the module for a given camera model instance & succeeds, we shouldn't - // call it again until the previous track is terminated (by calling RemoveStream) for a few reasons: - // 1. doing so would result in 2 webrtc tracks for the same camera sending the exact same RTP packets which would - // needlessly waste resources - // 2. b/c the signature of RemoveStream just takes the camera name, if there are 2 streams for the same camera - // & the module receives a call to RemoveStream, there is no way for the module to know which camera stream - // should be removed - if len(c.bufAndCBByID) == 0 { - // Wait for previous track to terminate or for the client to close - select { - case <-ctx.Done(): - err := errors.Wrap(ctx.Err(), "Track not closed within SubscribeRTP provided context") - c.logger.Error(err) - return rtppassthrough.NilSubscription, err - case <-c.ctx.Done(): - err := errors.Wrap(c.ctx.Err(), "Track not closed before client closed") - c.logger.Error(err) - return rtppassthrough.NilSubscription, err - case <-c.trackClosed: - } - trackReceived, trackClosed := make(chan struct{}), make(chan struct{}) - // add the camera model's addOnTrackSubFunc to the shared peer connection's - // slice of OnTrack callbacks. This is what allows - // all the bufAndCBByID's callback functions to be called with the - // RTP packets from the module's peer connection's track - sc.AddOnTrackSub(c.Name(), c.addOnTrackSubFunc(trackReceived, trackClosed, sub.ID)) - // remove the OnTrackSub once we either fail or succeed - defer sc.RemoveOnTrackSub(c.Name()) - if _, err := c.streamClient.AddStream(ctx, &streampb.AddStreamRequest{Name: c.Name().String()}); err != nil { - c.logger.CDebugw(ctx, "SubscribeRTP AddStream hit error", "subID", sub.ID.String(), "name", c.Name(), "err", err) - return rtppassthrough.NilSubscription, err - } - // NOTE: (Nick S) This is a workaround to a Pion bug / missing feature. - - // If the WebRTC peer on the other side of the PeerConnection calls pc.AddTrack followd by pc.RemoveTrack - // before the module writes RTP packets - // to the track, the client's PeerConnection.OnTrack callback is never called. - // That results in the client never receiving RTP packets on subscriptions which never terminate - // which is an unacceptable failure mode. - - // To prevent that failure mode, we exit with an error if a track is not received within - // the SubscribeRTP context. - select { - case <-ctx.Done(): - err := errors.Wrap(ctx.Err(), "Track not received within SubscribeRTP provided context") - c.logger.Error(err.Error()) - return rtppassthrough.NilSubscription, err - case <-c.ctx.Done(): - err := errors.Wrap(c.ctx.Err(), "Track not received before client closed") - c.logger.Error(err) - return rtppassthrough.NilSubscription, err - case <-trackReceived: - c.logger.Debug("received track") - } - // set up channel so we can detect when the track has closed (in response to an event / error internal to the - // peer or due to calling RemoveStream) - c.trackClosed = trackClosed - // the sub is the new parent of all subsequent subs until the number if subsriptions falls back to 0 - c.currentSubParentID = sub.ID - c.subParentToChildren[c.currentSubParentID] = []rtppassthrough.SubscriptionID{} - c.logger.CDebugw(ctx, "SubscribeRTP called AddStream and succeeded", "subID", sub.ID.String(), - "name", c.Name()) - } - c.subParentToChildren[c.currentSubParentID] = append(c.subParentToChildren[c.currentSubParentID], sub.ID) - // add the subscription to bufAndCBByID so the goroutine spawned by - // addOnTrackSubFunc can forward the packets it receives from the modular camera - // over WebRTC to the SubscribeRTP caller via the packetsCB callback - c.bufAndCBByID[sub.ID] = bufAndCB{ - cb: func(p *rtp.Packet) { packetsCB([]*rtp.Packet{p}) }, - buf: buf, - } - buf.Start() - g.Success() - c.logger.CDebugw(ctx, "SubscribeRTP succeeded", "subID", sub.ID.String(), - "name", c.Name(), "bufAndCBByID", c.bufAndCBByID.String()) - return sub, nil -} - -func (c *client) addOnTrackSubFunc( - trackReceived, trackClosed chan struct{}, - parentID rtppassthrough.SubscriptionID, -) rdkgrpc.OnTrackCB { - return func(tr *webrtc.TrackRemote, r *webrtc.RTPReceiver) { - close(trackReceived) - c.activeBackgroundWorkers.Add(1) - goutils.ManagedGo(func() { - for { - if c.ctx.Err() != nil { - c.logger.Debugw("SubscribeRTP: camera client", "name ", c.Name(), "parentID", parentID.String(), - "OnTrack callback terminating as the client is closing") - close(trackClosed) - return - } - - pkt, _, err := tr.ReadRTP() - if err != nil { - close(trackClosed) - // NOTE: (Nick S) We need to remember which subscriptions are consuming packets - // from to which tr *webrtc.TrackRemote so that we can terminate the child subscriptions - // when their track terminate. - c.unsubscribeChildrenSubs(parentID) - if errors.Is(err, io.EOF) { - c.logger.Debugw("SubscribeRTP: camera client", "name ", c.Name(), "parentID", parentID.String(), - "OnTrack callback terminating ReadRTP loop due to ", err.Error()) - return - } - c.logger.Errorw("SubscribeRTP: camera client", "name ", c.Name(), "parentID", parentID.String(), - "OnTrack callback hit unexpected error from ReadRTP err:", err.Error()) - return - } - - c.mu.Lock() - for _, tmp := range c.bufAndCBByID { - // This is needed to prevent the problem described here: - // https://go.dev/blog/loopvar-preview - bufAndCB := tmp - err := bufAndCB.buf.Publish(func() { bufAndCB.cb(pkt) }) - if err != nil { - c.logger.Debugw("SubscribeRTP: camera client", - "name", c.Name(), - "parentID", parentID.String(), - "dropped an RTP packet dropped due to", - "err", err.Error()) - } - } - c.mu.Unlock() - } - }, c.activeBackgroundWorkers.Done) - } -} - -// Unsubscribe terminates a subscription to receive RTP packets. -// It is strongly recommended to set a timeout on ctx as the underlying -// WebRTC peer connection's Track can be removed before sending an RTP packet which -// results in Unsubscribe blocking until the provided -// context is cancelled or the client is closed. -// This rare condition may happen in cases like reconfiguring concurrently with -// an Unsubscribe call. -func (c *client) Unsubscribe(ctx context.Context, id rtppassthrough.SubscriptionID) error { - ctx, span := trace.StartSpan(ctx, "camera::client::Unsubscribe") - defer span.End() - c.mu.Lock() - - if c.conn.PeerConn() == nil { - c.mu.Unlock() - return ErrNoPeerConnection - } - - _, ok := c.conn.(*rdkgrpc.SharedConn) - if !ok { - c.mu.Unlock() - return ErrNoSharedPeerConnection - } - c.logger.CDebugw(ctx, "Unsubscribe called with", "name", c.Name(), "subID", id.String()) - - bufAndCB, ok := c.bufAndCBByID[id] - if !ok { - c.logger.CWarnw(ctx, "Unsubscribe called with unknown subID ", "name", c.Name(), "subID", id.String()) - c.mu.Unlock() - return ErrUnknownSubscriptionID - } - - if len(c.bufAndCBByID) == 1 { - c.logger.CDebugw(ctx, "Unsubscribe calling RemoveStream", "name", c.Name(), "subID", id.String()) - if _, err := c.streamClient.RemoveStream(ctx, &streampb.RemoveStreamRequest{Name: c.Name().String()}); err != nil { - c.logger.CWarnw(ctx, "Unsubscribe RemoveStream returned err", "name", c.Name(), "subID", id.String(), "err", err) - c.mu.Unlock() - return err - } - - delete(c.bufAndCBByID, id) - bufAndCB.buf.Close() - - // unlock so that the OnTrack callback can get the lock if it needs to before the ReadRTP method returns an error - // which will close `c.trackClosed`. - c.mu.Unlock() - - select { - case <-ctx.Done(): - err := errors.Wrap(ctx.Err(), "track not closed within Unsubscribe provided context. Subscription may be left in broken state") - c.logger.Error(err) - return err - case <-c.ctx.Done(): - err := errors.Wrap(c.ctx.Err(), "track not closed before client closed") - c.logger.Error(err) - return err - case <-c.trackClosed: - } - return nil - } - c.mu.Unlock() - - delete(c.bufAndCBByID, id) - bufAndCB.buf.Close() - - return nil -} - -func (c *client) unsubscribeAll() { - if len(c.bufAndCBByID) > 0 { - for id, bufAndCB := range c.bufAndCBByID { - c.logger.Debugw("unsubscribeAll terminating sub", "name", c.Name(), "subID", id.String()) - delete(c.bufAndCBByID, id) - bufAndCB.buf.Close() - } - } -} - -func (c *client) unsubscribeChildrenSubs(parentID rtppassthrough.SubscriptionID) { - c.mu.Lock() - defer c.mu.Unlock() - c.logger.Debugw("client unsubscribeChildrenSubs called", "name", c.Name(), "parentID", parentID.String()) - defer c.logger.Debugw("client unsubscribeChildrenSubs done", "name", c.Name(), "parentID", parentID.String()) - for _, childID := range c.subParentToChildren[parentID] { - bufAndCB, ok := c.bufAndCBByID[childID] - if !ok { - continue - } - c.logger.Debugw("stopping child subscription", "name", c.Name(), "parentID", parentID.String(), "childID", childID.String()) - delete(c.bufAndCBByID, childID) - bufAndCB.buf.Close() - } - delete(c.subParentToChildren, parentID) -} - -func (s bufAndCBByID) String() string { - if len(s) == 0 { - return "len: 0" - } - strIds := []string{} - strIdsToCB := map[string]bufAndCB{} - for id, cb := range s { - strID := id.String() - strIds = append(strIds, strID) - strIdsToCB[strID] = cb - } - slices.Sort(strIds) - ret := fmt.Sprintf("len: %d, ", len(s)) - for _, strID := range strIds { - ret += fmt.Sprintf("%s: %v, ", strID, strIdsToCB[strID]) - } - return ret -} diff --git a/components/camera/client_test.go b/components/camera/client_test.go deleted file mode 100644 index a23bfc37972..00000000000 --- a/components/camera/client_test.go +++ /dev/null @@ -1,679 +0,0 @@ -package camera_test - -import ( - "bytes" - "context" - "image" - "image/color" - "image/png" - "net" - "sync" - "testing" - "time" - - "github.com/pion/rtp" - "go.viam.com/test" - "go.viam.com/utils/rpc" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/data" - "go.viam.com/rdk/gostream" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" - rutils "go.viam.com/rdk/utils" - "go.viam.com/rdk/utils/contextutils" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectCamera := &inject.Camera{} - img := image.NewNRGBA(image.Rect(0, 0, 4, 4)) - - var imgBuf bytes.Buffer - test.That(t, png.Encode(&imgBuf, img), test.ShouldBeNil) - imgPng, err := png.Decode(bytes.NewReader(imgBuf.Bytes())) - test.That(t, err, test.ShouldBeNil) - - pcA := pointcloud.New() - err = pcA.Set(pointcloud.NewVector(5, 5, 5), nil) - test.That(t, err, test.ShouldBeNil) - - var projA transform.Projector - intrinsics := &transform.PinholeCameraIntrinsics{ // not the real camera parameters -- fake for test - Width: 1280, - Height: 720, - Fx: 200, - Fy: 200, - Ppx: 100, - Ppy: 100, - } - projA = intrinsics - - var imageReleased bool - var imageReleasedMu sync.Mutex - // color camera - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pcA, nil - } - injectCamera.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{ - SupportsPCD: true, - IntrinsicParams: intrinsics, - }, nil - } - injectCamera.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return projA, nil - } - injectCamera.ImagesFunc = func(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - images := []camera.NamedImage{} - // one color image - color := rimage.NewImage(40, 50) - images = append(images, camera.NamedImage{color, "color"}) - // one depth image - depth := rimage.NewEmptyDepthMap(10, 20) - images = append(images, camera.NamedImage{depth, "depth"}) - // a timestamp of 12345 - ts := time.UnixMilli(12345) - return images, resource.ResponseMetadata{CapturedAt: ts}, nil - } - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader(gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - imageReleasedMu.Lock() - imageReleased = true - imageReleasedMu.Unlock() - return imgPng, func() {}, nil - })), nil - } - // depth camera - injectCameraDepth := &inject.Camera{} - depthImg := rimage.NewEmptyDepthMap(10, 20) - depthImg.Set(0, 0, rimage.Depth(40)) - depthImg.Set(0, 1, rimage.Depth(1)) - depthImg.Set(5, 6, rimage.Depth(190)) - depthImg.Set(9, 12, rimage.Depth(3000)) - depthImg.Set(5, 9, rimage.MaxDepth-rimage.Depth(1)) - injectCameraDepth.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pcA, nil - } - injectCameraDepth.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{ - SupportsPCD: true, - IntrinsicParams: intrinsics, - }, nil - } - injectCameraDepth.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return projA, nil - } - injectCameraDepth.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader(gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - imageReleasedMu.Lock() - imageReleased = true - imageReleasedMu.Unlock() - return depthImg, func() {}, nil - })), nil - } - // bad camera - injectCamera2 := &inject.Camera{} - injectCamera2.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return nil, errGeneratePointCloudFailed - } - injectCamera2.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, errPropertiesFailed - } - injectCamera2.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return nil, errCameraProjectorFailed - } - injectCamera2.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return nil, errStreamFailed - } - - resources := map[resource.Name]camera.Camera{ - camera.Named(testCameraName): injectCamera, - camera.Named(failCameraName): injectCamera2, - camera.Named(depthCameraName): injectCameraDepth, - } - cameraSvc, err := resource.NewAPIResourceCollection(camera.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[camera.Camera](camera.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, cameraSvc), test.ShouldBeNil) - - injectCamera.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("camera client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - camera1Client, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named(testCameraName), logger) - test.That(t, err, test.ShouldBeNil) - ctx := gostream.WithMIMETypeHint(context.Background(), rutils.MimeTypeRawRGBA) - frame, _, err := camera.ReadImage(ctx, camera1Client) - test.That(t, err, test.ShouldBeNil) - compVal, _, err := rimage.CompareImages(img, frame) - test.That(t, err, test.ShouldBeNil) - test.That(t, compVal, test.ShouldEqual, 0) // exact copy, no color conversion - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - - pcB, err := camera1Client.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - _, got := pcB.At(5, 5, 5) - test.That(t, got, test.ShouldBeTrue) - - projB, err := camera1Client.Projector(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, projB, test.ShouldNotBeNil) - - propsB, err := camera1Client.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, propsB.SupportsPCD, test.ShouldBeTrue) - test.That(t, propsB.IntrinsicParams, test.ShouldResemble, intrinsics) - - images, meta, err := camera1Client.Images(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, meta.CapturedAt, test.ShouldEqual, time.UnixMilli(12345)) - test.That(t, len(images), test.ShouldEqual, 2) - test.That(t, images[0].SourceName, test.ShouldEqual, "color") - test.That(t, images[0].Image.Bounds().Dx(), test.ShouldEqual, 40) - test.That(t, images[0].Image.Bounds().Dy(), test.ShouldEqual, 50) - test.That(t, images[0].Image, test.ShouldHaveSameTypeAs, &rimage.LazyEncodedImage{}) - test.That(t, images[0].Image.ColorModel(), test.ShouldHaveSameTypeAs, color.RGBAModel) - test.That(t, images[1].SourceName, test.ShouldEqual, "depth") - test.That(t, images[1].Image.Bounds().Dx(), test.ShouldEqual, 10) - test.That(t, images[1].Image.Bounds().Dy(), test.ShouldEqual, 20) - test.That(t, images[1].Image, test.ShouldHaveSameTypeAs, &rimage.LazyEncodedImage{}) - test.That(t, images[1].Image.ColorModel(), test.ShouldHaveSameTypeAs, color.Gray16Model) - - // Do - resp, err := camera1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, camera1Client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - t.Run("camera client depth", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := resourceAPI.RPCClient(context.Background(), conn, "", camera.Named(depthCameraName), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := gostream.WithMIMETypeHint( - context.Background(), rutils.WithLazyMIMEType(rutils.MimeTypePNG)) - frame, _, err := camera.ReadImage(ctx, client) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), frame) - test.That(t, err, test.ShouldBeNil) - test.That(t, dm, test.ShouldResemble, depthImg) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("camera client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", camera.Named(failCameraName), logger) - test.That(t, err, test.ShouldBeNil) - - _, _, err = camera.ReadImage(context.Background(), client2) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - _, err = client2.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGeneratePointCloudFailed.Error()) - - _, err = client2.Projector(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCameraProjectorFailed.Error()) - - _, err = client2.Properties(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPropertiesFailed.Error()) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - t.Run("camera client extra", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - camClient, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named(testCameraName), logger) - test.That(t, err, test.ShouldBeNil) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, extra, test.ShouldBeEmpty) - return nil, errStreamFailed - } - - ctx := context.Background() - _, _, err = camera.ReadImage(ctx, camClient) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(extra), test.ShouldEqual, 1) - test.That(t, extra["hello"], test.ShouldEqual, "world") - return nil, errStreamFailed - } - - // one kvp created with camera.Extra - ext := camera.Extra{"hello": "world"} - ctx = camera.NewContext(ctx, ext) - _, _, err = camera.ReadImage(ctx, camClient) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(extra), test.ShouldEqual, 1) - test.That(t, extra[data.FromDMString], test.ShouldBeTrue) - - return nil, errStreamFailed - } - - // one kvp created with data.FromDMContextKey - ctx = context.WithValue(context.Background(), data.FromDMContextKey{}, true) - _, _, err = camera.ReadImage(ctx, camClient) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(extra), test.ShouldEqual, 2) - test.That(t, extra["hello"], test.ShouldEqual, "world") - test.That(t, extra[data.FromDMString], test.ShouldBeTrue) - return nil, errStreamFailed - } - - // merge values from data and camera - ext = camera.Extra{"hello": "world"} - ctx = camera.NewContext(ctx, ext) - _, _, err = camera.ReadImage(ctx, camClient) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} - -func TestClientProperties(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - - server, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectCamera := &inject.Camera{} - resources := map[resource.Name]camera.Camera{camera.Named(testCameraName): injectCamera} - svc, err := resource.NewAPIResourceCollection(camera.API, resources) - test.That(t, err, test.ShouldBeNil) - - rSubType, ok, err := resource.LookupAPIRegistration[camera.Camera](camera.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, rSubType.RegisterRPCService(context.Background(), server, svc), test.ShouldBeNil) - - go test.That(t, server.Serve(listener), test.ShouldBeNil) - defer func() { test.That(t, server.Stop(), test.ShouldBeNil) }() - - fakeIntrinsics := &transform.PinholeCameraIntrinsics{ - Width: 1, - Height: 1, - Fx: 1, - Fy: 1, - Ppx: 1, - Ppy: 1, - } - fakeDistortion := &transform.BrownConrady{ - RadialK1: 1.0, - RadialK2: 1.0, - RadialK3: 1.0, - TangentialP1: 1.0, - TangentialP2: 1.0, - } - - testCases := []struct { - name string - props camera.Properties - }{ - { - name: "non-nil properties", - props: camera.Properties{ - SupportsPCD: true, - ImageType: camera.UnspecifiedStream, - IntrinsicParams: fakeIntrinsics, - DistortionParams: fakeDistortion, - }, - }, { - name: "nil intrinsic params", - props: camera.Properties{ - SupportsPCD: true, - ImageType: camera.UnspecifiedStream, - IntrinsicParams: nil, - DistortionParams: fakeDistortion, - }, - }, { - name: "nil distortion parameters", - props: camera.Properties{ - SupportsPCD: true, - ImageType: camera.UnspecifiedStream, - IntrinsicParams: fakeIntrinsics, - DistortionParams: nil, - }, - }, { - name: "empty properties", - props: camera.Properties{}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - injectCamera.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return testCase.props, nil - } - - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - client, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named(testCameraName), logger) - test.That(t, err, test.ShouldBeNil) - actualProps, err := client.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualProps, test.ShouldResemble, testCase.props) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - } -} - -func TestClientLazyImage(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectCamera := &inject.Camera{} - img := image.NewNRGBA64(image.Rect(0, 0, 4, 8)) - - var imgBuf bytes.Buffer - test.That(t, png.Encode(&imgBuf, img), test.ShouldBeNil) - imgPng, err := png.Decode(bytes.NewReader(imgBuf.Bytes())) - test.That(t, err, test.ShouldBeNil) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader(gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - mimeType, _ := rutils.CheckLazyMIMEType(gostream.MIMETypeHint(ctx, rutils.MimeTypeRawRGBA)) - switch mimeType { - case rutils.MimeTypePNG: - return imgPng, func() {}, nil - default: - return nil, nil, errInvalidMimeType - } - })), nil - } - - resources := map[resource.Name]camera.Camera{ - camera.Named(testCameraName): injectCamera, - } - cameraSvc, err := resource.NewAPIResourceCollection(camera.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[camera.Camera](camera.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, cameraSvc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - camera1Client, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named(testCameraName), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := gostream.WithMIMETypeHint(context.Background(), rutils.MimeTypePNG) - frame, _, err := camera.ReadImage(ctx, camera1Client) - test.That(t, err, test.ShouldBeNil) - // Should always lazily decode - test.That(t, frame, test.ShouldHaveSameTypeAs, &rimage.LazyEncodedImage{}) - frameLazy := frame.(*rimage.LazyEncodedImage) - test.That(t, frameLazy.RawData(), test.ShouldResemble, imgBuf.Bytes()) - - ctx = gostream.WithMIMETypeHint(context.Background(), rutils.WithLazyMIMEType(rutils.MimeTypePNG)) - frame, _, err = camera.ReadImage(ctx, camera1Client) - test.That(t, err, test.ShouldBeNil) - test.That(t, frame, test.ShouldHaveSameTypeAs, &rimage.LazyEncodedImage{}) - frameLazy = frame.(*rimage.LazyEncodedImage) - test.That(t, frameLazy.RawData(), test.ShouldResemble, imgBuf.Bytes()) - - test.That(t, frameLazy.MIMEType(), test.ShouldEqual, rutils.MimeTypePNG) - compVal, _, err := rimage.CompareImages(img, frame) - test.That(t, err, test.ShouldBeNil) - test.That(t, compVal, test.ShouldEqual, 0) // exact copy, no color conversion - - test.That(t, conn.Close(), test.ShouldBeNil) -} - -func TestClientWithInterceptor(t *testing.T) { - // Set up gRPC server - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - // Set up camera that adds timestamps into the gRPC response header. - injectCamera := &inject.Camera{} - - pcA := pointcloud.New() - err = pcA.Set(pointcloud.NewVector(5, 5, 5), nil) - test.That(t, err, test.ShouldBeNil) - - k, v := "hello", "world" - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - var grpcMetadata metadata.MD = make(map[string][]string) - grpcMetadata.Set(k, v) - grpc.SendHeader(ctx, grpcMetadata) - return pcA, nil - } - - // Register CameraService API in our gRPC server. - resources := map[resource.Name]camera.Camera{ - camera.Named(testCameraName): injectCamera, - } - cameraSvc, err := resource.NewAPIResourceCollection(camera.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[camera.Camera](camera.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, cameraSvc), test.ShouldBeNil) - - // Start serving requests. - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // Set up gRPC client with context with metadata interceptor. - conn, err := viamgrpc.Dial( - context.Background(), - listener1.Addr().String(), - logger, - rpc.WithUnaryClientInterceptor(contextutils.ContextWithMetadataUnaryClientInterceptor), - ) - test.That(t, err, test.ShouldBeNil) - camera1Client, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named(testCameraName), logger) - test.That(t, err, test.ShouldBeNil) - - // Construct a ContextWithMetadata to pass into NextPointCloud and check that the - // interceptor correctly injected the metadata from the gRPC response header into the - // context. - ctx, md := contextutils.ContextWithMetadata(context.Background()) - pcB, err := camera1Client.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - _, got := pcB.At(5, 5, 5) - test.That(t, got, test.ShouldBeTrue) - test.That(t, md[k][0], test.ShouldEqual, v) - - test.That(t, conn.Close(), test.ShouldBeNil) -} - -func TestClientStreamAfterClose(t *testing.T) { - // Set up gRPC server - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - // Set up camera that can stream images - img := image.NewNRGBA(image.Rect(0, 0, 4, 4)) - injectCamera := &inject.Camera{} - injectCamera.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader(gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - return img, func() {}, nil - })), nil - } - - // Register CameraService API in our gRPC server. - resources := map[resource.Name]camera.Camera{ - camera.Named(testCameraName): injectCamera, - } - cameraSvc, err := resource.NewAPIResourceCollection(camera.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[camera.Camera](camera.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, cameraSvc), test.ShouldBeNil) - - // Start serving requests. - go rpcServer.Serve(listener) - defer rpcServer.Stop() - - // Make client connection - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named(testCameraName), logger) - test.That(t, err, test.ShouldBeNil) - - // Get a stream - stream, err := client.Stream(context.Background()) - test.That(t, stream, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Read from stream - media, _, err := stream.Next(context.Background()) - test.That(t, media, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Close client and read from stream - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - media, _, err = stream.Next(context.Background()) - test.That(t, media, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "context canceled") - - // Get a new stream - stream, err = client.Stream(context.Background()) - test.That(t, stream, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Read from the new stream - media, _, err = stream.Next(context.Background()) - test.That(t, media, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - // Close client and connection - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) -} - -// See modmanager_test.go for the happy path (aka, when the -// client has a webrtc connection). -func TestRTPPassthroughWithoutWebRTC(t *testing.T) { - logger := logging.NewTestLogger(t) - camName := "rtp_passthrough_camera" - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectCamera := &inject.Camera{} - resources := map[resource.Name]camera.Camera{ - camera.Named(camName): injectCamera, - } - cameraSvc, err := resource.NewAPIResourceCollection(camera.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[camera.Camera](camera.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, cameraSvc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("rtp_passthrough client fails without webrtc connection", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - camera1Client, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named(camName), logger) - test.That(t, err, test.ShouldBeNil) - rtpPassthroughClient, ok := camera1Client.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - sub, err := rtpPassthroughClient.SubscribeRTP(context.Background(), 512, func(pkts []*rtp.Packet) { - t.Log("should not be called") - t.FailNow() - }) - test.That(t, err, test.ShouldBeError, camera.ErrNoPeerConnection) - test.That(t, sub, test.ShouldResemble, rtppassthrough.NilSubscription) - err = rtpPassthroughClient.Unsubscribe(context.Background(), rtppassthrough.NilSubscription.ID) - test.That(t, err, test.ShouldBeError, camera.ErrNoPeerConnection) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/camera/collectors.go b/components/camera/collectors.go deleted file mode 100644 index 2d6ee21da27..00000000000 --- a/components/camera/collectors.go +++ /dev/null @@ -1,175 +0,0 @@ -package camera - -import ( - "bytes" - "context" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - pb "go.viam.com/api/component/camera/v1" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/types/known/wrapperspb" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/utils" -) - -type method int64 - -const ( - nextPointCloud method = iota - readImage - getImages -) - -func (m method) String() string { - switch m { - case nextPointCloud: - return "NextPointCloud" - case readImage: - return "ReadImage" - case getImages: - return "GetImages" - } - return "Unknown" -} - -// TODO: add tests for this file. - -func newNextPointCloudCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - camera, err := assertCamera(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - _, span := trace.StartSpan(ctx, "camera::data::collector::CaptureFunc::NextPointCloud") - defer span.End() - - ctx = context.WithValue(ctx, data.FromDMContextKey{}, true) - - v, err := camera.NextPointCloud(ctx) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, nextPointCloud.String(), err) - } - - var buf bytes.Buffer - headerSize := 200 - if v != nil { - buf.Grow(headerSize + v.Size()*4*4) // 4 numbers per point, each 4 bytes - err = pointcloud.ToPCD(v, &buf, pointcloud.PCDBinary) - if err != nil { - return nil, errors.Errorf("failed to convert returned point cloud to PCD: %v", err) - } - } - return buf.Bytes(), nil - }) - return data.NewCollector(cFunc, params) -} - -func newReadImageCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - camera, err := assertCamera(resource) - if err != nil { - return nil, err - } - // choose the best/fastest representation - mimeType := params.MethodParams["mime_type"] - if mimeType == nil { - // TODO: Potentially log the actual mime type at collector instantiation or include in response. - strWrapper := wrapperspb.String(utils.MimeTypeRawRGBA) - mimeType, err = anypb.New(strWrapper) - if err != nil { - return nil, err - } - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - _, span := trace.StartSpan(ctx, "camera::data::collector::CaptureFunc::ReadImage") - defer span.End() - - ctx = context.WithValue(ctx, data.FromDMContextKey{}, true) - - img, release, err := ReadImage(ctx, camera) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - - return nil, data.FailedToReadErr(params.ComponentName, readImage.String(), err) - } - defer func() { - if release != nil { - release() - } - }() - - mimeStr := new(wrapperspb.StringValue) - if err := mimeType.UnmarshalTo(mimeStr); err != nil { - return nil, err - } - - outBytes, err := rimage.EncodeImage(ctx, img, mimeStr.Value) - if err != nil { - return nil, err - } - return outBytes, nil - }) - return data.NewCollector(cFunc, params) -} - -func newGetImagesCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - camera, err := assertCamera(resource) - if err != nil { - return nil, err - } - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - _, span := trace.StartSpan(ctx, "camera::data::collector::CaptureFunc::GetImages") - defer span.End() - - ctx = context.WithValue(ctx, data.FromDMContextKey{}, true) - - resImgs, resMetadata, err := camera.Images(ctx) - if err != nil { - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, getImages.String(), err) - } - - var imgsConverted []*pb.Image - for _, img := range resImgs { - format, imgBytes, err := encodeImageFromUnderlyingType(ctx, img.Image) - if err != nil { - return nil, err - } - imgPb := &pb.Image{ - SourceName: img.SourceName, - Format: format, - Image: imgBytes, - } - imgsConverted = append(imgsConverted, imgPb) - } - return pb.GetImagesResponse{ - ResponseMetadata: resMetadata.AsProto(), - Images: imgsConverted, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -func assertCamera(resource interface{}) (Camera, error) { - cam, ok := resource.(Camera) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - return cam, nil -} diff --git a/components/camera/config.go b/components/camera/config.go deleted file mode 100644 index f1bc2fa5bae..00000000000 --- a/components/camera/config.go +++ /dev/null @@ -1,20 +0,0 @@ -package camera - -import ( - "github.com/pkg/errors" -) - -// ImageType specifies what kind of image stream is coming from the camera. -type ImageType string - -// The allowed types of streams that can come from a VideoSource. -const ( - UnspecifiedStream = ImageType("") - ColorStream = ImageType("color") - DepthStream = ImageType("depth") -) - -// NewUnsupportedImageTypeError is when the stream type is unknown. -func NewUnsupportedImageTypeError(s ImageType) error { - return errors.Errorf("image type %q not supported", s) -} diff --git a/components/camera/extra.go b/components/camera/extra.go deleted file mode 100644 index 35dd04fbfb9..00000000000 --- a/components/camera/extra.go +++ /dev/null @@ -1,22 +0,0 @@ -package camera - -import "context" - -// Extra is the type of value stored in the Contexts. -type ( - Extra map[string]interface{} - key int -) - -var extraKey key - -// NewContext returns a new Context that carries value Extra. -func NewContext(ctx context.Context, e Extra) context.Context { - return context.WithValue(ctx, extraKey, e) -} - -// FromContext returns the Extra value stored in ctx, if any. -func FromContext(ctx context.Context) (Extra, bool) { - ext, ok := ctx.Value(extraKey).(Extra) - return ext, ok -} diff --git a/components/camera/extra_test.go b/components/camera/extra_test.go deleted file mode 100644 index be0f0a3f81a..00000000000 --- a/components/camera/extra_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package camera - -import ( - "context" - "testing" - - "go.viam.com/test" -) - -func TestExtraEmpty(t *testing.T) { - ctx := context.Background() - _, ok := FromContext(ctx) - test.That(t, ok, test.ShouldBeFalse) -} - -func TestExtraRoundtrip(t *testing.T) { - ctx := context.Background() - expected := Extra{ - "It goes one": "by one", - "even two": "by two", - } - - ctx = NewContext(ctx, expected) - actual, ok := FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, actual, test.ShouldEqual, expected) -} diff --git a/components/camera/fake/camera.go b/components/camera/fake/camera.go deleted file mode 100644 index 9353955520f..00000000000 --- a/components/camera/fake/camera.go +++ /dev/null @@ -1,435 +0,0 @@ -// Package fake implements a fake camera which always returns the same image with a user specified resolution. -package fake - -import ( - "bytes" - "context" - "encoding/base64" - "image" - "image/color" - "image/jpeg" - "math" - "os" - "sync" - "time" - - "github.com/bluenviron/gortsplib/v4/pkg/format" - "github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" - "github.com/bluenviron/gortsplib/v4/pkg/rtptime" - "github.com/bluenviron/mediacommon/pkg/codecs/h264" - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - rutils "go.viam.com/rdk/utils" -) - -var ( - // Model is the model of the fake buildin camera. - Model = resource.DefaultModelFamily.WithModel("fake") - // ErrRTPPassthroughNotEnabled indicates that rtp_passthrough is not enabled. - ErrRTPPassthroughNotEnabled = errors.New("rtp_passthrough not enabled") -) - -const ( - initialWidth = 1280 - initialHeight = 720 -) - -func init() { - resource.RegisterComponent( - camera.API, - Model, - resource.Registration[camera.Camera, *Config]{Constructor: NewCamera}, - ) -} - -// NewCamera returns a new fake camera. -func NewCamera( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (camera.Camera, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - _, paramErr := newConf.Validate("") - if paramErr != nil { - return nil, paramErr - } - resModel, width, height := fakeModel(newConf.Width, newConf.Height) - cancelCtx, cancelFn := context.WithCancel(context.Background()) - cam := &Camera{ - ctx: cancelCtx, - cancelFn: cancelFn, - Named: conf.ResourceName().AsNamed(), - Model: resModel, - Width: width, - Height: height, - Animated: newConf.Animated, - RTPPassthrough: newConf.RTPPassthrough, - bufAndCBByID: make(map[rtppassthrough.SubscriptionID]bufAndCB), - logger: logger, - } - src, err := camera.NewVideoSourceFromReader(ctx, cam, resModel, camera.ColorStream) - if err != nil { - return nil, err - } - - if cam.RTPPassthrough { - msg := "rtp_passthrough is enabled. GetImage will ignore width, height, and animated config params" - logger.CWarn(ctx, msg) - worldJpegBase64, err := os.ReadFile(rutils.ResolveFile("components/camera/fake/worldJpeg.base64")) - if err != nil { - return nil, err - } - d := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(worldJpegBase64)) - img, err := jpeg.Decode(d) - if err != nil { - return nil, err - } - - cam.cacheImage = img - if err := cam.startPassthrough(); err != nil { - return nil, err - } - } - return camera.FromVideoSource(conf.ResourceName(), src, logger), nil -} - -// Config are the attributes of the fake camera config. -type Config struct { - Width int `json:"width,omitempty"` - Height int `json:"height,omitempty"` - Animated bool `json:"animated,omitempty"` - RTPPassthrough bool `json:"rtp_passthrough,omitempty"` -} - -// Validate checks that the config attributes are valid for a fake camera. -func (conf *Config) Validate(path string) ([]string, error) { - if conf.Height > 10000 || conf.Width > 10000 { - return nil, errors.New("maximum supported pixel height or width for fake cameras is 10000 pixels") - } - - if conf.Height < 0 || conf.Width < 0 { - return nil, errors.New("cannot use negative pixel height and width for fake cameras") - } - - if conf.Height%2 != 0 { - return nil, errors.Errorf("odd-number resolutions cannot be rendered, cannot use a height of %d", conf.Height) - } - - if conf.Width%2 != 0 { - return nil, errors.Errorf("odd-number resolutions cannot be rendered, cannot use a width of %d", conf.Width) - } - - return nil, nil -} - -var fakeIntrinsics = &transform.PinholeCameraIntrinsics{ - Width: 1024, - Height: 768, - Fx: 821.32642889, - Fy: 821.68607359, - Ppx: 494.95941428, - Ppy: 370.70529534, -} - -var fakeDistortion = &transform.BrownConrady{ - RadialK1: 0.11297234, - RadialK2: -0.21375332, - RadialK3: -0.01584774, - TangentialP1: -0.00302002, - TangentialP2: 0.19969297, -} - -func fakeModel(width, height int) (*transform.PinholeCameraModel, int, int) { - fakeModelReshaped := &transform.PinholeCameraModel{ - PinholeCameraIntrinsics: fakeIntrinsics, - Distortion: fakeDistortion, - } - switch { - case width > 0 && height > 0: - widthRatio := float64(width) / float64(initialWidth) - heightRatio := float64(height) / float64(initialHeight) - intrinsics := &transform.PinholeCameraIntrinsics{ - Width: int(float64(fakeIntrinsics.Width) * widthRatio), - Height: int(float64(fakeIntrinsics.Height) * heightRatio), - Fx: fakeIntrinsics.Fx * widthRatio, - Fy: fakeIntrinsics.Fy * heightRatio, - Ppx: fakeIntrinsics.Ppx * widthRatio, - Ppy: fakeIntrinsics.Ppy * heightRatio, - } - fakeModelReshaped.PinholeCameraIntrinsics = intrinsics - return fakeModelReshaped, width, height - case width > 0 && height <= 0: - ratio := float64(width) / float64(initialWidth) - intrinsics := &transform.PinholeCameraIntrinsics{ - Width: int(float64(fakeIntrinsics.Width) * ratio), - Height: int(float64(fakeIntrinsics.Height) * ratio), - Fx: fakeIntrinsics.Fx * ratio, - Fy: fakeIntrinsics.Fy * ratio, - Ppx: fakeIntrinsics.Ppx * ratio, - Ppy: fakeIntrinsics.Ppy * ratio, - } - fakeModelReshaped.PinholeCameraIntrinsics = intrinsics - newHeight := int(float64(initialHeight) * ratio) - if newHeight%2 != 0 { - newHeight++ - } - return fakeModelReshaped, width, newHeight - case width <= 0 && height > 0: - ratio := float64(height) / float64(initialHeight) - intrinsics := &transform.PinholeCameraIntrinsics{ - Width: int(float64(fakeIntrinsics.Width) * ratio), - Height: int(float64(fakeIntrinsics.Height) * ratio), - Fx: fakeIntrinsics.Fx * ratio, - Fy: fakeIntrinsics.Fy * ratio, - Ppx: fakeIntrinsics.Ppx * ratio, - Ppy: fakeIntrinsics.Ppy * ratio, - } - fakeModelReshaped.PinholeCameraIntrinsics = intrinsics - newWidth := int(float64(initialWidth) * ratio) - if newWidth%2 != 0 { - newWidth++ - } - return fakeModelReshaped, newWidth, height - default: - return fakeModelReshaped, initialWidth, initialHeight - } -} - -// Camera is a fake camera that always returns the same image. -type Camera struct { - resource.Named - resource.AlwaysRebuild - mu sync.RWMutex - Model *transform.PinholeCameraModel - Width int - Height int - Animated bool - RTPPassthrough bool - ctx context.Context - cancelFn context.CancelFunc - activeBackgroundWorkers sync.WaitGroup - bufAndCBByID map[rtppassthrough.SubscriptionID]bufAndCB - cacheImage image.Image - cachePointCloud pointcloud.PointCloud - logger logging.Logger -} - -// Read always returns the same image of a yellow to blue gradient. -func (c *Camera) Read(ctx context.Context) (image.Image, func(), error) { - if c.cacheImage != nil { - return c.cacheImage, func() {}, nil - } - width := float64(c.Width) - height := float64(c.Height) - img := image.NewRGBA(image.Rect(0, 0, c.Width, c.Height)) - - totalDist := math.Sqrt(math.Pow(0-width, 2) + math.Pow(0-height, 2)) - - tick := time.Now().UnixMilli() / 20 - var x, y float64 - for x = 0; x < width; x++ { - for y = 0; y < height; y++ { - dist := math.Sqrt(math.Pow(0-x, 2) + math.Pow(0-y, 2)) - dist /= totalDist - thisColor := color.RGBA{uint8(255 - (255 * dist)), uint8(255 - (255 * dist)), uint8(0 + (255 * dist)), 255} - - var px, py int - if c.Animated { - px = int(int64(x)+tick) % int(width) - py = int(y) - } else { - px, py = int(x), int(y) - } - img.Set(px, py, thisColor) - } - } - if !c.Animated { - c.cacheImage = img - } - return rimage.ConvertImage(img), func() {}, nil -} - -// NextPointCloud always returns a pointcloud of a yellow to blue gradient, with the depth determined by the intensity of blue. -func (c *Camera) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - if c.cachePointCloud != nil { - return c.cachePointCloud, nil - } - dm := pointcloud.New() - width := float64(c.Width) - height := float64(c.Height) - - totalDist := math.Sqrt(math.Pow(0-width, 2) + math.Pow(0-height, 2)) - - var x, y float64 - for x = 0; x < width; x++ { - for y = 0; y < height; y++ { - dist := math.Sqrt(math.Pow(0-x, 2) + math.Pow(0-y, 2)) - dist /= totalDist - thisColor := color.NRGBA{uint8(255 - (255 * dist)), uint8(255 - (255 * dist)), uint8(0 + (255 * dist)), 255} - err := dm.Set(r3.Vector{X: x, Y: y, Z: 255 * dist}, pointcloud.NewColoredData(thisColor)) - if err != nil { - return nil, err - } - } - } - c.cachePointCloud = dm - return dm, nil -} - -type bufAndCB struct { - cb rtppassthrough.PacketCallback - buf *rtppassthrough.Buffer -} - -// SubscribeRTP begins a subscription to receive RTP packets. -func (c *Camera) SubscribeRTP( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, -) (rtppassthrough.Subscription, error) { - if !c.RTPPassthrough { - return rtppassthrough.NilSubscription, ErrRTPPassthroughNotEnabled - } - c.mu.Lock() - defer c.mu.Unlock() - - sub, buf, err := rtppassthrough.NewSubscription(bufferSize) - if err != nil { - return rtppassthrough.NilSubscription, err - } - webrtcPayloadMaxSize := 1188 // 1200 - 12 (RTP header) - encoder := &rtph264.Encoder{ - PayloadType: 96, - PayloadMaxSize: webrtcPayloadMaxSize, - } - - if err := encoder.Init(); err != nil { - buf.Close() - return rtppassthrough.NilSubscription, err - } - - c.bufAndCBByID[sub.ID] = bufAndCB{ - cb: packetsCB, - buf: buf, - } - buf.Start() - return sub, nil -} - -// Unsubscribe terminates the subscription. -func (c *Camera) Unsubscribe(ctx context.Context, id rtppassthrough.SubscriptionID) error { - if !c.RTPPassthrough { - return ErrRTPPassthroughNotEnabled - } - c.mu.Lock() - defer c.mu.Unlock() - bufAndCB, ok := c.bufAndCBByID[id] - if !ok { - return errors.New("id not found") - } - delete(c.bufAndCBByID, id) - bufAndCB.buf.Close() - return nil -} - -func (c *Camera) startPassthrough() error { - forma := &format.H264{ - PayloadTyp: 96, - PacketizationMode: 1, - SPS: []uint8{ - 0x67, 0x64, 0x0, 0x15, 0xac, 0xb2, 0x3, 0xc1, 0x1f, 0xd6, - 0x2, 0xdc, 0x8, 0x8, 0x16, 0x94, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, 0x0, 0x3, 0x0, - 0xf0, 0x3c, 0x58, 0xb9, 0x20, - }, - PPS: []uint8{0x68, 0xeb, 0xc3, 0xcb, 0x22, 0xc0}, - } - rtpEnc, err := forma.CreateEncoder() - if err != nil { - c.logger.Error(err.Error()) - return err - } - rtpTime := &rtptime.Encoder{ClockRate: forma.ClockRate()} - err = rtpTime.Initialize() - if err != nil { - c.logger.Error(err.Error()) - return err - } - start := time.Now() - worldH264Base64, err := os.ReadFile(rutils.ResolveFile("components/camera/fake/worldH264.base64")) - if err != nil { - return err - } - b, err := base64.StdEncoding.DecodeString(string(worldH264Base64)) - if err != nil { - c.logger.Error(err.Error()) - return err - } - aus, err := h264.AnnexBUnmarshal(b) - if err != nil { - c.logger.Error(err.Error()) - return err - } - f := func() { - defer c.unsubscribeAll() - ticker := time.NewTicker(200 * time.Millisecond) - defer ticker.Stop() - for range ticker.C { - if c.ctx.Err() != nil { - return - } - - pkts, err := rtpEnc.Encode(aus) - if err != nil { - c.logger.Error(err) - return - } - - ts := rtpTime.Encode(time.Since(start)) - for _, pkt := range pkts { - pkt.Timestamp = ts - } - - // get current timestamp - c.mu.RLock() - for _, bufAndCB := range c.bufAndCBByID { - // write packets - if err := bufAndCB.buf.Publish(func() { bufAndCB.cb(pkts) }); err != nil { - c.logger.Warn("Publish err: %s", err.Error()) - } - } - c.mu.RUnlock() - } - } - c.activeBackgroundWorkers.Add(1) - utils.ManagedGo(f, c.activeBackgroundWorkers.Done) - return nil -} - -func (c *Camera) unsubscribeAll() { - c.mu.Lock() - defer c.mu.Unlock() - for id, bufAndCB := range c.bufAndCBByID { - delete(c.bufAndCBByID, id) - bufAndCB.buf.Close() - } -} - -// Close does nothing. -func (c *Camera) Close(ctx context.Context) error { - c.cancelFn() - c.activeBackgroundWorkers.Wait() - return nil -} diff --git a/components/camera/fake/camera_test.go b/components/camera/fake/camera_test.go deleted file mode 100644 index 9bfe7a07dda..00000000000 --- a/components/camera/fake/camera_test.go +++ /dev/null @@ -1,253 +0,0 @@ -package fake - -import ( - "context" - "errors" - "image" - "sync/atomic" - "testing" - - "github.com/google/uuid" - "github.com/pion/rtp" - "go.viam.com/test" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" -) - -//nolint:dupl -func TestFakeCameraHighResolution(t *testing.T) { - model, width, height := fakeModel(1280, 720) - cancelCtx, cancelFn := context.WithCancel(context.Background()) - camOri := &Camera{ - ctx: cancelCtx, cancelFn: cancelFn, - Named: camera.Named("test_high").AsNamed(), Model: model, Width: width, Height: height, - } - src, err := camera.NewVideoSourceFromReader(context.Background(), camOri, model, camera.ColorStream) - test.That(t, err, test.ShouldBeNil) - cameraTest(t, src, 1280, 720, 921600, model.PinholeCameraIntrinsics, model.Distortion) - // (0,0) entry defaults to (1280, 720) - model, width, height = fakeModel(0, 0) - cancelCtx2, cancelFn2 := context.WithCancel(context.Background()) - camOri = &Camera{ - ctx: cancelCtx2, cancelFn: cancelFn2, - Named: camera.Named("test_high_zero").AsNamed(), Model: model, Width: width, Height: height, - } - src, err = camera.NewVideoSourceFromReader(context.Background(), camOri, model, camera.ColorStream) - test.That(t, err, test.ShouldBeNil) - cameraTest(t, src, 1280, 720, 921600, model.PinholeCameraIntrinsics, model.Distortion) -} - -func TestFakeCameraMedResolution(t *testing.T) { - model, width, height := fakeModel(640, 360) - cancelCtx, cancelFn := context.WithCancel(context.Background()) - camOri := &Camera{ - ctx: cancelCtx, cancelFn: cancelFn, - Named: camera.Named("test_high").AsNamed(), Model: model, Width: width, Height: height, - } - src, err := camera.NewVideoSourceFromReader(context.Background(), camOri, model, camera.ColorStream) - test.That(t, err, test.ShouldBeNil) - cameraTest(t, src, 640, 360, 230400, model.PinholeCameraIntrinsics, model.Distortion) - err = src.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -//nolint:dupl -func TestFakeCameraUnspecified(t *testing.T) { - // one unspecified side should keep 16:9 aspect ratio - // (320, 0) -> (320, 180) - model, width, height := fakeModel(320, 0) - cancelCtx, cancelFn := context.WithCancel(context.Background()) - camOri := &Camera{ - ctx: cancelCtx, cancelFn: cancelFn, - Named: camera.Named("test_320").AsNamed(), Model: model, Width: width, Height: height, - } - src, err := camera.NewVideoSourceFromReader(context.Background(), camOri, model, camera.ColorStream) - test.That(t, err, test.ShouldBeNil) - cameraTest(t, src, 320, 180, 57600, model.PinholeCameraIntrinsics, model.Distortion) - // (0, 180) -> (320, 180) - model, width, height = fakeModel(0, 180) - cancelCtx2, cancelFn2 := context.WithCancel(context.Background()) - camOri = &Camera{ - ctx: cancelCtx2, cancelFn: cancelFn2, - Named: camera.Named("test_180").AsNamed(), Model: model, Width: width, Height: height, - } - src, err = camera.NewVideoSourceFromReader(context.Background(), camOri, model, camera.ColorStream) - test.That(t, err, test.ShouldBeNil) - cameraTest(t, src, 320, 180, 57600, model.PinholeCameraIntrinsics, model.Distortion) -} - -func TestFakeCameraParams(t *testing.T) { - // test odd width and height - cfg := &Config{ - Width: 321, - Height: 0, - } - _, err := cfg.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - cfg = &Config{ - Width: 0, - Height: 321, - } - _, err = cfg.Validate("path") - test.That(t, err, test.ShouldNotBeNil) -} - -func cameraTest( - t *testing.T, - cam camera.VideoSource, - width, height, points int, - intrinsics *transform.PinholeCameraIntrinsics, - distortion transform.Distorter, -) { - t.Helper() - stream, err := cam.Stream(context.Background()) - test.That(t, err, test.ShouldBeNil) - img, _, err := stream.Next(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, img.Bounds().Dx(), test.ShouldEqual, width) - test.That(t, img.Bounds().Dy(), test.ShouldEqual, height) - pc, err := cam.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldEqual, points) - prop, err := cam.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.IntrinsicParams, test.ShouldResemble, intrinsics) - if distortion == nil { - test.That(t, prop.DistortionParams, test.ShouldBeNil) - } else { - test.That(t, prop.DistortionParams, test.ShouldResemble, distortion) - } - err = cam.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestCameraValidationAndCreation(t *testing.T) { - attrCfg := &Config{Width: 200000, Height: 10} - cfg := resource.Config{ - Name: "test1", - API: camera.API, - Model: Model, - ConvertedAttributes: attrCfg, - } - - // error with a ridiculously large pixel value - deps, err := cfg.Validate("", camera.API.SubtypeName) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, deps, test.ShouldBeNil) - - // error with a zero pixel value - attrCfg.Width = 0 - cfg.ConvertedAttributes = attrCfg - deps, err = cfg.Validate("", camera.API.SubtypeName) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, deps, test.ShouldBeNil) - - // error with a negative pixel value - attrCfg.Width = -20 - cfg.ConvertedAttributes = attrCfg - deps, err = cfg.Validate("", camera.API.SubtypeName) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, deps, test.ShouldBeNil) - - attrCfg.Width = 10 - cfg.ConvertedAttributes = attrCfg - deps, err = cfg.Validate("", camera.API.SubtypeName) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, deps, test.ShouldBeNil) - - logger := logging.NewTestLogger(t) - camera, err := NewCamera(context.Background(), nil, cfg, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, camera, test.ShouldNotBeNil) - - test.That(t, camera.Close(context.Background()), test.ShouldBeNil) -} - -func TestRTPPassthrough(t *testing.T) { - logger := logging.NewTestLogger(t) - - t.Run("when rtp_passthrough is enabled", func(t *testing.T) { - cfg := resource.Config{ - Name: "test1", - API: camera.API, - Model: Model, - ConvertedAttributes: &Config{RTPPassthrough: true}, - } - - // passes validations - _, err := cfg.Validate("", camera.API.SubtypeName) - test.That(t, err, test.ShouldBeNil) - - camera, err := NewCamera(context.Background(), nil, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - stream, err := camera.Stream(context.Background()) - test.That(t, err, test.ShouldBeNil) - img, _, err := stream.Next(context.Background()) - test.That(t, err, test.ShouldBeNil) - // GetImage returns the world jpeg - test.That(t, img.Bounds(), test.ShouldResemble, image.Rectangle{Max: image.Point{X: 480, Y: 270}}) - test.That(t, camera, test.ShouldNotBeNil) - - // implements rtppassthrough.Source - cam, ok := camera.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - var called atomic.Bool - pktChan := make(chan []*rtp.Packet) - // SubscribeRTP succeeds - sub, err := cam.SubscribeRTP(context.Background(), 512, func(pkts []*rtp.Packet) { - if called.Load() { - return - } - called.Store(true) - pktChan <- pkts - }) - test.That(t, err, test.ShouldBeNil) - pkts := <-pktChan - test.That(t, len(pkts), test.ShouldEqual, 3) - - // Unsubscribe fails when provided an ID for which there is no subscription - test.That(t, cam.Unsubscribe(context.Background(), uuid.New()), test.ShouldBeError, errors.New("id not found")) - - test.That(t, sub.Terminated.Err(), test.ShouldBeNil) - // Unsubscribe succeeds when provided an ID for which there is a subscription - test.That(t, cam.Unsubscribe(context.Background(), sub.ID), test.ShouldBeNil) - // Unsubscribe cancels the subscription - test.That(t, sub.Terminated.Err(), test.ShouldBeError, context.Canceled) - - // subscriptions are cleaned up after Close is called - sub2, err := cam.SubscribeRTP(context.Background(), 512, func(pkts []*rtp.Packet) {}) - test.That(t, err, test.ShouldBeNil) - test.That(t, sub2.Terminated.Err(), test.ShouldBeNil) - test.That(t, camera.Close(context.Background()), test.ShouldBeNil) - test.That(t, sub2.Terminated.Err(), test.ShouldBeError, context.Canceled) - }) - - t.Run("when rtp_passthrough is not enabled", func(t *testing.T) { - cfg := resource.Config{ - Name: "test1", - API: camera.API, - Model: Model, - ConvertedAttributes: &Config{}, - } - camera, err := NewCamera(context.Background(), nil, cfg, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, camera, test.ShouldNotBeNil) - - cam, ok := camera.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - - // SubscribeRTP returns rtppassthrough.NilSubscription, ErrRTPPassthroughNotEnabled - sub, err := cam.SubscribeRTP(context.Background(), 512, func(pkts []*rtp.Packet) {}) - test.That(t, err, test.ShouldBeError, ErrRTPPassthroughNotEnabled) - test.That(t, sub, test.ShouldResemble, rtppassthrough.NilSubscription) - - // Unsubscribe returns ErrRTPPassthroughNotEnabled - test.That(t, cam.Unsubscribe(context.Background(), uuid.New()), test.ShouldBeError, ErrRTPPassthroughNotEnabled) - test.That(t, camera.Close(context.Background()), test.ShouldBeNil) - }) -} diff --git a/components/camera/fake/worldH264.base64 b/components/camera/fake/worldH264.base64 deleted file mode 100644 index e3e9c20079b..00000000000 --- a/components/camera/fake/worldH264.base64 +++ /dev/null @@ -1 +0,0 @@ -AAAAAWdkABWs2UHgj+sBbgQEC0oAAAMAAgAAAwB4HixbLAAAAAFo6+PLIsAAAAEGBf//qtxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNjQgcjMxMDggMzFlMTlmOSAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMjMgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0xIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDM6MHgxMTMgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTEgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz04IGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MyBiX3B5cmFtaWQ9MiBiX2FkYXB0PTEgYl9iaWFzPTAgZGlyZWN0PTEgd2VpZ2h0Yj0xIG9wZW5fZ29wPTAgd2VpZ2h0cD0yIGtleWludD0yNTAga2V5aW50X21pbj0yNSBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmNfbG9va2FoZWFkPTQwIHJjPWNyZiBtYnRyZWU9MSBjcmY9MjMuMCBxY29tcD0wLjYwIHFwbWluPTAgcXBtYXg9NjkgcXBzdGVwPTQgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAWWIhAAn//71sXwKa1D8igzoMi7hlyTJrrYi4m0AwAAAAwAAErliq1WYNPCjgSH+AA59VJw3/oiamWuuY/7d8Tiko43c4yOy3VXlQES4V/p63IR7koa8FWUSxyUvQKLeMF41TWvxFYILOJTq+9eNNgW+foQigBen/WlYCLvPYNsA2icDhYAC176Ru+I37dSgrc/5GUMunIm7rUBlqoHgnZzVxmCCdE8KNKMdYFlFp542zS07dKD3XEsT206HQqn0/qlJFYqDRFZjYCDQH7eUx5rO06VRte2ZlQsSI8Nz0wA+NMcZWXxzkp5fd5Qw9P/K4T4eBW7u/IKzc1W0CGA55qKN2NYaDMed7udvAcr88iulvJfFVdcAABz8MP/yi+QI+T6aNjPBsc9wWID7B/kWFbpfBv2WBpGH6CkwVhCyUWe2Um+tdy6CJL1kaX6QSjzKskUJraN1VuQjvnYO6HDhxH9sQvo60iSm0SNPCQtFx5Mr9476zTTUV9hwO0YEZShVyDqHUBERz5/CNDX4WAv/V3CPoejYwPe1uycNbx9vNvkiwR/Ie/SPzzb1rXqQBsegfcy827eK2G3oEY77NSMP8XW3/jKSYq6vR2H5V5x72i8tADDKN578rGw/gJ8cwxSH04n+68zdahePhZWDkgMN+4EFR121Zu8VqHsylpUy+sansvVs8SdwiPprpF5kX3It1skAshLU0FMxhlrmaBGmMl0Kz/wS9HrI9JhkzJXQBRuwgF7eDPWaVgLj3J8pE210B0S8YRO9D09bGqhRYrhxt2lJlTlt0hxwT/2EWeNUBvRPSPeK5Tbeg+Ty6HdL10yMAAsD8TRshBvQckyLxogLwazemjWCEP0I7KsEJ/cGIO/P1HEBpMTeXNQVfCCLZnqNvvgQCAxPeSulor5HFbvcNpJWSQC3pbSR0+dn1ENieUxjblibKZseX0RNFgyl8fqLjv8m5qpI8qbpI4EPrZcuZDSXsoBeYqM4EE43vf+y5sGO+QiFslXoDwF4QNk2J4qWlRXw5hMcgaHP6jowOXTonU0AhS0NXNXqbBBGchoWaNPCOuhd7hr4wG14tVUbALNADMe8MghYqXIzfFZeBPDFlF5nMHh41kKu4MlbEc7bVRYw1U3Nm0LnzL0hyQ9p69gYMcjESlYVxYeFLLK3I8QyPSQMQGnAwyDjW6F32IDW1KciW9bFieBVDHWLrgAB7uGf+ZhKfFN9LN1NwF0Yz508zFp4lqpSyWDTfeCwjBCOcnJjVkfPlVcP9d1rpCXPieW9Nw7WEIFslryAMkwA4iftR4KSMeGuB7yAwTPkSL26DWt1wTLs5BLLop38aagRov3iILwm+tEJa9N5UNMymJIe+g1kN11PTK/x454+cu9jc/fN6jFbMUp5KILaWNUk60jAcuDvJoYXSgp/LvnyymIS1oJ803DvKbarnlTw/a+LEj94NBKIS+vSmXe3JXS+O2igDJyitFY8Pg9VQL7r9Ia683WXJK5yWz5m1/XD/c1x+pncbOC4f8pMsn+RwHKKFxoyrVsayv8T/opWRbUnhjue5S66g3gSSqeP4QZM+RdYWDZ+Ae1tYc+WnYvlB0b9mLlYiAQHJVOZp5DeO20pB0pawiAg2g7D+BuAd3T+CaBDYCEVSvzeBDkU5EAWmhyQFLA6bvgR5mwrTpgWAy0NvXGDeH7qrXpVrEWE9k9ztRcKjd8Bzl38TU4VTQTWuonWhjonIi/T3LEPQ/V9EiQ5si5IKw5Dx5dUbaFLsLy6Uleda/cnd/PRQqgOwpKwTVgAPitm+WjoFdQzvgMg/OhyqMBPNfUdmfXOf/6QICGzt42mlJJs0fJSNsl3GFMhXlMDwJYklV4XqoACWemVHreV1k3QY7ORxFK2z7lI5o/A2vHdF/xNzF/wV62VZXa48LxAAD2ZcoDTnw5I7mrtG1OowT1Rt69NzJ9cfWN5BpNThehTEvZ0j5QQSBvaZT8ZzE2rulNiNbQfEU0Qw9YObxIR9PckMJ5Kcmw0EpCGZZr9sZrIw6+nRnNP41CmzjHmLfMtbiNXHaVdEon4yICf4AABelBIuftWccgNDg/KOzRZUAnagrn+QkcA8I6B1xW4PuySkMeMFzQMwjG6EAf6GeA1E/decjpI4ySkJU6R++BXD34AvPiGDrL6VP0xSn9VXSjUakl0r9DL/oOb0s59A/riSzfrm5DE1UVx2/6xoecJQevKsigVgV18EplaIEWGvusHOGyXT5maRs9XyewLSzbX6lWRLRbGx6BtW+mViZRlzijt1ysv5BtT8CveMNAABGd7S93/ezG+umK4qVl9pBoxjRpEv/8iMeHBbVIZL53sxGwW4g7ZgXK7Iaf6gSppgNfTeUprnQ/qAh/nCno7XUmLIFWoTjJEaGgvvx1B6KdJdAH016d8ozWxd9QSCK7kpZL2kowF412iJi6YudF44PRgDvGnBw1Evre0CdnKZgpi/OZR6LfL8oQ45HcY8aSh3Jg7LSyWYjwh5h2z1BkMtI70WrByNVpM/4T7MDbOrIAKI754SehKnoR6KcUFPNuB822EeLBrmepwYlazXCZw9zEjfgv6p926GWp91aihKejMxEi0iRtBa8WPPEnQX9b/n5E3m6sNZzpUwBQl+w/crvehVS3Y2b+p8kIyVOMrVNdRiVHZ3MzGRO6A0KOEfgiU3klIJLMeR/fL55X/NrRi6noRxQngACe3ZelEAG69D5Uy90+2SIQUh42+y/mMTciu9KMETpPt0PV6Fmp3pt+zH5yo/olNHZiZWf1ou712PVsly1vzX+AZgMzvLUWd38ksQpfuOQj9w12vFyT16XH0ruPTyXIhvWEQDfKqvyq0uXqLNwawVI01QZEk4R3UCEjRZGgz6bn+394KqQziqNPIAAAlvvLgRRzOXlgIIi+bhx9ukpKsNBj2s4QOFVV6RU0Ur3q0mtkEFRRim6gqRvWI0DHOBgeBtWT+SUWASA6vb0HfsktyuHoHrTgIeOGDn0C4bkCQOzN5U9D7LpKP1+wGhN2Vyn96MYFPX4xPEIhagrzEK/A1RS6kbEgAAKP17yobsMoFjJdT5y0o0lHV6ZTG2zss7+8ZFyeSk5BgKPEFfHtAxLMaAppsZpccygmABfBOUVz6HXuyCs40JvsKa78mhUirkd0lXXGwexp1Cyaw11QOaVgxpZUV77CABmO+UESL5NPur+AA6W1f/48tG8XA6bMTEHaJh5Ep7hgjxMs+CWnHGlIy9DpaQjLa4lzUvZr+SRBU+URuhv/FWj+h3p+N8yCFp22DNcba2oaKCkFaHbFbXMDG6uPg0hUf9PJlD2TedajGWRIVPn8za76tcY5mKhI9x/5nUG4HWYumHeTourcELQ== diff --git a/components/camera/fake/worldJpeg.base64 b/components/camera/fake/worldJpeg.base64 deleted file mode 100644 index debc7b4bb9a..00000000000 --- a/components/camera/fake/worldJpeg.base64 +++ /dev/null @@ -1 +0,0 @@ -/9j/4AAQSkZJRgABAgAAAQABAAD//gAPTGF2YzYxLjMuMTAwAP/bAEMACAYGBwYHCAgICAgICQkJCgoKCQkJCQoKCgoKCgwMDAoKCgoKCgoMDAwMDQ4NDQ0MDQ4ODw8PEhIRERUVFRkZH//EAJMAAQACAwEBAQAAAAAAAAAAAAACAwUEAQYHCAEBAQEBAQAAAAAAAAAAAAAAAAIBAwQQAAICAAQDBgQDBAgEBwEAAAABAwIhERIEBTEiUQZhMhNBUnFCFCNigbFyB5GSsqHBMyRzFUOCotIW0cNFwuE1UxEBAAMAAwACAwEBAQAAAAAAAAIBESEDEjIxQRMiUWEE/8AAEQgBDgHgAwEiAAIRAAMRAP/aAAwDAQACEQMRAD8A/P4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWwbeXc3UcMd5LvlWlXZ/wAkBUDL/wCy/bY7yasVv/4xtSSr97J6K/zb8Cq3p0f4EVafnk/Eu/6XSv0qBj60tbkmd9OxsX1W81tRHSV5Ea7aS3YS+yk7USpJejw5GxHLe3PLIrwlrfZyeBH7SU3bzOn0kfurfCPA0LRXq8mmRaaMhaTXzKLUTMuLdagL7Q9hD0mT5arAyBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2+HcN3fFdzHtdpFaaeR5VpX9rfJJe7Z9Ag4PwzudCpJrR7zieFtSSvFB4RdtsfO2/kEPO7DujJHEt5xS/wBpBlqpC8vuZVn7Rt9H/OXbzike3q9vw+Ku2i5aqf40n78nM1uJbzfcU1TWeHqYLX1Wt8jEv1W8S1VaV73b5t/Mgo9Wbs8jvWWxxO9urDx9l8y2a13HiHRrmZKm1iqtTureCX/2UzWVrfJZZe2S+YNa0cTs8C7Q6nNWXIarM6IcstRH00TDqYKtI9MlaorfLBhqOnI5lUus6/Mrsk8UT5bVqbxp4MovE68jYs2uZxPN48iLi3WoDYlgyxryNc5qAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjwXgu849vKbPZ01SWxbeFI6LzXvb2qijh/D9zxTdRbTaxWmmltppSvu32vkl4s+wR7bY9zeGfY7W6k3N0nutzljLdfSuemOmOmviWic/OflrpcO7m7J7Ph7rJPJX/Mbz65H71r71i7K/0jwnEt9bcy2vezyb59pt8R3tprWbb/8AMwt7ORlBWuu2Isvb3rgSpFzztp6W/fq8Bpa5l+WejBexF3JFZWYJ621pSzfMqtSzfzxJEs7P9gEfTeHL+kmXKiVSutHmWPTptq/QMUXskQz/ADEGnmSUeYUZO2ZDSTTdXkH2mCm1iNcy3KhGxo67KyyZW1kAS1ZST2Zy8VJMVzKzq1E3HRRarq8mRNqaPVXUapzUAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPc/w27s14xv77/dVrbY8OSlkrbPKWV5+lEvm07P8Ady9wX8bt6jufwWndPh3326pV8R30XSnlnt9s8+l4vK8n1/l6TFcc3zm1Y54ma47xK26ls28OXh8jy28/FpL+R1Osr+nDrjvLCdVLavPp+m3wmtCsWbm420r02VbZP3IqP3r8si4rtXZcjj5G1Xbq9LWtfTar06fcjF9r5ZfVrp1arVqtXhgyktTIemTqn8T9yyser6mixUoSS01JJ2omlZ5NEMgOWfYQ0uxPI6jWKfRXMlpSLtJG1cvZmY3datqO16ll9r0atRZSJ3eZZu6ehStbWra1q6rafNW3w2scpLY+1dKeXYVLqZO18MirEa1ZemSTIZZl0173jjftlkcip2gKQP3LFTT7FurApvKazUJ2tOlGk6l93myDOcqbSkHbHDkoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFu3gvuJo4qLO17JJfM+/7fh9O6fAdnwquEulTbzt+5kXWn/proPnX8KeArinHq72ev+W4XF91fPk75P0Oz6+vD4T3HePe23E18ebf8jp1uPY8zxjeep6mita11uxjrwy22VJ6U/Dzzta31ar/F+U7u30O306tJrzb22n7TKVQ0gjVqPCmT6ta/etYSrV9dMbu5JHb03Jqjrq0abdK7XX5lTu1XShfqrm+ermMDtFEnFeyIqtsW01njiXQRuSRVSM/uOGTScN12dbWrlaqTxSx/b/cJ9tQza+7xUY+nnESKjp1QlmcIkdQHbWaLK1zyzw7cyh2O1kfleNewhTJOiivadYxW6a9X0+XqqI6Z31ef0/xOn96vmsaVNzBo6o7Wk6dNtS0/0Tfj3VI4X0/iXtmvkc5qbXEdvuuIql6UhhV79VfrfxX0fDY8xuM7zN66afya9J6WPXvI/Qjpe8t6eanm/wCk89NtZdrLaOWt62ryrfBPL3IhHdVJmd/3Xca9TazLcQ+jWW1/L9PVTS3nqqYCu1ks8jNx72ePaU0X/Bv+BL/y18hqycQ+0untOj6q3t5xHY1l8tWX4c4thM7WpVw3r0X6ZbatXtiYj1NB3cbybcW1SXdn4vA128y4/wApWWlbK8wcH2OnAOZmCFkQLCDRxtrgAMaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOKK0160rja1lVfNvID7r/DjY/7N3Pe6eEvFJrSePoQ9FFn2O2t5ZfqYXicrtJZ558z3PHIf9p4XsOHRZVrs9rSFKvLOtKqzWL52Tf6nzvd3zkuemqcbYqSeipel66tRr//AKU2u16bWKOLzV80np/TWv6rTqfIr3XN5Gmp3DjnkT+vVVbWruPU1a8GXQpSWSWJjpJ9c97/ABMti3DUiVHpzwLiSZTXouvS6fptb3PQcMk9LbWT3Gev6Hp1LxPP6NFb5862I6ieyH7Kz6b13mp73a3cjccd+16jSydcHg0ZGPfOJZNan2v2/Urkj9f8VUWn36jY7D7ZKmPbIm1eLLrccdk/qNWXV5V5fhXJsv1qccNuC8SgvHaOupvP1Hz0r2X88/0NWtS29K0eNq2wTw8TJKipn9N3aj6qrlbLIvhyrRSWi9StejnbzPHn4ZFNvxLZ1Wn+5HaatPmt05P8pKmzDu5trerjlcfa/deDMh3i4kuLSxzXo3atPTXl6qfT+e2r8xjNntPuJLK96UrXG17309JGbb/hxbrX55L1ipf64qfV8OnpyJqq3c5o/GNaa/pV6fqNF3zNppy2xf6exRLFoZd8prhWAWRxazMarLo6VuS+z8TYhirT2K8sa94VlgUaTfkyTNe6XYRcW01WQsX2j9yElUqo5ypSkAHNoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHoO5Gz+/7ycJgazq95Fa3hWO3qN/yqefPdfwjid+9m2uln6UG5k/VQ2S/aXXMqpHb8LfUu9G89aWVrkj55v31s9nxu/wCJKvE8TxLPWz03eOEZcawu83NI/Ewm43blbS6a+yN3iFcszFEU7U6WRLqIVyyeZOLmGsxDjDbqzxI+3gVbXyPqxzNjOun07LDtLpity500qv6k9daeW3Tp55adJr2ulbRVWy7SmXV4mmpa8S3TqqaiVjaj8oxjjwI0jvNetdVaosusCq1bUMlRS6OPR5/j06jK8L4rXhcG5z2UG6im6fU3NPrr8P19JiHvGsFrrTR9PxGPkmtZnK4VOrq/pbY3W6vJWserora1lSuFUVT76aasEb8sFdFGueTta2b8cSulNZtQ7dIqqFFNXmfScltreRu3hWRp3j02wKYgoG7G5HHpROGiVc3zLC4xShkV2wLSmSyWBgYXwZBx5I4rZMs1KyJFUlV6RqXXSjalun0o174JHOSqazWTOErETgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD6J/BmurvLL4bDcf/E+dn0X+C9su896/FsNz/ZpL6vnSO34W9rx5fiyfM8bxLzHtuPf4knzZ4biLzuztJxi8zxB+YxGZmN+s9RitAp1pAlVh1Z2NJvENb2yeJvS6cs8tJoQTRxsttdzK2OCLpFu5R0evPEpvN6jyrzKbtfEWbetW9RTV6jwFcCzUEaxs0+3e3s7avW1dK+nSal8Sz2INEih16LGlYyG8mj02019PU/Lz6TT26TtyzRmKq0Y3pZkYr10lPoxu3YizprhVG4zUpL4Gjq67WL5ngaVnlgZdtq2QivrRZmYyOW1OTNik1rlehsuxpTX6mWXkNW7bZG6LY3q5l+jzY5I1I31GzrJHPYr3FMqVLU6lU8up6PZCTaaV+ZE6+Zw8ywAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPbfwn3Hod8OHrPL1q7iH564L4f2HiTM91N6uG8f4Vu22lDvYLNrml6iTf8i+r50jt+FvtXeWL0nJ82fPt75mfS++EWMnzZ873sWazPTKnnjJ5vdUz1e5jax31+XpM5LFzNOSiRFO1MdeN6slyH2t11F091RLT5jXvupLFNVXTTO1ltVZZiuPmOWS9sSQz1F0b6chFBq59Js0ipH4+LLjQjWzRdV5odGXJEM2uRScbNfLiQyJw3ytnZVssvK/2lU+C6WLFc96V9tRq1s7W/uRG2q18i9VrH4sndVS6lcvNzEklarBYnHZZGu3qbNZbtZM82zXvXqZfozOOMm201sixW6SNqZM7WueZOtcdmy2tdFdXNld6aUiVJelpm1YrXmeRs6I687Y5Gv4ndfb7gTcqpnka1rZ5vtO2tn8itsiTaRfM4AcFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdVnVprDI4BQ/Rsu4pxzgHDOI16vX2USk+JSx0VLaseedcs8ln2HhN5XmjNfwn4j/und/f8Lktql2N/Xiq3/wAGbzaf3L1z7XqNbi+1rSW6r25nsj/cdeOXEseUnrkzDb95cjPbymTMFvFiRTtTHRNa9VyclvUtpVcil1yZKjyztqxMWjfpekuioks2a7s2yevKpY263qiE8vT0lOerT8iEj9jfQ7HParxNj7ihpAn0MhXcUJSvOqt7GNNiCWr6ZLZVfTq+H8xvpmOqTsR3M3dxwi1batvuIdxXtjw/6bFU/Cd9BS17x9Nem1tS8xPpuNe0jon+ZZHKlF7Mlr6UiqMX60NZRVomYI3xZGua5HdQqzGu2btzK8i1sr1cgxc6rT4mvdNcydpGVOzZg7kn7kruijwxZWQsc5qpEAHNoAAAAAAAAAAAAAAAAAAAAAAACWRw6cDHAAGgAAAAAAAAAA9d/DjvAu7veLbTyP8Ay89bbbcV9rRTZYP5WVbfofUO8mw9DdSKqw1YNPNOrxq180fAj7x3Y4tXvZ3Xivd6t/w1Lbbr4pI0vwZ34uq02yWSdTv09nOf683/AKK43/HkOJQaWzzu7hPbcU22Ct2HmN5Dk2VbYTeXnrpZQZLdw5ZmOfMx1cJO2qviROATrbSzsltbzIs4aBw6nkAOE6kSSNal41tpJ/dbmvT6l9PzK+kjqAlaW75tfyRDMM4gxPV7HaPPpLYdnaTqLFt/St4kihxtFVueRkHHqqattvbV7GtcddNEVM2padCNVksRzxLEqlbOphqVkjXZdI8Cg5zbQACGgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOnpe5Hee3dfi1NxZu21mXobuNfVBfnZfmo0rVPNAVxKrLqrq6utffuN8PqkrxtSQz0UsV1ytSyzTX6Hh97t8s1kb38OO9dd5Au7u/ky9+HzX5Vtjq29reyvg6N4K2a9zKcZ4TaGS+HJ4rsPdxONY8PMJXtPn+8h5mGnhyxR7De7Pngef3m3abOL0Qmwx1Yk5I9LIoLROltosMyBogCQyAiCzSR0sCIO5NFkNU3iBH0b2NvabCSR5vkS5G1Dfo83m6vNpA6obQdNukq3E3rSuxsz7qSVfjrO1enP6tPw42NS9Pq05JgK30/3HDiqLPIpFuM05q6bfM2rXyNWaRWaJXFUF2nCNrexzlbUbWzIgHO1UAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOKW8N63pZ1tV5prBpo+0d1O9MHfHZR8P3t1Xi8FNNZLNJbuOvvZ8vVr7/EfFC7bbibaTUmhvaOSOytW9Xk00dOnslG9pz7uup1lvrfFOGXhtatqtNHl97s888D2Xdrvjte+MVdlxK1dvxalco5eUe7y/srP/W+k1uLcItDeydcmv7T0X/Tyx9dcrq6fON1tWszViqk9LPUbzZ5+2JhtxtXR5pZHN6dVenXI0ZaOlmjbrLlgyqbrKtlNYI66tDT4mLSAOAGszsfTYHaWdGmuaea+aA2QUOSxB3szU43pN07R0pd1yrn1fVbP2ZV6tbfUjWImDd1ld75ldb+zIyW6unkPSvJLcoOtN4srtY5yvFUWsQBw5a0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEq3tVpp5Ne6wZ9K7s/xGhmhrw/vDW8qS0xcQXVLXsrOsutfn8yPmYLh2XC9pE+up1lvtnEOCV0rc7Vxbjb3Wqk0WKaZ5XiGw5tI8zwHvVxTu9Jq2kuqJ/4m2l64L+FqPDHwxPomx7zd3e8qVLyV4XvLLyT/wCA3+Sf27Os7evThKv1f9fPt3s7VbdUaD9SPmfTeJd25Yqu2hXq+V6ZNP8AkeU3nCMX05ErjPfw89rrZY4Mjgbe42F4/Y1vSKdFYJ2o0RyMHdXghqXYcOAWYEdCzwGbLKV9wK7UZUbN+Rqt4mXId1DMg5EVO7ZzuSqpO8meCwKjoJu9bjgAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGe4L3t4zwTKu13LcPvtpfxIWv3Lcv0aPVbfv5wffrRxPht9vf3n2l9VG/8ARv8A9x83zyO5s6Rk53Cn1T0OA8UTey4rs82s1FuLehJl4qXSll8zS3nc3drqjprzxzj6k125rBnzjN9psQb7dbR6tvPNDb3cclq55duTWfP3FSKgz+64FvIbNWjay8DHybKWuFomWRd7+PR/+4T3/wBS3qf18yz/AMY8ZfOaK/7+227/APTJ1rS9Nr6TnpvsNiTvPxKTneH9NtB/2GrfjHEJf+PZfuVrT+qkNE/Rl+BkbyWph0rw1Y/2GpLNNI85JL3f5rWf7SoapdaZsrd2yIM1rgAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//2Q== diff --git a/components/camera/ffmpeg/ffmpeg.go b/components/camera/ffmpeg/ffmpeg.go deleted file mode 100644 index 8291e404354..00000000000 --- a/components/camera/ffmpeg/ffmpeg.go +++ /dev/null @@ -1,222 +0,0 @@ -// Package ffmpeg provides an implementation for an ffmpeg based camera -package ffmpeg - -import ( - "context" - "errors" - "fmt" - "image" - "image/jpeg" - "io" - "os/exec" - "sync" - "sync/atomic" - - ffmpeg "github.com/u2takey/ffmpeg-go" - viamutils "go.viam.com/utils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" -) - -// Config is the attribute struct for ffmpeg cameras. -type Config struct { - CameraParameters *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters,omitempty"` - DistortionParameters *transform.BrownConrady `json:"distortion_parameters,omitempty"` - Debug bool `json:"debug,omitempty"` - VideoPath string `json:"video_path"` - InputKWArgs map[string]interface{} `json:"input_kw_args,omitempty"` - Filters []FilterConfig `json:"filters,omitempty"` - OutputKWArgs map[string]interface{} `json:"output_kw_args,omitempty"` -} - -// FilterConfig is a struct to used to configure ffmpeg filters. -type FilterConfig struct { - Name string `json:"name"` - Args []string `json:"args"` - KWArgs map[string]interface{} `json:"kw_args"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.CameraParameters != nil { - if cfg.CameraParameters.Height < 0 || cfg.CameraParameters.Width < 0 { - return nil, fmt.Errorf( - "got illegal negative dimensions for width_px and height_px (%d, %d) fields set in intrinsic_parameters for ffmpeg camera", - cfg.CameraParameters.Width, cfg.CameraParameters.Height) - } - } - return []string{}, nil -} - -var model = resource.DefaultModelFamily.WithModel("ffmpeg") - -func init() { - resource.RegisterComponent(camera.API, model, resource.Registration[camera.Camera, *Config]{ - Constructor: func( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (camera.Camera, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - src, err := NewFFMPEGCamera(ctx, newConf, logger) - if err != nil { - return nil, err - } - return camera.FromVideoSource(conf.ResourceName(), src, logger), nil - }, - }) -} - -type ffmpegCamera struct { - resource.Named - gostream.VideoReader - cancelFunc context.CancelFunc - activeBackgroundWorkers sync.WaitGroup - inClose func() error - outClose func() error - logger logging.Logger -} - -type stderrWriter struct { - logger logging.Logger -} - -func (writer stderrWriter) Write(p []byte) (n int, err error) { - writer.logger.Debug(string(p)) - return len(p), nil -} - -// NewFFMPEGCamera instantiates a new camera which leverages ffmpeg to handle a variety of potential video types. -func NewFFMPEGCamera(ctx context.Context, conf *Config, logger logging.Logger) (camera.VideoSource, error) { - // make sure ffmpeg is in the path before doing anything else - if _, err := exec.LookPath("ffmpeg"); err != nil { - return nil, err - } - // parse attributes into ffmpeg keyword maps - outArgs := make(map[string]interface{}, len(conf.OutputKWArgs)) - for key, value := range conf.OutputKWArgs { - outArgs[key] = value - } - outArgs["update"] = 1 // always interpret the filename as just a filename, not a pattern - outArgs["format"] = "image2" // select image file muxer, used to write video frames to image files - - // instantiate camera with cancellable context that will be applied to all spawned processes - cancelableCtx, cancel := context.WithCancel(context.Background()) - ffCam := &ffmpegCamera{cancelFunc: cancel, logger: logger} - - // We configure ffmpeg to output images to stdout. A goroutine will read those images via the - // `in` end of the pipe. - in, out := io.Pipe() - - // Note(erd): For some reason, when running with the race detector, we need to close the pipe - // even if we kill the process in order for the underlying command Wait to complete. - ffCam.inClose = in.Close - ffCam.outClose = out.Close - - // We will launch two goroutines: - // - One to shell out to ffmpeg and wait on it exiting. - // - Another to read the image output of ffmpeg and write it to a shared pointer. - // - // In addition, there are two other actors in this system: - // - The application servicing GetImage and video streams will execute the callback registered - // via `VideoReaderFunc`. - // - The robot reconfigure goroutine. All reconfigures are processed as a `Close` followed by - // `NewFFMPEGCamera`. - ffCam.activeBackgroundWorkers.Add(1) - viamutils.ManagedGo(func() { - for { - select { - case <-cancelableCtx.Done(): - return - default: - } - stream := ffmpeg.Input(conf.VideoPath, conf.InputKWArgs) - for _, filter := range conf.Filters { - stream = stream.Filter(filter.Name, filter.Args, filter.KWArgs) - } - stream = stream.Output("pipe:", outArgs) - stream.Context = cancelableCtx - - // The `ffmpeg` command can return for two reasons: - // - This camera object was `Close`d. Which will close the I/O of ffmpeg causing it to - // shutdown. - // - (Dan): I've observed ffmpeg just returning on its own accord, approximately every - // 30 seconds. Only on a rpi. This is presumably due to some form of resource - // exhaustion. - // - // We always want to return to the top of the loop to check the `cancelableCtx`. If the - // camera was explicitly closed, this goroutine will observe `cancelableCtx` as canceled - // and gracefully shutdown. If the camera was not closed, we restart ffmpeg. We depend - // on golang's Command object to not close the I/O pipe such that it can be reused - // across process invocations. - cmd := stream.WithOutput(out).WithErrorOutput(stderrWriter{ - logger: logger, - }).Compile() - logger.Infow("Execing ffmpeg", "cmd", cmd.String()) - err := cmd.Run() - logger.Debugw("ffmpeg exited", "err", err) - } - }, func() { - ffCam.activeBackgroundWorkers.Done() - }) - - var latestFrame atomic.Pointer[image.Image] - // Pause the GetImage reader until the producer provides a first item. - var gotFirstFrameOnce bool - gotFirstFrame := make(chan struct{}) - - ffCam.activeBackgroundWorkers.Add(1) - viamutils.ManagedGo(func() { - for { - if cancelableCtx.Err() != nil { - return - } - img, err := jpeg.Decode(in) - if err != nil { - continue - } - latestFrame.Store(&img) - if !gotFirstFrameOnce { - close(gotFirstFrame) - gotFirstFrameOnce = true - } - } - }, ffCam.activeBackgroundWorkers.Done) - - // when next image is requested simply load the image from where it is stored in shared memory - reader := gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - select { - case <-cancelableCtx.Done(): - return nil, nil, cancelableCtx.Err() - case <-ctx.Done(): - return nil, nil, ctx.Err() - case <-gotFirstFrame: - } - latest := latestFrame.Load() - if latest == nil { - return nil, func() {}, errors.New("no frame yet") - } - return *latest, func() {}, nil - }) - - ffCam.VideoReader = reader - return camera.NewVideoSourceFromReader( - ctx, - ffCam, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: conf.CameraParameters}, - camera.ColorStream) -} - -func (fc *ffmpegCamera) Close(ctx context.Context) error { - fc.cancelFunc() - viamutils.UncheckedError(fc.inClose()) - viamutils.UncheckedError(fc.outClose()) - fc.activeBackgroundWorkers.Wait() - return nil -} diff --git a/components/camera/ffmpeg/ffmpeg_test.go b/components/camera/ffmpeg/ffmpeg_test.go deleted file mode 100644 index f3b36771ea3..00000000000 --- a/components/camera/ffmpeg/ffmpeg_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package ffmpeg - -import ( - "context" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" -) - -func TestFFMPEGCamera(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - path := artifact.MustPath("components/camera/ffmpeg/testsrc.mpg") - cam, err := NewFFMPEGCamera(ctx, &Config{VideoPath: path}, logger) - test.That(t, err, test.ShouldBeNil) - stream, err := cam.Stream(ctx) - test.That(t, err, test.ShouldBeNil) - for i := 0; i < 5; i++ { - _, _, err := stream.Next(ctx) - test.That(t, err, test.ShouldBeNil) - } - test.That(t, stream.Close(context.Background()), test.ShouldBeNil) - test.That(t, cam.Close(context.Background()), test.ShouldBeNil) -} - -func TestFFMPEGNotFound(t *testing.T) { - oldpath := os.Getenv("PATH") - defer func() { - os.Setenv("PATH", oldpath) - }() - os.Unsetenv("PATH") - _, err := NewFFMPEGCamera(context.Background(), nil, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") -} diff --git a/components/camera/ffmpeg/verify_main_test.go b/components/camera/ffmpeg/verify_main_test.go deleted file mode 100644 index 82c949413f2..00000000000 --- a/components/camera/ffmpeg/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package ffmpeg - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/camera/platforms/jetson/README.md b/components/camera/platforms/jetson/README.md deleted file mode 100644 index 68c39daa55a..00000000000 --- a/components/camera/platforms/jetson/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Jetson Camera - -## Orin AGX Setup - -* Follow instructions to install E-Con Systems [e-CAM20_CUOAGX](https://www.e-consystems.com/nvidia-cameras/jetson-agx-orin-cameras/full-hd-ar0234-color-global-shutter-camera.asp) AR0234 driver. -* Ensure driver has successfully installed. `dmesg | grep ar0234` should return a log that looks like `ar0234 Detected Ar0234 sensor R01_RC1`. -* Connect AR0234 camera module and daughterboard to J509 port located at the bottom of the Developer Kit. \ No newline at end of file diff --git a/components/camera/platforms/jetson/camera.go b/components/camera/platforms/jetson/camera.go deleted file mode 100644 index 3e810185759..00000000000 --- a/components/camera/platforms/jetson/camera.go +++ /dev/null @@ -1,145 +0,0 @@ -package jetsoncamera - -// #include -// #include -import "C" - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "runtime" - "unsafe" - - "go.uber.org/multierr" -) - -// DetectOSInformation pulls relevant OS attributes as an OSInformation struct -// Kernel and Device will be "unknown" if unable to retrieve info from the filesystem -// returns an error if kernel version or device name is unavailable -func DetectOSInformation() (OSInformation, error) { - kernelVersion, err := getKernelVersion() - if err != nil { - return OSInformation{}, fmt.Errorf("failed to get kernel version: %w", err) - } - deviceName, err := getDeviceName() - if err != nil { - return OSInformation{}, fmt.Errorf("failed to get device name: %w", err) - } - osInfo := OSInformation{ - Name: runtime.GOOS, - Arch: runtime.GOARCH, - Kernel: kernelVersion, - Device: deviceName, - } - return osInfo, nil -} - -// getKernelVersion returns the Linux kernel version -// $ uname -r -func getKernelVersion() (string, error) { - var utsName C.struct_utsname - if C.uname(&utsName) == -1 { - return Unknown, fmt.Errorf("uname information unavailable (%v)", utsName) - } - release := C.GoString((*C.char)(unsafe.Pointer(&utsName.release[0]))) - return release, nil -} - -// getDeviceName returns the model name of the device -// $ cat /sys/firmware/devicetree/base/model -func getDeviceName() (string, error) { - const devicePath = "/sys/firmware/devicetree/base/model" - device, err := os.ReadFile(devicePath) - if err != nil { - return Unknown, err - } - return string(bytes.TrimRight(device, "\x00")), nil -} - -// ValidateSetup wraps an error from NewWebcamSource with a more helpful message -func ValidateSetup(deviceName, daughterboardName, driverName string, err error) error { - osInfo, osErr := DetectOSInformation() - if osErr != nil { - return err - } - if osInfo.Device != deviceName { - return err - } - detectErr := DetectError(osInfo, daughterboardName, driverName) - if detectErr != nil { - return multierr.Append(err, detectErr) - } - return err -} - -// DetectError checks daughterboard and camera setup to determine -// our best guess of what is wrong with an unsuccessful camera open -func DetectError(osInfo OSInformation, daughterboardName, driverName string) error { - board, ok := cameraInfoMappings[osInfo.Device] - if !ok { - return fmt.Errorf("the %s device is not supported on this platform", osInfo.Device) - } - daughterboard, ok := board.Daughterboards[daughterboardName] - if !ok { - return fmt.Errorf("the %s daughterboard is not supported on this platform", daughterboardName) - } - driver, ok := board.Modules[driverName] - if !ok { - return fmt.Errorf("the %s driver is not supported on this platform", driverName) - } - if err := checkDaughterBoardConnected(daughterboard); err != nil { - return fmt.Errorf("the %s daughterboard is not connected or not powerd on."+ - "Please check daughter-board connection to the %s", - daughterboardName, osInfo.Device) - } - if err := checkDriverInstalled(osInfo.Kernel, driver); err != nil { - return fmt.Errorf("the %s driver not installed. Please follow instructions for driver installation", driverName) - } - - return fmt.Errorf("the %s daughterboard is connected and "+ - "%s camera driver is installed on the %s."+ - "please check that the video path is correct and driver is working", - daughterboardName, driverName, osInfo.Device) -} - -// checkDaughterBoardConnected checks if the daughterboard is connected -// by looking for the I2C bus interfaces associated with the board -func checkDaughterBoardConnected(daughterboard []string) error { - for _, i2c := range daughterboard { - err := checkI2CInterface(i2c) - if err != nil { - return fmt.Errorf("unable to verify that daughterboard is connected: %w", err) - } - } - return nil -} - -// checkI2CInterface checks if the I2C bus is available -func checkI2CInterface(bus string) error { - i2cPath := filepath.Join("/dev", bus) - if err := checkFileExists(i2cPath); err != nil { - return fmt.Errorf("unable to verify that i2c bus is available: %w", err) - } - return nil -} - -// checkDriverInstalled checks if the driver is installed for the -// given kernel version and object file target -func checkDriverInstalled(kernel, driver string) error { - driverPath := filepath.Join("/lib/modules", kernel, "extra", driver) - if err := checkFileExists(driverPath); err != nil { - return fmt.Errorf("unable to verify that camera driver is installed: %w", err) - } - return nil -} - -// checkFileExists is a helper function that wraps os.Stat -func checkFileExists(path string) error { - _, err := os.Stat(path) - if err != nil { - return fmt.Errorf("unable to access %s: %w", path, err) - } - return nil -} diff --git a/components/camera/platforms/jetson/data.go b/components/camera/platforms/jetson/data.go deleted file mode 100644 index ea86650dbfe..00000000000 --- a/components/camera/platforms/jetson/data.go +++ /dev/null @@ -1,43 +0,0 @@ -// Package jetsoncamera contains information about the -// daughterboards and camera modules that are supported on jetson platforms. -package jetsoncamera - -const ( - // OrinAGX is the name of the Jetson Orin AGX development kit. - OrinAGX = "Jetson AGX Orin" - // Unknown is the default value for unknown OS attributes. - Unknown = "unknown" - // ECAM https://www.e-consystems.com/nvidia-cameras/jetson-agx-orin-cameras/full-hd-ar0234-color-global-shutter-camera.asp - ECAM = "e-CAM20_CUOAGX" - // AR0234 https://www.e-consystems.com/camera-modules/ar0234-global-shutter-camera-module.asp - AR0234 = "AR0234CS" -) - -// OSInformation contains information about the OS -// that the camera is running on. -type OSInformation struct { - Name string // e.g. "linux" - Arch string // e.g. "arm64" - Kernel string // e.g. "4.9.140-tegra" - Device string // e.g. "NVIDIA Jetson AGX Xavier" -} - -// CameraInformation contains information about the -// daughterboards and camera modules that are supported. -type CameraInformation struct { - // map of daughterboard name to I2C bus names - Daughterboards map[string][]string // e.g. "i2c-30", "i2c-31" - // map of camera product name to object-file camera driver - Modules map[string]string // e.g. "ar0234.ko" -} - -var cameraInfoMappings = map[string]CameraInformation{ - OrinAGX: { - Daughterboards: map[string][]string{ - ECAM: {"i2c-30", "i2c-31", "i2c-32", "i2c-33", "i2c-34", "i2c-35", "i2c-36", "i2c-37"}, - }, - Modules: map[string]string{ - AR0234: "ar0234.ko", - }, - }, -} diff --git a/components/camera/register/register.go b/components/camera/register/register.go deleted file mode 100644 index a75158d74e9..00000000000 --- a/components/camera/register/register.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !no_cgo || android - -// Package register registers all relevant cameras and also API specific functions -package register - -import ( - // for cameras. - _ "go.viam.com/rdk/components/camera/fake" - _ "go.viam.com/rdk/components/camera/transformpipeline" -) diff --git a/components/camera/register/register_cgo.go b/components/camera/register/register_cgo.go deleted file mode 100644 index c5e598fdbbf..00000000000 --- a/components/camera/register/register_cgo.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build !no_cgo - -// Package register registers all relevant cameras and also API specific functions -package register - -import ( - // for cameras. - _ "go.viam.com/rdk/components/camera/align" - _ "go.viam.com/rdk/components/camera/ffmpeg" - _ "go.viam.com/rdk/components/camera/replaypcd" - _ "go.viam.com/rdk/components/camera/ultrasonic" - _ "go.viam.com/rdk/components/camera/velodyne" - _ "go.viam.com/rdk/components/camera/videosource" -) diff --git a/components/camera/register/register_nocgo_noandroid.go b/components/camera/register/register_nocgo_noandroid.go deleted file mode 100644 index caa42b16004..00000000000 --- a/components/camera/register/register_nocgo_noandroid.go +++ /dev/null @@ -1,2 +0,0 @@ -//go:build no_cgo && !android -package register diff --git a/components/camera/replaypcd/replaypcd.go b/components/camera/replaypcd/replaypcd.go deleted file mode 100644 index 3fa7a806f30..00000000000 --- a/components/camera/replaypcd/replaypcd.go +++ /dev/null @@ -1,477 +0,0 @@ -// Package replaypcd implements a replay camera that can return point cloud data. -package replaypcd - -import ( - "bytes" - "context" - "net/http" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - datapb "go.viam.com/api/app/data/v1" - goutils "go.viam.com/utils" - "go.viam.com/utils/rpc" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/internal/cloud" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils/contextutils" -) - -const ( - timeFormat = time.RFC3339 - grpcConnectionTimeout = 10 * time.Second - downloadTimeout = 30 * time.Second - maxCacheSize = 100 -) - -var ( - // model is the model of a replay camera. - model = resource.DefaultModelFamily.WithModel("replay_pcd") - - // ErrEndOfDataset represents that the replay sensor has reached the end of the dataset. - ErrEndOfDataset = errors.New("reached end of dataset") -) - -func init() { - resource.RegisterComponent(camera.API, model, resource.Registration[camera.Camera, *Config]{ - Constructor: newPCDCamera, - }) -} - -// Config describes how to configure the replay camera component. -type Config struct { - Source string `json:"source,omitempty"` - RobotID string `json:"robot_id,omitempty"` - LocationID string `json:"location_id,omitempty"` - OrganizationID string `json:"organization_id,omitempty"` - Interval TimeInterval `json:"time_interval,omitempty"` - BatchSize *uint64 `json:"batch_size,omitempty"` - APIKey string `json:"api_key,omitempty"` - APIKeyID string `json:"api_key_id,omitempty"` -} - -// TimeInterval holds the start and end time used to filter data. -type TimeInterval struct { - Start string `json:"start,omitempty"` - End string `json:"end,omitempty"` -} - -// cacheEntry stores data that was downloaded from a previous operation but has not yet been passed -// to the caller. -type cacheEntry struct { - pc pointcloud.PointCloud - timeRequested *timestamppb.Timestamp - timeReceived *timestamppb.Timestamp - uri string - err error -} - -// Validate checks that the config attributes are valid for a replay camera. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.Source == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "source") - } - - if cfg.RobotID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "robot_id") - } - - if cfg.LocationID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "location_id") - } - - if cfg.OrganizationID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "organization_id") - } - if cfg.APIKey == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "api_key") - } - if cfg.APIKeyID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "api_key_id") - } - - var err error - var startTime time.Time - if cfg.Interval.Start != "" { - startTime, err = time.Parse(timeFormat, cfg.Interval.Start) - if err != nil { - return nil, errors.New("invalid time format for start time (UTC), use RFC3339") - } - } - - var endTime time.Time - if cfg.Interval.End != "" { - endTime, err = time.Parse(timeFormat, cfg.Interval.End) - if err != nil { - return nil, errors.New("invalid time format for end time (UTC), use RFC3339") - } - } - - if cfg.Interval.Start != "" && cfg.Interval.End != "" && startTime.After(endTime) { - return nil, errors.New("invalid config, end time (UTC) must be after start time (UTC)") - } - - if cfg.BatchSize != nil && (*cfg.BatchSize > uint64(maxCacheSize) || *cfg.BatchSize == 0) { - return nil, errors.Errorf("batch_size must be between 1 and %d", maxCacheSize) - } - - return []string{cloud.InternalServiceName.String()}, nil -} - -// pcdCamera is a camera model that plays back pre-captured point cloud data. -type pcdCamera struct { - resource.Named - logger logging.Logger - - APIKey string - APIKeyID string - cloudConnSvc cloud.ConnectionService - cloudConn rpc.ClientConn - dataClient datapb.DataServiceClient - httpClient *http.Client - - lastData string - limit uint64 - filter *datapb.Filter - - cache []*cacheEntry - - mu sync.RWMutex - closed bool -} - -// newPCDCamera creates a new replay camera based on the inputted config and dependencies. -func newPCDCamera( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (camera.Camera, error) { - cam := &pcdCamera{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - - if err := cam.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return cam, nil -} - -// NextPointCloud returns the next point cloud retrieved from cloud storage based on the applied filter. -func (replay *pcdCamera) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - // First acquire the lock, so that it's safe to populate the cache and/or retrieve and - // remove the next data point from the cache. Note that if multiple threads call - // NextPointCloud concurrently, they may get data out-of-order, since there's no guarantee - // about who acquires the lock first. - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return nil, errors.New("session closed") - } - - // Retrieve next cached data and remove from cache, if no data remains in the cache, download a - // new batch - if len(replay.cache) != 0 { - return replay.getDataFromCache(ctx) - } - - // Retrieve data from the cloud. If the batch size is > 1, only metadata is returned here, otherwise - // IncludeBinary can be set to true and the data can be downloaded directly via BinaryDataByFilter - resp, err := replay.dataClient.BinaryDataByFilter(ctx, &datapb.BinaryDataByFilterRequest{ - DataRequest: &datapb.DataRequest{ - Filter: replay.filter, - Limit: replay.limit, - Last: replay.lastData, - SortOrder: datapb.Order_ORDER_ASCENDING, - }, - CountOnly: false, - IncludeBinary: replay.limit == 1, - }) - if err != nil { - return nil, err - } - - if len(resp.GetData()) == 0 { - return nil, ErrEndOfDataset - } - replay.lastData = resp.GetLast() - - // If using a batch size of 1, we already received the data itself, so decode and return the - // binary data directly - if replay.limit == 1 { - pc, err := decodeResponseData(resp.GetData()) - if err != nil { - return nil, err - } - if err := addGRPCMetadata(ctx, - resp.GetData()[0].GetMetadata().GetTimeRequested(), - resp.GetData()[0].GetMetadata().GetTimeReceived()); err != nil { - return nil, err - } - return pc, nil - } - - // Otherwise if using a batch size > 1, use the metadata from BinaryDataByFilter to download - // data in parallel and cache the results - replay.cache = make([]*cacheEntry, len(resp.Data)) - for i, dataResponse := range resp.Data { - md := dataResponse.GetMetadata() - replay.cache[i] = &cacheEntry{ - uri: md.GetUri(), - timeRequested: md.GetTimeRequested(), - timeReceived: md.GetTimeReceived(), - } - } - - ctxTimeout, cancelTimeout := context.WithTimeout(ctx, downloadTimeout) - defer cancelTimeout() - replay.downloadBatch(ctxTimeout) - if ctxTimeout.Err() != nil { - return nil, errors.Wrap(ctxTimeout.Err(), "failed to download batch") - } - - return replay.getDataFromCache(ctx) -} - -// downloadBatch iterates through the current cache, performing the download of the respective data in -// parallel and adds all of them to the cache before returning. -func (replay *pcdCamera) downloadBatch(ctx context.Context) { - // Parallelize download of data based on ids in cache - var wg sync.WaitGroup - wg.Add(len(replay.cache)) - for _, dataToCache := range replay.cache { - data := dataToCache - - goutils.PanicCapturingGo(func() { - defer wg.Done() - data.pc, data.err = replay.getDataFromHTTP(ctx, data.uri) - if data.err != nil { - return - } - }) - } - wg.Wait() -} - -// getDataFromHTTP makes a request to an http endpoint app serves, which gets redirected to GCS. -func (replay *pcdCamera) getDataFromHTTP(ctx context.Context, dataURL string) (pointcloud.PointCloud, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, dataURL, nil) - if err != nil { - return nil, err - } - req.Header.Add("key_id", replay.APIKeyID) - req.Header.Add("key", replay.APIKey) - - res, err := replay.httpClient.Do(req) - if err != nil { - return nil, err - } - - pc, err := pointcloud.ReadPCD(res.Body) - if err != nil { - return nil, multierr.Combine(err, res.Body.Close()) - } - if res.StatusCode != http.StatusOK { - return nil, multierr.Combine(errors.New(res.Status), res.Body.Close()) - } - - if err := res.Body.Close(); err != nil { - return nil, err - } - - return pc, nil -} - -// getDataFromCache retrieves the next cached data and removes it from the cache. It assumes the -// write lock is being held. -func (replay *pcdCamera) getDataFromCache(ctx context.Context) (pointcloud.PointCloud, error) { - // Grab the next cached data and update the cache immediately, even if there's an error, - // so we don't get stuck in a loop checking for and returning the same error. - data := replay.cache[0] - replay.cache = replay.cache[1:] - if data.err != nil { - return nil, errors.Wrap(data.err, "cache data contained an error") - } - - if err := addGRPCMetadata(ctx, data.timeRequested, data.timeReceived); err != nil { - return nil, err - } - - return data.pc, nil -} - -// addGRPCMetadata adds timestamps from the data response to the gRPC response header if one is -// found in the context. -func addGRPCMetadata(ctx context.Context, timeRequested, timeReceived *timestamppb.Timestamp) error { - if stream := grpc.ServerTransportStreamFromContext(ctx); stream != nil { - var grpcMetadata metadata.MD = make(map[string][]string) - if timeRequested != nil { - grpcMetadata.Set(contextutils.TimeRequestedMetadataKey, timeRequested.AsTime().Format(time.RFC3339Nano)) - } - if timeReceived != nil { - grpcMetadata.Set(contextutils.TimeReceivedMetadataKey, timeReceived.AsTime().Format(time.RFC3339Nano)) - } - if err := grpc.SetHeader(ctx, grpcMetadata); err != nil { - return err - } - } - - return nil -} - -// Images is a part of the camera interface but is not implemented for replay. -func (replay *pcdCamera) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - return nil, resource.ResponseMetadata{}, errors.New("Images is unimplemented") -} - -// Properties is a part of the camera interface and returns the camera.Properties struct with SupportsPCD set to true. -func (replay *pcdCamera) Properties(ctx context.Context) (camera.Properties, error) { - props := camera.Properties{ - SupportsPCD: true, - } - return props, nil -} - -// Projector is a part of the camera interface but is not implemented for replay. -func (replay *pcdCamera) Projector(ctx context.Context) (transform.Projector, error) { - var proj transform.Projector - return proj, errors.New("Projector is unimplemented") -} - -// Stream is a part of the camera interface but is not implemented for replay. -func (replay *pcdCamera) Stream(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - var stream gostream.VideoStream - return stream, errors.New("Stream is unimplemented") -} - -// Close stops replay camera, closes the channels and its connections to the cloud. -func (replay *pcdCamera) Close(ctx context.Context) error { - replay.mu.Lock() - defer replay.mu.Unlock() - - replay.closed = true - // Close cloud connection - replay.closeCloudConnection(ctx) - return nil -} - -// Reconfigure finishes the bring up of the replay camera by evaluating given arguments and setting up the required cloud -// connection. -func (replay *pcdCamera) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return errors.New("session closed") - } - - replayCamConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - replay.APIKey = replayCamConfig.APIKey - replay.APIKeyID = replayCamConfig.APIKeyID - - cloudConnSvc, err := resource.FromDependencies[cloud.ConnectionService](deps, cloud.InternalServiceName) - if err != nil { - return err - } - - // Update cloud connection if needed - if replay.cloudConnSvc != cloudConnSvc { - replay.closeCloudConnection(ctx) - replay.cloudConnSvc = cloudConnSvc - - if err := replay.initCloudConnection(ctx); err != nil { - replay.closeCloudConnection(ctx) - return errors.Wrap(err, "failure to connect to the cloud") - } - } - - if replayCamConfig.BatchSize == nil { - replay.limit = 1 - } else { - replay.limit = *replayCamConfig.BatchSize - } - replay.cache = nil - - replay.filter = &datapb.Filter{ - ComponentName: replayCamConfig.Source, - RobotId: replayCamConfig.RobotID, - LocationIds: []string{replayCamConfig.LocationID}, - OrganizationIds: []string{replayCamConfig.OrganizationID}, - MimeType: []string{"pointcloud/pcd"}, - Interval: &datapb.CaptureInterval{}, - } - replay.lastData = "" - - if replayCamConfig.Interval.Start != "" { - startTime, err := time.Parse(timeFormat, replayCamConfig.Interval.Start) - if err != nil { - replay.closeCloudConnection(ctx) - return errors.New("invalid time format for start time, missed during config validation") - } - replay.filter.Interval.Start = timestamppb.New(startTime) - } - - if replayCamConfig.Interval.End != "" { - endTime, err := time.Parse(timeFormat, replayCamConfig.Interval.End) - if err != nil { - replay.closeCloudConnection(ctx) - return errors.New("invalid time format for end time, missed during config validation") - } - replay.filter.Interval.End = timestamppb.New(endTime) - } - - return nil -} - -// closeCloudConnection closes all parts of the cloud connection used by the replay camera. -func (replay *pcdCamera) closeCloudConnection(ctx context.Context) { - if replay.cloudConn != nil { - goutils.UncheckedError(replay.cloudConn.Close()) - } - - if replay.cloudConnSvc != nil { - goutils.UncheckedError(replay.cloudConnSvc.Close(ctx)) - } -} - -// initCloudConnection creates a rpc client connection and data service. -func (replay *pcdCamera) initCloudConnection(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, grpcConnectionTimeout) - defer cancel() - - _, conn, err := replay.cloudConnSvc.AcquireConnectionAPIKey(ctx, replay.APIKey, replay.APIKeyID) - if err != nil { - return err - } - dataServiceClient := datapb.NewDataServiceClient(conn) - - replay.cloudConn = conn - replay.dataClient = dataServiceClient - replay.httpClient = &http.Client{} - return nil -} - -// decodeResponseData decodes the pcd file byte array. -func decodeResponseData(respData []*datapb.BinaryData) (pointcloud.PointCloud, error) { - if len(respData) == 0 { - return nil, errors.New("no response data; this should never happen") - } - - pc, err := pointcloud.ReadPCD(bytes.NewBuffer(respData[0].GetBinary())) - if err != nil { - return nil, err - } - - return pc, nil -} diff --git a/components/camera/replaypcd/replaypcd_test.go b/components/camera/replaypcd/replaypcd_test.go deleted file mode 100644 index 8040778c676..00000000000 --- a/components/camera/replaypcd/replaypcd_test.go +++ /dev/null @@ -1,823 +0,0 @@ -package replaypcd - -import ( - "context" - "fmt" - "math/rand" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "google.golang.org/grpc" - - "go.viam.com/rdk/internal/cloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/utils/contextutils" -) - -const ( - datasetDirectory = "slam/mock_lidar/%d.pcd" - validSource = "source" - validRobotID = "robot_id" - validOrganizationID = "organization_id" - validLocationID = "location_id" - validAPIKey = "a key" - validAPIKeyID = "a key id" - numPCDFilesOriginal = 15 -) - -var ( - numPCDFiles = numPCDFilesOriginal - batchSize0 = uint64(0) - batchSize1 = uint64(1) - batchSize2 = uint64(2) - batchSize3 = uint64(3) - batchSize4 = uint64(4) - batchSizeLarge = uint64(50) - batchSizeTooLarge = uint64(1000) -) - -func TestReplayPCDNew(t *testing.T) { - ctx := context.Background() - - cases := []struct { - description string - cfg *Config - expectedErr error - validCloudConnection bool - }{ - { - description: "valid config with internal cloud service", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: true, - }, - { - description: "bad internal cloud service", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: false, - expectedErr: errors.New("failure to connect to the cloud: cloud connection error"), - }, - { - description: "bad start timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "bad timestamp", - }, - }, - validCloudConnection: true, - expectedErr: errors.New("invalid time format for start time, missed during config validation"), - }, - { - description: "bad end timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - End: "bad timestamp", - }, - }, - validCloudConnection: true, - expectedErr: errors.New("invalid time format for end time, missed during config validation"), - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - replayCamera, _, serverClose, err := createNewReplayPCDCamera(ctx, t, tt.cfg, tt.validCloudConnection) - if err != nil { - test.That(t, err, test.ShouldBeError, tt.expectedErr) - test.That(t, replayCamera, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldBeNil) - test.That(t, replayCamera, test.ShouldNotBeNil) - - err = replayCamera.Close(ctx) - test.That(t, err, test.ShouldBeNil) - } - - if tt.validCloudConnection { - test.That(t, serverClose(), test.ShouldBeNil) - } - }) - } -} - -func TestReplayPCDNextPointCloud(t *testing.T) { - ctx := context.Background() - - cases := []struct { - description string - cfg *Config - startFileNum int - endFileNum int - }{ - { - description: "Calling NextPointCloud no filter", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: 0, - endFileNum: numPCDFiles, - }, - { - description: "Calling NextPointCloud with bad source", - cfg: &Config{ - Source: "bad_source", - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: -1, - endFileNum: -1, - }, - { - description: "Calling NextPointCloud with bad robot_id", - cfg: &Config{ - Source: validSource, - RobotID: "bad_robot_id", - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: -1, - endFileNum: -1, - }, - { - description: "Calling NextPointCloud with bad location_id", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: "bad_location_id", - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: -1, - endFileNum: -1, - }, - { - description: "Calling NextPointCloud with bad organization_id", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: "bad_organization_id", - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: -1, - endFileNum: -1, - }, - { - description: "Calling NextPointCloud with filter no data", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSize1, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:30Z", - End: "2000-01-01T12:00:40Z", - }, - }, - startFileNum: -1, - endFileNum: -1, - }, - { - description: "Calling NextPointCloud with end filter", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSize1, - Interval: TimeInterval{ - End: "2000-01-01T12:00:10Z", - }, - }, - startFileNum: 0, - endFileNum: 10, - }, - { - description: "Calling NextPointCloud with start filter", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSize1, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:05Z", - }, - }, - startFileNum: 5, - endFileNum: numPCDFiles, - }, - { - description: "Calling NextPointCloud with start and end filter", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSize1, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:05Z", - End: "2000-01-01T12:00:10Z", - }, - }, - startFileNum: 5, - endFileNum: 10, - }, - { - description: "Calling NextPointCloud with non-divisible batch size, last batch size 1", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSize2, - }, - startFileNum: 0, - endFileNum: numPCDFiles, - }, - { - description: "Calling NextPointCloud with non-divisible batch size, last batch > 1", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - BatchSize: &batchSize4, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: 0, - endFileNum: numPCDFiles, - }, - { - description: "Calling NextPointCloud with divisible batch size", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - BatchSize: &batchSize3, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: 0, - endFileNum: numPCDFiles, - }, - { - description: "Calling NextPointCloud with batching and a start and end filter", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSize2, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:05Z", - End: "2000-01-01T12:00:10Z", - }, - }, - startFileNum: 5, - endFileNum: 11, - }, - { - description: "Calling NextPointCloud with a large batch size", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - BatchSize: &batchSizeLarge, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: 0, - endFileNum: numPCDFiles, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - replayCamera, _, serverClose, err := createNewReplayPCDCamera(ctx, t, tt.cfg, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, replayCamera, test.ShouldNotBeNil) - - // Iterate through all files that meet the provided filter - if tt.startFileNum != -1 { - for i := tt.startFileNum; i < tt.endFileNum; i++ { - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - pcExpected, err := getPointCloudFromArtifact(i) - if err != nil { - test.That(t, err.Error, test.ShouldContainSubstring, "artifact not found") - test.That(t, pc, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldResemble, pcExpected) - } - } - } - - // Confirm the end of the dataset was reached when expected - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, ErrEndOfDataset.Error()) - test.That(t, pc, test.ShouldBeNil) - - err = replayCamera.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) - }) - } -} - -// TestLiveNextPointCloud checks the replay pcd camera's ability to handle new data being added to the -// database the pool during a session, proving that NextPointCloud can return new data even after -// returning errEndOfDataset. -func TestReplayPCDLiveNextPointCloud(t *testing.T) { - ctx := context.Background() - - numPCDFiles = 10 - defer func() { numPCDFiles = numPCDFilesOriginal }() - - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - } - - replayCamera, _, serverClose, err := createNewReplayPCDCamera(ctx, t, cfg, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, replayCamera, test.ShouldNotBeNil) - - // Iterate through all files that meet the provided filter - i := 0 - for { - pc, err := replayCamera.NextPointCloud(ctx) - if i == numPCDFiles { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, ErrEndOfDataset.Error()) - test.That(t, pc, test.ShouldBeNil) - - // Add new files for future processing - numPCDFiles += rand.Intn(3) - - if numPCDFiles >= numPCDFilesOriginal { - break - } - } else { - pcExpected, err := getPointCloudFromArtifact(i) - if err != nil { - test.That(t, err.Error, test.ShouldContainSubstring, "artifact not found") - test.That(t, pc, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldResemble, pcExpected) - } - i++ - } - } - - err = replayCamera.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} - -func TestReplayPCDConfigValidation(t *testing.T) { - cases := []struct { - description string - cfg *Config - expectedDeps []string - expectedErr error - }{ - { - description: "Valid config with source and no timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - Interval: TimeInterval{}, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Valid config with no source", - cfg: &Config{ - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - Interval: TimeInterval{}, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validSource), - }, - { - description: "Valid config with no robot_id", - cfg: &Config{ - Source: validSource, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - Interval: TimeInterval{}, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validRobotID), - }, - { - description: "Valid config with no location_id", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - OrganizationID: validOrganizationID, - Interval: TimeInterval{}, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validLocationID), - }, - { - description: "Valid config with no organization_id", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - Interval: TimeInterval{}, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validOrganizationID), - }, - { - description: "Valid config with start timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - }, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Valid config with end timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - End: "2000-01-01T12:00:00Z", - }, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Valid config with start and end timestamps", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - End: "2000-01-01T12:00:01Z", - }, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Invalid config with bad start timestamp format", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "gibberish", - }, - }, - expectedErr: errors.New("invalid time format for start time (UTC), use RFC3339"), - }, - { - description: "Invalid config with bad end timestamp format", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - End: "gibberish", - }, - }, - expectedErr: errors.New("invalid time format for end time (UTC), use RFC3339"), - }, - { - description: "Invalid config with start after end timestamps", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:01Z", - End: "2000-01-01T12:00:00Z", - }, - }, - expectedErr: errors.New("invalid config, end time (UTC) must be after start time (UTC)"), - }, - { - description: "Invalid config with batch size above max", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - End: "2000-01-01T12:00:01Z", - }, - BatchSize: &batchSizeTooLarge, - }, - expectedErr: errors.New("batch_size must be between 1 and 100"), - }, - { - description: "Invalid config with batch size 0", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - End: "2000-01-01T12:00:01Z", - }, - BatchSize: &batchSize0, - }, - expectedErr: errors.New("batch_size must be between 1 and 100"), - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - deps, err := tt.cfg.Validate("") - if tt.expectedErr != nil { - test.That(t, err, test.ShouldBeError, tt.expectedErr) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, deps, test.ShouldResemble, tt.expectedDeps) - }) - } -} - -func TestReplayPCDUnimplementedFunctions(t *testing.T) { - ctx := context.Background() - - replayCamCfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - } - replayCamera, _, serverClose, err := createNewReplayPCDCamera(ctx, t, replayCamCfg, true) - test.That(t, err, test.ShouldBeNil) - - t.Run("Stream", func(t *testing.T) { - _, err := replayCamera.Stream(ctx, nil) - test.That(t, err.Error(), test.ShouldEqual, "Stream is unimplemented") - }) - - t.Run("Projector", func(t *testing.T) { - _, err := replayCamera.Projector(ctx) - test.That(t, err.Error(), test.ShouldEqual, "Projector is unimplemented") - }) - - err = replayCamera.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} - -func TestReplayPCDTimestamps(t *testing.T) { - testCameraWithCfg := func(cfg *Config) { - // Construct replay camera. - ctx := context.Background() - replayCamera, _, serverClose, err := createNewReplayPCDCamera(ctx, t, cfg, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, replayCamera, test.ShouldNotBeNil) - - // Repeatedly call NextPointCloud, checking for timestamps in the gRPC header. - for i := 0; i < numPCDFiles; i++ { - serverStream := testutils.NewServerTransportStream() - ctx = grpc.NewContextWithServerTransportStream(ctx, serverStream) - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - pcExpected, err := getPointCloudFromArtifact(i) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldResemble, pcExpected) - - expectedTimeReq := fmt.Sprintf(testTime, i) - expectedTimeRec := fmt.Sprintf(testTime, i+1) - - actualTimeReq := serverStream.Value(contextutils.TimeRequestedMetadataKey)[0] - actualTimeRec := serverStream.Value(contextutils.TimeReceivedMetadataKey)[0] - - test.That(t, expectedTimeReq, test.ShouldEqual, actualTimeReq) - test.That(t, expectedTimeRec, test.ShouldEqual, actualTimeRec) - } - - // Confirm the end of the dataset was reached when expected - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, ErrEndOfDataset.Error()) - test.That(t, pc, test.ShouldBeNil) - - err = replayCamera.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) - } - - t.Run("no batching", func(t *testing.T) { - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - } - testCameraWithCfg(cfg) - }) - t.Run("with batching", func(t *testing.T) { - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - BatchSize: &batchSize2, - } - testCameraWithCfg(cfg) - }) -} - -func TestReplayPCDProperties(t *testing.T) { - // Construct replay camera. - ctx := context.Background() - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - BatchSize: &batchSize1, - } - replayCamera, _, serverClose, err := createNewReplayPCDCamera(ctx, t, cfg, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, replayCamera, test.ShouldNotBeNil) - - props, err := replayCamera.Properties(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, props.SupportsPCD, test.ShouldBeTrue) - - err = replayCamera.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} - -func TestReplayPCDReconfigure(t *testing.T) { - // Construct replay camera - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - } - ctx := context.Background() - replayCamera, deps, serverClose, err := createNewReplayPCDCamera(ctx, t, cfg, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, replayCamera, test.ShouldNotBeNil) - - // Call NextPointCloud to iterate through a few files - for i := 0; i < 3; i++ { - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - pcExpected, err := getPointCloudFromArtifact(i) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldResemble, pcExpected) - } - - // Reconfigure with a new batch size - cfg = &Config{Source: validSource, BatchSize: &batchSize4} - replayCamera.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - - // Call NextPointCloud a couple more times, ensuring that we start over from the beginning - // of the dataset after calling Reconfigure - for i := 0; i < 5; i++ { - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - pcExpected, err := getPointCloudFromArtifact(i) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldResemble, pcExpected) - } - - // Reconfigure again, batch size 1 - cfg = &Config{Source: validSource, BatchSize: &batchSize1} - replayCamera.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - - // Again verify dataset starts from beginning - for i := 0; i < numPCDFiles; i++ { - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - pcExpected, err := getPointCloudFromArtifact(i) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldResemble, pcExpected) - } - - // Confirm the end of the dataset was reached when expected - pc, err := replayCamera.NextPointCloud(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, ErrEndOfDataset.Error()) - test.That(t, pc, test.ShouldBeNil) - - err = replayCamera.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} diff --git a/components/camera/replaypcd/replaypcd_utils_test.go b/components/camera/replaypcd/replaypcd_utils_test.go deleted file mode 100644 index f8979a5fdcd..00000000000 --- a/components/camera/replaypcd/replaypcd_utils_test.go +++ /dev/null @@ -1,319 +0,0 @@ -package replaypcd - -import ( - "context" - "fmt" - "math" - "net" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strconv" - "testing" - "time" - - "github.com/pkg/errors" - datapb "go.viam.com/api/app/data/v1" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/artifact" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/components/camera" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/internal/cloud" - cloudinject "go.viam.com/rdk/internal/testutils/inject" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testTime = "2000-01-01T12:00:%02dZ" - orgID = "slam_org_id" - locationID = "slam_location_id" - testingHTTPPattern = "/myurl" -) - -// mockDataServiceServer is a struct that includes unimplemented versions of all the Data Service endpoints. These -// can be overwritten to allow developers to trigger desired behaviors during testing. -type mockDataServiceServer struct { - datapb.UnimplementedDataServiceServer - httpMock []*httptest.Server -} - -// BinaryDataByFilter is a mocked version of the Data Service function of a similar name. It returns a response with -// data corresponding to a stored pcd artifact based on the filter and last file accessed. -func (mDServer *mockDataServiceServer) BinaryDataByFilter(ctx context.Context, req *datapb.BinaryDataByFilterRequest, -) (*datapb.BinaryDataByFilterResponse, error) { - // Parse request - filter := req.DataRequest.GetFilter() - last := req.DataRequest.GetLast() - limit := req.DataRequest.GetLimit() - includeBinary := req.IncludeBinary - - newFileNum, err := getNextDataAfterFilter(filter, last) - if err != nil { - return nil, err - } - - // Construct response - var resp datapb.BinaryDataByFilterResponse - if includeBinary { - data, err := getCompressedBytesFromArtifact(fmt.Sprintf(datasetDirectory, newFileNum)) - if err != nil { - return nil, err - } - - timeReq, timeRec, err := timestampsFromFileNum(newFileNum) - if err != nil { - return nil, err - } - binaryData := datapb.BinaryData{ - Binary: data, - Metadata: &datapb.BinaryMetadata{ - Id: fmt.Sprintf(datasetDirectory, newFileNum), - TimeRequested: timeReq, - TimeReceived: timeRec, - CaptureMetadata: &datapb.CaptureMetadata{ - OrganizationId: orgID, - LocationId: locationID, - }, - Uri: mDServer.httpMock[newFileNum].URL + testingHTTPPattern, - }, - } - - resp.Data = []*datapb.BinaryData{&binaryData} - resp.Last = fmt.Sprint(newFileNum) - } else { - for i := 0; i < int(limit); i++ { - if newFileNum+i >= numPCDFiles { - break - } - - timeReq, timeRec, err := timestampsFromFileNum(newFileNum + i) - if err != nil { - return nil, err - } - - binaryData := datapb.BinaryData{ - Metadata: &datapb.BinaryMetadata{ - Id: fmt.Sprintf(datasetDirectory, newFileNum+i), - TimeRequested: timeReq, - TimeReceived: timeRec, - CaptureMetadata: &datapb.CaptureMetadata{ - OrganizationId: orgID, - LocationId: locationID, - }, - Uri: mDServer.httpMock[newFileNum+i].URL + testingHTTPPattern, - }, - } - resp.Data = append(resp.Data, &binaryData) - } - resp.Last = fmt.Sprint(newFileNum + int(limit) - 1) - } - - return &resp, nil -} - -func timestampsFromFileNum(fileNum int) (*timestamppb.Timestamp, *timestamppb.Timestamp, error) { - timeReq, err := time.Parse(time.RFC3339, fmt.Sprintf(testTime, fileNum)) - if err != nil { - return nil, nil, errors.Wrap(err, "failed parsing time") - } - timeRec := timeReq.Add(time.Second) - return timestamppb.New(timeReq), timestamppb.New(timeRec), nil -} - -// createMockCloudDependencies creates a mockDataServiceServer and rpc client connection to it which is then -// stored in a mockCloudConnectionService. -func createMockCloudDependencies(ctx context.Context, t *testing.T, logger logging.Logger, b bool) (resource.Dependencies, func() error) { - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - // This creates a mock server for each pcd file used in testing - srv := newHTTPMock(testingHTTPPattern, http.StatusOK) - test.That(t, rpcServer.RegisterServiceServer( - ctx, - &datapb.DataService_ServiceDesc, - &mockDataServiceServer{httpMock: srv}, - datapb.RegisterDataServiceHandlerFromEndpoint, - ), test.ShouldBeNil) - - go rpcServer.Serve(listener) - - conn, err := viamgrpc.Dial(ctx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - mockCloudConnectionService := &cloudinject.CloudConnectionService{ - Named: cloud.InternalServiceName.AsNamed(), - Conn: conn, - } - if !b { - mockCloudConnectionService.AcquireConnectionErr = errors.New("cloud connection error") - } - - r := &inject.Robot{} - rs := map[resource.Name]resource.Resource{} - rs[cloud.InternalServiceName] = mockCloudConnectionService - r.MockResourcesFromMap(rs) - - return resourcesFromDeps(t, r, []string{cloud.InternalServiceName.String()}), rpcServer.Stop -} - -// createNewReplayPCDCamera will create a new replay_pcd camera based on the provided config with either -// a valid or invalid data client. -func createNewReplayPCDCamera(ctx context.Context, t *testing.T, replayCamCfg *Config, validDeps bool, -) (camera.Camera, resource.Dependencies, func() error, error) { - logger := logging.NewTestLogger(t) - - resources, closeRPCFunc := createMockCloudDependencies(ctx, t, logger, validDeps) - - cfg := resource.Config{ConvertedAttributes: replayCamCfg} - cam, err := newPCDCamera(ctx, resources, cfg, logger) - - return cam, resources, closeRPCFunc, err -} - -// resourcesFromDeps returns a list of dependencies from the provided robot. -func resourcesFromDeps(t *testing.T, r robot.Robot, deps []string) resource.Dependencies { - t.Helper() - resources := resource.Dependencies{} - for _, dep := range deps { - resName, err := resource.NewFromString(dep) - test.That(t, err, test.ShouldBeNil) - res, err := r.ResourceByName(resName) - if err == nil { - // some resources are weakly linked - resources[resName] = res - } - } - return resources -} - -// getNextDataAfterFilter returns the artifact index of the next point cloud data to be return based on -// the provided filter and last returned artifact. -func getNextDataAfterFilter(filter *datapb.Filter, last string) (int, error) { - // Basic component part (source) filter - if filter.ComponentName != "" && filter.ComponentName != validSource { - return 0, ErrEndOfDataset - } - - // Basic robot_id filter - if filter.RobotId != "" && filter.RobotId != validRobotID { - return 0, ErrEndOfDataset - } - - // Basic location_id filter - if len(filter.LocationIds) == 0 { - return 0, errors.New("LocationIds in filter is empty") - } - if filter.LocationIds[0] != "" && filter.LocationIds[0] != validLocationID { - return 0, ErrEndOfDataset - } - - // Basic organization_id filter - if len(filter.OrganizationIds) == 0 { - return 0, errors.New("OrganizationIds in filter is empty") - } - if filter.OrganizationIds[0] != "" && filter.OrganizationIds[0] != validOrganizationID { - return 0, ErrEndOfDataset - } - - // Apply the time-based filter based on the seconds value in the start and end fields. Because artifacts - // do not have timestamps associated with them but are numerically ordered we can approximate the filtering - // by sorting for the files which occur after the start second count and before the end second count. - // For example, if there are 15 files in the artifact directory, the start time is 2000-01-01T12:00:10Z - // and the end time is 2000-01-01T12:00:14Z, we will return files 10-14. - start := 0 - end := numPCDFiles - if filter.Interval.Start != nil { - start = filter.Interval.Start.AsTime().Second() - } - if filter.Interval.End != nil { - end = int(math.Min(float64(filter.Interval.End.AsTime().Second()), float64(end))) - } - - if last == "" { - return getFile(start, end) - } - lastFileNum, err := strconv.Atoi(last) - if err != nil { - return 0, err - } - return getFile(lastFileNum+1, end) -} - -// getFile will return the next file to be returned after checking it satisfies the end condition. -func getFile(i, end int) (int, error) { - if i < end { - return i, nil - } - return 0, ErrEndOfDataset -} - -// getCompressedBytesFromArtifact will return an array of bytes from the -// provided artifact path. -func getCompressedBytesFromArtifact(inputPath string) ([]byte, error) { - artifactPath, err := artifact.Path(inputPath) - if err != nil { - return nil, ErrEndOfDataset - } - path := filepath.Clean(artifactPath) - data, err := os.ReadFile(path) - if err != nil { - return nil, ErrEndOfDataset - } - - return data, nil -} - -// getPointCloudFromArtifact will return a point cloud based on the provided artifact path. -func getPointCloudFromArtifact(i int) (pointcloud.PointCloud, error) { - path := filepath.Clean(artifact.MustPath(fmt.Sprintf(datasetDirectory, i))) - pcdFile, err := os.Open(path) - if err != nil { - return nil, err - } - defer utils.UncheckedErrorFunc(pcdFile.Close) - - pcExpected, err := pointcloud.ReadPCD(pcdFile) - if err != nil { - return nil, err - } - - return pcExpected, nil -} - -type ctrl struct { - statusCode int - pcdFileNumber int -} - -// mockHandler will return the pcd file attached to the mock server. -func (c *ctrl) mockHandler(w http.ResponseWriter, r *http.Request) { - path := fmt.Sprintf(datasetDirectory, c.pcdFileNumber) - pcdFile, _ := getCompressedBytesFromArtifact(path) - - w.WriteHeader(c.statusCode) - w.Write(pcdFile) -} - -// newHTTPMock creates a set of mock http servers based on the number of PCD files used for testing. -func newHTTPMock(pattern string, statusCode int) []*httptest.Server { - httpServers := []*httptest.Server{} - for i := 0; i < numPCDFilesOriginal; i++ { - c := &ctrl{statusCode, i} - handler := http.NewServeMux() - handler.HandleFunc(pattern, c.mockHandler) - httpServers = append(httpServers, httptest.NewServer(handler)) - } - - return httpServers -} diff --git a/components/camera/rtppassthrough/buffer.go b/components/camera/rtppassthrough/buffer.go deleted file mode 100644 index 3627a3dcdb1..00000000000 --- a/components/camera/rtppassthrough/buffer.go +++ /dev/null @@ -1,119 +0,0 @@ -package rtppassthrough - -// heavily inspired by https://github.com/bluenviron/mediamtx/blob/main/internal/asyncwriter/async_writer.go - -// NOTE: (Nick S) -// *Buffer is what powers camera.SubscribeRTP. -// It runs a single goroutine which pulls and runs callback functions -// in order published from a fixed capacity ringbuffer. -// When the ringbuffer's capacity is reached, `Publish` returns an error. -// This is important to maintain a bounded -// amount of queuing as stale video stream packets degrade video quality. - -// One callback function is created & executed per RTP packet per subscribed client with the nuance that: -// (at time of writing) gostream is handling multiplexing from 1 track (a camera video feed) to N web -// browser peer connections interested in that track: https://github.com/viamrobotics/rdk/blob/main/gostream/webrtc_track.go#L126 -// As a result, at time of writing, a *Buffer only ever has a single publisher and a single subscriber. - -// NOTE: At time of writing github.com/bluenviron/gortsplib/v4/pkg/ringbuffer is not a ringbuffer (despite the name). -// It drops the newest data when full, not the oldest. - -import ( - "context" - "sync" - "sync/atomic" - - "github.com/bluenviron/gortsplib/v4/pkg/ringbuffer" - "github.com/google/uuid" - "github.com/pkg/errors" - "go.viam.com/utils" -) - -var ( - // ErrQueueFull indicates the Buffer's queue is full and that - // the callback passed to Publish will not be executed. - ErrQueueFull = errors.New("Buffer Publish queue full") - // ErrClosed indicates the Buffer is not running. - ErrClosed = errors.New("Buffer has been closed") - // ErrBufferSize indicates that the Buffer size - // can't be less than 0. - ErrBufferSize = errors.New("Buffer size can't be negative") -) - -// Buffer executes the callbacks sent to Publish -// in a single goroutine & drops Publish callbacks if the -// buffer is full. -// This is desirable behavior for streaming protocols where -// dropping stale packets is desirable to minimize latency. -type Buffer struct { - terminatedFn context.CancelFunc - buffer *ringbuffer.RingBuffer - err atomic.Value - wg sync.WaitGroup -} - -// NewSubscription allocates an rtppassthrough *Buffer and -// a Subscription. -// The *Buffer is intended to be used by the rtppassthrough.Source -// implemnter. The Subscription is intended to be returned -// to the SubscribeRTP caller (aka the subscriber). -// When the Subscription has terminated, the rtppassthrough.Source -// implemnter should call Close() on the *Buffer to notify the subscriber -// that the subscription has terminated. -func NewSubscription(size int) (Subscription, *Buffer, error) { - if size < 0 { - return NilSubscription, nil, ErrBufferSize - } - buffer, err := ringbuffer.New(uint64(size)) - if err != nil { - return NilSubscription, nil, err - } - - terminated, terminatedFn := context.WithCancel(context.Background()) - return Subscription{ID: uuid.New(), Terminated: terminated}, - &Buffer{terminatedFn: terminatedFn, buffer: buffer}, - nil -} - -// Start starts the buffer goroutine. -func (w *Buffer) Start() { - w.wg.Add(1) - utils.ManagedGo(w.run, w.wg.Done) -} - -// Close closes the buffer goroutine -// and terminates the Subscription. -func (w *Buffer) Close() { - w.buffer.Close() - w.wg.Wait() - w.terminatedFn() -} - -// Publish publishes adds the callback to the buffer -// where it will be run in the future. -// If the buffer is full, it returnns an error and does -// add the callback to the buffer. -func (w *Buffer) Publish(cb func()) error { - rawErr := w.err.Load() - - if err, ok := rawErr.(error); ok && err != nil { - return err - } - ok := w.buffer.Push(cb) - if !ok { - return ErrQueueFull - } - return nil -} - -func (w *Buffer) run() { - for { - cb, ok := w.buffer.Pull() - if !ok { - w.err.Store(ErrClosed) - return - } - - cb.(func())() - } -} diff --git a/components/camera/rtppassthrough/buffer_test.go b/components/camera/rtppassthrough/buffer_test.go deleted file mode 100644 index ca5dd49ab63..00000000000 --- a/components/camera/rtppassthrough/buffer_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package rtppassthrough - -import ( - "context" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" -) - -const queueSize int = 16 - -func TestStreamSubscription(t *testing.T) { - t.Run("NewSubscription", func(t *testing.T) { - t.Run("returns an err if queueSize is negative power of two", func(t *testing.T) { - _, _, err := NewSubscription(-1) - test.That(t, err, test.ShouldBeError, ErrBufferSize) - }) - - t.Run("returns an err if queueSize is not power of two", func(t *testing.T) { - _, _, err := NewSubscription(3) - test.That(t, err, test.ShouldBeError, errors.New("size must be a power of two")) - }) - - t.Run("returns no err otherwise", func(t *testing.T) { - _, _, err := NewSubscription(0) - test.That(t, err, test.ShouldBeNil) - _, _, err = NewSubscription(1) - test.That(t, err, test.ShouldBeNil) - _, _, err = NewSubscription(2) - test.That(t, err, test.ShouldBeNil) - _, _, err = NewSubscription(4) - test.That(t, err, test.ShouldBeNil) - _, _, err = NewSubscription(8) - test.That(t, err, test.ShouldBeNil) - }) - }) - - t.Run("ID", func(t *testing.T) { - t.Run("is unique", func(t *testing.T) { - subA, _, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - subB, _, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - test.That(t, subA.ID, test.ShouldNotResemble, subB.ID) - }) - }) - - t.Run("Publish", func(t *testing.T) { - t.Run("defers processing callbacks until after Start is called", func(t *testing.T) { - _, buffer, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - - publishCalledChan := make(chan struct{}, queueSize*2) - err = buffer.Publish(func() { - publishCalledChan <- struct{}{} - }) - - test.That(t, err, test.ShouldBeNil) - select { - case <-publishCalledChan: - t.Log("should not happen") - t.FailNow() - default: - } - - buffer.Start() - defer buffer.Close() - <-publishCalledChan - }) - - t.Run("returns err if called after Close is called and does not process callback", func(t *testing.T) { - _, buffer, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - - buffer.Start() - buffer.Close() - - err = buffer.Publish(func() { - t.Log("should not happen") - t.FailNow() - }) - - test.That(t, err, test.ShouldBeError, ErrClosed) - }) - - t.Run("drops callbacks after the queue size is reached", func(t *testing.T) { - _, buffer, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - defer buffer.Close() - - publishCalledChan := make(chan int, queueSize*2) - for i := 0; i < queueSize; i++ { - tmp := i - err = buffer.Publish(func() { - publishCalledChan <- tmp - }) - test.That(t, err, test.ShouldBeNil) - } - - err = buffer.Publish(func() { - t.Log("should not happen") - t.FailNow() - }) - - test.That(t, err, test.ShouldBeError, ErrQueueFull) - - buffer.Start() - - for i := 0; i < queueSize; i++ { - tmp := <-publishCalledChan - test.That(t, tmp, test.ShouldEqual, i) - } - select { - case <-publishCalledChan: - t.Log("should not happen") - t.FailNow() - default: - } - }) - }) - - t.Run("Close", func(t *testing.T) { - t.Run("succeeds if called before Start()", func(t *testing.T) { - _, buffer, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - buffer.Close() - }) - - t.Run("succeeds if called after Start()", func(t *testing.T) { - _, buffer, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - buffer.Start() - buffer.Close() - }) - - t.Run("terminates the Subscription", func(t *testing.T) { - sub, buffer, err := NewSubscription(queueSize) - test.That(t, err, test.ShouldBeNil) - test.That(t, sub.Terminated.Err(), test.ShouldBeNil) - buffer.Close() - test.That(t, sub.Terminated.Err(), test.ShouldBeError, context.Canceled) - }) - }) -} diff --git a/components/camera/rtppassthrough/rtppassthrough.go b/components/camera/rtppassthrough/rtppassthrough.go deleted file mode 100644 index 064dac83ed2..00000000000 --- a/components/camera/rtppassthrough/rtppassthrough.go +++ /dev/null @@ -1,41 +0,0 @@ -// Package rtppassthrough defines a Source of RTP packets -package rtppassthrough - -import ( - "context" - - "github.com/google/uuid" - "github.com/pion/rtp" -) - -// NilSubscription is the value of a nil Subscription. -var NilSubscription = Subscription{ID: uuid.Nil} - -type ( - // SubscriptionID is the ID of a Subscription. - SubscriptionID = uuid.UUID - // Subscription is the return value of a call to SubscribeRTP. - Subscription struct { - // ID is the ID of the Subscription - ID SubscriptionID - // The Terminated context will be cancelled when the RTP Subscription has terminated. - // A successful call to Unsubscribe terminates the RTP Subscription with that ID - // An RTP Subscription may also terminate for other internal to the Source - // (IO errors, reconfiguration, etc) - Terminated context.Context - } -) - -type ( - // PacketCallback is the signature of the SubscribeRTP callback. - PacketCallback func(pkts []*rtp.Packet) - // Source is a source of video codec data. - Source interface { - // SubscribeRTP begins a subscription to receive RTP packets. - // When the Subscription terminates the context in the returned Subscription - // is cancelled - SubscribeRTP(ctx context.Context, bufferSize int, packetsCB PacketCallback) (Subscription, error) - // Unsubscribe terminates the subscription with the provided SubscriptionID. - Unsubscribe(ctx context.Context, id SubscriptionID) error - } -) diff --git a/components/camera/server.go b/components/camera/server.go deleted file mode 100644 index 069e5909de4..00000000000 --- a/components/camera/server.go +++ /dev/null @@ -1,278 +0,0 @@ -package camera - -import ( - "bytes" - "context" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/camera/v1" - "google.golang.org/genproto/googleapis/api/httpbody" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/utils" -) - -// serviceServer implements the CameraService from camera.proto. -type serviceServer struct { - pb.UnimplementedCameraServiceServer - coll resource.APIResourceCollection[Camera] - imgTypes map[string]ImageType - logger logging.Logger -} - -// NewRPCServiceServer constructs an camera gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Camera]) interface{} { - logger := logging.NewLogger("camserver") - imgTypes := make(map[string]ImageType) - return &serviceServer{coll: coll, logger: logger, imgTypes: imgTypes} -} - -// GetImage returns an image from a camera of the underlying robot. If a specific MIME type -// is requested and is not available, an error is returned. -func (s *serviceServer) GetImage( - ctx context.Context, - req *pb.GetImageRequest, -) (*pb.GetImageResponse, error) { - ctx, span := trace.StartSpan(ctx, "camera::server::GetImage") - defer span.End() - cam, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - // Determine the mimeType we should try to use based on camera properties - if req.MimeType == "" { - if _, ok := s.imgTypes[req.Name]; !ok { - props, err := cam.Properties(ctx) - if err != nil { - s.logger.CWarnf(ctx, "camera properties not found for %s, assuming color images: %v", req.Name, err) - s.imgTypes[req.Name] = ColorStream - } else { - s.imgTypes[req.Name] = props.ImageType - } - } - switch s.imgTypes[req.Name] { - case ColorStream, UnspecifiedStream: - req.MimeType = utils.MimeTypeJPEG - case DepthStream: - req.MimeType = utils.MimeTypeRawDepth - default: - req.MimeType = utils.MimeTypeJPEG - } - } - - req.MimeType = utils.WithLazyMIMEType(req.MimeType) - - ext := req.Extra.AsMap() - ctx = NewContext(ctx, ext) - - img, release, err := ReadImage(gostream.WithMIMETypeHint(ctx, req.MimeType), cam) - if err != nil { - return nil, err - } - defer func() { - if release != nil { - release() - } - }() - actualMIME, _ := utils.CheckLazyMIMEType(req.MimeType) - resp := pb.GetImageResponse{ - MimeType: actualMIME, - } - outBytes, err := rimage.EncodeImage(ctx, img, req.MimeType) - if err != nil { - return nil, err - } - resp.Image = outBytes - return &resp, nil -} - -// GetImages returns a list of images and metadata from a camera of the underlying robot. -func (s *serviceServer) GetImages( - ctx context.Context, - req *pb.GetImagesRequest, -) (*pb.GetImagesResponse, error) { - ctx, span := trace.StartSpan(ctx, "camera::server::GetImages") - defer span.End() - cam, err := s.coll.Resource(req.Name) - if err != nil { - return nil, errors.Wrap(err, "camera server GetImages had an error getting the camera component") - } - // request the images, and then check to see what the underlying type is to determine - // what to encode as. If it's color, just encode as JPEG. - imgs, metadata, err := cam.Images(ctx) - if err != nil { - return nil, errors.Wrap(err, "camera server GetImages could not call Images on the camera") - } - imagesMessage := make([]*pb.Image, 0, len(imgs)) - for _, img := range imgs { - format, outBytes, err := encodeImageFromUnderlyingType(ctx, img.Image) - if err != nil { - return nil, errors.Wrap(err, "camera server GetImages could not encode the images") - } - imgMes := &pb.Image{ - SourceName: img.SourceName, - Format: format, - Image: outBytes, - } - imagesMessage = append(imagesMessage, imgMes) - } - // right now the only metadata is timestamp - resp := &pb.GetImagesResponse{ - Images: imagesMessage, - ResponseMetadata: metadata.AsProto(), - } - - return resp, nil -} - -func encodeImageFromUnderlyingType(ctx context.Context, img image.Image) (pb.Format, []byte, error) { - switch v := img.(type) { - case *rimage.LazyEncodedImage: - format := pb.Format_FORMAT_UNSPECIFIED - switch v.MIMEType() { - case utils.MimeTypeRawDepth: - format = pb.Format_FORMAT_RAW_DEPTH - case utils.MimeTypeRawRGBA: - format = pb.Format_FORMAT_RAW_RGBA - case utils.MimeTypeJPEG: - format = pb.Format_FORMAT_JPEG - case utils.MimeTypePNG: - format = pb.Format_FORMAT_PNG - default: - } - return format, v.RawData(), nil - case *rimage.DepthMap: - format := pb.Format_FORMAT_RAW_DEPTH - outBytes, err := rimage.EncodeImage(ctx, v, utils.MimeTypeRawDepth) - if err != nil { - return pb.Format_FORMAT_UNSPECIFIED, nil, err - } - return format, outBytes, nil - case *image.Gray16: - format := pb.Format_FORMAT_PNG - outBytes, err := rimage.EncodeImage(ctx, v, utils.MimeTypePNG) - if err != nil { - return pb.Format_FORMAT_UNSPECIFIED, nil, err - } - return format, outBytes, nil - default: - format := pb.Format_FORMAT_JPEG - outBytes, err := rimage.EncodeImage(ctx, v, utils.MimeTypeJPEG) - if err != nil { - return pb.Format_FORMAT_UNSPECIFIED, nil, err - } - return format, outBytes, nil - } -} - -// RenderFrame renders a frame from a camera of the underlying robot to an HTTP response. A specific MIME type -// can be requested but may not necessarily be the same one returned. -func (s *serviceServer) RenderFrame( - ctx context.Context, - req *pb.RenderFrameRequest, -) (*httpbody.HttpBody, error) { - ctx, span := trace.StartSpan(ctx, "camera::server::RenderFrame") - defer span.End() - if req.MimeType == "" { - req.MimeType = utils.MimeTypeJPEG // default rendering - } - resp, err := s.GetImage(ctx, (*pb.GetImageRequest)(req)) - if err != nil { - return nil, err - } - - return &httpbody.HttpBody{ - ContentType: resp.MimeType, - Data: resp.Image, - }, nil -} - -// GetPointCloud returns a frame from a camera of the underlying robot. A specific MIME type -// can be requested but may not necessarily be the same one returned. -func (s *serviceServer) GetPointCloud( - ctx context.Context, - req *pb.GetPointCloudRequest, -) (*pb.GetPointCloudResponse, error) { - ctx, span := trace.StartSpan(ctx, "camera::server::GetPointCloud") - defer span.End() - camera, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - pc, err := camera.NextPointCloud(ctx) - if err != nil { - return nil, err - } - - var buf bytes.Buffer - buf.Grow(200 + (pc.Size() * 4 * 4)) // 4 numbers per point, each 4 bytes - _, pcdSpan := trace.StartSpan(ctx, "camera::server::NextPointCloud::ToPCD") - err = pointcloud.ToPCD(pc, &buf, pointcloud.PCDBinary) - pcdSpan.End() - if err != nil { - return nil, err - } - - return &pb.GetPointCloudResponse{ - MimeType: utils.MimeTypePCD, - PointCloud: buf.Bytes(), - }, nil -} - -func (s *serviceServer) GetProperties( - ctx context.Context, - req *pb.GetPropertiesRequest, -) (*pb.GetPropertiesResponse, error) { - result := &pb.GetPropertiesResponse{} - camera, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - props, err := camera.Properties(ctx) - if err != nil { - return nil, err - } - intrinsics := props.IntrinsicParams - if intrinsics != nil { - result.IntrinsicParameters = &pb.IntrinsicParameters{ - WidthPx: uint32(intrinsics.Width), - HeightPx: uint32(intrinsics.Height), - FocalXPx: intrinsics.Fx, - FocalYPx: intrinsics.Fy, - CenterXPx: intrinsics.Ppx, - CenterYPx: intrinsics.Ppy, - } - } - result.SupportsPcd = props.SupportsPCD - if props.DistortionParams != nil { - result.DistortionParameters = &pb.DistortionParameters{ - Model: string(props.DistortionParams.ModelType()), - Parameters: props.DistortionParams.Parameters(), - } - } - - result.MimeTypes = props.MimeTypes - return result, nil -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - camera, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, camera, req) -} diff --git a/components/camera/server_test.go b/components/camera/server_test.go deleted file mode 100644 index c1c1a0cbbd3..00000000000 --- a/components/camera/server_test.go +++ /dev/null @@ -1,506 +0,0 @@ -package camera_test - -import ( - "bytes" - "context" - "errors" - "image" - "image/png" - "sync" - "testing" - "time" - - pb "go.viam.com/api/component/camera/v1" - "go.viam.com/test" - goprotoutils "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/data" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -var ( - errInvalidMimeType = errors.New("invalid mime type") - errGeneratePointCloudFailed = errors.New("can't generate next point cloud") - errPropertiesFailed = errors.New("can't get camera properties") - errCameraProjectorFailed = errors.New("can't get camera properties") - errStreamFailed = errors.New("can't generate stream") - errCameraUnimplemented = errors.New("not found") -) - -func newServer() (pb.CameraServiceServer, *inject.Camera, *inject.Camera, *inject.Camera, error) { - injectCamera := &inject.Camera{} - injectCameraDepth := &inject.Camera{} - injectCamera2 := &inject.Camera{} - cameras := map[resource.Name]camera.Camera{ - camera.Named(testCameraName): injectCamera, - camera.Named(depthCameraName): injectCameraDepth, - camera.Named(failCameraName): injectCamera2, - } - cameraSvc, err := resource.NewAPIResourceCollection(camera.API, cameras) - if err != nil { - return nil, nil, nil, nil, err - } - return camera.NewRPCServiceServer(cameraSvc).(pb.CameraServiceServer), injectCamera, injectCameraDepth, injectCamera2, nil -} - -func TestServer(t *testing.T) { - cameraServer, injectCamera, injectCameraDepth, injectCamera2, err := newServer() - test.That(t, err, test.ShouldBeNil) - - img := image.NewRGBA(image.Rect(0, 0, 4, 4)) - var imgBuf bytes.Buffer - test.That(t, png.Encode(&imgBuf, img), test.ShouldBeNil) - var imgBufJpeg bytes.Buffer - - test.That(t, rimage.EncodeJPEG(&imgBufJpeg, img), test.ShouldBeNil) - - imgPng, err := png.Decode(bytes.NewReader(imgBuf.Bytes())) - test.That(t, err, test.ShouldBeNil) - imgJpeg, err := rimage.DecodeJPEG(bytes.NewReader(imgBufJpeg.Bytes())) - - test.That(t, err, test.ShouldBeNil) - - var projA transform.Projector - intrinsics := &transform.PinholeCameraIntrinsics{ // not the real camera parameters -- fake for test - Width: 1280, - Height: 720, - Fx: 200, - Fy: 200, - Ppx: 100, - Ppy: 100, - } - projA = intrinsics - pcA := pointcloud.New() - err = pcA.Set(pointcloud.NewVector(5, 5, 5), nil) - test.That(t, err, test.ShouldBeNil) - - var imageReleased bool - var imageReleasedMu sync.Mutex - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pcA, nil - } - injectCamera.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{ - SupportsPCD: true, - IntrinsicParams: intrinsics, - MimeTypes: []string{utils.MimeTypeJPEG, utils.MimeTypePNG, utils.MimeTypeH264}, - }, nil - } - injectCamera.ImagesFunc = func(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - images := []camera.NamedImage{} - // one color image - color := rimage.NewImage(40, 50) - images = append(images, camera.NamedImage{color, "color"}) - // one depth image - depth := rimage.NewEmptyDepthMap(10, 20) - images = append(images, camera.NamedImage{depth, "depth"}) - // a timestamp of 12345 - ts := time.UnixMilli(12345) - return images, resource.ResponseMetadata{ts}, nil - } - injectCamera.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return projA, nil - } - wooMIME := "image/woohoo" - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader(gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - imageReleasedMu.Lock() - imageReleased = true - imageReleasedMu.Unlock() - mimeType, _ := utils.CheckLazyMIMEType(gostream.MIMETypeHint(ctx, utils.MimeTypeRawRGBA)) - switch mimeType { - case "", utils.MimeTypeRawRGBA: - return img, func() {}, nil - case utils.MimeTypePNG: - return imgPng, func() {}, nil - case utils.MimeTypeJPEG: - return imgJpeg, func() {}, nil - case "image/woohoo": - return rimage.NewLazyEncodedImage([]byte{1, 2, 3}, mimeType), func() {}, nil - default: - return nil, nil, errInvalidMimeType - } - })), nil - } - // depth camera - depthImage := rimage.NewEmptyDepthMap(10, 20) - depthImage.Set(0, 0, rimage.Depth(40)) - depthImage.Set(0, 1, rimage.Depth(1)) - depthImage.Set(5, 6, rimage.Depth(190)) - depthImage.Set(9, 12, rimage.Depth(3000)) - depthImage.Set(5, 9, rimage.MaxDepth-rimage.Depth(1)) - var depthBuf bytes.Buffer - test.That(t, png.Encode(&depthBuf, depthImage), test.ShouldBeNil) - injectCameraDepth.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pcA, nil - } - injectCameraDepth.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{ - SupportsPCD: true, - IntrinsicParams: intrinsics, - ImageType: camera.DepthStream, - }, nil - } - injectCameraDepth.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return projA, nil - } - injectCameraDepth.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader(gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - imageReleasedMu.Lock() - imageReleased = true - imageReleasedMu.Unlock() - return depthImage, func() {}, nil - })), nil - } - // bad camera - injectCamera2.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return nil, errGeneratePointCloudFailed - } - injectCamera2.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, errPropertiesFailed - } - injectCamera2.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return nil, errCameraProjectorFailed - } - injectCamera2.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return nil, errStreamFailed - } - // does a depth camera transfer its depth image properly - t.Run("GetImage", func(t *testing.T) { - _, err := cameraServer.GetImage(context.Background(), &pb.GetImageRequest{Name: missingCameraName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCameraUnimplemented.Error()) - - // color camera - // ensure that explicit RawRGBA mimetype request will return RawRGBA mimetype response - resp, err := cameraServer.GetImage( - context.Background(), - &pb.GetImageRequest{Name: testCameraName, MimeType: utils.MimeTypeRawRGBA}, - ) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.MimeType, test.ShouldEqual, utils.MimeTypeRawRGBA) - test.That(t, resp.Image[rimage.RawRGBAHeaderLength:], test.ShouldResemble, img.Pix) - - // ensure that empty mimetype request from color cam will return JPEG mimetype response - resp, err = cameraServer.GetImage( - context.Background(), - &pb.GetImageRequest{Name: testCameraName, MimeType: ""}, - ) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.MimeType, test.ShouldEqual, utils.MimeTypeJPEG) - test.That(t, resp.Image, test.ShouldNotBeNil) - - // ensure that empty mimetype request from depth cam will return PNG mimetype response - resp, err = cameraServer.GetImage( - context.Background(), - &pb.GetImageRequest{Name: depthCameraName, MimeType: ""}, - ) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.MimeType, test.ShouldEqual, utils.MimeTypeRawDepth) - test.That(t, resp.Image, test.ShouldNotBeNil) - - imageReleasedMu.Lock() - imageReleased = false - imageReleasedMu.Unlock() - resp, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - MimeType: "image/png", - }) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.MimeType, test.ShouldEqual, utils.MimeTypePNG) - test.That(t, resp.Image, test.ShouldResemble, imgBuf.Bytes()) - - imageReleasedMu.Lock() - imageReleased = false - imageReleasedMu.Unlock() - _, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - MimeType: "image/who", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errInvalidMimeType.Error()) - - // depth camera - imageReleasedMu.Lock() - imageReleased = false - imageReleasedMu.Unlock() - resp, err = cameraServer.GetImage( - context.Background(), - &pb.GetImageRequest{Name: depthCameraName, MimeType: utils.MimeTypePNG}, - ) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.MimeType, test.ShouldEqual, utils.MimeTypePNG) - test.That(t, resp.Image, test.ShouldNotBeNil) - decodedDepth, err := rimage.DecodeImage( - context.Background(), - resp.Image, - resp.MimeType, - ) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), decodedDepth) - test.That(t, err, test.ShouldBeNil) - test.That(t, dm, test.ShouldResemble, depthImage) - - imageReleasedMu.Lock() - imageReleased = false - imageReleasedMu.Unlock() - resp, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: depthCameraName, - MimeType: "image/png", - }) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.MimeType, test.ShouldEqual, utils.MimeTypePNG) - test.That(t, resp.Image, test.ShouldResemble, depthBuf.Bytes()) - // bad camera - _, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{Name: failCameraName, MimeType: utils.MimeTypeRawRGBA}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - }) - - t.Run("GetImage with lazy", func(t *testing.T) { - // we know its lazy if it's a mime we can't actually handle internally - resp, err := cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - MimeType: wooMIME, - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.MimeType, test.ShouldEqual, wooMIME) - test.That(t, resp.Image, test.ShouldResemble, []byte{1, 2, 3}) - - _, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - MimeType: "image/notwoo", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errInvalidMimeType.Error()) - }) - - t.Run("GetImage with +lazy default", func(t *testing.T) { - for _, mimeType := range []string{ - utils.MimeTypePNG, - utils.MimeTypeJPEG, - utils.MimeTypeRawRGBA, - } { - req := pb.GetImageRequest{ - Name: testCameraName, - MimeType: mimeType, - } - resp, err := cameraServer.GetImage(context.Background(), &req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Image, test.ShouldNotBeNil) - test.That(t, req.MimeType, test.ShouldEqual, utils.WithLazyMIMEType(mimeType)) - } - }) - - t.Run("RenderFrame", func(t *testing.T) { - _, err := cameraServer.RenderFrame(context.Background(), &pb.RenderFrameRequest{Name: missingCameraName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCameraUnimplemented.Error()) - - resp, err := cameraServer.RenderFrame(context.Background(), &pb.RenderFrameRequest{ - Name: testCameraName, - }) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.ContentType, test.ShouldEqual, "image/jpeg") - test.That(t, resp.Data, test.ShouldResemble, imgBufJpeg.Bytes()) - - imageReleasedMu.Lock() - imageReleased = false - imageReleasedMu.Unlock() - resp, err = cameraServer.RenderFrame(context.Background(), &pb.RenderFrameRequest{ - Name: testCameraName, - MimeType: "image/png", - }) - test.That(t, err, test.ShouldBeNil) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - test.That(t, resp.ContentType, test.ShouldEqual, "image/png") - test.That(t, resp.Data, test.ShouldResemble, imgBuf.Bytes()) - - imageReleasedMu.Lock() - imageReleased = false - imageReleasedMu.Unlock() - - _, err = cameraServer.RenderFrame(context.Background(), &pb.RenderFrameRequest{ - Name: testCameraName, - MimeType: "image/who", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errInvalidMimeType.Error()) - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - - _, err = cameraServer.RenderFrame(context.Background(), &pb.RenderFrameRequest{Name: failCameraName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - }) - - t.Run("GetPointCloud", func(t *testing.T) { - _, err := cameraServer.GetPointCloud(context.Background(), &pb.GetPointCloudRequest{Name: missingCameraName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCameraUnimplemented.Error()) - - pcA := pointcloud.New() - err = pcA.Set(pointcloud.NewVector(5, 5, 5), nil) - test.That(t, err, test.ShouldBeNil) - - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pcA, nil - } - _, err = cameraServer.GetPointCloud(context.Background(), &pb.GetPointCloudRequest{ - Name: testCameraName, - }) - test.That(t, err, test.ShouldBeNil) - - _, err = cameraServer.GetPointCloud(context.Background(), &pb.GetPointCloudRequest{ - Name: failCameraName, - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGeneratePointCloudFailed.Error()) - }) - t.Run("GetImages", func(t *testing.T) { - _, err := cameraServer.GetImages(context.Background(), &pb.GetImagesRequest{Name: missingCameraName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCameraUnimplemented.Error()) - - resp, err := cameraServer.GetImages(context.Background(), &pb.GetImagesRequest{Name: testCameraName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.ResponseMetadata.CapturedAt.AsTime(), test.ShouldEqual, time.UnixMilli(12345)) - test.That(t, len(resp.Images), test.ShouldEqual, 2) - test.That(t, resp.Images[0].Format, test.ShouldEqual, pb.Format_FORMAT_JPEG) - test.That(t, resp.Images[0].SourceName, test.ShouldEqual, "color") - test.That(t, resp.Images[1].Format, test.ShouldEqual, pb.Format_FORMAT_RAW_DEPTH) - test.That(t, resp.Images[1].SourceName, test.ShouldEqual, "depth") - }) - - t.Run("GetProperties", func(t *testing.T) { - _, err := cameraServer.GetProperties(context.Background(), &pb.GetPropertiesRequest{Name: missingCameraName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCameraUnimplemented.Error()) - - resp, err := cameraServer.GetProperties(context.Background(), &pb.GetPropertiesRequest{Name: testCameraName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.SupportsPcd, test.ShouldBeTrue) - test.That(t, resp.IntrinsicParameters.WidthPx, test.ShouldEqual, 1280) - test.That(t, resp.IntrinsicParameters.HeightPx, test.ShouldEqual, 720) - test.That(t, resp.IntrinsicParameters.FocalXPx, test.ShouldEqual, 200) - test.That(t, resp.IntrinsicParameters.FocalYPx, test.ShouldEqual, 200) - test.That(t, resp.IntrinsicParameters.CenterXPx, test.ShouldEqual, 100) - test.That(t, resp.IntrinsicParameters.CenterYPx, test.ShouldEqual, 100) - test.That(t, resp.MimeTypes, test.ShouldContain, utils.MimeTypeJPEG) - test.That(t, resp.MimeTypes, test.ShouldContain, utils.MimeTypePNG) - test.That(t, resp.MimeTypes, test.ShouldContain, utils.MimeTypeH264) - }) - - t.Run("GetImage with extra", func(t *testing.T) { - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, extra, test.ShouldBeEmpty) - return nil, errStreamFailed - } - - _, err := cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - }) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(extra), test.ShouldEqual, 1) - test.That(t, extra["hello"], test.ShouldEqual, "world") - return nil, errStreamFailed - } - - ext, err := goprotoutils.StructToStructPb(camera.Extra{"hello": "world"}) - test.That(t, err, test.ShouldBeNil) - - _, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - Extra: ext, - }) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(extra), test.ShouldEqual, 1) - test.That(t, extra[data.FromDMString], test.ShouldBeTrue) - - return nil, errStreamFailed - } - - // one kvp created with data.FromDMContextKey - ext, err = goprotoutils.StructToStructPb(map[string]interface{}{data.FromDMString: true}) - test.That(t, err, test.ShouldBeNil) - - _, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - Extra: ext, - }) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - extra, ok := camera.FromContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(extra), test.ShouldEqual, 2) - test.That(t, extra["hello"], test.ShouldEqual, "world") - test.That(t, extra[data.FromDMString], test.ShouldBeTrue) - return nil, errStreamFailed - } - - // use values from data and camera - ext, err = goprotoutils.StructToStructPb( - map[string]interface{}{ - data.FromDMString: true, - "hello": "world", - }, - ) - test.That(t, err, test.ShouldBeNil) - - _, err = cameraServer.GetImage(context.Background(), &pb.GetImageRequest{ - Name: testCameraName, - Extra: ext, - }) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStreamFailed.Error()) - }) -} diff --git a/components/camera/transformpipeline/classifier.go b/components/camera/transformpipeline/classifier.go deleted file mode 100644 index 5db1f7c43e8..00000000000 --- a/components/camera/transformpipeline/classifier.go +++ /dev/null @@ -1,115 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - "strings" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/classification" -) - -// classifierConfig is the attribute struct for classifiers. -type classifierConfig struct { - ClassifierName string `json:"classifier_name"` - ConfidenceThreshold float64 `json:"confidence_threshold"` - MaxClassifications uint32 `json:"max_classifications"` - ValidLabels []string `json:"valid_labels"` -} - -// classifierSource takes an image from the camera, and overlays labels from the classifier. -type classifierSource struct { - stream gostream.VideoStream - classifierName string - maxClassifications uint32 - labelFilter classification.Postprocessor - confFilter classification.Postprocessor - r robot.Robot -} - -func newClassificationsTransform( - ctx context.Context, - source gostream.VideoSource, r robot.Robot, am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*classifierConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - - props, err := propsFromVideoSource(ctx, source) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - validLabels := make(map[string]interface{}) - for _, l := range conf.ValidLabels { - validLabels[strings.ToLower(l)] = struct{}{} - } - labelFilter := classification.NewLabelFilter(validLabels) - confFilter := classification.NewScoreFilter(conf.ConfidenceThreshold) - var maxClassifications uint32 = 1 - if conf.MaxClassifications != 0 { - maxClassifications = conf.MaxClassifications - } - classifier := &classifierSource{ - gostream.NewEmbeddedVideoStream(source), - conf.ClassifierName, - maxClassifications, - labelFilter, - confFilter, - r, - } - src, err := camera.NewVideoSourceFromReader(ctx, classifier, &cameraModel, camera.ColorStream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, camera.ColorStream, err -} - -// Read returns the image overlaid with at most max_classifications labels from the classification. -// It overlays the labels along with the confidence scores, as long as the scores are above the -// confidence threshold. -func (cs *classifierSource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::classifier::Read") - defer span.End() - - srv, err := vision.FromRobot(cs.r, cs.classifierName) - if err != nil { - return nil, nil, errors.Wrap(err, "source_classifier can't find vision service") - } - // get image from source camera - img, release, err := cs.stream.Next(ctx) - if err != nil { - return nil, nil, errors.Wrap(err, "could not get next source image") - } - classifications, err := srv.Classifications(ctx, img, int(cs.maxClassifications), map[string]interface{}{}) - if err != nil { - return nil, nil, errors.Wrap(err, "could not get classifications") - } - // overlay labels on the source image - classifications = cs.confFilter(classifications) - classifications = cs.labelFilter(classifications) - res, err := classification.Overlay(img, classifications) - if err != nil { - return nil, nil, errors.Wrap(err, "could not overlay labels") - } - return res, release, nil -} - -func (cs *classifierSource) Close(ctx context.Context) error { - return cs.stream.Close(ctx) -} diff --git a/components/camera/transformpipeline/classifier_test.go b/components/camera/transformpipeline/classifier_test.go deleted file mode 100644 index 122d13de236..00000000000 --- a/components/camera/transformpipeline/classifier_test.go +++ /dev/null @@ -1,128 +0,0 @@ -//go:build !no_tflite - -package transformpipeline - -import ( - "context" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/robot" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/services/mlmodel" - _ "go.viam.com/rdk/services/mlmodel/register" - "go.viam.com/rdk/services/vision" - _ "go.viam.com/rdk/services/vision/register" - rutils "go.viam.com/rdk/utils" -) - -func buildRobotWithClassifier(logger logging.Logger) (robot.Robot, error) { - cfg := &config.Config{} - - // create fake source camera - tfliteSrv1 := resource.Config{ - Name: "object_classifier", - API: mlmodel.API, - Model: resource.DefaultModelFamily.WithModel("tflite_cpu"), - Attributes: rutils.AttributeMap{ - "model_path": artifact.MustPath("vision/classification/object_classifier.tflite"), - "label_path": artifact.MustPath("vision/classification/object_labels.txt"), - "num_threads": 1, - }, - } - cfg.Services = append(cfg.Services, tfliteSrv1) - visionSrv1 := resource.Config{ - Name: "vision_classifier", - API: vision.API, - Model: resource.DefaultModelFamily.WithModel("mlmodel"), - Attributes: rutils.AttributeMap{ - "mlmodel_name": "object_classifier", - }, - DependsOn: []string{"object_classifier"}, - } - cfg.Services = append(cfg.Services, visionSrv1) - cameraComp := resource.Config{ - Name: "fake_cam", - API: camera.API, - Model: resource.DefaultModelFamily.WithModel("image_file"), - Attributes: rutils.AttributeMap{ - "color_image_file_path": artifact.MustPath("vision/classification/keyboard.jpg"), - "depth_image_file_path": "", - }, - } - cfg.Components = append(cfg.Components, cameraComp) - - // create classification transform camera - classifierComp := resource.Config{ - Name: "classification_transform_camera", - API: camera.API, - Model: resource.DefaultModelFamily.WithModel("transform"), - Attributes: rutils.AttributeMap{ - "source": "fake_cam", - "pipeline": []rutils.AttributeMap{ - { - "type": "classifications", - "attributes": rutils.AttributeMap{ - "classifier_name": "vision_classifier", - "confidence_threshold": 0.35, - "max_classifications": 5, - }, - }, - }, - }, - DependsOn: []string{"fake_cam"}, - } - cfg.Components = append(cfg.Components, classifierComp) - if err := cfg.Ensure(false, logger); err != nil { - return nil, err - } - - newConfFile, err := writeTempConfig(cfg) - if err != nil { - return nil, err - } - defer os.Remove(newConfFile) - - // make the robot from new config - r, err := robotimpl.RobotFromConfigPath(context.Background(), newConfFile, logger) - if err != nil { - return nil, err - } - return r, nil -} - -//nolint:dupl -func TestClassifierSource(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - r, err := buildRobotWithClassifier(logger) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - - classifier, err := camera.FromRobot(r, "classification_transform_camera") - test.That(t, err, test.ShouldBeNil) - defer classifier.Close(ctx) - - resImg, _, err := camera.ReadImage(ctx, classifier) - test.That(t, err, test.ShouldBeNil) - ovImg := rimage.ConvertImage(resImg) - - // Max classifications was 5, but this image gets classified with just 2 labels, so we - // test that each label is present. - test.That(t, ovImg.GetXY(149, 48), test.ShouldResemble, rimage.Red) - test.That(t, ovImg.GetXY(100, 75), test.ShouldResemble, rimage.Red) - test.That(t, classifier.Close(context.Background()), test.ShouldBeNil) -} diff --git a/components/camera/transformpipeline/composed.go b/components/camera/transformpipeline/composed.go deleted file mode 100644 index 333d9682c3e..00000000000 --- a/components/camera/transformpipeline/composed.go +++ /dev/null @@ -1,177 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -// depthToPretty takes a depth image and turns into a colorful image, with blue being -// farther away, and red being closest. Actual depth information is lost in the transform. -type depthToPretty struct { - originalStream gostream.VideoStream - cameraModel *transform.PinholeCameraModel -} - -func propsFromVideoSource(ctx context.Context, source gostream.VideoSource) (camera.Properties, error) { - var camProps camera.Properties - //nolint:staticcheck - if cameraSrc, ok := source.(camera.Camera); ok { - props, err := cameraSrc.Properties(ctx) - if err != nil { - return camProps, err - } - camProps = props - } - return camProps, nil -} - -func newDepthToPrettyTransform( - ctx context.Context, - source gostream.VideoSource, - stream camera.ImageType, -) (gostream.VideoSource, camera.ImageType, error) { - if stream != camera.DepthStream { - return nil, camera.UnspecifiedStream, - errors.Errorf("source has stream type %s, depth_to_pretty only supports depth stream inputs", stream) - } - props, err := propsFromVideoSource(ctx, source) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - depthStream := gostream.NewEmbeddedVideoStream(source) - reader := &depthToPretty{ - originalStream: depthStream, - cameraModel: &cameraModel, - } - src, err := camera.NewVideoSourceFromReader(ctx, reader, &cameraModel, camera.ColorStream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, camera.ColorStream, err -} - -func (dtp *depthToPretty) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::depthToPretty::Read") - defer span.End() - i, release, err := dtp.originalStream.Next(ctx) - if err != nil { - return nil, nil, err - } - dm, err := rimage.ConvertImageToDepthMap(ctx, i) - if err != nil { - return nil, nil, errors.Wrapf(err, "source camera does not make depth maps") - } - return dm.ToPrettyPicture(0, rimage.MaxDepth), release, nil -} - -func (dtp *depthToPretty) Close(ctx context.Context) error { - return dtp.originalStream.Close(ctx) -} - -func (dtp *depthToPretty) PointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::depthToPretty::PointCloud") - defer span.End() - if dtp.cameraModel == nil || dtp.cameraModel.PinholeCameraIntrinsics == nil { - return nil, errors.Wrapf(transform.ErrNoIntrinsics, "depthToPretty transform cannot project to pointcloud") - } - i, release, err := dtp.originalStream.Next(ctx) - if err != nil { - return nil, err - } - defer release() - dm, err := rimage.ConvertImageToDepthMap(ctx, i) - if err != nil { - return nil, errors.Wrapf(err, "source camera does not make depth maps") - } - img := dm.ToPrettyPicture(0, rimage.MaxDepth) - return dtp.cameraModel.RGBDToPointCloud(img, dm) -} - -// overlayConfig are the attributes for an overlay transform. -type overlayConfig struct { - IntrinsicParams *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters"` -} - -// overlaySource overlays the depth and color 2D images in order to debug the alignment of the two images. -type overlaySource struct { - src gostream.VideoSource - cameraModel *transform.PinholeCameraModel -} - -func newOverlayTransform( - ctx context.Context, - src gostream.VideoSource, - stream camera.ImageType, - am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*overlayConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - - props, err := propsFromVideoSource(ctx, src) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - if conf.IntrinsicParams != nil && conf.IntrinsicParams.Height > 0. && - conf.IntrinsicParams.Width > 0. && conf.IntrinsicParams.Fx > 0. && conf.IntrinsicParams.Fy > 0. { - cameraModel.PinholeCameraIntrinsics = conf.IntrinsicParams - } - if cameraModel.PinholeCameraIntrinsics == nil { - return nil, camera.UnspecifiedStream, transform.ErrNoIntrinsics - } - reader := &overlaySource{src, &cameraModel} - src, err = camera.NewVideoSourceFromReader(ctx, reader, &cameraModel, stream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, stream, err -} - -func (os *overlaySource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::overlay::Read") - defer span.End() - if os.cameraModel == nil || os.cameraModel.PinholeCameraIntrinsics == nil { - return nil, nil, transform.ErrNoIntrinsics - } - srcPointCloud, ok := os.src.(camera.PointCloudSource) - if !ok { - return nil, nil, errors.New("source of overlay transform does not have PointCloud method") - } - pc, err := srcPointCloud.NextPointCloud(ctx) - if err != nil { - return nil, nil, err - } - col, dm, err := os.cameraModel.PointCloudToRGBD(pc) - if err != nil { - return nil, nil, err - } - return rimage.Overlay(col, dm), func() {}, nil -} - -func (os *overlaySource) Close(ctx context.Context) error { - return nil -} diff --git a/components/camera/transformpipeline/composed_test.go b/components/camera/transformpipeline/composed_test.go deleted file mode 100644 index bf288cf938e..00000000000 --- a/components/camera/transformpipeline/composed_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - "image/color" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -type streamTest struct{} - -// Next will stream a color image. -func (*streamTest) Next(ctx context.Context) (image.Image, func(), error) { - return rimage.NewImage(1280, 720), func() {}, nil -} -func (*streamTest) Close(ctx context.Context) error { return nil } - -func TestComposed(t *testing.T) { - // create pointcloud source and fake robot - robot := &inject.Robot{} - logger := logging.NewTestLogger(t) - cloudSource := &inject.Camera{} - cloudSource.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - p := pointcloud.New() - return p, p.Set(pointcloud.NewVector(0, 0, 0), pointcloud.NewColoredData(color.NRGBA{255, 1, 2, 255})) - } - cloudSource.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return &streamTest{}, nil - } - cloudSource.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - // get intrinsic parameters, and make config - am := utils.AttributeMap{ - "intrinsic_parameters": &transform.PinholeCameraIntrinsics{ - Width: 1280, - Height: 720, - Fx: 100, - Fy: 100, - }, - } - // make transform pipeline, expected result with correct config - conf := &transformConfig{ - Pipeline: []Transformation{ - { - Type: "overlay", - Attributes: am, - }, - }, - } - pc, err := cloudSource.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 1) - - myOverlay, stream, err := newOverlayTransform(context.Background(), cloudSource, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - pic, _, err := camera.ReadImage(context.Background(), myOverlay) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic.Bounds(), test.ShouldResemble, image.Rect(0, 0, 1280, 720)) - - myPipeline, err := newTransformPipeline(context.Background(), cloudSource, conf, robot, logger) - test.That(t, err, test.ShouldBeNil) - defer myPipeline.Close(context.Background()) - pic, _, err = camera.ReadImage(context.Background(), myPipeline) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic.Bounds(), test.ShouldResemble, image.Rect(0, 0, 1280, 720)) - - // wrong result with bad config - _, _, err = newOverlayTransform(context.Background(), cloudSource, camera.ColorStream, utils.AttributeMap{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldWrap, transform.ErrNoIntrinsics) - // wrong config, still no intrinsics - am = utils.AttributeMap{ - "intrinsic_parameters": &transform.PinholeCameraIntrinsics{ - Width: 1280, - Height: 720, - }, - } - _, _, err = newOverlayTransform(context.Background(), cloudSource, camera.ColorStream, am) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldWrap, transform.ErrNoIntrinsics) - // wrong config, no attributes - conf = &transformConfig{ - Pipeline: []Transformation{ - { - Type: "overlay", // no attributes - }, - }, - } - _, err = newTransformPipeline(context.Background(), cloudSource, conf, robot, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldWrap, transform.ErrNoIntrinsics) -} diff --git a/components/camera/transformpipeline/data/gripper_parameters.json b/components/camera/transformpipeline/data/gripper_parameters.json deleted file mode 100644 index 83e58e1bca3..00000000000 --- a/components/camera/transformpipeline/data/gripper_parameters.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "color": { - "height": 768, - "width": 1024, - "fx": 821.32642889, - "fy": 821.68607359, - "ppx": 494.95941428, - "ppy": 370.70529534, - "distortion": { - "rk1": 0.11297234, - "rk2": -0.21375332, - "rk3": -0.01584774, - "tp1": -0.00302002, - "tp2": 0.19969297 - } - }, - "depth": { - "height": 171, - "width": 224, - "fx": 216.583, - "fy": 216.583, - "ppx": 116.385, - "ppy": 87.7112, - "distortion": { - "rk1": 0.10837, - "rk2": -3.06437, - "rk3": 5.76528, - "tp1": 2.67302E-16, - "tp2": 6.04658E-16 - } - }, - "extrinsics_depth_to_color": { - "rotation": [ - 1.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 1.0 - ], - "translation": [ - 0.0, - 0.0, - 0.0 - ] - } -} diff --git a/components/camera/transformpipeline/data/intel515_parameters.json b/components/camera/transformpipeline/data/intel515_parameters.json deleted file mode 100644 index 99021e4cf6b..00000000000 --- a/components/camera/transformpipeline/data/intel515_parameters.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "color": { - "height": 720, - "width": 1280, - "fx": 900.538000, - "fy": 900.818000, - "ppx": 648.934000, - "ppy": 367.736000, - "distortion": { - "rk1": 0.158701, - "rk2": -0.485405, - "rk3": 0.435342, - "tp1": -0.00143327, - "tp2": -0.000705919 - } - }, - "depth": { - "height": 768, - "width": 1024, - "fx": 734.938, - "fy": 735.516, - "ppx": 542.078, - "ppy": 398.016, - "distortion": { - "rk1": 0.0, - "rk2": 0.0, - "rk3": 0.0, - "tp1": 0.0, - "tp2": 0.0 - } - }, - "extrinsics_depth_to_color": { - "rotation": [ - 0.999958, - -0.00838489, - 0.00378392, - 0.00824708, - 0.999351, - 0.0350734, - -0.00407554, - -0.0350407, - 0.999378 - ], - "translation": [ - -0.000828434, - 0.0139185, - -0.0033418 - ] - } -} diff --git a/components/camera/transformpipeline/depth_edges.go b/components/camera/transformpipeline/depth_edges.go deleted file mode 100644 index e5c0f558126..00000000000 --- a/components/camera/transformpipeline/depth_edges.go +++ /dev/null @@ -1,77 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -type depthEdgesConfig struct { - HiThresh float64 `json:"high_threshold_pct"` - LoThresh float64 `json:"low_threshold_pct"` - BlurRadius float64 `json:"blur_radius_px"` -} - -// depthEdgesSource applies a Canny Edge Detector to the depth map. -type depthEdgesSource struct { - stream gostream.VideoStream - detector *rimage.CannyEdgeDetector - blurRadius float64 -} - -func newDepthEdgesTransform(ctx context.Context, source gostream.VideoSource, am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*depthEdgesConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - props, err := propsFromVideoSource(ctx, source) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - canny := rimage.NewCannyDericheEdgeDetectorWithParameters(conf.HiThresh, conf.LoThresh, true) - videoSrc := &depthEdgesSource{gostream.NewEmbeddedVideoStream(source), canny, 3.0} - src, err := camera.NewVideoSourceFromReader(ctx, videoSrc, &cameraModel, camera.DepthStream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, camera.DepthStream, err -} - -// Next applies a canny edge detector on the depth map of the next image. -func (os *depthEdgesSource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::depthEdges::Read") - defer span.End() - i, closer, err := os.stream.Next(ctx) - if err != nil { - return nil, nil, err - } - dm, err := rimage.ConvertImageToDepthMap(ctx, i) - if err != nil { - return nil, nil, errors.Wrapf(err, "cannot transform for depth edges") - } - edges, err := os.detector.DetectDepthEdges(dm, os.blurRadius) - if err != nil { - return nil, nil, err - } - return edges, closer, nil -} - -func (os *depthEdgesSource) Close(ctx context.Context) error { - return os.stream.Close(ctx) -} diff --git a/components/camera/transformpipeline/depth_edges_test.go b/components/camera/transformpipeline/depth_edges_test.go deleted file mode 100644 index 1db77683105..00000000000 --- a/components/camera/transformpipeline/depth_edges_test.go +++ /dev/null @@ -1,132 +0,0 @@ -//go:build !no_cgo - -package transformpipeline - -import ( - "context" - "image" - "testing" - - "github.com/pion/mediadevices/pkg/prop" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/depthadapter" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -func TestDepthSource(t *testing.T) { - img, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board1_gray_small.png")) - test.That(t, err, test.ShouldBeNil) - source := &videosource.StaticSource{DepthImg: img} - am := utils.AttributeMap{ - "high_threshold": 0.85, - "low_threshold": 0.40, - "blur_radius": 3.0, - } - ds, stream, err := newDepthEdgesTransform(context.Background(), gostream.NewVideoSource(source, prop.Video{}), am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - _, _, err = camera.ReadImage(context.Background(), ds) - test.That(t, err, test.ShouldBeNil) - test.That(t, ds.Close(context.Background()), test.ShouldBeNil) -} - -type depthSourceTestHelper struct { - proj transform.Projector -} - -func (h *depthSourceTestHelper) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - img image.Image, - img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - dm, err := rimage.ConvertImageToDepthMap(context.Background(), img) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "aligned-depth") - - // create edge map - source := &videosource.StaticSource{DepthImg: dm} - am := utils.AttributeMap{ - "high_threshold": 0.85, - "low_threshold": 0.40, - "blur_radius": 3.0, - } - ds, stream, err := newDepthEdgesTransform(context.Background(), gostream.NewVideoSource(source, prop.Video{}), am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - edges, _, err := camera.ReadImage(context.Background(), ds) - test.That(t, err, test.ShouldBeNil) - test.That(t, ds.Close(context.Background()), test.ShouldBeNil) - - pCtx.GotDebugImage(edges, "edges-aligned-depth") - - // make point cloud - fixedPointCloud := depthadapter.ToPointCloud(dm, h.proj) - test.That(t, fixedPointCloud.MetaData().HasColor, test.ShouldBeFalse) - pCtx.GotDebugPointCloud(fixedPointCloud, "aligned-pointcloud") - - // preprocess depth map - source = &videosource.StaticSource{DepthImg: dm} - rs, stream, err := newDepthPreprocessTransform(context.Background(), gostream.NewVideoSource(source, prop.Video{})) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - - output, _, err := camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - preprocessed, err := rimage.ConvertImageToDepthMap(context.Background(), output) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - pCtx.GotDebugImage(preprocessed.ToPrettyPicture(0, rimage.MaxDepth), "preprocessed-aligned-depth") - preprocessedPointCloud := depthadapter.ToPointCloud(preprocessed, h.proj) - test.That(t, preprocessedPointCloud.MetaData().HasColor, test.ShouldBeFalse) - pCtx.GotDebugPointCloud(preprocessedPointCloud, "preprocessed-aligned-pointcloud") - - source = &videosource.StaticSource{DepthImg: preprocessed} - ds, stream, err = newDepthEdgesTransform(context.Background(), gostream.NewVideoSource(source, prop.Video{}), am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - processedEdges, _, err := camera.ReadImage(context.Background(), ds) - test.That(t, err, test.ShouldBeNil) - test.That(t, ds.Close(context.Background()), test.ShouldBeNil) - - pCtx.GotDebugImage(processedEdges, "edges-preprocessed-aligned-depth") - - return nil -} - -func TestDepthSourceGripper(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "align/gripper1/depth", "*.png", "") - - proj, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile( - utils.ResolveFile("components/camera/transformpipeline/data/gripper_parameters.json"), - ) - test.That(t, err, test.ShouldBeNil) - - err = d.Process(t, &depthSourceTestHelper{proj}) - test.That(t, err, test.ShouldBeNil) -} - -func TestDepthSourceIntel(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "align/intel515/depth", "*.png", "") - - proj, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile( - utils.ResolveFile("components/camera/transformpipeline/data/intel515_parameters.json"), - ) - test.That(t, err, test.ShouldBeNil) - - err = d.Process(t, &depthSourceTestHelper{proj}) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/camera/transformpipeline/detector.go b/components/camera/transformpipeline/detector.go deleted file mode 100644 index 241103353d0..00000000000 --- a/components/camera/transformpipeline/detector.go +++ /dev/null @@ -1,109 +0,0 @@ -package transformpipeline - -import ( - "context" - "fmt" - "image" - "strings" - - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/objectdetection" -) - -// detectorConfig is the attribute struct for detectors (their name as found in the vision service). -type detectorConfig struct { - DetectorName string `json:"detector_name"` - ConfidenceThreshold float64 `json:"confidence_threshold"` - ValidLabels []string `json:"valid_labels"` -} - -// detectorSource takes an image from the camera, and overlays the detections from the detector. -type detectorSource struct { - stream gostream.VideoStream - detectorName string - labelFilter objectdetection.Postprocessor // must build from ValidLabels - confFilter objectdetection.Postprocessor - r robot.Robot -} - -func newDetectionsTransform( - ctx context.Context, - source gostream.VideoSource, - r robot.Robot, - am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*detectorConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - - props, err := propsFromVideoSource(ctx, source) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - confFilter := objectdetection.NewScoreFilter(conf.ConfidenceThreshold) - validLabels := make(map[string]interface{}) - for _, l := range conf.ValidLabels { - validLabels[strings.ToLower(l)] = struct{}{} - } - labelFilter := objectdetection.NewLabelFilter(validLabels) - detector := &detectorSource{ - gostream.NewEmbeddedVideoStream(source), - conf.DetectorName, - labelFilter, - confFilter, - r, - } - src, err := camera.NewVideoSourceFromReader(ctx, detector, &cameraModel, camera.ColorStream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, camera.ColorStream, err -} - -// Read returns the image overlaid with the detection bounding boxes. -func (ds *detectorSource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::detector::Read") - defer span.End() - // get the bounding boxes from the service - srv, err := vision.FromRobot(ds.r, ds.detectorName) - if err != nil { - return nil, nil, fmt.Errorf("source_detector cant find vision service: %w", err) - } - // get image from source camera - img, release, err := ds.stream.Next(ctx) - if err != nil { - return nil, nil, fmt.Errorf("could not get next source image: %w", err) - } - dets, err := srv.Detections(ctx, img, map[string]interface{}{}) - if err != nil { - return nil, nil, fmt.Errorf("could not get detections: %w", err) - } - // overlay detections of the source image - dets = ds.confFilter(dets) - dets = ds.labelFilter(dets) - - res, err := objectdetection.Overlay(img, dets) - if err != nil { - return nil, nil, fmt.Errorf("could not overlay bounding boxes: %w", err) - } - return res, release, nil -} - -func (ds *detectorSource) Close(ctx context.Context) error { - return ds.stream.Close(ctx) -} diff --git a/components/camera/transformpipeline/detector_test.go b/components/camera/transformpipeline/detector_test.go deleted file mode 100644 index 45f5444dccd..00000000000 --- a/components/camera/transformpipeline/detector_test.go +++ /dev/null @@ -1,241 +0,0 @@ -//go:build !no_tflite - -package transformpipeline - -import ( - "context" - "encoding/json" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/robot" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/services/mlmodel" - _ "go.viam.com/rdk/services/mlmodel/register" - "go.viam.com/rdk/services/vision" - _ "go.viam.com/rdk/services/vision/register" - rutils "go.viam.com/rdk/utils" -) - -func writeTempConfig(cfg *config.Config) (string, error) { - newConf, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return "", err - } - tmpFile, err := os.CreateTemp(os.TempDir(), "objdet_config-") - if err != nil { - return "", err - } - _, err = tmpFile.Write(newConf) - if err != nil { - return "", err - } - err = tmpFile.Close() - if err != nil { - return "", err - } - return tmpFile.Name(), nil -} - -// make a fake robot with a vision service. -func buildRobotWithFakeCamera(logger logging.Logger) (robot.Robot, error) { - // add a fake camera to the config - cfg, err := config.Read(context.Background(), artifact.MustPath("components/camera/transformpipeline/vision.json"), logger) - if err != nil { - return nil, err - } - // create fake source camera - colorSrv1 := resource.Config{ - Name: "detector_color", - API: vision.API, - Model: resource.DefaultModelFamily.WithModel("color_detector"), - Attributes: rutils.AttributeMap{ - "detect_color": "#4F3815", - "hue_tolerance_pct": 0.013, - "segment_size_px": 15000, - }, - } - cfg.Services = append(cfg.Services, colorSrv1) - tfliteSrv2 := resource.Config{ - Name: "detector_tflite", - API: mlmodel.API, - Model: resource.DefaultModelFamily.WithModel("tflite_cpu"), - Attributes: rutils.AttributeMap{ - "model_path": artifact.MustPath("vision/tflite/effdet0.tflite"), - "label_path": artifact.MustPath("vision/tflite/effdetlabels.txt"), - "num_threads": 1, - }, - } - cfg.Services = append(cfg.Services, tfliteSrv2) - visionSrv2 := resource.Config{ - Name: "vision_detector", - API: vision.API, - Model: resource.DefaultModelFamily.WithModel("mlmodel"), - Attributes: rutils.AttributeMap{ - "mlmodel_name": "detector_tflite", - }, - DependsOn: []string{"detector_tflite"}, - } - cfg.Services = append(cfg.Services, visionSrv2) - cameraComp := resource.Config{ - Name: "fake_cam", - API: camera.API, - Model: resource.DefaultModelFamily.WithModel("image_file"), - Attributes: rutils.AttributeMap{ - "color_image_file_path": artifact.MustPath("vision/objectdetection/detection_test.jpg"), - "depth_image_file_path": "", - }, - } - cfg.Components = append(cfg.Components, cameraComp) - // create fake detector camera - detectorComp := resource.Config{ - Name: "color_detect", - API: camera.API, - Model: resource.DefaultModelFamily.WithModel("transform"), - Attributes: rutils.AttributeMap{ - "source": "fake_cam", - "pipeline": []rutils.AttributeMap{ - { - "type": "detections", - "attributes": rutils.AttributeMap{ - "detector_name": "detector_color", - "confidence_threshold": 0.35, - }, - }, - }, - }, - DependsOn: []string{"fake_cam"}, - } - cfg.Components = append(cfg.Components, detectorComp) - // create 2nd fake detector camera - tfliteComp := resource.Config{ - Name: "tflite_detect", - API: camera.API, - Model: resource.DefaultModelFamily.WithModel("transform"), - Attributes: rutils.AttributeMap{ - "source": "fake_cam", - "pipeline": []rutils.AttributeMap{ - { - "type": "detections", - "attributes": rutils.AttributeMap{ - "detector_name": "vision_detector", - "confidence_threshold": 0.35, - }, - }, - }, - }, - DependsOn: []string{"fake_cam"}, - } - cfg.Components = append(cfg.Components, tfliteComp) - if err := cfg.Ensure(false, logger); err != nil { - return nil, err - } - - newConfFile, err := writeTempConfig(cfg) - if err != nil { - return nil, err - } - defer os.Remove(newConfFile) - // make the robot from new config - return robotimpl.RobotFromConfigPath(context.Background(), newConfFile, logger) -} - -//nolint:dupl -func TestColorDetectionSource(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - r, err := buildRobotWithFakeCamera(logger) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - - detector, err := camera.FromRobot(r, "color_detect") - test.That(t, err, test.ShouldBeNil) - defer detector.Close(ctx) - - resImg, _, err := camera.ReadImage(ctx, detector) - test.That(t, err, test.ShouldBeNil) - ovImg := rimage.ConvertImage(resImg) - test.That(t, ovImg.GetXY(852, 431), test.ShouldResemble, rimage.Red) - test.That(t, ovImg.GetXY(984, 561), test.ShouldResemble, rimage.Red) - test.That(t, detector.Close(context.Background()), test.ShouldBeNil) -} - -func TestTFLiteDetectionSource(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - r, err := buildRobotWithFakeCamera(logger) - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - test.That(t, err, test.ShouldBeNil) - - detector, err := camera.FromRobot(r, "tflite_detect") - test.That(t, err, test.ShouldBeNil) - defer detector.Close(ctx) - - resImg, _, err := camera.ReadImage(ctx, detector) - test.That(t, err, test.ShouldBeNil) - ovImg := rimage.ConvertImage(resImg) - test.That(t, ovImg.GetXY(624, 402), test.ShouldResemble, rimage.Red) - test.That(t, ovImg.GetXY(815, 647), test.ShouldResemble, rimage.Red) - test.That(t, detector.Close(context.Background()), test.ShouldBeNil) -} - -func BenchmarkColorDetectionSource(b *testing.B) { - logger := logging.NewTestLogger(b) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - r, err := buildRobotWithFakeCamera(logger) - defer func() { - test.That(b, r.Close(context.Background()), test.ShouldBeNil) - }() - test.That(b, err, test.ShouldBeNil) - detector, err := camera.FromRobot(r, "color_detect") - test.That(b, err, test.ShouldBeNil) - defer detector.Close(ctx) - - b.ResetTimer() - // begin benchmarking - for i := 0; i < b.N; i++ { - _, _, _ = camera.ReadImage(ctx, detector) - } - test.That(b, detector.Close(context.Background()), test.ShouldBeNil) -} - -func BenchmarkTFLiteDetectionSource(b *testing.B) { - logger := logging.NewTestLogger(b) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - r, err := buildRobotWithFakeCamera(logger) - defer func() { - test.That(b, r.Close(context.Background()), test.ShouldBeNil) - }() - test.That(b, err, test.ShouldBeNil) - detector, err := camera.FromRobot(r, "tflite_detect") - test.That(b, err, test.ShouldBeNil) - defer detector.Close(ctx) - - b.ResetTimer() - // begin benchmarking - for i := 0; i < b.N; i++ { - _, _, _ = camera.ReadImage(ctx, detector) - } - test.That(b, detector.Close(context.Background()), test.ShouldBeNil) -} diff --git a/components/camera/transformpipeline/mods.go b/components/camera/transformpipeline/mods.go deleted file mode 100644 index 776d51856fa..00000000000 --- a/components/camera/transformpipeline/mods.go +++ /dev/null @@ -1,233 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - "image/color" - - "github.com/disintegration/imaging" - "github.com/pkg/errors" - "go.opencensus.io/trace" - "golang.org/x/image/draw" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -// rotateConfig are the attributes for a rotate transform. -type rotateConfig struct { - Angle float64 `json:"angle_degs"` -} - -// rotateSource is the source to be rotated and the kind of image type. -type rotateSource struct { - originalStream gostream.VideoStream - stream camera.ImageType - angle float64 -} - -// newRotateTransform creates a new rotation transform. -func newRotateTransform(ctx context.Context, source gostream.VideoSource, stream camera.ImageType, am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*rotateConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, errors.Wrap(err, "cannot parse rotate attribute map") - } - - if !am.Has("angle_degs") { - conf.Angle = 180 // Default to 180 for backwards-compatibility - } - - props, err := propsFromVideoSource(ctx, source) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - reader := &rotateSource{gostream.NewEmbeddedVideoStream(source), stream, conf.Angle} - src, err := camera.NewVideoSourceFromReader(ctx, reader, &cameraModel, stream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, stream, err -} - -// Read rotates the 2D image depending on the stream type. -func (rs *rotateSource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::rotate::Read") - defer span.End() - orig, release, err := rs.originalStream.Next(ctx) - if err != nil { - return nil, nil, err - } - switch rs.stream { - case camera.ColorStream, camera.UnspecifiedStream: - // imaging.Rotate rotates an image counter-clockwise but our rotate function rotates in the - // clockwise direction. The angle is negated here for consistency. - return imaging.Rotate(orig, -(rs.angle), color.Black), release, nil - case camera.DepthStream: - dm, err := rimage.ConvertImageToDepthMap(ctx, orig) - if err != nil { - return nil, nil, err - } - return dm.Rotate(int(rs.angle)), release, nil - default: - return nil, nil, camera.NewUnsupportedImageTypeError(rs.stream) - } -} - -// Close closes the original stream. -func (rs *rotateSource) Close(ctx context.Context) error { - return rs.originalStream.Close(ctx) -} - -// resizeConfig are the attributes for a resize transform. -type resizeConfig struct { - Height int `json:"height_px"` - Width int `json:"width_px"` -} - -type resizeSource struct { - originalStream gostream.VideoStream - stream camera.ImageType - height int - width int -} - -// newResizeTransform creates a new resize transform. -func newResizeTransform( - ctx context.Context, source gostream.VideoSource, stream camera.ImageType, am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*resizeConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - if conf.Width == 0 { - return nil, camera.UnspecifiedStream, errors.New("new width for resize transform cannot be 0") - } - if conf.Height == 0 { - return nil, camera.UnspecifiedStream, errors.New("new height for resize transform cannot be 0") - } - - reader := &resizeSource{gostream.NewEmbeddedVideoStream(source), stream, conf.Height, conf.Width} - src, err := camera.NewVideoSourceFromReader(ctx, reader, nil, stream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, stream, err -} - -// Read resizes the 2D image depending on the stream type. -func (rs *resizeSource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::resize::Read") - defer span.End() - orig, release, err := rs.originalStream.Next(ctx) - if err != nil { - return nil, nil, err - } - switch rs.stream { - case camera.ColorStream, camera.UnspecifiedStream: - dst := image.NewRGBA(image.Rect(0, 0, rs.width, rs.height)) - draw.NearestNeighbor.Scale(dst, dst.Bounds(), orig, orig.Bounds(), draw.Over, nil) - return dst, release, nil - case camera.DepthStream: - dm, err := rimage.ConvertImageToGray16(orig) - if err != nil { - return nil, nil, err - } - dst := image.NewGray16(image.Rect(0, 0, rs.width, rs.height)) - draw.NearestNeighbor.Scale(dst, dst.Bounds(), dm, dm.Bounds(), draw.Over, nil) - return dst, release, nil - default: - return nil, nil, camera.NewUnsupportedImageTypeError(rs.stream) - } -} - -// Close closes the original stream. -func (rs *resizeSource) Close(ctx context.Context) error { - return rs.originalStream.Close(ctx) -} - -// cropConfig are the attributes for a crop transform. -type cropConfig struct { - XMin int `json:"x_min_px"` - YMin int `json:"y_min_px"` - XMax int `json:"x_max_px"` - YMax int `json:"y_max_px"` -} - -type cropSource struct { - originalStream gostream.VideoStream - imgType camera.ImageType - cropWindow image.Rectangle -} - -// newCropTransform creates a new crop transform. -func newCropTransform( - ctx context.Context, source gostream.VideoSource, stream camera.ImageType, am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*cropConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - if conf.XMin < 0 || conf.YMin < 0 { - return nil, camera.UnspecifiedStream, errors.New("cannot set x_min or y_min to a negative number") - } - if conf.XMin >= conf.XMax { - return nil, camera.UnspecifiedStream, errors.New("cannot crop image to 0 width (x_min is >= x_max)") - } - if conf.YMin >= conf.YMax { - return nil, camera.UnspecifiedStream, errors.New("cannot crop image to 0 height (y_min is >= y_max)") - } - cropRect := image.Rect(conf.XMin, conf.YMin, conf.XMax, conf.YMax) - - reader := &cropSource{gostream.NewEmbeddedVideoStream(source), stream, cropRect} - src, err := camera.NewVideoSourceFromReader(ctx, reader, nil, stream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, stream, err -} - -// Read crops the 2D image depending on the crop window. -func (cs *cropSource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::crop::Read") - defer span.End() - orig, release, err := cs.originalStream.Next(ctx) - if err != nil { - return nil, nil, err - } - switch cs.imgType { - case camera.ColorStream, camera.UnspecifiedStream: - newImg := imaging.Crop(orig, cs.cropWindow) - if newImg.Bounds().Empty() { - return nil, nil, errors.New("crop transform cropped image to 0 pixels") - } - return newImg, release, nil - case camera.DepthStream: - dm, err := rimage.ConvertImageToDepthMap(ctx, orig) - if err != nil { - return nil, nil, err - } - newImg := dm.SubImage(cs.cropWindow) - if newImg.Bounds().Empty() { - return nil, nil, errors.New("crop transform cropped image to 0 pixels") - } - return newImg, release, nil - default: - return nil, nil, camera.NewUnsupportedImageTypeError(cs.imgType) - } -} - -// Close closes the original stream. -func (cs *cropSource) Close(ctx context.Context) error { - return cs.originalStream.Close(ctx) -} diff --git a/components/camera/transformpipeline/mods_test.go b/components/camera/transformpipeline/mods_test.go deleted file mode 100644 index ee441e56538..00000000000 --- a/components/camera/transformpipeline/mods_test.go +++ /dev/null @@ -1,560 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - "testing" - - "github.com/pion/mediadevices/pkg/prop" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/utils" -) - -func TestCrop(t *testing.T) { - am := utils.AttributeMap{ - "x_min_px": 10, - "y_min_px": 30, - "x_max_px": 20, - "y_max_px": 40, - } - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board1_gray_small.png")) - test.That(t, err, test.ShouldBeNil) - - // test depth source - source := gostream.NewVideoSource(&videosource.StaticSource{DepthImg: dm}, prop.Video{}) - out, _, err := camera.ReadImage(context.Background(), source) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 72) - - rs, stream, err := newCropTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - out, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 10) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 10) - test.That(t, out, test.ShouldHaveSameTypeAs, &rimage.DepthMap{}) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - // test color source - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - out, _, err = camera.ReadImage(context.Background(), source) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 72) - - // crop within bounds - rs, stream, err = newCropTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - out, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 10) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 10) - test.That(t, out, test.ShouldHaveSameTypeAs, &image.NRGBA{}) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - // crop has limits bigger than the image dimensions, but just takes the window - am = utils.AttributeMap{"x_min_px": 127, "x_max_px": 150, "y_min_px": 71, "y_max_px": 110} - rs, stream, err = newCropTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - out, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 1) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 1) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - // error - crop limits are outside of original image - am = utils.AttributeMap{"x_min_px": 1000, "x_max_px": 2000, "y_min_px": 300, "y_max_px": 400} - rs, stream, err = newCropTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - _, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "cropped image to 0 pixels") - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - // error - empty attribute - am = utils.AttributeMap{} - _, _, err = newCropTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot crop image") - // error - negative attributes - am = utils.AttributeMap{"x_min_px": -4} - _, _, err = newCropTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "negative number") - - // close the source - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestResizeColor(t *testing.T) { - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - - am := utils.AttributeMap{ - "height_px": 20, - "width_px": 30, - } - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - out, _, err := camera.ReadImage(context.Background(), source) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 72) - - rs, stream, err := newResizeTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - out, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 30) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 20) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestResizeDepth(t *testing.T) { - img, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board1_gray_small.png")) - test.That(t, err, test.ShouldBeNil) - - am := utils.AttributeMap{ - "height_px": 40, - "width_px": 60, - } - source := gostream.NewVideoSource(&videosource.StaticSource{DepthImg: img}, prop.Video{}) - out, _, err := camera.ReadImage(context.Background(), source) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 72) - - rs, stream, err := newResizeTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - out, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, out.Bounds().Dx(), test.ShouldEqual, 60) - test.That(t, out.Bounds().Dy(), test.ShouldEqual, 40) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestRotateColorSource(t *testing.T) { - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - am := utils.AttributeMap{ - "angle_degs": 180, - } - rs, stream, err := newRotateTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - - rawImage, _, err := camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_color_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - img2 := rimage.ConvertImage(rawImage) - - am = utils.AttributeMap{ - // defaults to 180 - } - - rsDefault, stream, err := newRotateTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - - rawImageDefault, _, err := camera.ReadImage(context.Background(), rsDefault) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_color_source.png", rawImageDefault) - test.That(t, err, test.ShouldBeNil) - - img3 := rimage.ConvertImage(rawImageDefault) - - for x := 0; x < img.Width(); x++ { - p1 := image.Point{x, 0} - p2 := image.Point{img.Width() - x - 1, img.Height() - 1} - p3 := image.Point{img.Width() - x - 1, img.Height() - 1} - - a := img.Get(p1) - b := img2.Get(p2) - c := img3.Get(p3) - - d := a.Distance(b) - test.That(t, d, test.ShouldEqual, 0) - - d = a.Distance(c) - test.That(t, d, test.ShouldEqual, 0) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": 90, - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_color_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - img2 = rimage.ConvertImage(rawImage) - - for x := 0; x < img.Width(); x++ { - p1 := image.Point{X: x} - p2 := image.Point{X: img2.Width() - 1, Y: x} - - a := img.Get(p1) - b := img2.Get(p2) - - d := a.Distance(b) - test.That(t, d, test.ShouldEqual, 0) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": -90, - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_color_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - img2 = rimage.ConvertImage(rawImage) - - for x := 0; x < img.Width(); x++ { - p1 := image.Point{X: x} - p2 := image.Point{Y: img2.Height() - 1 - x} - - a := img.Get(p1) - b := img2.Get(p2) - - d := a.Distance(b) - test.That(t, d, test.ShouldEqual, 0) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": 270, - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_color_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - img2 = rimage.ConvertImage(rawImage) - - for x := 0; x < img.Width(); x++ { - p1 := image.Point{X: x} - p2 := image.Point{Y: img2.Height() - 1 - x} - - a := img.Get(p1) - b := img2.Get(p2) - - d := a.Distance(b) - test.That(t, d, test.ShouldEqual, 0) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": 0, // no-op - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_color_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - img2 = rimage.ConvertImage(rawImage) - - for x := 0; x < img.Width(); x++ { - p := image.Point{X: x} - - a := img.Get(p) - b := img2.Get(p) - - d := a.Distance(b) - test.That(t, d, test.ShouldEqual, 0) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestRotateDepthSource(t *testing.T) { - pc, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board1_gray_small.png")) - test.That(t, err, test.ShouldBeNil) - - source := gostream.NewVideoSource(&videosource.StaticSource{DepthImg: pc}, prop.Video{}) - am := utils.AttributeMap{ - "angle_degs": 180, - } - rs, stream, err := newRotateTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - - rawImage, _, err := camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_depth_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - dm, err := rimage.ConvertImageToDepthMap(context.Background(), rawImage) - test.That(t, err, test.ShouldBeNil) - - am = utils.AttributeMap{ - // defaults to 180 - } - - rsDefault, stream, err := newRotateTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - - rawImageDefault, _, err := camera.ReadImage(context.Background(), rsDefault) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_depth_source.png", rawImageDefault) - test.That(t, err, test.ShouldBeNil) - - dmDefault, err := rimage.ConvertImageToDepthMap(context.Background(), rawImageDefault) - test.That(t, err, test.ShouldBeNil) - - for x := 0; x < pc.Width(); x++ { - p1 := image.Point{x, 0} - p2 := image.Point{pc.Width() - x - 1, pc.Height() - 1} - p3 := image.Point{pc.Width() - x - 1, pc.Height() - 1} - - d1 := pc.Get(p1) - d2 := dm.Get(p2) - d3 := dmDefault.Get(p3) - - test.That(t, d1, test.ShouldEqual, d2) - test.That(t, d1, test.ShouldEqual, d3) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{DepthImg: pc}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": 90, - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_depth_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - dm, err = rimage.ConvertImageToDepthMap(context.Background(), rawImage) - test.That(t, err, test.ShouldBeNil) - - for x := 0; x < pc.Width(); x++ { - p1 := image.Point{X: x} - p2 := image.Point{X: dm.Width() - 1, Y: x} - - d1 := pc.Get(p1) - d2 := dm.Get(p2) - - test.That(t, d1, test.ShouldEqual, d2) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{DepthImg: pc}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": -90, - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_depth_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - dm, err = rimage.ConvertImageToDepthMap(context.Background(), rawImage) - test.That(t, err, test.ShouldBeNil) - - for x := 0; x < pc.Width(); x++ { - p1 := image.Point{X: x} - p2 := image.Point{Y: dm.Height() - 1 - x} - - d1 := pc.Get(p1) - d2 := dm.Get(p2) - - test.That(t, d1, test.ShouldEqual, d2) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{DepthImg: pc}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": 270, - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_depth_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - dm, err = rimage.ConvertImageToDepthMap(context.Background(), rawImage) - test.That(t, err, test.ShouldBeNil) - - for x := 0; x < pc.Width(); x++ { - p1 := image.Point{X: x} - p2 := image.Point{Y: dm.Height() - 1 - x} - - d1 := pc.Get(p1) - d2 := dm.Get(p2) - - test.That(t, d1, test.ShouldEqual, d2) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - source = gostream.NewVideoSource(&videosource.StaticSource{DepthImg: pc}, prop.Video{}) - am = utils.AttributeMap{ - "angle_degs": 0, // no-op - } - rs, stream, err = newRotateTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - - rawImage, _, err = camera.ReadImage(context.Background(), rs) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - - err = rimage.WriteImageToFile(t.TempDir()+"/test_rotate_depth_source.png", rawImage) - test.That(t, err, test.ShouldBeNil) - - dm, err = rimage.ConvertImageToDepthMap(context.Background(), rawImage) - test.That(t, err, test.ShouldBeNil) - - for x := 0; x < pc.Width(); x++ { - p := image.Point{X: x} - - d1 := pc.Get(p) - d2 := dm.Get(p) - - test.That(t, d1, test.ShouldEqual, d2) - } - - test.That(t, rs.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func BenchmarkColorRotate(b *testing.B) { - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1.png")) - test.That(b, err, test.ShouldBeNil) - - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - src, err := camera.WrapVideoSourceWithProjector(context.Background(), source, nil, camera.ColorStream) - test.That(b, err, test.ShouldBeNil) - am := utils.AttributeMap{ - "angle_degs": 180, - } - rs, stream, err := newRotateTransform(context.Background(), src, camera.ColorStream, am) - test.That(b, err, test.ShouldBeNil) - test.That(b, stream, test.ShouldEqual, camera.ColorStream) - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - camera.ReadImage(context.Background(), rs) - } - test.That(b, rs.Close(context.Background()), test.ShouldBeNil) - test.That(b, source.Close(context.Background()), test.ShouldBeNil) -} - -func BenchmarkDepthRotate(b *testing.B) { - img, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board1.dat.gz")) - test.That(b, err, test.ShouldBeNil) - - source := gostream.NewVideoSource(&videosource.StaticSource{DepthImg: img}, prop.Video{}) - src, err := camera.WrapVideoSourceWithProjector(context.Background(), source, nil, camera.DepthStream) - test.That(b, err, test.ShouldBeNil) - am := utils.AttributeMap{ - "angle_degs": 180, - } - rs, stream, err := newRotateTransform(context.Background(), src, camera.DepthStream, am) - test.That(b, err, test.ShouldBeNil) - test.That(b, stream, test.ShouldEqual, camera.DepthStream) - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - camera.ReadImage(context.Background(), rs) - } - test.That(b, rs.Close(context.Background()), test.ShouldBeNil) - test.That(b, source.Close(context.Background()), test.ShouldBeNil) -} diff --git a/components/camera/transformpipeline/pipeline.go b/components/camera/transformpipeline/pipeline.go deleted file mode 100644 index 254913d941b..00000000000 --- a/components/camera/transformpipeline/pipeline.go +++ /dev/null @@ -1,180 +0,0 @@ -// Package transformpipeline defines image sources that apply transforms on images, and can be composed into -// an image transformation pipeline. The image sources are not original generators of image, but require an image source -// from a real camera or video in order to function. -package transformpipeline - -import ( - "context" - "fmt" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - "go.uber.org/multierr" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("transform") - -func init() { - resource.RegisterComponent( - camera.API, - model, - resource.Registration[camera.Camera, *transformConfig]{ - DeprecatedRobotConstructor: func( - ctx context.Context, - r any, - conf resource.Config, - logger logging.Logger, - ) (camera.Camera, error) { - actualR, err := utils.AssertType[robot.Robot](r) - if err != nil { - return nil, err - } - newConf, err := resource.NativeConfig[*transformConfig](conf) - if err != nil { - return nil, err - } - sourceName := newConf.Source - source, err := camera.FromRobot(actualR, sourceName) - if err != nil { - return nil, fmt.Errorf("no source camera for transform pipeline (%s): %w", sourceName, err) - } - src, err := newTransformPipeline(ctx, source, newConf, actualR, logger) - if err != nil { - return nil, err - } - return camera.FromVideoSource(conf.ResourceName(), src, logger), nil - }, - }) -} - -// transformConfig specifies a stream and list of transforms to apply on images/pointclouds coming from a source camera. -type transformConfig struct { - CameraParameters *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters,omitempty"` - DistortionParameters *transform.BrownConrady `json:"distortion_parameters,omitempty"` - Debug bool `json:"debug,omitempty"` - Source string `json:"source"` - Pipeline []Transformation `json:"pipeline"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *transformConfig) Validate(path string) ([]string, error) { - var deps []string - if len(cfg.Source) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "source") - } - - if cfg.CameraParameters != nil { - if cfg.CameraParameters.Height < 0 || cfg.CameraParameters.Width < 0 { - return nil, errors.Errorf( - "got illegal negative dimensions for width_px and height_px (%d, %d) fields set in intrinsic_parameters for transform camera", - cfg.CameraParameters.Width, cfg.CameraParameters.Height, - ) - } - } - - deps = append(deps, cfg.Source) - return deps, nil -} - -func newTransformPipeline( - ctx context.Context, - source gostream.VideoSource, - cfg *transformConfig, - r robot.Robot, - logger logging.Logger, -) (camera.VideoSource, error) { - if source == nil { - return nil, errors.New("no source camera for transform pipeline") - } - if len(cfg.Pipeline) == 0 { - return nil, errors.New("pipeline has no transforms in it") - } - // check if the source produces a depth image or color image - img, release, err := camera.ReadImage(ctx, source) - - var streamType camera.ImageType - if err != nil { - streamType = camera.UnspecifiedStream - } else if _, ok := img.(*rimage.DepthMap); ok { - streamType = camera.DepthStream - } else if _, ok := img.(*image.Gray16); ok { - streamType = camera.DepthStream - } else { - streamType = camera.ColorStream - } - if release != nil { - release() - } - // loop through the pipeline and create the image flow - pipeline := make([]gostream.VideoSource, 0, len(cfg.Pipeline)) - lastSource := source - for _, tr := range cfg.Pipeline { - src, newStreamType, err := buildTransform(ctx, r, lastSource, streamType, tr, cfg.Source) - if err != nil { - return nil, err - } - pipeline = append(pipeline, src) - lastSource = src - streamType = newStreamType - } - lastSourceStream := gostream.NewEmbeddedVideoStream(lastSource) - cameraModel := camera.NewPinholeModelWithBrownConradyDistortion(cfg.CameraParameters, cfg.DistortionParameters) - return camera.NewVideoSourceFromReader( - ctx, - transformPipeline{pipeline, lastSourceStream, cfg.CameraParameters, logger}, - &cameraModel, - streamType, - ) -} - -type transformPipeline struct { - pipeline []gostream.VideoSource - stream gostream.VideoStream - intrinsicParameters *transform.PinholeCameraIntrinsics - logger logging.Logger -} - -func (tp transformPipeline) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::Read") - defer span.End() - return tp.stream.Next(ctx) -} - -func (tp transformPipeline) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::NextPointCloud") - defer span.End() - if lastElem, ok := tp.pipeline[len(tp.pipeline)-1].(camera.PointCloudSource); ok { - pc, err := lastElem.NextPointCloud(ctx) - if err != nil { - return nil, errors.Wrap(err, "function NextPointCloud not defined for last videosource in transform pipeline") - } - return pc, nil - } - return nil, errors.New("function NextPointCloud not defined for last videosource in transform pipeline") -} - -func (tp transformPipeline) Close(ctx context.Context) error { - var errs error - for _, src := range tp.pipeline { - errs = multierr.Combine(errs, func() (err error) { - defer func() { - if panicErr := recover(); panicErr != nil { - err = multierr.Combine(err, errors.Errorf("panic: %v", panicErr)) - } - }() - return src.Close(ctx) - }()) - } - return multierr.Combine(tp.stream.Close(ctx), errs) -} diff --git a/components/camera/transformpipeline/pipeline_test.go b/components/camera/transformpipeline/pipeline_test.go deleted file mode 100644 index 5644df3d98d..00000000000 --- a/components/camera/transformpipeline/pipeline_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package transformpipeline - -import ( - "context" - "testing" - - "github.com/pion/mediadevices/pkg/prop" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -func TestTransformPipelineColor(t *testing.T) { - transformConf := &transformConfig{ - Source: "source", - Pipeline: []Transformation{ - {Type: "rotate", Attributes: utils.AttributeMap{}}, - {Type: "resize", Attributes: utils.AttributeMap{"height_px": 20, "width_px": 10}}, - }, - } - r := &inject.Robot{} - logger := logging.NewTestLogger(t) - - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - src, err := camera.WrapVideoSourceWithProjector(context.Background(), source, nil, camera.ColorStream) - test.That(t, err, test.ShouldBeNil) - inImg, _, err := camera.ReadImage(context.Background(), src) - test.That(t, err, test.ShouldBeNil) - test.That(t, inImg.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, inImg.Bounds().Dy(), test.ShouldEqual, 72) - - color, err := newTransformPipeline(context.Background(), src, transformConf, r, logger) - test.That(t, err, test.ShouldBeNil) - - outImg, _, err := camera.ReadImage(context.Background(), color) - test.That(t, err, test.ShouldBeNil) - test.That(t, outImg.Bounds().Dx(), test.ShouldEqual, 10) - test.That(t, outImg.Bounds().Dy(), test.ShouldEqual, 20) - _, err = color.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldWrap, transform.ErrNoIntrinsics) - _, err = color.Projector(context.Background()) - test.That(t, err, test.ShouldWrap, transform.ErrNoIntrinsics) - test.That(t, color.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestTransformPipelineDepth(t *testing.T) { - intrinsics := &transform.PinholeCameraIntrinsics{ - Width: 128, - Height: 72, - Fx: 900.538000, - Fy: 900.818000, - Ppx: 64.8934000, - Ppy: 36.7736000, - } - - transformConf := &transformConfig{ - CameraParameters: intrinsics, - Source: "source", - Pipeline: []Transformation{ - {Type: "rotate", Attributes: utils.AttributeMap{}}, - {Type: "resize", Attributes: utils.AttributeMap{"height_px": 30, "width_px": 40}}, - }, - } - r := &inject.Robot{} - logger := logging.NewTestLogger(t) - - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board1_gray_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{DepthImg: dm}, prop.Video{}) - src, err := camera.WrapVideoSourceWithProjector(context.Background(), source, nil, camera.DepthStream) - test.That(t, err, test.ShouldBeNil) - inImg, _, err := camera.ReadImage(context.Background(), src) - test.That(t, err, test.ShouldBeNil) - test.That(t, inImg.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, inImg.Bounds().Dy(), test.ShouldEqual, 72) - - depth, err := newTransformPipeline(context.Background(), src, transformConf, r, logger) - test.That(t, err, test.ShouldBeNil) - - outImg, _, err := camera.ReadImage(context.Background(), depth) - test.That(t, err, test.ShouldBeNil) - test.That(t, outImg.Bounds().Dx(), test.ShouldEqual, 40) - test.That(t, outImg.Bounds().Dy(), test.ShouldEqual, 30) - prop, err := depth.Projector(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop, test.ShouldResemble, intrinsics) - outPc, err := depth.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not defined for last videosource") - test.That(t, outPc, test.ShouldBeNil) - - test.That(t, depth.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestTransformPipelineDepth2(t *testing.T) { - transform1 := &transformConfig{ - Source: "source", - Pipeline: []Transformation{ - {Type: "depth_preprocess", Attributes: utils.AttributeMap{}}, - {Type: "rotate", Attributes: utils.AttributeMap{}}, - {Type: "resize", Attributes: utils.AttributeMap{"height_px": 20, "width_px": 10}}, - {Type: "depth_to_pretty", Attributes: utils.AttributeMap{}}, - }, - } - transform2 := &transformConfig{ - Source: "source", - Pipeline: []Transformation{ - {Type: "depth_preprocess", Attributes: utils.AttributeMap{}}, - {Type: "rotate", Attributes: utils.AttributeMap{}}, - {Type: "resize", Attributes: utils.AttributeMap{"height_px": 30, "width_px": 40}}, - {Type: "depth_edges", Attributes: utils.AttributeMap{"high_threshold_pct": 0.85, "low_threshold_pct": 0.3, "blur_radius_px": 3.0}}, - }, - } - r := &inject.Robot{} - logger := logging.NewTestLogger(t) - - dm, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board1_gray_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{DepthImg: dm}, prop.Video{}) - // first depth transform - depth1, err := newTransformPipeline(context.Background(), source, transform1, r, logger) - test.That(t, err, test.ShouldBeNil) - outImg, _, err := camera.ReadImage(context.Background(), depth1) - test.That(t, err, test.ShouldBeNil) - test.That(t, outImg.Bounds().Dx(), test.ShouldEqual, 10) - test.That(t, outImg.Bounds().Dy(), test.ShouldEqual, 20) - test.That(t, depth1.Close(context.Background()), test.ShouldBeNil) - // second depth image - depth2, err := newTransformPipeline(context.Background(), source, transform2, r, logger) - test.That(t, err, test.ShouldBeNil) - outImg, _, err = camera.ReadImage(context.Background(), depth2) - test.That(t, err, test.ShouldBeNil) - test.That(t, outImg.Bounds().Dx(), test.ShouldEqual, 40) - test.That(t, outImg.Bounds().Dy(), test.ShouldEqual, 30) - test.That(t, depth2.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestNullPipeline(t *testing.T) { - transform1 := &transformConfig{ - Source: "source", - Pipeline: []Transformation{}, - } - r := &inject.Robot{} - logger := logging.NewTestLogger(t) - - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - _, err = newTransformPipeline(context.Background(), source, transform1, r, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "pipeline has no transforms") - - transform2 := &transformConfig{ - Source: "source", - Pipeline: []Transformation{{Type: "identity", Attributes: nil}}, - } - pipe, err := newTransformPipeline(context.Background(), source, transform2, r, logger) - test.That(t, err, test.ShouldBeNil) - outImg, _, err := camera.ReadImage(context.Background(), pipe) // should not transform anything - test.That(t, err, test.ShouldBeNil) - test.That(t, outImg.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, outImg.Bounds().Dy(), test.ShouldEqual, 72) - test.That(t, pipe.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestPipeIntoPipe(t *testing.T) { - r := &inject.Robot{} - logger := logging.NewTestLogger(t) - - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - - intrinsics1 := &transform.PinholeCameraIntrinsics{Width: 128, Height: 72} - transform1 := &transformConfig{ - CameraParameters: intrinsics1, - Source: "source", - Pipeline: []Transformation{{Type: "rotate", Attributes: utils.AttributeMap{}}}, - } - intrinsics2 := &transform.PinholeCameraIntrinsics{Width: 10, Height: 20} - transform2 := &transformConfig{ - CameraParameters: intrinsics2, - Source: "transform2", - Pipeline: []Transformation{ - {Type: "resize", Attributes: utils.AttributeMap{"height_px": 20, "width_px": 10}}, - }, - } - - pipe1, err := newTransformPipeline(context.Background(), source, transform1, r, logger) - test.That(t, err, test.ShouldBeNil) - outImg, _, err := camera.ReadImage(context.Background(), pipe1) - test.That(t, err, test.ShouldBeNil) - test.That(t, outImg.Bounds().Dx(), test.ShouldEqual, 128) - test.That(t, outImg.Bounds().Dy(), test.ShouldEqual, 72) - prop, err := pipe1.Projector(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.(*transform.PinholeCameraIntrinsics).Width, test.ShouldEqual, 128) - test.That(t, prop.(*transform.PinholeCameraIntrinsics).Height, test.ShouldEqual, 72) - // transform pipeline into pipeline - pipe2, err := newTransformPipeline(context.Background(), pipe1, transform2, r, logger) - test.That(t, err, test.ShouldBeNil) - outImg, _, err = camera.ReadImage(context.Background(), pipe2) - test.That(t, err, test.ShouldBeNil) - test.That(t, outImg.Bounds().Dx(), test.ShouldEqual, 10) - test.That(t, outImg.Bounds().Dy(), test.ShouldEqual, 20) - test.That(t, err, test.ShouldBeNil) - prop, err = pipe2.Projector(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.(*transform.PinholeCameraIntrinsics).Width, test.ShouldEqual, 10) - test.That(t, prop.(*transform.PinholeCameraIntrinsics).Height, test.ShouldEqual, 20) - // Close everything - test.That(t, pipe2.Close(context.Background()), test.ShouldBeNil) - test.That(t, pipe1.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestTransformPipelineValidatePass(t *testing.T) { - transformConf := &transformConfig{ - Source: "source", - Pipeline: []Transformation{ - {Type: "rotate", Attributes: utils.AttributeMap{}}, - {Type: "resize", Attributes: utils.AttributeMap{"height_px": 20, "width_px": 10}}, - }, - } - deps, err := transformConf.Validate("path") - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldResemble, []string{"source"}) -} - -func TestTransformPipelineValidateFail(t *testing.T) { - transformConf := &transformConfig{ - Pipeline: []Transformation{ - {Type: "rotate", Attributes: utils.AttributeMap{}}, - {Type: "resize", Attributes: utils.AttributeMap{"height_px": 20, "width_px": 10}}, - }, - } - path := "path" - deps, err := transformConf.Validate(path) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "source") - test.That(t, deps, test.ShouldBeNil) -} diff --git a/components/camera/transformpipeline/preprocessing.go b/components/camera/transformpipeline/preprocessing.go deleted file mode 100644 index 79ff11d5e00..00000000000 --- a/components/camera/transformpipeline/preprocessing.go +++ /dev/null @@ -1,63 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" -) - -// preprocessDepthTransform applies pre-processing functions to depth maps in order to smooth edges and fill holes. -type preprocessDepthTransform struct { - stream gostream.VideoStream -} - -func newDepthPreprocessTransform(ctx context.Context, source gostream.VideoSource, -) (gostream.VideoSource, camera.ImageType, error) { - reader := &preprocessDepthTransform{gostream.NewEmbeddedVideoStream(source)} - - props, err := propsFromVideoSource(ctx, source) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - var cameraModel transform.PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = props.IntrinsicParams - - if props.DistortionParams != nil { - cameraModel.Distortion = props.DistortionParams - } - src, err := camera.NewVideoSourceFromReader(ctx, reader, &cameraModel, camera.DepthStream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, camera.DepthStream, err -} - -// Next applies depth preprocessing to the next image. -func (os *preprocessDepthTransform) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::depthPreprocess::Read") - defer span.End() - i, release, err := os.stream.Next(ctx) - if err != nil { - return nil, nil, err - } - dm, err := rimage.ConvertImageToDepthMap(ctx, i) - if err != nil { - return nil, nil, errors.Wrap(err, "transform source does not provide depth image") - } - dm, err = rimage.PreprocessDepthMap(dm, nil) - if err != nil { - return nil, nil, err - } - return dm, release, nil -} - -func (os *preprocessDepthTransform) Close(ctx context.Context) error { - return os.stream.Close(ctx) -} diff --git a/components/camera/transformpipeline/segmenter.go b/components/camera/transformpipeline/segmenter.go deleted file mode 100644 index 3e0f306e2e5..00000000000 --- a/components/camera/transformpipeline/segmenter.go +++ /dev/null @@ -1,121 +0,0 @@ -package transformpipeline - -import ( - "context" - "fmt" - "image" - - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -// segmenterConfig is the attribute struct for segementers (their name as found in the vision service). -type segmenterConfig struct { - SegmenterName string `json:"segmenter_name"` -} - -// segmenterSource takes a pointcloud from the camera and applies a segmenter to it. -type segmenterSource struct { - stream gostream.VideoStream - cameraName string - segmenterName string - r robot.Robot -} - -func newSegmentationsTransform( - ctx context.Context, - source gostream.VideoSource, - r robot.Robot, - am utils.AttributeMap, - sourceString string, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*segmenterConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - - props, err := propsFromVideoSource(ctx, source) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - - segmenter := &segmenterSource{ - gostream.NewEmbeddedVideoStream(source), - sourceString, - conf.SegmenterName, - r, - } - src, err := camera.NewVideoSourceFromReader(ctx, segmenter, nil, props.ImageType) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - - return src, props.ImageType, err -} - -// Validate ensures all parts of the config are valid. -func (cfg *segmenterConfig) Validate(path string) ([]string, error) { - var deps []string - if len(cfg.SegmenterName) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "segmenter_name") - } - return deps, nil -} - -// NextPointCloud function calls a segmenter service on the underlying camera and returns a pointcloud. -func (ss *segmenterSource) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::segmenter::NextPointCloud") - defer span.End() - - // get the service - srv, err := vision.FromRobot(ss.r, ss.segmenterName) - if err != nil { - return nil, fmt.Errorf("source_segmenter cant find vision service: %w", err) - } - - // apply service - clouds, err := srv.GetObjectPointClouds(ctx, ss.cameraName, map[string]interface{}{}) - if err != nil { - return nil, fmt.Errorf("could not get point clouds: %w", err) - } - if clouds == nil { - return pointcloud.New(), nil - } - - // merge pointclouds - cloudsWithOffset := make([]pointcloud.CloudAndOffsetFunc, 0, len(clouds)) - for _, cloud := range clouds { - cloudCopy := cloud - cloudFunc := func(ctx context.Context) (pointcloud.PointCloud, spatialmath.Pose, error) { - return cloudCopy, nil, nil - } - cloudsWithOffset = append(cloudsWithOffset, cloudFunc) - } - mergedCloud, err := pointcloud.MergePointClouds(context.Background(), cloudsWithOffset, nil) - if err != nil { - return nil, fmt.Errorf("could not merge point clouds: %w", err) - } - return mergedCloud, nil -} - -// Read returns the image if the stream is valid, else error. -func (ss *segmenterSource) Read(ctx context.Context) (image.Image, func(), error) { - img, release, err := ss.stream.Next(ctx) - if err != nil { - return nil, nil, fmt.Errorf("could not get next source image: %w", err) - } - return img, release, nil -} - -// Close closes the underlying stream. -func (ss *segmenterSource) Close(ctx context.Context) error { - return ss.stream.Close(ctx) -} diff --git a/components/camera/transformpipeline/segmenter_test.go b/components/camera/transformpipeline/segmenter_test.go deleted file mode 100644 index a00242a3999..00000000000 --- a/components/camera/transformpipeline/segmenter_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - "image/color" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - vizservices "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" - segment "go.viam.com/rdk/vision/segmentation" -) - -func TestTransformSegmenterProps(t *testing.T) { - r := &inject.Robot{} - cam := &inject.Camera{} - vizServ := &inject.VisionService{} - logger := logging.NewTestLogger(t) - - cam.StreamFunc = func(ctx context.Context, - errHandlers ...gostream.ErrorHandler, - ) (gostream.MediaStream[image.Image], error) { - return &streamTest{}, nil - } - cam.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "fakeCamera": - return cam, nil - case "fakeVizService": - return vizServ, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - - transformConf := &transformConfig{ - Source: "fakeCamera", - Pipeline: []Transformation{ - { - Type: "segmentations", Attributes: utils.AttributeMap{ - "segmenter_name": "fakeVizService", - }, - }, - }, - } - - am := transformConf.Pipeline[0].Attributes - conf, err := resource.TransformAttributeMap[*segmenterConfig](am) - test.That(t, err, test.ShouldBeNil) - _, err = conf.Validate("path") - test.That(t, err, test.ShouldBeNil) - - _, err = newTransformPipeline(context.Background(), cam, transformConf, r, logger) - test.That(t, err, test.ShouldBeNil) - - transformConf = &transformConfig{ - Pipeline: []Transformation{ - { - Type: "segmentations", Attributes: utils.AttributeMap{}, - }, - }, - } - - am = transformConf.Pipeline[0].Attributes - conf, err = resource.TransformAttributeMap[*segmenterConfig](am) - test.That(t, err, test.ShouldBeNil) - _, err = conf.Validate("path") - test.That(t, err, test.ShouldNotBeNil) -} - -func TestTransformSegmenterFunctionality(t *testing.T) { - // TODO(RSDK-1200): remove skip when complete - t.Skip("remove skip once RSDK-1200 improvement is complete") - - r := &inject.Robot{} - cam := &inject.Camera{} - vizServ := &inject.VisionService{} - logger := logging.NewTestLogger(t) - - cam.StreamFunc = func(ctx context.Context, - errHandlers ...gostream.ErrorHandler, - ) (gostream.MediaStream[image.Image], error) { - return &streamTest{}, nil - } - cam.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - - vizServ.GetObjectPointCloudsFunc = func(ctx context.Context, cameraName string, - extra map[string]interface{}, - ) ([]*vision.Object, error) { - segments := make([]pc.PointCloud, 3) - segments[0] = pc.New() - err := segments[0].Set(pc.NewVector(0, 0, 1), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - if err != nil { - return nil, err - } - segments[1] = pc.New() - err = segments[1].Set(pc.NewVector(0, 1, 0), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - if err != nil { - return nil, err - } - segments[2] = pc.New() - err = segments[2].Set(pc.NewVector(1, 0, 0), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - if err != nil { - return nil, err - } - - objects, err := segment.NewSegmentsFromSlice(segments, "fake") - if err != nil { - return nil, err - } - return objects.Objects, nil - } - - r.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{camera.Named("fakeCamera"), vizservices.Named("fakeVizService")} - } - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "fakeCamera": - return cam, nil - case "fakeVizService": - return vizServ, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - - transformConf := &transformConfig{ - Source: "fakeCamera", - Pipeline: []Transformation{ - { - Type: "segmentations", Attributes: utils.AttributeMap{ - "segmenter_name": "fakeVizService", - }, - }, - }, - } - - pipeline, err := newTransformPipeline(context.Background(), cam, transformConf, r, logger) - test.That(t, err, test.ShouldBeNil) - - pc, err := pipeline.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldNotBeNil) - _, isValid := pc.At(0, 0, 1) - test.That(t, isValid, test.ShouldBeTrue) - _, isValid = pc.At(1, 0, 0) - test.That(t, isValid, test.ShouldBeTrue) - _, isValid = pc.At(0, 1, 0) - test.That(t, isValid, test.ShouldBeTrue) -} diff --git a/components/camera/transformpipeline/transform.go b/components/camera/transformpipeline/transform.go deleted file mode 100644 index c8d401997e4..00000000000 --- a/components/camera/transformpipeline/transform.go +++ /dev/null @@ -1,173 +0,0 @@ -package transformpipeline - -import ( - "context" - - "github.com/invopop/jsonschema" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/utils" -) - -// transformType is the list of allowed transforms that can be used in the pipeline. -type transformType string - -// the allowed transforms. -const ( - transformTypeUnspecified = transformType("") - transformTypeIdentity = transformType("identity") - transformTypeRotate = transformType("rotate") - transformTypeResize = transformType("resize") - transformTypeCrop = transformType("crop") - transformTypeDepthPretty = transformType("depth_to_pretty") - transformTypeOverlay = transformType("overlay") - transformTypeUndistort = transformType("undistort") - transformTypeDetections = transformType("detections") - transformTypeClassifications = transformType("classifications") - transformTypeSegmentations = transformType("segmentations") - transformTypeDepthEdges = transformType("depth_edges") - transformTypeDepthPreprocess = transformType("depth_preprocess") -) - -// transformRegistration holds pertinent information regarding the available transforms. -type transformRegistration struct { - name string - retType interface{} - description string -} - -// Do not share the same config type across multile configs because otherwise -// multiple json-schema reflected types will share the same $id -// (which is not valid jsonschema and will fail to compile on the FE). -type ( - idenityConfig struct{} - depthPrettyConfig struct{} - depthPreprocessConfig struct{} -) - -// registeredTransformConfigs is a map of all available transform configs, used for populating fields in the front-end. -var registeredTransformConfigs = map[transformType]*transformRegistration{ - transformTypeIdentity: { - string(transformTypeIdentity), - &idenityConfig{}, - "Does nothing to the image. Can use this to duplicate camera sources, or change the source's stream or parameters.", - }, - transformTypeRotate: { - string(transformTypeRotate), - &rotateConfig{}, - "Rotate the image by 180 degrees. Used when the camera is installed upside down.", - }, - transformTypeResize: { - string(transformTypeResize), - &resizeConfig{}, - "Resizes the image to the specified height and width", - }, - transformTypeCrop: { - string(transformTypeCrop), - &cropConfig{}, - "Crop the image to the specified rectangle in pixels", - }, - transformTypeDepthPretty: { - string(transformTypeDepthPretty), - &depthPrettyConfig{}, - "Turns a depth image source into a colorful image, with blue indicating distant points and red indicating nearby points.", - }, - transformTypeOverlay: { - string(transformTypeOverlay), - &overlayConfig{}, - "Projects a point cloud to a 2D RGB and Depth image, and overlays the two images. Used to debug the RGB+D alignment.", - }, - transformTypeUndistort: { - string(transformTypeUndistort), - &undistortConfig{}, - "Uses intrinsics and modified Brown-Conrady parameters to undistort the source image.", - }, - transformTypeDetections: { - string(transformTypeDetections), - &detectorConfig{}, - "Overlays object detections on the image. Can use any detector registered in the vision service.", - }, - transformTypeClassifications: { - string(transformTypeClassifications), - &classifierConfig{}, - "Overlays image classifications on the image. Can use any classifier registered in the vision service.", - }, - transformTypeSegmentations: { - string(transformTypeSegmentations), - &segmenterConfig{}, - "Segments the camera's point cloud. Can use any segmenter registered in the vision service.", - }, - transformTypeDepthEdges: { - string(transformTypeDepthEdges), - &depthEdgesConfig{}, - "Applies a Canny edge detector to find edges. Only works on cameras that produce depth maps.", - }, - transformTypeDepthPreprocess: { - string(transformTypeDepthPreprocess), - &depthPreprocessConfig{}, - "Applies some basic hole-filling and edge smoothing to a depth map.", - }, -} - -// Transformation states the type of transformation and the attributes that are specific to the given type. -type Transformation struct { - Type string `json:"type"` - Attributes utils.AttributeMap `json:"attributes"` -} - -// JSONSchema defines the schema for each of the possible transforms in the pipeline in a OneOf. -func (Transformation) JSONSchema() *jsonschema.Schema { - schemas := make([]*jsonschema.Schema, 0, len(registeredTransformConfigs)) - for _, transformReg := range registeredTransformConfigs { - transformSchema := jsonschema.Reflect(transformReg.retType) - transformSchema.Title = transformReg.name - transformSchema.Type = "object" - transformSchema.Description = transformReg.description - schemas = append(schemas, transformSchema) - } - return &jsonschema.Schema{ - OneOf: schemas, - } -} - -// buildTransform uses the Transformation config to build the desired transform ImageSource. -func buildTransform( - ctx context.Context, - r robot.Robot, - source gostream.VideoSource, - stream camera.ImageType, - tr Transformation, - sourceString string, -) (gostream.VideoSource, camera.ImageType, error) { - switch transformType(tr.Type) { - case transformTypeUnspecified, transformTypeIdentity: - return source, stream, nil - case transformTypeRotate: - return newRotateTransform(ctx, source, stream, tr.Attributes) - case transformTypeResize: - return newResizeTransform(ctx, source, stream, tr.Attributes) - case transformTypeCrop: - return newCropTransform(ctx, source, stream, tr.Attributes) - case transformTypeDepthPretty: - return newDepthToPrettyTransform(ctx, source, stream) - case transformTypeOverlay: - return newOverlayTransform(ctx, source, stream, tr.Attributes) - case transformTypeUndistort: - return newUndistortTransform(ctx, source, stream, tr.Attributes) - case transformTypeDetections: - return newDetectionsTransform(ctx, source, r, tr.Attributes) - case transformTypeClassifications: - return newClassificationsTransform(ctx, source, r, tr.Attributes) - case transformTypeSegmentations: - return newSegmentationsTransform(ctx, source, r, tr.Attributes, sourceString) - case transformTypeDepthEdges: - return newDepthEdgesTransform(ctx, source, tr.Attributes) - case transformTypeDepthPreprocess: - return newDepthPreprocessTransform(ctx, source) - default: - return nil, camera.UnspecifiedStream, errors.Errorf("do not know camera transform of type %q", tr.Type) - } -} diff --git a/components/camera/transformpipeline/transform_test.go b/components/camera/transformpipeline/transform_test.go deleted file mode 100644 index e4dfcd8c168..00000000000 --- a/components/camera/transformpipeline/transform_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package transformpipeline - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/invopop/jsonschema" - "go.viam.com/test" -) - -func TestJSONSchema(t *testing.T) { - tr := &transformConfig{} - schema := jsonschema.Reflect(tr) - jsonBytes, err := json.MarshalIndent(schema, "", " ") - test.That(t, err, test.ShouldBeNil) - jsonString := string(jsonBytes) - for transformName := range registeredTransformConfigs { - test.That(t, jsonString, test.ShouldContainSubstring, fmt.Sprintf("\"title\": \"%s\"", transformName)) - } -} diff --git a/components/camera/transformpipeline/undistort.go b/components/camera/transformpipeline/undistort.go deleted file mode 100644 index 7254fe2f64e..00000000000 --- a/components/camera/transformpipeline/undistort.go +++ /dev/null @@ -1,88 +0,0 @@ -package transformpipeline - -import ( - "context" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -type undistortConfig struct { - CameraParams *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters"` - DistortionParams *transform.BrownConrady `json:"distortion_parameters"` -} - -// undistortSource will undistort the original image according to the Distortion parameters -// within the intrinsic parameters. -type undistortSource struct { - originalStream gostream.VideoStream - stream camera.ImageType - cameraParams *transform.PinholeCameraModel -} - -func newUndistortTransform( - ctx context.Context, source gostream.VideoSource, stream camera.ImageType, am utils.AttributeMap, -) (gostream.VideoSource, camera.ImageType, error) { - conf, err := resource.TransformAttributeMap[*undistortConfig](am) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - if conf.CameraParams == nil { - return nil, camera.UnspecifiedStream, errors.Wrapf(transform.ErrNoIntrinsics, "cannot create undistort transform") - } - cameraModel := camera.NewPinholeModelWithBrownConradyDistortion(conf.CameraParams, conf.DistortionParams) - reader := &undistortSource{ - gostream.NewEmbeddedVideoStream(source), - stream, - &cameraModel, - } - src, err := camera.NewVideoSourceFromReader(ctx, reader, &cameraModel, stream) - if err != nil { - return nil, camera.UnspecifiedStream, err - } - return src, stream, err -} - -// Read undistorts the original image according to the camera parameters. -func (us *undistortSource) Read(ctx context.Context) (image.Image, func(), error) { - ctx, span := trace.StartSpan(ctx, "camera::transformpipeline::undistort::Read") - defer span.End() - orig, release, err := us.originalStream.Next(ctx) - if err != nil { - return nil, nil, err - } - switch us.stream { - case camera.ColorStream, camera.UnspecifiedStream: - color := rimage.ConvertImage(orig) - color, err = us.cameraParams.UndistortImage(color) - if err != nil { - return nil, nil, err - } - return color, release, nil - case camera.DepthStream: - depth, err := rimage.ConvertImageToDepthMap(ctx, orig) - if err != nil { - return nil, nil, err - } - depth, err = us.cameraParams.UndistortDepthMap(depth) - if err != nil { - return nil, nil, err - } - return depth, release, nil - default: - return nil, nil, errors.Errorf("do not know how to decode stream type %q", string(us.stream)) - } -} - -// Close closes the original stream. -func (us *undistortSource) Close(ctx context.Context) error { - return us.originalStream.Close(ctx) -} diff --git a/components/camera/transformpipeline/undistort_test.go b/components/camera/transformpipeline/undistort_test.go deleted file mode 100644 index abdf83dd2c3..00000000000 --- a/components/camera/transformpipeline/undistort_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package transformpipeline - -import ( - "context" - "errors" - "testing" - - "github.com/pion/mediadevices/pkg/prop" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -// not the real intrinsic parameters of the image, only for testing purposes. -var undistortTestParams = &transform.PinholeCameraIntrinsics{ - Width: 128, - Height: 72, - Fx: 1., - Fy: 1., - Ppx: 0., - Ppy: 0., -} - -var undistortTestBC = &transform.BrownConrady{ - RadialK1: 0., - RadialK2: 0., - TangentialP1: 0., - TangentialP2: 0., - RadialK3: 0., -} - -func TestUndistortSetup(t *testing.T) { - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - - // no camera parameters - am := utils.AttributeMap{} - _, _, err = newUndistortTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, errors.Is(err, transform.ErrNoIntrinsics), test.ShouldBeTrue) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) - - // bad stream type - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - am = utils.AttributeMap{"intrinsic_parameters": undistortTestParams, "distortion_parameters": undistortTestBC} - us, _, err := newUndistortTransform(context.Background(), source, camera.ImageType("fake"), am) - test.That(t, err, test.ShouldBeNil) - _, _, err = camera.ReadImage(context.Background(), us) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "do not know how to decode stream") - test.That(t, us.Close(context.Background()), test.ShouldBeNil) - - // success - conf has camera parameters - us, stream, err := newUndistortTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - _, _, err = camera.ReadImage(context.Background(), us) - test.That(t, err, test.ShouldBeNil) - - test.That(t, us.Close(context.Background()), test.ShouldBeNil) - test.That(t, source.Close(context.Background()), test.ShouldBeNil) -} - -func TestUndistortImage(t *testing.T) { - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board1_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{ColorImg: img}, prop.Video{}) - - // success - am := utils.AttributeMap{"intrinsic_parameters": undistortTestParams, "distortion_parameters": undistortTestBC} - us, stream, err := newUndistortTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - corrected, _, err := camera.ReadImage(context.Background(), us) - test.That(t, err, test.ShouldBeNil) - result, ok := corrected.(*rimage.Image) - test.That(t, ok, test.ShouldEqual, true) - test.That(t, us.Close(context.Background()), test.ShouldBeNil) - - sourceImg, _, err := camera.ReadImage(context.Background(), source) - test.That(t, err, test.ShouldBeNil) - expected, ok := sourceImg.(*rimage.Image) - test.That(t, ok, test.ShouldEqual, true) - - test.That(t, result, test.ShouldResemble, expected) - - // bad source - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: rimage.NewImage(10, 10)}, prop.Video{}) - us, stream, err = newUndistortTransform(context.Background(), source, camera.ColorStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.ColorStream) - _, _, err = camera.ReadImage(context.Background(), us) - test.That(t, err.Error(), test.ShouldContainSubstring, "img dimension and intrinsics don't match") - test.That(t, us.Close(context.Background()), test.ShouldBeNil) -} - -func TestUndistortDepthMap(t *testing.T) { - img, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board1_gray_small.png")) - test.That(t, err, test.ShouldBeNil) - source := gostream.NewVideoSource(&videosource.StaticSource{DepthImg: img}, prop.Video{}) - - // success - am := utils.AttributeMap{"intrinsic_parameters": undistortTestParams, "distortion_parameters": undistortTestBC} - us, stream, err := newUndistortTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - corrected, _, err := camera.ReadImage(context.Background(), us) - test.That(t, err, test.ShouldBeNil) - result, ok := corrected.(*rimage.DepthMap) - test.That(t, ok, test.ShouldEqual, true) - test.That(t, us.Close(context.Background()), test.ShouldBeNil) - - sourceImg, _, err := camera.ReadImage(context.Background(), source) - test.That(t, err, test.ShouldBeNil) - expected, ok := sourceImg.(*rimage.DepthMap) - test.That(t, ok, test.ShouldEqual, true) - - test.That(t, result, test.ShouldResemble, expected) - - // bad source - source = gostream.NewVideoSource(&videosource.StaticSource{DepthImg: rimage.NewEmptyDepthMap(10, 10)}, prop.Video{}) - us, stream, err = newUndistortTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, err, test.ShouldBeNil) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - _, _, err = camera.ReadImage(context.Background(), us) - test.That(t, err.Error(), test.ShouldContainSubstring, "img dimension and intrinsics don't match") - test.That(t, us.Close(context.Background()), test.ShouldBeNil) - - // can't convert image to depth map - source = gostream.NewVideoSource(&videosource.StaticSource{ColorImg: rimage.NewImage(10, 10)}, prop.Video{}) - us, stream, err = newUndistortTransform(context.Background(), source, camera.DepthStream, am) - test.That(t, stream, test.ShouldEqual, camera.DepthStream) - test.That(t, err, test.ShouldBeNil) - _, _, err = camera.ReadImage(context.Background(), us) - test.That(t, err.Error(), test.ShouldContainSubstring, "don't know how to make DepthMap") - test.That(t, us.Close(context.Background()), test.ShouldBeNil) -} diff --git a/components/camera/transformpipeline/verify_main_test.go b/components/camera/transformpipeline/verify_main_test.go deleted file mode 100644 index c20601fd823..00000000000 --- a/components/camera/transformpipeline/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package transformpipeline - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/camera/ultrasonic/ultrasonic.go b/components/camera/ultrasonic/ultrasonic.go deleted file mode 100644 index 6e3d6c8fb0a..00000000000 --- a/components/camera/ultrasonic/ultrasonic.go +++ /dev/null @@ -1,100 +0,0 @@ -// Package ultrasonic provides an implementation for an ultrasonic sensor wrapped as a camera -package ultrasonic - -import ( - "context" - "errors" - "image" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/sensor" - ultrasense "go.viam.com/rdk/components/sensor/ultrasonic" - "go.viam.com/rdk/logging" - pointcloud "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("ultrasonic") - -type ultrasonicWrapper struct { - usSensor sensor.Sensor -} - -func init() { - resource.RegisterComponent( - camera.API, - model, - resource.Registration[camera.Camera, *ultrasense.Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (camera.Camera, error) { - newConf, err := resource.NativeConfig[*ultrasense.Config](conf) - if err != nil { - return nil, err - } - return newCamera(ctx, deps, conf.ResourceName(), newConf, logger) - }, - }) -} - -func newCamera(ctx context.Context, deps resource.Dependencies, name resource.Name, - newConf *ultrasense.Config, logger logging.Logger, -) (camera.Camera, error) { - usSensor, err := ultrasense.NewSensor(ctx, deps, name, newConf, logger) - if err != nil { - return nil, err - } - return cameraFromSensor(ctx, name, usSensor, logger) -} - -func cameraFromSensor(ctx context.Context, name resource.Name, usSensor sensor.Sensor, logger logging.Logger) (camera.Camera, error) { - usWrapper := ultrasonicWrapper{usSensor: usSensor} - - usVideoSource, err := camera.NewVideoSourceFromReader(ctx, &usWrapper, nil, camera.UnspecifiedStream) - if err != nil { - return nil, err - } - - return camera.FromVideoSource(name, usVideoSource, logger), nil -} - -// NextPointCloud queries the ultrasonic sensor then returns the result as a pointcloud, -// with a single point at (0, 0, distance). -func (usvs *ultrasonicWrapper) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - readings, err := usvs.usSensor.Readings(ctx, nil) - if err != nil { - return nil, err - } - pcToReturn := pointcloud.New() - distFloat, ok := readings["distance"].(float64) - if !ok { - return nil, errors.New("unable to convert distance to float64") - } - basicData := pointcloud.NewBasicData() - distVector := pointcloud.NewVector(0, 0, distFloat*1000) - err = pcToReturn.Set(distVector, basicData) - if err != nil { - return nil, err - } - - return pcToReturn, nil -} - -// Properties returns the properties of the ultrasonic camera. -func (usvs *ultrasonicWrapper) Properties(ctx context.Context) (camera.Properties, error) { - return camera.Properties{SupportsPCD: true, ImageType: camera.UnspecifiedStream}, nil -} - -// Close closes the underlying ultrasonic sensor and the camera itself. -func (usvs *ultrasonicWrapper) Close(ctx context.Context) error { - err := usvs.usSensor.Close(ctx) - return err -} - -// Read returns a not yet implemented error, as it is not needed for the ultrasonic camera. -func (usvs *ultrasonicWrapper) Read(ctx context.Context) (image.Image, func(), error) { - return nil, nil, errors.New("not yet implemented") -} diff --git a/components/camera/ultrasonic/ultrasonic_test.go b/components/camera/ultrasonic/ultrasonic_test.go deleted file mode 100644 index 7b0c98edc47..00000000000 --- a/components/camera/ultrasonic/ultrasonic_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package ultrasonic - -import ( - "context" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/sensor/ultrasonic" - "go.viam.com/rdk/logging" - pointcloud "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testSensorName = "ultrasonic1" - triggerPin = "some-pin" - echoInterrupt = "some-echo-interrupt" - board1 = "some-board" -) - -func setupDependencies(t *testing.T) resource.Dependencies { - t.Helper() - - deps := make(resource.Dependencies) - - actualBoard := inject.NewBoard(board1) - actualBoard.DigitalInterruptNamesFunc = func() []string { - return []string{echoInterrupt} - } - injectDigi := &inject.DigitalInterrupt{} - actualBoard.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - return injectDigi, nil - } - pin := &inject.GPIOPin{} - pin.SetFunc = func(ctx context.Context, high bool, extra map[string]interface{}) error { - return nil - } - actualBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return pin, nil - } - deps[board.Named(board1)] = actualBoard - - return deps -} - -func TestNewCamera(t *testing.T) { - fakecfg := &ultrasonic.Config{TriggerPin: triggerPin, EchoInterrupt: echoInterrupt, Board: board1} - name := resource.Name{API: camera.API} - ctx := context.Background() - deps := setupDependencies(t) - logger := logging.NewTestLogger(t) - _, err := newCamera(ctx, deps, name, fakecfg, logger) - test.That(t, err, test.ShouldBeNil) -} - -func TestUnderlyingSensor(t *testing.T) { - name := resource.Name{API: camera.API} - ctx := context.Background() - - fakeUS := inject.NewSensor("mySensor") - fakeUS.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return map[string]interface{}{"distance": 3.2}, nil - } - logger := logging.NewTestLogger(t) - cam, err := cameraFromSensor(ctx, name, fakeUS, logger) - test.That(t, err, test.ShouldBeNil) - - pc, err := cam.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - - values := []float64{} - count := 0 - - pc.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - values = append(values, p.Z) - count++ - return true - }) - - test.That(t, count, test.ShouldEqual, 1) - test.That(t, values[0], test.ShouldEqual, 3200) - stream, err := cam.Stream(ctx) - test.That(t, err, test.ShouldBeNil) - - _, _, err = stream.Next(ctx) - test.That(t, err.Error(), test.ShouldEqual, "not yet implemented") -} diff --git a/components/camera/velodyne/sample.json b/components/camera/velodyne/sample.json deleted file mode 100644 index 9f63cde29c2..00000000000 --- a/components/camera/velodyne/sample.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "components": [ - { - "name": "velodyne", - "type": "camera", - "model": "velodyne", - "attributes": { - "ttl_ms": 5000 - } - } - ] -} diff --git a/components/camera/velodyne/velodyne.go b/components/camera/velodyne/velodyne.go deleted file mode 100644 index 7f8cd8236b2..00000000000 --- a/components/camera/velodyne/velodyne.go +++ /dev/null @@ -1,390 +0,0 @@ -// Package velodyne implements a general velodyne LIDAR as a camera. -package velodyne - -import ( - "context" - "fmt" - "image" - "image/color" - "sync" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.einride.tech/vlp16" - gutils "go.viam.com/utils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -type channelConfig struct { - elevationAngle float64 - azimuthOffset float64 - verticalOffset float64 -} - -type productConfig []channelConfig - -var allProductData = map[vlp16.ProductID]productConfig{ - vlp16.ProductIDVLP32C: { - channelConfig{-25, 1.4, 0}, - channelConfig{-1, -4.2, 0}, - channelConfig{-1.667, 1.4, 0}, - channelConfig{-15.639, -1.4, 0}, - channelConfig{-11.31, 1.4, 0}, - channelConfig{0, -1.4, 0}, - channelConfig{-0.667, 4.2, 0}, - channelConfig{-8.843, -1.4, 0}, - channelConfig{-7.254, 1.4, 0}, - channelConfig{0.333, -4.2, 0}, - channelConfig{-0.333, 1.4, 0}, - channelConfig{-6.148, -1.4, 0}, - channelConfig{-5.333, 4.2, 0}, - channelConfig{1.333, -1.4, 0}, - channelConfig{0.667, 4.2, 0}, - channelConfig{-4, -1.4, 0}, - channelConfig{-4.667, 1.4, 0}, - channelConfig{1.667, -4.2, 0}, - channelConfig{1, 1.4, 0}, - channelConfig{-3.667, -4.2, 0}, - channelConfig{-3.333, 4.2, 0}, - channelConfig{3.333, -1.4, 0}, - channelConfig{2.333, 1.4, 0}, - channelConfig{-2.667, -1.4, 0}, - channelConfig{-3, 1.4, 0}, - channelConfig{7, -1.4, 0}, - channelConfig{4.667, 1.4, 0}, - channelConfig{-2.333, -4.2, 0}, - channelConfig{-2, 4.2, 0}, - channelConfig{15, -1.4, 0}, - channelConfig{10.333, 1.4, 0}, - channelConfig{-1.333, -1.4, 0}, - }, - vlp16.ProductIDVLP16: { // This also covers VLP Puck LITE - channelConfig{-15, 0, 11.2}, - channelConfig{1, 0, -0.7}, - channelConfig{-13, 0, 9.7}, - channelConfig{3, 0, -2.2}, - channelConfig{11, 0, 8.1}, - channelConfig{5, 0, -3.7}, - channelConfig{-9, 0, 6.6}, - channelConfig{7, 0, -5.1}, - channelConfig{-7, 0, 5.1}, - channelConfig{9, 0, -6.6}, - channelConfig{-5, 0, 3.7}, - channelConfig{11, 0, -8.1}, - channelConfig{-3, 0, 2.2}, - channelConfig{13, 0, -9.7}, - channelConfig{-1, 0, 0.7}, - channelConfig{15, 0, -11.2}, - }, - vlp16.ProductIDPuckHiRes: { - channelConfig{-10, 0, 7.4}, - channelConfig{.67, 0, -0.9}, - channelConfig{-8.67, 0, 6.5}, - channelConfig{2, 0, -1.8}, - channelConfig{-7.33, 0, 5.5}, - channelConfig{3.33, 0, -2.7}, - channelConfig{-6, 0, 4.6}, - channelConfig{4.67, 0, -3.7}, - channelConfig{-4.67, 0, 3.7}, - channelConfig{6, 0, -4.6}, - channelConfig{-3.3, 0, 2.7}, - channelConfig{7.33, 0, -5.5}, - channelConfig{-2, 0, 1.8}, - channelConfig{8.67, 0, -6.5}, - channelConfig{-0.67, 0, 0.9}, - channelConfig{10, 0, -7.4}, - }, -} - -// Config is the config for a veldoyne LIDAR. -type Config struct { - Port int `json:"port"` - TTLMS int `json:"ttl_ms"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - if conf.Port == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "port") - } - - if conf.TTLMS == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "ttl_ms") - } - return nil, nil -} - -var model = resource.DefaultModelFamily.WithModel("velodyne") - -func init() { - resource.RegisterComponent( - camera.API, - model, - resource.Registration[camera.Camera, *Config]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (camera.Camera, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - port := newConf.Port - if port == 0 { - port = 2368 - } - - ttl := newConf.TTLMS - if ttl == 0 { - return nil, errors.New("need to specify a ttl") - } - - return New(ctx, conf.ResourceName(), logger, port, ttl) - }, - }) -} - -type client struct { - resource.Named - resource.AlwaysRebuild - bindAddress string - ttlMilliseconds int - - logger logging.Logger - - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup - - mu sync.Mutex - - lastError error - product vlp16.ProductID - ip string - packets []vlp16.Packet -} - -// New creates a connection to a Velodyne lidar and generates pointclouds from it. -func New(ctx context.Context, name resource.Name, logger logging.Logger, port, ttlMilliseconds int) (camera.Camera, error) { - bindAddress := fmt.Sprintf("0.0.0.0:%d", port) - listener, err := vlp16.ListenUDP(ctx, bindAddress) - if err != nil { - return nil, err - } - // Listen for and print packets. - - c := &client{ - Named: name.AsNamed(), - bindAddress: bindAddress, - ttlMilliseconds: ttlMilliseconds, - logger: logger, - } - - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - c.cancelFunc = cancelFunc - c.activeBackgroundWorkers.Add(1) - gutils.PanicCapturingGo(func() { - c.run(cancelCtx, listener) - }) - - src, err := camera.NewVideoSourceFromReader(ctx, c, nil, camera.DepthStream) - if err != nil { - return nil, err - } - return camera.FromVideoSource(name, src, logger), nil -} - -func (c *client) setLastError(err error) { - c.mu.Lock() - defer c.mu.Unlock() - c.lastError = err -} - -func (c *client) run(ctx context.Context, listener *vlp16.PacketListener) { - defer gutils.UncheckedErrorFunc(listener.Close) - defer c.activeBackgroundWorkers.Done() - - for { - err := ctx.Err() - if err != nil { - // cancelled - return - } - - if listener == nil { - listener, err = vlp16.ListenUDP(ctx, c.bindAddress) - if err != nil { - c.setLastError(err) - c.logger.CInfof(ctx, "velodyne connect error: %w", err) - if !gutils.SelectContextOrWait(ctx, time.Second) { - return - } - continue - } - } - - err = c.runLoop(listener) - c.setLastError(err) - if err != nil { - c.logger.CInfof(ctx, "velodyne client error: %w", err) - err = listener.Close() - if err != nil { - c.logger.CWarn(ctx, "trying to close connection after error got", "error", err) - } - listener = nil - if !gutils.SelectContextOrWait(ctx, time.Second) { - return - } - } - } -} - -func (c *client) runLoop(listener *vlp16.PacketListener) error { - if err := listener.ReadPacket(); err != nil { - return err - } - - p := listener.Packet() - - c.mu.Lock() - defer c.mu.Unlock() - - ipString := listener.SourceIP().String() - if c.ip == "" { - c.ip = ipString - } else if c.ip != ipString { - c.packets = []vlp16.Packet{} - c.product = 0 - err := fmt.Errorf("velodyne ip changed from %s -> %s", c.ip, ipString) - c.ip = ipString - return err - } - - if c.product == 0 { - c.product = p.ProductID - } else if c.product != p.ProductID { - c.packets = []vlp16.Packet{} - err := fmt.Errorf("velodyne product changed from %s -> %s", c.product, p.ProductID) - c.product = 0 - return err - } - - // we remove the packets too old - firstToRemove := -1 - for idx, old := range c.packets { - age := int(p.Timestamp) - int(old.Timestamp) - if age < c.ttlMilliseconds*1000 { - break - } - firstToRemove = idx - } - - if firstToRemove >= 0 { - c.packets = c.packets[firstToRemove+1:] - } - - c.packets = append(c.packets, *p) - return nil -} - -func pointFrom(yaw, pitch, distance float64) r3.Vector { - ea := spatialmath.NewEulerAngles() - ea.Yaw = yaw - ea.Pitch = pitch - - pose1 := spatialmath.NewPoseFromOrientation(ea) - pose2 := spatialmath.NewPoseFromPoint(r3.Vector{distance, 0, 0}) - p := spatialmath.Compose(pose1, pose2).Point() - - return pointcloud.NewVector(p.X*1000, p.Y*1000, p.Z*1000) -} - -func (c *client) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - c.mu.Lock() - defer c.mu.Unlock() - if c.lastError != nil { - return nil, c.lastError - } - - config, ok := allProductData[c.product] - if !ok { - return nil, fmt.Errorf("no config for %s", c.product) - } - - pc := pointcloud.New() - for _, p := range c.packets { - for _, b := range p.Blocks { - yaw := float64(b.Azimuth) / 100 - for channelID, c := range b.Channels { - if channelID >= len(config) { - return nil, fmt.Errorf("channel (%d)out of range %d", channelID, len(config)) - } - pitch := config[channelID].elevationAngle - yaw += config[channelID].azimuthOffset - - p := pointFrom(utils.DegToRad(yaw), utils.DegToRad(pitch), float64(c.Distance)/1000) - p.Z += config[channelID].verticalOffset - - err := pc.Set(p, pointcloud.NewBasicData().SetIntensity(uint16(c.Reflectivity)*255)) - if err != nil { - return nil, err - } - } - } - } - - return pc, nil -} - -func (c *client) Read(ctx context.Context) (image.Image, func(), error) { - pc, err := c.NextPointCloud(ctx) - if err != nil { - return nil, nil, err - } - - meta := pc.MetaData() - - width := 800 - height := 800 - - scale := func(x, y float64) (int, int) { - return int(float64(width) * ((x - meta.MinX) / (meta.MaxX - meta.MinX))), - int(float64(height) * ((y - meta.MinY) / (meta.MaxY - meta.MinY))) - } - - img := image.NewNRGBA(image.Rect(0, 0, width, height)) - - set := func(xpc, ypc float64, clr color.NRGBA) { - x, y := scale(xpc, ypc) - img.SetNRGBA(x, y, clr) - } - - pc.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - set(p.X, p.Y, color.NRGBA{255, 0, 0, 255}) - return true - }) - - centerSize := .1 - for x := -1 * centerSize; x < centerSize; x += .01 { - for y := -1 * centerSize; y < centerSize; y += .01 { - set(x, y, color.NRGBA{0, 255, 0, 255}) - } - } - - return img, nil, nil -} - -func (c *client) Close(ctx context.Context) error { - c.cancelFunc() - c.activeBackgroundWorkers.Wait() - return nil -} diff --git a/components/camera/verify_main_test.go b/components/camera/verify_main_test.go deleted file mode 100644 index 7b2cf79cb31..00000000000 --- a/components/camera/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package camera - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/camera/videosource/data/intrinsics.json b/components/camera/videosource/data/intrinsics.json deleted file mode 100644 index 99af8484fec..00000000000 --- a/components/camera/videosource/data/intrinsics.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "FaceTime HD Camera": { - "ppx": 446, - "ppy": 585, - "height_px": 720, - "fx": 1055, - "fy": 1209, - "width_px": 1280 - }, - "Logitech BRIO": { - "fx": 740.0304302692135, - "fy": 740.4105603669158, - "height_px": 720, - "ppx": 640.4552253799196, - "ppy": 371.8080142996358, - "width_px": 1280 - }, - "C270 HD WEBCAM": { - "fx": 911.4644040206691, - "fy": 912.7619272761438, - "height_px": 480, - "ppx": 352.3560568978409, - "ppy": 218.83896179564766, - "width_px": 640 - }, - "HD Webcam eMeet C950": { - "fx": 1630.8007951390407, - "fy": 1640.6785145222568, - "height_px": 1080, - "ppx": 1060.5854553632203, - "ppy": 485.6573920452437, - "width_px": 1920 - }, - "GENERAL WEBCAM": { - "fx": 724.2002169657624, - "fy": 729.8226006721098, - "height_px": 480, - "ppx": 324.1225833049127, - "ppy": 244.44003874099673, - "width_px": 640 - } -} diff --git a/components/camera/videosource/join_pointcloud.go b/components/camera/videosource/join_pointcloud.go deleted file mode 100644 index cec33d0138f..00000000000 --- a/components/camera/videosource/join_pointcloud.go +++ /dev/null @@ -1,350 +0,0 @@ -package videosource - -import ( - "context" - "fmt" - "image" - "math" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/spatialmath" -) - -const numThreadsVideoSource = 8 // This should be a param - -var modelJoinPC = resource.DefaultModelFamily.WithModel("join_pointclouds") - -func init() { - resource.RegisterComponent( - camera.API, - modelJoinPC, - resource.Registration[camera.Camera, *Config]{ - Constructor: newJoinPointCloudCamera, - }, - ) -} - -// Config is the attribute struct for joinPointCloudSource. -type Config struct { - TargetFrame string `json:"target_frame"` - SourceCameras []string `json:"source_cameras"` - // Closeness defines how close 2 points should be together to be considered the same point when merged. - Closeness float64 `json:"proximity_threshold_mm,omitempty"` - MergeMethod string `json:"merge_method,omitempty"` - CameraParameters *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters,omitempty"` - DistortionParameters *transform.BrownConrady `json:"distortion_parameters,omitempty"` - Debug bool `json:"debug,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - if len(cfg.SourceCameras) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "source_cameras") - } - - if cfg.CameraParameters != nil { - if cfg.CameraParameters.Height < 0 || cfg.CameraParameters.Width < 0 { - return nil, fmt.Errorf( - "got illegal negative dimensions for width_px and height_px (%d, %d) fields set in intrinsic_parameters for join_pointclouds camera", - cfg.CameraParameters.Width, - cfg.CameraParameters.Height, - ) - } - } - - deps = append(deps, cfg.SourceCameras...) - deps = append(deps, framesystem.InternalServiceName.String()) - return deps, nil -} - -type ( - // MergeMethodType Defines which strategy is used for merging. - MergeMethodType string - // MergeMethodUnsupportedError is returned when the merge method is not supported. - MergeMethodUnsupportedError error -) - -const ( - // Null is a default value for the merge method. - Null = MergeMethodType("") - // Naive is the naive merge method. - Naive = MergeMethodType("naive") - // ICP is the ICP merge method. - ICP = MergeMethodType("icp") -) - -func newMergeMethodUnsupportedError(method string) MergeMethodUnsupportedError { - return errors.Errorf("merge method %s not supported", method) -} - -// joinPointCloudSource takes image sources that can produce point clouds and merges them together from -// the point of view of targetName. The model needs to have the entire robot available in order to build the correct offsets -// between robot components for the frame system transform. -type joinPointCloudCamera struct { - resource.Named - resource.AlwaysRebuild - sourceCameras []camera.Camera - sourceNames []string - targetName string - fsService framesystem.Service - mergeMethod MergeMethodType - logger logging.Logger - debug bool - closeness float64 - src camera.VideoSource -} - -// newJoinPointCloudSource creates a camera that combines point cloud sources into one point cloud in the -// reference frame of targetName. -func newJoinPointCloudCamera( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (camera.Camera, error) { - joinCam := &joinPointCloudCamera{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - - if err := joinCam.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return camera.FromVideoSource(conf.ResourceName(), joinCam.src, logger), nil -} - -func (jpcc *joinPointCloudCamera) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - cfg, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - // frame to merge from - jpcc.sourceCameras = make([]camera.Camera, len(cfg.SourceCameras)) - jpcc.sourceNames = make([]string, len(cfg.SourceCameras)) - for i, source := range cfg.SourceCameras { - jpcc.sourceNames[i] = source - camSource, err := camera.FromDependencies(deps, source) - if err != nil { - return fmt.Errorf("no camera source called (%s): %w", source, err) - } - jpcc.sourceCameras[i] = camSource - } - // frame to merge to - jpcc.targetName = cfg.TargetFrame - jpcc.fsService, err = framesystem.FromDependencies(deps) - if err != nil { - return err - } - jpcc.closeness = cfg.Closeness - - jpcc.debug = cfg.Debug - - jpcc.mergeMethod = MergeMethodType(cfg.MergeMethod) - - if idx, ok := contains(jpcc.sourceNames, jpcc.targetName); ok { - parentCamera := jpcc.sourceCameras[idx] - var intrinsicParams *transform.PinholeCameraIntrinsics - if parentCamera != nil { - props, err := parentCamera.Properties(ctx) - if err != nil { - return camera.NewPropertiesError(fmt.Sprintf("point cloud source at index %d for target %s", idx, jpcc.targetName)) - } - intrinsicParams = props.IntrinsicParams - } - jpcc.src, err = camera.NewVideoSourceFromReader( - ctx, - jpcc, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: intrinsicParams}, - camera.ColorStream, - ) - return err - } - jpcc.src, err = camera.NewVideoSourceFromReader(ctx, jpcc, nil, camera.ColorStream) - return err -} - -// NextPointCloud gets all the point clouds from the source cameras, -// and puts the points in one point cloud in the frame of targetFrame. -func (jpcc *joinPointCloudCamera) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - switch jpcc.mergeMethod { - case Naive, Null: - return jpcc.NextPointCloudNaive(ctx) - case ICP: - return jpcc.NextPointCloudICP(ctx) - default: - return nil, newMergeMethodUnsupportedError(string(jpcc.mergeMethod)) - } -} - -func (jpcc *joinPointCloudCamera) NextPointCloudNaive(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "joinPointCloudSource::NextPointCloudNaive") - defer span.End() - - fs, err := jpcc.fsService.FrameSystem(ctx, nil) - if err != nil { - return nil, err - } - - inputs, _, err := jpcc.fsService.CurrentInputs(ctx) - if err != nil { - return nil, err - } - cloudFuncs := make([]pointcloud.CloudAndOffsetFunc, len(jpcc.sourceCameras)) - for i, cam := range jpcc.sourceCameras { - iCopy := i - camCopy := cam - pcSrc := func(ctx context.Context) (pointcloud.PointCloud, spatialmath.Pose, error) { - ctx, span := trace.StartSpan(ctx, "camera::joinPointCloudSource::NextPointCloud::"+jpcc.sourceNames[iCopy]+"-NextPointCloud") - defer span.End() - var framePose spatialmath.Pose - if jpcc.sourceNames[iCopy] != jpcc.targetName { - sourceFrame := referenceframe.NewPoseInFrame(jpcc.sourceNames[iCopy], spatialmath.NewZeroPose()) - theTransform, err := fs.Transform(inputs, sourceFrame, jpcc.targetName) - if err != nil { - return nil, nil, err - } - framePose = theTransform.(*referenceframe.PoseInFrame).Pose() - } - pc, err := camCopy.NextPointCloud(ctx) - if err != nil { - return nil, nil, err - } - if pc == nil { - return nil, nil, errors.Errorf("camera %q returned a nil point cloud", jpcc.sourceNames[iCopy]) - } - return pc, framePose, nil - } - cloudFuncs[iCopy] = pcSrc - } - - return pointcloud.MergePointClouds(ctx, cloudFuncs, jpcc.logger) -} - -func (jpcc *joinPointCloudCamera) NextPointCloudICP(ctx context.Context) (pointcloud.PointCloud, error) { - ctx, span := trace.StartSpan(ctx, "joinPointCloudSource::NextPointCloudICP") - defer span.End() - - fs, err := jpcc.fsService.FrameSystem(ctx, nil) - if err != nil { - return nil, err - } - - inputs, _, err := jpcc.fsService.CurrentInputs(ctx) - if err != nil { - return nil, err - } - - targetIndex := 0 - - for i, camName := range jpcc.sourceNames { - if camName == jpcc.targetName { - targetIndex = i - break - } - } - - targetPointCloud, err := jpcc.sourceCameras[targetIndex].NextPointCloud(ctx) - if err != nil { - return nil, err - } - - finalPointCloud := pointcloud.ToKDTree(targetPointCloud) - for i := range jpcc.sourceCameras { - if i == targetIndex { - continue - } - - pcSrc, err := jpcc.sourceCameras[i].NextPointCloud(ctx) - if err != nil { - return nil, err - } - - sourceFrame := referenceframe.NewPoseInFrame(jpcc.sourceNames[i], spatialmath.NewZeroPose()) - theTransform, err := fs.Transform(inputs, sourceFrame, jpcc.targetName) - if err != nil { - return nil, err - } - - registeredPointCloud, info, err := pointcloud.RegisterPointCloudICP(pcSrc, finalPointCloud, - theTransform.(*referenceframe.PoseInFrame).Pose(), jpcc.debug, numThreadsVideoSource) - if err != nil { - return nil, err - } - if jpcc.debug { - jpcc.logger.CDebugf(ctx, "Learned Transform = %v", info.OptResult.Location.X) - } - transformDist := math.Sqrt(math.Pow(info.OptResult.Location.X[0]-info.X0[0], 2) + - math.Pow(info.OptResult.Location.X[1]-info.X0[1], 2) + - math.Pow(info.OptResult.Location.X[2]-info.X0[2], 2)) - if transformDist > 100 { - jpcc.logger.CWarnf(ctx, `Transform is %f away from transform defined in frame system. - This may indicate an incorrect frame system.`, transformDist) - } - registeredPointCloud.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - nearest, _, _, _ := finalPointCloud.NearestNeighbor(p) - distance := math.Sqrt(math.Pow(p.X-nearest.X, 2) + math.Pow(p.Y-nearest.Y, 2) + math.Pow(p.Z-nearest.Z, 2)) - if distance > jpcc.closeness { - err = finalPointCloud.Set(p, d) - if err != nil { - return false - } - } - return true - }) - if err != nil { - return nil, err - } - } - - return finalPointCloud, nil -} - -// Read gets the merged point cloud from all sources, and then uses a projection to turn it into a 2D image. -func (jpcc *joinPointCloudCamera) Read(ctx context.Context) (image.Image, func(), error) { - var proj transform.Projector - var err error - if idx, ok := contains(jpcc.sourceNames, jpcc.targetName); ok { - proj, err = jpcc.sourceCameras[idx].Projector(ctx) - if err != nil && !errors.Is(err, transform.ErrNoIntrinsics) { - return nil, nil, err - } - } - if proj == nil { // use a default projector if target frame doesn't have one - proj = &transform.ParallelProjection{} - } - - pc, err := jpcc.NextPointCloud(ctx) - if err != nil { - return nil, nil, err - } - if jpcc.debug && pc != nil { - jpcc.logger.CDebugf(ctx, "joinPointCloudSource Read: number of points in pointcloud: %d", pc.Size()) - } - img, _, err := proj.PointCloudToRGBD(pc) - return img, func() {}, err // return color image -} - -func (jpcc *joinPointCloudCamera) Close(ctx context.Context) error { - return nil -} - -func contains(s []string, str string) (int, bool) { - for i, v := range s { - if v == str { - return i, true - } - } - return -1, false -} diff --git a/components/camera/videosource/join_pointcloud_test.go b/components/camera/videosource/join_pointcloud_test.go deleted file mode 100644 index 3acdc911e3f..00000000000 --- a/components/camera/videosource/join_pointcloud_test.go +++ /dev/null @@ -1,417 +0,0 @@ -package videosource - -import ( - "context" - "image" - "image/color" - "os" - "testing" - "time" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -func makeFakeRobot(t *testing.T) resource.Dependencies { - t.Helper() - logger := logging.NewTestLogger(t) - cam1 := &inject.Camera{} - cam1.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - pc1 := pointcloud.NewWithPrealloc(1) - err := pc1.Set(pointcloud.NewVector(1, 0, 0), pointcloud.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - return pc1, nil - } - cam1.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - cam1.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return nil, transform.NewNoIntrinsicsError("") - } - cam2 := &inject.Camera{} - cam2.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - pc2 := pointcloud.NewWithPrealloc(1) - err := pc2.Set(pointcloud.NewVector(0, 1, 0), pointcloud.NewColoredData(color.NRGBA{0, 255, 0, 255})) - test.That(t, err, test.ShouldBeNil) - return pc2, nil - } - cam2.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - cam2.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return nil, transform.NewNoIntrinsicsError("") - } - cam3 := &inject.Camera{} - cam3.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - pc3 := pointcloud.NewWithPrealloc(1) - err := pc3.Set(pointcloud.NewVector(0, 0, 1), pointcloud.NewColoredData(color.NRGBA{0, 0, 255, 255})) - test.That(t, err, test.ShouldBeNil) - return pc3, nil - } - cam3.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - cam3.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return nil, transform.NewNoIntrinsicsError("") - } - base1 := &inject.Base{} - - fsParts := []*referenceframe.FrameSystemPart{ - { - FrameConfig: referenceframe.NewLinkInFrame(referenceframe.World, spatialmath.NewZeroPose(), "base1", nil), - }, - { - FrameConfig: referenceframe.NewLinkInFrame( - referenceframe.World, - spatialmath.NewPoseFromPoint(r3.Vector{100, 0, 0}), - "cam1", - nil), - }, - { - FrameConfig: referenceframe.NewLinkInFrame("cam1", spatialmath.NewPoseFromPoint(r3.Vector{0, 0, 100}), "cam2", nil), - }, - { - FrameConfig: referenceframe.NewLinkInFrame("cam2", spatialmath.NewPoseFromPoint(r3.Vector{0, 100, 0}), "cam3", nil), - }, - } - deps := make(resource.Dependencies) - deps[camera.Named("cam1")] = cam1 - deps[camera.Named("cam2")] = cam2 - deps[camera.Named("cam3")] = cam3 - deps[base.Named("base1")] = base1 - fsSvc, err := framesystem.New(context.Background(), resource.Dependencies{}, logger) - test.That(t, err, test.ShouldBeNil) - err = fsSvc.Reconfigure(context.Background(), deps, resource.Config{ConvertedAttributes: &framesystem.Config{Parts: fsParts}}) - test.That(t, err, test.ShouldBeNil) - deps[framesystem.InternalServiceName] = fsSvc - return deps -} - -func TestJoinPointCloudNaive(t *testing.T) { - // TODO(RSDK-1200): remove skip when complete - t.Skip("remove skip once RSDK-1200 improvement is complete") - deps := makeFakeRobot(t) - - // PoV from base1 - conf := &Config{ - Debug: true, - SourceCameras: []string{"cam1", "cam2", "cam3"}, - TargetFrame: "base1", - MergeMethod: "naive", - } - logger := logging.Global() - joinedCam, err := newJoinPointCloudCamera(context.Background(), deps, resource.Config{ConvertedAttributes: conf}, logger) - test.That(t, err, test.ShouldBeNil) - pc, err := joinedCam.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldNotBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 3) - - data, got := pc.At(101, 0, 0) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{255, 0, 0, 255}) - - data, got = pc.At(100, 1, 100) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{0, 255, 0, 255}) - - data, got = pc.At(100, 100, 101) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{0, 0, 255, 255}) - - img, _, err := camera.ReadImage(context.Background(), joinedCam) - test.That(t, err, test.ShouldBeNil) - test.That(t, img.Bounds(), test.ShouldResemble, image.Rect(0, 0, 1, 100)) - test.That(t, joinedCam.Close(context.Background()), test.ShouldBeNil) - - // PoV from cam1 - conf2 := &Config{ - Debug: true, - SourceCameras: []string{"cam1", "cam2", "cam3"}, - TargetFrame: "cam1", - MergeMethod: "naive", - } - joinedCam2, err := newJoinPointCloudCamera(context.Background(), deps, resource.Config{ConvertedAttributes: conf2}, logger) - test.That(t, err, test.ShouldBeNil) - pc, err = joinedCam2.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldNotBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 3) - - data, got = pc.At(1, 0, 0) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{255, 0, 0, 255}) - - data, got = pc.At(0, 1, 100) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{0, 255, 0, 255}) - - data, got = pc.At(0, 100, 101) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{0, 0, 255, 255}) - - img, _, err = camera.ReadImage(context.Background(), joinedCam2) - test.That(t, err, test.ShouldBeNil) - test.That(t, img.Bounds(), test.ShouldResemble, image.Rect(0, 0, 1, 100)) - test.That(t, joinedCam2.Close(context.Background()), test.ShouldBeNil) -} - -func makePointCloudFromArtifact(t *testing.T, artifactPath string, numPoints int) (pointcloud.PointCloud, error) { - t.Helper() - pcdFile, err := os.Open(artifact.MustPath(artifactPath)) - if err != nil { - return nil, err - } - pc, err := pointcloud.ReadPCD(pcdFile) - if err != nil { - return nil, err - } - - if numPoints == 0 { - return pc, nil - } - - shortenedPC := pointcloud.NewWithPrealloc(numPoints) - - counter := numPoints - pc.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - if counter > 0 { - err = shortenedPC.Set(p, d) - counter-- - } - return err == nil - }) - if err != nil { - return nil, err - } - - return shortenedPC, nil -} - -func makeFakeRobotICP(t *testing.T) resource.Dependencies { - // Makes a fake robot with a fake frame system and multiple cameras for testing. - // Cam 1: Read from a test PCD file. A smaller sample of points. - // Cam 2: A direct transformation applied to Cam 1. - // This is useful for basic checking of the ICP algorithm, as it should converge immediately. - // Cam 3: Read from a test PCD file. Representative of a real pointcloud captured in tandem with Cam 4. - // Cam 4: Read from a test PCD file. Captured in a real environment with a known rough offset from Cam 3. - - // Cam 1 and 2 Are programatically set to have a difference of 100 in the Z direction. - // Cam 3 and 4 Sensors are approximately 33 cm apart with an unknown slight rotation. - t.Helper() - logger := logging.NewTestLogger(t) - cam1 := &inject.Camera{} - startPC, err := makePointCloudFromArtifact(t, "pointcloud/test.pcd", 100) - if err != nil { - t.Fatal(err) - } - cam1.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return startPC, nil - } - cam1.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - cam1.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return nil, transform.NewNoIntrinsicsError("") - } - cam2 := &inject.Camera{} - transformedPC := pointcloud.NewWithPrealloc(100) - transformPose := spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 100}) - counter := 100 - startPC.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - if counter > 0 { - pointPose := spatialmath.NewPoseFromPoint(p) - transPoint := spatialmath.Compose(transformPose, pointPose) - err = transformedPC.Set(transPoint.Point(), d) - if err != nil { - return false - } - counter-- - } - return true - }) - test.That(t, err, test.ShouldBeNil) - cam2.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return transformedPC, nil - } - cam2.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - cam2.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return nil, transform.NewNoIntrinsicsError("") - } - - cam3 := &inject.Camera{} - pc3, err := makePointCloudFromArtifact(t, "pointcloud/bun000.pcd", 0) - if err != nil { - t.Fatal(err) - } - - cam3.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pc3, nil - } - cam3.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - - cam4 := &inject.Camera{} - pc4, err := makePointCloudFromArtifact(t, "pointcloud/bun045.pcd", 0) - if err != nil { - t.Fatal(err) - } - - cam4.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pc4, nil - } - cam4.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - - cam5 := &inject.Camera{} - pc5, err := makePointCloudFromArtifact(t, "pointcloud/bun090.pcd", 0) - if err != nil { - t.Fatal(err) - } - - cam5.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pc5, nil - } - cam5.PropertiesFunc = func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - } - - base1 := &inject.Base{} - - o1 := &spatialmath.EulerAngles{Roll: 0, Pitch: 0.6, Yaw: 0} - o2 := &spatialmath.EulerAngles{Roll: 0, Pitch: 0.6, Yaw: -0.3} - - fsParts := []*referenceframe.FrameSystemPart{ - { - FrameConfig: referenceframe.NewLinkInFrame(referenceframe.World, spatialmath.NewZeroPose(), "base1", nil), - }, - { - FrameConfig: referenceframe.NewLinkInFrame(referenceframe.World, spatialmath.NewZeroPose(), "cam1", nil), - }, - { - FrameConfig: referenceframe.NewLinkInFrame("cam1", spatialmath.NewPoseFromPoint(r3.Vector{0, 0, -100}), "cam2", nil), - }, - { - FrameConfig: referenceframe.NewLinkInFrame(referenceframe.World, spatialmath.NewZeroPose(), "cam3", nil), - }, - { - FrameConfig: referenceframe.NewLinkInFrame( - "cam3", - spatialmath.NewPose(r3.Vector{-60, 0, -10}, o1), - "cam4", - nil, - ), - }, - { - FrameConfig: referenceframe.NewLinkInFrame( - "cam4", - spatialmath.NewPose(r3.Vector{-60, 0, -10}, o2), - "cam5", - nil, - ), - }, - } - - deps := make(resource.Dependencies) - deps[camera.Named("cam1")] = cam1 - deps[camera.Named("cam2")] = cam2 - deps[camera.Named("cam3")] = cam3 - deps[camera.Named("cam4")] = cam4 - deps[camera.Named("cam5")] = cam5 - deps[base.Named("base1")] = base1 - fsSvc, err := framesystem.New(context.Background(), resource.Dependencies{}, logger) - test.That(t, err, test.ShouldBeNil) - err = fsSvc.Reconfigure(context.Background(), deps, resource.Config{ConvertedAttributes: &framesystem.Config{Parts: fsParts}}) - test.That(t, err, test.ShouldBeNil) - deps[framesystem.InternalServiceName] = fsSvc - return deps -} - -func TestFixedPointCloudICP(t *testing.T) { - ctx := context.Background() - deps := makeFakeRobotICP(t) - time.Sleep(500 * time.Millisecond) - // PoV from base1 - conf := &Config{ - Debug: true, - SourceCameras: []string{"cam1", "cam2"}, - TargetFrame: "base1", - MergeMethod: "icp", - Closeness: 0.01, - } - - joinedCam, err := newJoinPointCloudCamera( - context.Background(), deps, resource.Config{ConvertedAttributes: conf}, logging.Global()) - test.That(t, err, test.ShouldBeNil) - defer joinedCam.Close(context.Background()) - pc, err := joinedCam.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 100) -} - -func TestTwinPointCloudICP(t *testing.T) { - t.Skip("Test is too large for now.") - deps := makeFakeRobotICP(t) - time.Sleep(500 * time.Millisecond) - - conf := &Config{ - Debug: true, - SourceCameras: []string{"cam3", "cam4"}, - TargetFrame: "cam3", - MergeMethod: "icp", - } - joinedCam, err := newJoinPointCloudCamera( - context.Background(), deps, resource.Config{ConvertedAttributes: conf}, logging.Global()) - test.That(t, err, test.ShouldBeNil) - defer joinedCam.Close(context.Background()) - pc, err := joinedCam.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - filename := "test_twin_" + time.Now().Format(time.RFC3339) + "*.pcd" - file, err := os.CreateTemp(t.TempDir(), filename) - pointcloud.ToPCD(pc, file, pointcloud.PCDBinary) - - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldNotBeNil) -} - -func TestMultiPointCloudICP(t *testing.T) { - t.Skip("Test is too large for now.") - deps := makeFakeRobotICP(t) - time.Sleep(500 * time.Millisecond) - - conf := &Config{ - Debug: true, - SourceCameras: []string{"cam3", "cam4", "cam5"}, - TargetFrame: "cam3", - MergeMethod: "icp", - } - joinedCam, err := newJoinPointCloudCamera( - context.Background(), deps, resource.Config{ConvertedAttributes: conf}, logging.Global()) - test.That(t, err, test.ShouldBeNil) - defer joinedCam.Close(context.Background()) - pc, err := joinedCam.NextPointCloud(context.Background()) - test.That(t, err, test.ShouldBeNil) - - filename := "test_multi_" + time.Now().Format(time.RFC3339) + "*.pcd" - file, err := os.CreateTemp(t.TempDir(), filename) - pointcloud.ToPCD(pc, file, pointcloud.PCDBinary) - - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldNotBeNil) -} diff --git a/components/camera/videosource/logging/logger.go b/components/camera/videosource/logging/logger.go deleted file mode 100644 index 2978d447826..00000000000 --- a/components/camera/videosource/logging/logger.go +++ /dev/null @@ -1,304 +0,0 @@ -// Package logging is a thread-safe way to log video device information to a file. On startup, this package creates a -// unique filename and uses that filename throughout the lifetime of the program to log information such as which video -// devices are V4L2 compatible and the current operating system. -package logging - -import ( - "context" - "fmt" - "io/fs" - "os" - "os/exec" - "path/filepath" - "reflect" - "runtime" - "strings" - "sync/atomic" - "time" - - "github.com/jedib0t/go-pretty/v6/table" - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" -) - -var ( - // GLoggerCamComp is the global logger-to-file for camera components. - GLoggerCamComp *Logger - filePath string -) - -func init() { - t := time.Now().UTC().Format(time.RFC3339) - filePath = filepath.Join(config.ViamDotDir, "debug", "components", "camera", fmt.Sprintf("%s.txt", t)) - - var err error - if GLoggerCamComp, err = NewLogger(); err != nil { - logging.NewLogger("camera").Info(err) - } -} - -// InfoMap is a map of information to be written to the log. -type InfoMap = map[string]string - -// Logger is a thread-safe logger that manages a single log file in config.ViamDotDir. -type Logger struct { - infoCh chan info - logger logging.Logger - isRunning atomic.Bool - seenPath map[string]bool - seenMap map[string]InfoMap -} - -type info struct { - title string - m InfoMap -} - -const ( - // keep at most 3 log files in dir. - maxFiles = 3 - linux = "linux" -) - -// NewLogger creates a new logger. Call Logger.Start to start logging. -func NewLogger() (*Logger, error) { - // TODO: support non-Linux platforms - if runtime.GOOS != linux { - return nil, errors.Errorf("camera logger not supported on OS %s", runtime.GOOS) - } - - dir := filepath.Dir(filePath) - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return nil, errors.Wrap(err, "camera logger: cannot mkdir "+dir) - } - - // remove enough entries to keep the number of files <= maxFiles - for entries, err := os.ReadDir(dir); len(entries) >= maxFiles; entries, err = os.ReadDir(dir) { - if err != nil { - utils.UncheckedError(errors.Wrap(err, "camera logger: cannot read directory "+dir)) - break - } - - // because entries are sorted by name (timestamp), earlier entries are removed first - if err = os.Remove(filepath.Join(dir, entries[0].Name())); err != nil { - utils.UncheckedError(errors.Wrap(err, "camera logger: cannot remove file "+filepath.Join(dir, entries[0].Name()))) - break - } - } - - cfg := logging.NewZapLoggerConfig() - cfg.OutputPaths = []string{filePath} - - // only keep message - cfg.EncoderConfig.TimeKey = "" - cfg.EncoderConfig.LevelKey = "" - cfg.EncoderConfig.NameKey = "" - cfg.EncoderConfig.CallerKey = "" - cfg.EncoderConfig.StacktraceKey = "" - - logger, err := cfg.Build() - if err != nil { - return nil, err - } - - return &Logger{ - infoCh: make(chan info), - logger: logging.FromZapCompatible(logger.Sugar().Named("camera_debugger")), - }, nil -} - -// Start creates and initializes the logging file and periodically emits logs to it. This method is thread-safe. -func (l *Logger) Start(ctx context.Context) error { - if l == nil { - return nil - } - - if prevVal := l.isRunning.Swap(true); prevVal { - return nil // already running; nothing to do - } - - utils.PanicCapturingGo(func() { - vsourceMetaLogger := logging.Global().Sublogger("videosource") - vsourceMetaLogger.CInfo(ctx, "Starting videosource logger") - defer vsourceMetaLogger.CInfo(ctx, "Terminating videosource logger") - - l.init(ctx) - ticker := time.NewTicker(1 * time.Second) - shouldReset := time.NewTimer(12 * time.Hour) - for { - select { - case <-ctx.Done(): - return - case <-shouldReset.C: - l.init(ctx) - default: - } - - select { - case <-ctx.Done(): - return - case info := <-l.infoCh: - l.write(info.title, info.m) - case <-ticker.C: - l.captureV4L2info(ctx) - } - } - }) - return nil -} - -// Log emits the data stored in the given InfoMap with the given title to the log file. This method is thread-safe. -func (l *Logger) Log(title string, m InfoMap) error { - if l == nil { - return nil - } - - if !l.isRunning.Load() { - return errors.New("must start logger") - } - - l.infoCh <- info{title, m} - return nil -} - -func (l *Logger) captureV4L2info(ctx context.Context) { - v4l2Info := make(InfoMap) - v4l2Compliance := make(InfoMap) - err := filepath.Walk("/dev", func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - - if !strings.HasPrefix(path, "/dev/video") { - return nil - } - if l.seenPath[path] { - return nil - } - - // some devices may not have a symbolic link under /dev/v4l so we log info from all /dev/videoN paths we find. - v4l2Info[path] = runCommand(ctx, "v4l2-ctl", "--device", path, "--all") - v4l2Compliance[path] = runCommand(ctx, "v4l2-compliance", "--device", path) - l.seenPath[path] = true - return nil - }) - l.logError(err, "cannot walk filepath") - - v4l2Path := make(InfoMap) - err = filepath.Walk("/dev/v4l/by-path", func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - - if l.seenPath[path] { - return nil - } - v4l2Path["by-path"] = filepath.Base(path) - l.seenPath[path] = true - return nil - }) - l.logError(err, "cannot walk filepath") - - v4l2ID := make(InfoMap) - err = filepath.Walk("/dev/v4l/by-id", func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - - if l.seenPath[path] { - return nil - } - v4l2ID["by-id"] = filepath.Base(path) - l.seenPath[path] = true - return nil - }) - l.logError(err, "cannot walk filepath") - - // Video capture and overlay devices' minor numbers range from [0,63] - // https://www.kernel.org/doc/html/v4.16/media/uapi/v4l/diff-v4l.html - for n := 0; n < 63; n++ { - path := fmt.Sprintf("/dev/video%d", n) - if _, err := os.Stat(path); os.IsNotExist(err) { - // when this file is re-created we won't know whether it's the same device. Better to assume not. - l.seenPath[path] = false - } - } - - l.write("v4l2 control", v4l2Info) - l.write("v4l2 compliance", v4l2Compliance) - l.write("v4l2 paths", v4l2Path) - l.write("v4l2 ID", v4l2ID) -} - -func (l *Logger) init(ctx context.Context) { - err := os.Truncate(filePath, 0) - l.logError(err, "cannot truncate file") - - l.seenPath = make(map[string]bool) - l.seenMap = make(map[string]InfoMap) - l.write("system information", InfoMap{ - "kernel": runCommand(ctx, "uname", "--kernel-name"), - "machine": runCommand(ctx, "uname", "--machine"), - "processor": runCommand(ctx, "uname", "--processor"), - "platform": runCommand(ctx, "uname", "--hardware-platform"), - "OS": runCommand(ctx, "uname", "--operating-system"), - "model": runCommand(ctx, "cat", "/proc/device-tree/model"), - }) -} - -func runCommand(ctx context.Context, name string, args ...string) string { - //nolint:errcheck - out, _ := exec.CommandContext(ctx, name, args...).CombinedOutput() - return string(out) -} - -func (l *Logger) write(title string, m InfoMap) { - if len(m) == 0 { - return - } - if oldM, ok := l.seenMap[title]; ok && reflect.DeepEqual(oldM, m) { - return // don't log the same info twice - } - - l.seenMap[title] = m - t := table.NewWriter() - t.SetAllowedRowLength(120) - t.SuppressEmptyColumns() - t.SetStyle(table.StyleLight) - t.SetTitle(strings.ToUpper(title)) - - splitLine := func(line string) table.Row { - var row table.Row - for _, ele := range strings.Split(line, ":") { - row = append(row, strings.TrimSpace(ele)) - } - return row - } - - for k, v := range m { - lines := strings.Split(v, "\n") - t.AppendRow(append(table.Row{k}, splitLine(lines[0])...)) - for i := 1; i < len(lines); i++ { - line := lines[i] - if strings.ReplaceAll(line, " ", "") == "" { - continue - } - - t.AppendRow(append(table.Row{""}, splitLine(line)...)) - } - t.AppendSeparator() - } - - t.AppendFooter(table.Row{time.Now().UTC().Format(time.RFC3339)}) - l.logger.Info(t.Render()) - l.logger.Info() -} - -func (l *Logger) logError(err error, msg string) { - if l != nil && err != nil { - l.write("error", InfoMap{msg: err.Error()}) - } -} diff --git a/components/camera/videosource/static.go b/components/camera/videosource/static.go deleted file mode 100644 index 4a7113492d8..00000000000 --- a/components/camera/videosource/static.go +++ /dev/null @@ -1,231 +0,0 @@ -//go:build !no_cgo - -package videosource - -import ( - "context" - "fmt" - "image" - "time" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/depthadapter" - "go.viam.com/rdk/rimage/transform" -) - -var fileModel = resource.DefaultModelFamily.WithModel("image_file") - -func init() { - resource.RegisterComponent(camera.API, fileModel, - resource.Registration[camera.Camera, *fileSourceConfig]{ - Constructor: func(ctx context.Context, _ resource.Dependencies, - conf resource.Config, logger logging.Logger, - ) (camera.Camera, error) { - newConf, err := resource.NativeConfig[*fileSourceConfig](conf) - if err != nil { - return nil, err - } - return newCamera(context.Background(), conf.ResourceName(), newConf, logger) - }, - }) -} - -func newCamera(ctx context.Context, name resource.Name, newConf *fileSourceConfig, logger logging.Logger) (camera.Camera, error) { - videoSrc := &fileSource{newConf.Color, newConf.Depth, newConf.PointCloud, newConf.CameraParameters} - imgType := camera.ColorStream - if newConf.Color == "" { - imgType = camera.DepthStream - } - cameraModel := camera.NewPinholeModelWithBrownConradyDistortion(newConf.CameraParameters, newConf.DistortionParameters) - src, err := camera.NewVideoSourceFromReader( - ctx, - videoSrc, - &cameraModel, - imgType, - ) - if err != nil { - return nil, err - } - return camera.FromVideoSource(name, src, logger), nil -} - -// fileSource stores the paths to a color and depth image and a pointcloud. -type fileSource struct { - ColorFN string - DepthFN string - PointCloudFN string - Intrinsics *transform.PinholeCameraIntrinsics -} - -// fileSourceConfig is the attribute struct for fileSource. -type fileSourceConfig struct { - CameraParameters *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters,omitempty"` - DistortionParameters *transform.BrownConrady `json:"distortion_parameters,omitempty"` - Debug bool `json:"debug,omitempty"` - Color string `json:"color_image_file_path,omitempty"` - Depth string `json:"depth_image_file_path,omitempty"` - PointCloud string `json:"pointcloud_file_path,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (c fileSourceConfig) Validate(path string) ([]string, error) { - if c.CameraParameters != nil { - if c.CameraParameters.Width < 0 || c.CameraParameters.Height < 0 { - return nil, fmt.Errorf( - "got illegal negative dimensions for width_px and height_px (%d, %d) fields set in intrinsic_parameters for image_file camera", - c.CameraParameters.Height, c.CameraParameters.Width) - } - } - - return []string{}, nil -} - -// Read returns just the RGB image if it is present, or the depth map if the RGB image is not present. -func (fs *fileSource) Read(ctx context.Context) (image.Image, func(), error) { - if fs.ColorFN == "" && fs.DepthFN == "" { - return nil, nil, errors.New("no image file to read, so not implemented") - } - if fs.ColorFN == "" { // only depth info - img, err := rimage.NewDepthMapFromFile(context.Background(), fs.DepthFN) - if err != nil { - return nil, nil, err - } - return img, func() {}, err - } - - img, err := rimage.NewImageFromFile(fs.ColorFN) - if err != nil { - return nil, nil, err - } - - // x264 only supports even resolutions. Not every call to this function will - // be in the context of an x264 stream, but we crop every image to even - // dimensions anyways. - oddWidth := img.Bounds().Dx()%2 != 0 - oddHeight := img.Bounds().Dy()%2 != 0 - if oddWidth || oddHeight { - newWidth := img.Bounds().Dx() - newHeight := img.Bounds().Dy() - if oddWidth { - newWidth-- - } - if oddHeight { - newHeight-- - } - img = img.SubImage(image.Rect(0, 0, newWidth, newHeight)) - } - return img, func() {}, err -} - -// Images returns the saved color and depth image if they are present. -func (fs *fileSource) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - if fs.ColorFN == "" && fs.DepthFN == "" { - return nil, resource.ResponseMetadata{}, errors.New("no image file to read, so not implemented") - } - imgs := []camera.NamedImage{} - if fs.ColorFN != "" { - img, err := rimage.NewImageFromFile(fs.ColorFN) - if err != nil { - return nil, resource.ResponseMetadata{}, err - } - imgs = append(imgs, camera.NamedImage{img, "color"}) - } - if fs.DepthFN != "" { - dm, err := rimage.NewDepthMapFromFile(context.Background(), fs.DepthFN) - if err != nil { - return nil, resource.ResponseMetadata{}, err - } - imgs = append(imgs, camera.NamedImage{dm, "depth"}) - } - ts := time.Now() - return imgs, resource.ResponseMetadata{CapturedAt: ts}, nil -} - -// NextPointCloud returns the point cloud from projecting the rgb and depth image using the intrinsic parameters, -// or the pointcloud from file if set. -func (fs *fileSource) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - if fs.PointCloudFN != "" { - return pointcloud.NewFromFile(fs.PointCloudFN, nil) - } - if fs.Intrinsics == nil { - return nil, transform.NewNoIntrinsicsError("camera intrinsics not found in config") - } - if fs.ColorFN == "" { // only depth info - img, err := rimage.NewDepthMapFromFile(context.Background(), fs.DepthFN) - if err != nil { - return nil, err - } - return depthadapter.ToPointCloud(img, fs.Intrinsics), nil - } - img, err := rimage.NewImageFromFile(fs.ColorFN) - if err != nil { - return nil, err - } - dm, err := rimage.NewDepthMapFromFile(context.Background(), fs.DepthFN) - if err != nil { - return nil, err - } - return fs.Intrinsics.RGBDToPointCloud(img, dm) -} - -func (fs *fileSource) Close(ctx context.Context) error { - return nil -} - -// StaticSource is a fixed, stored image. Used primarily for testing. -type StaticSource struct { - ColorImg image.Image - DepthImg image.Image - Proj transform.Projector -} - -// Read returns the stored image. -func (ss *StaticSource) Read(ctx context.Context) (image.Image, func(), error) { - if ss.ColorImg != nil { - return ss.ColorImg, func() {}, nil - } - return ss.DepthImg, func() {}, nil -} - -// Images returns the saved color and depth image if they are present. -func (ss *StaticSource) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - if ss.ColorImg == nil && ss.DepthImg == nil { - return nil, resource.ResponseMetadata{}, errors.New("no image files stored, so not implemented") - } - imgs := []camera.NamedImage{} - if ss.ColorImg != nil { - imgs = append(imgs, camera.NamedImage{ss.ColorImg, "color"}) - } - if ss.DepthImg != nil { - imgs = append(imgs, camera.NamedImage{ss.DepthImg, "depth"}) - } - ts := time.Now() - return imgs, resource.ResponseMetadata{CapturedAt: ts}, nil -} - -// NextPointCloud returns the point cloud from projecting the rgb and depth image using the intrinsic parameters. -func (ss *StaticSource) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - if ss.Proj == nil { - return nil, transform.NewNoIntrinsicsError("camera intrinsics not found in config") - } - if ss.DepthImg == nil { - return nil, errors.New("no depth info to project to pointcloud") - } - col := rimage.ConvertImage(ss.ColorImg) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), ss.DepthImg) - if err != nil { - return nil, err - } - return ss.Proj.RGBDToPointCloud(col, dm) -} - -// Close does nothing. -func (ss *StaticSource) Close(ctx context.Context) error { - return nil -} diff --git a/components/camera/videosource/static_test.go b/components/camera/videosource/static_test.go deleted file mode 100644 index 2c43c17cfa9..00000000000 --- a/components/camera/videosource/static_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package videosource - -import ( - "context" - "image" - "image/color" - "image/jpeg" - "os" - "path/filepath" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" -) - -func TestPCD(t *testing.T) { - pcdPath := filepath.Clean(artifact.MustPath("pointcloud/octagonspace.pcd")) - cfg := &fileSourceConfig{PointCloud: pcdPath} - ctx := context.Background() - logger := logging.NewTestLogger(t) - cam, err := newCamera(ctx, resource.Name{API: camera.API}, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - _, err = cam.Stream(ctx) - test.That(t, err, test.ShouldBeNil) - - pc, err := cam.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 628) - - pc, err = cam.NextPointCloud(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 628) - - err = cam.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - colorImgPath := artifact.MustPath("vision/objectdetection/detection_test.jpg") - cfg.Color = colorImgPath - cam, err = newCamera(ctx, resource.Name{API: camera.API}, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - stream, err := cam.Stream(ctx) - test.That(t, err, test.ShouldBeNil) - - readInImage, err := rimage.NewImageFromFile(artifact.MustPath("vision/objectdetection/detection_test.jpg")) - test.That(t, err, test.ShouldBeNil) - - strmImg, _, err := stream.Next(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, strmImg, test.ShouldResemble, readInImage) - test.That(t, strmImg.Bounds(), test.ShouldResemble, readInImage.Bounds()) - - err = cam.Close(ctx) - test.That(t, err, test.ShouldBeNil) -} - -func TestColor(t *testing.T) { - colorImgPath := artifact.MustPath("vision/objectdetection/detection_test.jpg") - cfg := &fileSourceConfig{Color: colorImgPath} - ctx := context.Background() - logger := logging.NewTestLogger(t) - cam, err := newCamera(ctx, resource.Name{API: camera.API}, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - stream, err := cam.Stream(ctx) - test.That(t, err, test.ShouldBeNil) - - _, err = cam.NextPointCloud(ctx) - test.That(t, err, test.ShouldNotBeNil) - - readInImage, err := rimage.NewImageFromFile(artifact.MustPath("vision/objectdetection/detection_test.jpg")) - test.That(t, err, test.ShouldBeNil) - - strmImg, _, err := stream.Next(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, strmImg, test.ShouldResemble, readInImage) - test.That(t, strmImg.Bounds(), test.ShouldResemble, readInImage.Bounds()) - - err = cam.Close(ctx) - test.That(t, err, test.ShouldBeNil) -} - -func TestColorOddResolution(t *testing.T) { - imgFilePath := t.TempDir() + "/test_img.jpg" - imgFile, err := os.Create(imgFilePath) - test.That(t, err, test.ShouldBeNil) - - img := image.NewRGBA(image.Rect(0, 0, 3, 3)) - for x := 0; x < img.Bounds().Dx(); x++ { - for y := 0; y < img.Bounds().Dy(); y++ { - img.Set(x, y, color.White) - } - } - err = jpeg.Encode(imgFile, img, nil) - test.That(t, err, test.ShouldBeNil) - err = imgFile.Close() - test.That(t, err, test.ShouldBeNil) - - cfg := &fileSourceConfig{Color: imgFilePath} - ctx := context.Background() - logger := logging.NewTestLogger(t) - cam, err := newCamera(ctx, resource.Name{API: camera.API}, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - stream, err := cam.Stream(ctx) - test.That(t, err, test.ShouldBeNil) - - readInImage, err := rimage.NewImageFromFile(imgFilePath) - test.That(t, err, test.ShouldBeNil) - - strmImg, _, err := stream.Next(ctx) - test.That(t, err, test.ShouldBeNil) - - expectedBounds := image.Rect(0, 0, readInImage.Bounds().Dx()-1, readInImage.Bounds().Dy()-1) - test.That(t, strmImg, test.ShouldResemble, readInImage.SubImage(expectedBounds)) - test.That(t, strmImg.Bounds(), test.ShouldResemble, expectedBounds) - - err = cam.Close(ctx) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/camera/videosource/verify_main_test.go b/components/camera/videosource/verify_main_test.go deleted file mode 100644 index 8ba082d34ca..00000000000 --- a/components/camera/videosource/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package videosource - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/camera/videosource/webcam.go b/components/camera/videosource/webcam.go deleted file mode 100644 index f4ce78e87f3..00000000000 --- a/components/camera/videosource/webcam.go +++ /dev/null @@ -1,738 +0,0 @@ -// Package videosource implements various camera models including webcam -package videosource - -import ( - "context" - _ "embed" - "encoding/json" - "fmt" - "image" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/pion/mediadevices" - "github.com/pion/mediadevices/pkg/driver" - "github.com/pion/mediadevices/pkg/driver/availability" - mediadevicescamera "github.com/pion/mediadevices/pkg/driver/camera" - "github.com/pion/mediadevices/pkg/frame" - "github.com/pion/mediadevices/pkg/prop" - "github.com/pkg/errors" - "go.uber.org/multierr" - pb "go.viam.com/api/component/camera/v1" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/camera" - jetsoncamera "go.viam.com/rdk/components/camera/platforms/jetson" - debugLogger "go.viam.com/rdk/components/camera/videosource/logging" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" -) - -// ModelWebcam is the name of the webcam component. -var ModelWebcam = resource.DefaultModelFamily.WithModel("webcam") - -//go:embed data/intrinsics.json -var intrinsics []byte - -var data map[string]transform.PinholeCameraIntrinsics - -func init() { - resource.RegisterComponent( - camera.API, - ModelWebcam, - resource.Registration[camera.Camera, *WebcamConfig]{ - Constructor: NewWebcam, - Discover: func(ctx context.Context, logger logging.Logger) (interface{}, error) { - return Discover(ctx, getVideoDrivers, logger) - }, - }) - if err := json.Unmarshal(intrinsics, &data); err != nil { - logging.Global().Errorw("cannot parse intrinsics json", "error", err) - } -} - -func getVideoDrivers() []driver.Driver { - return driver.GetManager().Query(driver.FilterVideoRecorder()) -} - -// CameraConfig is collection of configuration options for a camera. -type CameraConfig struct { - Label string - Status driver.State - Properties []prop.Media -} - -// Discover webcam attributes. -func Discover(ctx context.Context, getDrivers func() []driver.Driver, logger logging.Logger) (*pb.Webcams, error) { - mediadevicescamera.Initialize() - var webcams []*pb.Webcam - drivers := getDrivers() - for _, d := range drivers { - driverInfo := d.Info() - - props, err := getProperties(d) - if len(props) == 0 { - logger.CDebugw(ctx, "no properties detected for driver, skipping discovery...", "driver", driverInfo.Label) - continue - } else if err != nil { - logger.CDebugw(ctx, "cannot access driver properties, skipping discovery...", "driver", driverInfo.Label, "error", err) - continue - } - - if d.Status() == driver.StateRunning { - logger.CDebugw(ctx, "driver is in use, skipping discovery...", "driver", driverInfo.Label) - continue - } - - labelParts := strings.Split(driverInfo.Label, mediadevicescamera.LabelSeparator) - label := labelParts[0] - - name, id := func() (string, string) { - nameParts := strings.Split(driverInfo.Name, mediadevicescamera.LabelSeparator) - if len(nameParts) > 1 { - return nameParts[0], nameParts[1] - } - // fallback to the label if the name does not have an any additional parts to use. - return nameParts[0], label - }() - - wc := &pb.Webcam{ - Name: name, - Id: id, - Label: label, - Status: string(d.Status()), - Properties: make([]*pb.Property, 0, len(d.Properties())), - } - - for _, prop := range props { - pbProp := &pb.Property{ - WidthPx: int32(prop.Video.Width), - HeightPx: int32(prop.Video.Height), - FrameRate: prop.Video.FrameRate, - FrameFormat: string(prop.Video.FrameFormat), - } - wc.Properties = append(wc.Properties, pbProp) - } - webcams = append(webcams, wc) - } - - if err := debugLogger.GLoggerCamComp.Log("discovery service", webcamsToMap(webcams)); err != nil { - logger.Debug(err) - } - return &pb.Webcams{Webcams: webcams}, nil -} - -func webcamsToMap(webcams []*pb.Webcam) debugLogger.InfoMap { - info := make(debugLogger.InfoMap) - for _, w := range webcams { - k := w.Name - v := fmt.Sprintf("ID: %s\n", w.Id) - v += fmt.Sprintf("Status: %s\n", w.Status) - v += fmt.Sprintf("Label: %s\n", w.Label) - v += "Properties:" - for _, p := range w.Properties { - v += fmt.Sprintf(" :%s=%-4d | %s=%-4d | %s=%-5s | %s=%-4.2f\n", - "width_px", p.GetWidthPx(), - "height_px", p.GetHeightPx(), - "frame_format", p.GetFrameFormat(), - "frame_rate", p.GetFrameRate(), - ) - } - info[k] = v - } - return info -} - -func getProperties(d driver.Driver) (_ []prop.Media, err error) { - // Need to open driver to get properties - if d.Status() == driver.StateClosed { - errOpen := d.Open() - if errOpen != nil { - return nil, errOpen - } - defer func() { - if errClose := d.Close(); errClose != nil { - err = errClose - } - }() - } - return d.Properties(), err -} - -// WebcamConfig is the attribute struct for webcams. -type WebcamConfig struct { - CameraParameters *transform.PinholeCameraIntrinsics `json:"intrinsic_parameters,omitempty"` - DistortionParameters *transform.BrownConrady `json:"distortion_parameters,omitempty"` - Debug bool `json:"debug,omitempty"` - Format string `json:"format,omitempty"` - Path string `json:"video_path"` - Width int `json:"width_px,omitempty"` - Height int `json:"height_px,omitempty"` - FrameRate float32 `json:"frame_rate,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (c WebcamConfig) Validate(path string) ([]string, error) { - if c.Width < 0 || c.Height < 0 { - return nil, fmt.Errorf( - "got illegal negative dimensions for width_px and height_px (%d, %d) fields set for webcam camera", - c.Height, c.Width) - } - - return []string{}, nil -} - -func (c WebcamConfig) needsDriverReinit(other WebcamConfig) bool { - return !(c.Format == other.Format && - c.Path == other.Path && - c.Width == other.Width && - c.Height == other.Height) -} - -func makeConstraints(conf *WebcamConfig, debug bool, logger logging.Logger) mediadevices.MediaStreamConstraints { - return mediadevices.MediaStreamConstraints{ - Video: func(constraint *mediadevices.MediaTrackConstraints) { - if conf.Width > 0 { - constraint.Width = prop.IntExact(conf.Width) - } else { - constraint.Width = prop.IntRanged{Min: 0, Ideal: 640, Max: 4096} - } - - if conf.Height > 0 { - constraint.Height = prop.IntExact(conf.Height) - } else { - constraint.Height = prop.IntRanged{Min: 0, Ideal: 480, Max: 2160} - } - - if conf.FrameRate > 0.0 { - constraint.FrameRate = prop.FloatExact(conf.FrameRate) - } else { - constraint.FrameRate = prop.FloatRanged{Min: 0.0, Ideal: 30.0, Max: 140.0} - } - - if conf.Format == "" { - constraint.FrameFormat = prop.FrameFormatOneOf{ - frame.FormatI420, - frame.FormatI444, - frame.FormatYUY2, - frame.FormatUYVY, - frame.FormatRGBA, - frame.FormatMJPEG, - frame.FormatNV12, - frame.FormatNV21, - frame.FormatZ16, - } - } else { - constraint.FrameFormat = prop.FrameFormatExact(conf.Format) - } - - if debug { - logger.Debugf("constraints: %v", constraint) - } - }, - } -} - -// findAndMakeVideoSource finds a video device and returns a video source with that video device as the source. -func findAndMakeVideoSource( - ctx context.Context, - conf *WebcamConfig, - label string, - logger logging.Logger, -) (gostream.VideoSource, string, error) { - mediadevicescamera.Initialize() - debug := conf.Debug - constraints := makeConstraints(conf, debug, logger) - if label != "" { - cam, err := tryWebcamOpen(ctx, conf, label, false, constraints, logger) - if err != nil { - return nil, "", errors.Wrap(err, "cannot open webcam") - } - return cam, label, nil - } - - source, err := gostream.GetAnyVideoSource(constraints, logger.AsZap()) - if err != nil { - return nil, "", errors.Wrap(err, "found no webcams") - } - - if label == "" { - label = getLabelFromVideoSource(source, logger) - } - - return source, label, nil -} - -// getLabelFromVideoSource returns the path from the camera or an empty string if a path is not found. -func getLabelFromVideoSource(src gostream.VideoSource, logger logging.Logger) string { - labels, err := gostream.LabelsFromMediaSource[image.Image, prop.Video](src) - if err != nil || len(labels) == 0 { - logger.Errorw("could not get labels from media source", "error", err) - return "" - } - - return labels[0] -} - -// NewWebcam returns a new source based on a webcam discovered from the given config. -func NewWebcam( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (camera.Camera, error) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cam := &monitoredWebcam{ - Named: conf.ResourceName().AsNamed(), - logger: logging.FromZapCompatible(logger.With("camera_name", conf.ResourceName().ShortName())), - originalLogger: logger, - cancelCtx: cancelCtx, - cancel: cancel, - } - if err := cam.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - cam.Monitor() - - s, err := cam.Stream(ctx) - if err != nil { - if err := debugLogger.GLoggerCamComp.Log("camera test results", - debugLogger.InfoMap{ - "name": cam.Name().Name, - "error": fmt.Sprint(err), - }, - ); err != nil { - logger.Debug(err) - } - return cam, nil - } - - img, _, err := s.Next(ctx) - if err := debugLogger.GLoggerCamComp.Log("camera test results", - debugLogger.InfoMap{ - "camera name": cam.Name().Name, - "has non-nil image?": fmt.Sprintf("%t", img != nil), - "error:": fmt.Sprintf("%s", err), - }, - ); err != nil { - logger.Debug(err) - } - - return cam, nil -} - -type noopCloser struct { - gostream.VideoSource -} - -func (n *noopCloser) Close(ctx context.Context) error { - return nil -} - -func (c *monitoredWebcam) Reconfigure( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, -) error { - newConf, err := resource.NativeConfig[*WebcamConfig](conf) - if err != nil { - return err - } - - c.mu.Lock() - defer c.mu.Unlock() - - cameraModel := camera.NewPinholeModelWithBrownConradyDistortion(newConf.CameraParameters, newConf.DistortionParameters) - projector, err := camera.WrapVideoSourceWithProjector( - ctx, - &noopCloser{c}, - &cameraModel, - camera.ColorStream, - ) - if err != nil { - return err - } - - needDriverReinit := c.conf.needsDriverReinit(*newConf) - if c.exposedProjector != nil { - goutils.UncheckedError(c.exposedProjector.Close(ctx)) - } - c.exposedProjector = projector - - if c.underlyingSource != nil && !needDriverReinit { - c.conf = *newConf - return nil - } - c.logger.CDebug(ctx, "reinitializing driver") - - c.targetPath = newConf.Path - if err := c.reconnectCamera(newConf); err != nil { - return err - } - - c.hasLoggedIntrinsicsInfo = false - - // only set once we're good - c.conf = *newConf - return nil -} - -// tryWebcamOpen uses getNamedVideoSource to try and find a video device (gostream.MediaSource). -// If successful, it will wrap that MediaSource in a camera. -func tryWebcamOpen( - ctx context.Context, - conf *WebcamConfig, - path string, - fromLabel bool, - constraints mediadevices.MediaStreamConstraints, - logger logging.Logger, -) (gostream.VideoSource, error) { - source, err := getNamedVideoSource(path, fromLabel, constraints, logger) - if err != nil { - return nil, err - } - - if conf.Width != 0 && conf.Height != 0 { - img, release, err := gostream.ReadMedia(ctx, source) - if release != nil { - defer release() - } - if err != nil { - return nil, err - } - if img.Bounds().Dx() != conf.Width || img.Bounds().Dy() != conf.Height { - return nil, errors.Errorf("requested width and height (%dx%d) are not available for this webcam"+ - " (closest driver found by gostream supports resolution %dx%d)", - conf.Width, conf.Height, img.Bounds().Dx(), img.Bounds().Dy()) - } - } - return source, nil -} - -// getNamedVideoSource attempts to find a video device (not a screen) by the given name. -// First it will try to use the path name after evaluating any symbolic links. If -// evaluation fails, it will try to use the path name as provided. -func getNamedVideoSource( - path string, - fromLabel bool, - constraints mediadevices.MediaStreamConstraints, - logger logging.Logger, -) (gostream.MediaSource[image.Image], error) { - if !fromLabel { - resolvedPath, err := filepath.EvalSymlinks(path) - if err == nil { - path = resolvedPath - } - } - return gostream.GetNamedVideoSource(filepath.Base(path), constraints, logger.AsZap()) -} - -// monitoredWebcam tries to ensure its underlying camera stays connected. -type monitoredWebcam struct { - resource.Named - mu sync.RWMutex - hasLoggedIntrinsicsInfo bool - - underlyingSource gostream.VideoSource - exposedSwapper gostream.HotSwappableVideoSource - exposedProjector camera.VideoSource - - // this is returned to us as a label in mediadevices but our config - // treats it as a video path. - targetPath string - conf WebcamConfig - - cancelCtx context.Context - cancel func() - closed bool - disconnected bool - activeBackgroundWorkers sync.WaitGroup - logger logging.Logger - originalLogger logging.Logger -} - -func (c *monitoredWebcam) MediaProperties(ctx context.Context) (prop.Video, error) { - c.mu.RLock() - defer c.mu.RUnlock() - if c.underlyingSource == nil { - return prop.Video{}, errors.New("no configured camera") - } - if provider, ok := c.underlyingSource.(gostream.VideoPropertyProvider); ok { - return provider.MediaProperties(ctx) - } - return prop.Video{}, nil -} - -func (c *monitoredWebcam) isCameraConnected() (bool, error) { - c.mu.RLock() - defer c.mu.RUnlock() - if c.underlyingSource == nil { - return true, errors.New("no configured camera") - } - d, err := gostream.DriverFromMediaSource[image.Image, prop.Video](c.underlyingSource) - if err != nil { - return true, errors.Wrap(err, "cannot get driver from media source") - } - - // TODO(RSDK-1959): this only works for linux - _, err = driver.IsAvailable(d) - return !errors.Is(err, availability.ErrNoDevice), nil -} - -// reconnectCamera assumes a write lock is held. -func (c *monitoredWebcam) reconnectCamera(conf *WebcamConfig) error { - if c.underlyingSource != nil { - c.logger.Debug("closing current camera") - if err := c.underlyingSource.Close(c.cancelCtx); err != nil { - c.logger.Errorw("failed to close currents camera", "error", err) - } - c.underlyingSource = nil - } - - newSrc, foundLabel, err := findAndMakeVideoSource(c.cancelCtx, conf, c.targetPath, c.logger) - if err != nil { - // If we are on a Jetson Orin AGX, we need to validate hardware/software setup. - // If not, simply pass through the error. - err = jetsoncamera.ValidateSetup( - jetsoncamera.OrinAGX, - jetsoncamera.ECAM, - jetsoncamera.AR0234, - err, - ) - return errors.Wrap(err, "failed to find camera") - } - - if c.exposedSwapper == nil { - c.exposedSwapper = gostream.NewHotSwappableVideoSource(newSrc) - } else { - c.exposedSwapper.Swap(newSrc) - } - c.underlyingSource = newSrc - c.disconnected = false - c.closed = false - if c.targetPath == "" { - c.targetPath = foundLabel - } - c.logger = logging.FromZapCompatible(c.originalLogger.With("camera_label", c.targetPath)) - - return nil -} - -// Monitor is responsible for monitoring the liveness of a camera. An example -// is connectivity. Since the model itself knows best about how to maintain this state, -// the reconfigurable offers a safe way to notify if a state needs to be reset due -// to some exceptional event (like a reconnect). -// It is expected that the monitoring code is tied to the lifetime of the resource -// and once the resource is closed, so should the monitor. That is, it should -// no longer send any resets once a Close on its associated resource has returned. -func (c *monitoredWebcam) Monitor() { - const wait = 500 * time.Millisecond - c.activeBackgroundWorkers.Add(1) - - goutils.ManagedGo(func() { - for { - if !goutils.SelectContextOrWait(c.cancelCtx, wait) { - return - } - - c.mu.RLock() - logger := c.logger - c.mu.RUnlock() - - ok, err := c.isCameraConnected() - if err != nil { - logger.Debugw("cannot determine camera status", "error", err) - continue - } - - if !ok { - c.mu.Lock() - c.disconnected = true - c.mu.Unlock() - - logger.Error("camera no longer connected; reconnecting") - for { - if !goutils.SelectContextOrWait(c.cancelCtx, wait) { - return - } - cont := func() bool { - c.mu.Lock() - defer c.mu.Unlock() - - if err := c.reconnectCamera(&c.conf); err != nil { - c.logger.Errorw("failed to reconnect camera", "error", err) - return true - } - c.logger.Infow("camera reconnected") - return false - }() - if cont { - continue - } - break - } - } - } - }, c.activeBackgroundWorkers.Done) -} - -func (c *monitoredWebcam) Projector(ctx context.Context) (transform.Projector, error) { - c.mu.RLock() - defer c.mu.RUnlock() - if err := c.ensureActive(); err != nil { - return nil, err - } - return c.exposedProjector.Projector(ctx) -} - -func (c *monitoredWebcam) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - if c, ok := c.underlyingSource.(camera.ImagesSource); ok { - return c.Images(ctx) - } - img, release, err := camera.ReadImage(ctx, c.underlyingSource) - if err != nil { - return nil, resource.ResponseMetadata{}, errors.Wrap(err, "monitoredWebcam: call to get Images failed") - } - defer func() { - if release != nil { - release() - } - }() - return []camera.NamedImage{{img, c.Name().Name}}, resource.ResponseMetadata{time.Now()}, nil -} - -func (c *monitoredWebcam) Stream(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - c.mu.RLock() - defer c.mu.RUnlock() - if err := c.ensureActive(); err != nil { - return nil, err - } - return c.exposedSwapper.Stream(ctx, errHandlers...) -} - -func (c *monitoredWebcam) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - c.mu.RLock() - defer c.mu.RUnlock() - if err := c.ensureActive(); err != nil { - return nil, err - } - return c.exposedProjector.NextPointCloud(ctx) -} - -func (c *monitoredWebcam) driverInfo() (driver.Info, error) { - c.mu.RLock() - defer c.mu.RUnlock() - if c.underlyingSource == nil { - return driver.Info{}, errors.New("no underlying source found in camera") - } - d, err := gostream.DriverFromMediaSource[image.Image, prop.Video](c.underlyingSource) - if err != nil { - return driver.Info{}, errors.Wrap(err, "cannot get driver from media source") - } - return d.Info(), nil -} - -func (c *monitoredWebcam) Properties(ctx context.Context) (camera.Properties, error) { - c.mu.RLock() - defer c.mu.RUnlock() - if err := c.ensureActive(); err != nil { - return camera.Properties{}, err - } - - props, err := c.exposedProjector.Properties(ctx) - if err != nil { - return camera.Properties{}, err - } - // Looking for intrinsics in map built using viam camera - // calibration here https://github.com/viam-labs/camera-calibration/tree/main - if props.IntrinsicParams == nil { - dInfo, err := c.driverInfo() - if err != nil { - if !c.hasLoggedIntrinsicsInfo { - c.logger.CErrorw(ctx, "can't find driver info for camera") - c.hasLoggedIntrinsicsInfo = true - } - } - - cameraIntrinsics, exists := data[dInfo.Name] - if !exists { - if !c.hasLoggedIntrinsicsInfo { - c.logger.CInfo(ctx, "camera model not found in known camera models for: ", dInfo.Name, ". returning "+ - "properties without intrinsics") - c.hasLoggedIntrinsicsInfo = true - } - return props, nil - } - if c.conf.Width != 0 { - if c.conf.Width != cameraIntrinsics.Width { - if !c.hasLoggedIntrinsicsInfo { - c.logger.CInfo(ctx, "camera model found in known camera models for: ", dInfo.Name, " but "+ - "intrinsics width doesn't match configured image width") - c.hasLoggedIntrinsicsInfo = true - } - return props, nil - } - } - if c.conf.Height != 0 { - if c.conf.Height != cameraIntrinsics.Height { - if !c.hasLoggedIntrinsicsInfo { - c.logger.CInfo(ctx, "camera model found in known camera models for: ", dInfo.Name, " but "+ - "intrinsics height doesn't match configured image height") - c.hasLoggedIntrinsicsInfo = true - } - return props, nil - } - } - if !c.hasLoggedIntrinsicsInfo { - c.logger.CInfo(ctx, "Intrinsics are known for camera model: ", dInfo.Name, ". adding intrinsics "+ - "to camera properties") - c.hasLoggedIntrinsicsInfo = true - } - props.IntrinsicParams = &cameraIntrinsics - } - return props, nil -} - -var ( - errClosed = errors.New("camera has been closed") - errDisconnected = errors.New("camera is disconnected; please try again in a few moments") -) - -func (c *monitoredWebcam) ensureActive() error { - if c.closed { - return errClosed - } - if c.disconnected { - return errDisconnected - } - return nil -} - -func (c *monitoredWebcam) Close(ctx context.Context) error { - c.mu.Lock() - if c.closed { - c.mu.Unlock() - return errors.New("webcam already closed") - } - c.closed = true - c.mu.Unlock() - c.cancel() - c.activeBackgroundWorkers.Wait() - - var err error - if c.exposedSwapper != nil { - err = multierr.Combine(err, c.exposedSwapper.Close(ctx)) - } - if c.exposedProjector != nil { - err = multierr.Combine(err, c.exposedProjector.Close(ctx)) - } - if c.underlyingSource != nil { - err = multierr.Combine(err, c.underlyingSource.Close(ctx)) - } - return err -} diff --git a/components/camera/videosource/webcam_e2e_test.go b/components/camera/videosource/webcam_e2e_test.go deleted file mode 100644 index 7e5537a1c11..00000000000 --- a/components/camera/videosource/webcam_e2e_test.go +++ /dev/null @@ -1,122 +0,0 @@ -//go:build linux && vcamera - -package videosource_test - -import ( - "context" - "testing" - - pb "go.viam.com/api/component/camera/v1" - "go.viam.com/rdk/logging" - "go.viam.com/test" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/vcamera" -) - -func findWebcam(t *testing.T, webcams []*pb.Webcam, name string) *pb.Webcam { - t.Helper() - for _, w := range webcams { - if w.Name == name { - return w - } - } - t.Fatalf("could not find webcam %s", name) - return nil -} - -func TestWebcamDiscovery(t *testing.T) { - logger := logging.NewTestLogger(t) - - reg, ok := resource.LookupRegistration(camera.API, videosource.ModelWebcam) - test.That(t, ok, test.ShouldBeTrue) - - ctx := context.Background() - discoveries, err := reg.Discover(ctx, logger) - test.That(t, err, test.ShouldBeNil) - - webcams, ok := discoveries.(*pb.Webcams) - test.That(t, ok, test.ShouldBeTrue) - webcamsLen := len(webcams.Webcams) - - // Video capture and overlay minor numbers range == [0, 63] - // Start from the end of the range to avoid conflicts with other devices - // Source: https://www.kernel.org/doc/html/v4.9/media/uapi/v4l/diff-v4l.html - config, err := vcamera.Builder(logger). - NewCamera(62, "Lo Res Webcam", vcamera.Resolution{Width: 640, Height: 480}). - NewCamera(63, "Hi Res Webcam", vcamera.Resolution{Width: 1280, Height: 720}). - Stream() - - test.That(t, err, test.ShouldBeNil) - defer config.Shutdown() - - discoveries, err = reg.Discover(ctx, logger) - test.That(t, err, test.ShouldBeNil) - - webcams, ok = discoveries.(*pb.Webcams) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, len(webcams.Webcams), test.ShouldEqual, webcamsLen+2) - - webcam := findWebcam(t, webcams.Webcams, "Hi Res Webcam") - test.That(t, webcam.Properties[0].WidthPx, test.ShouldEqual, 1280) - test.That(t, webcam.Properties[0].HeightPx, test.ShouldEqual, 720) - - webcam = findWebcam(t, webcams.Webcams, "Lo Res Webcam") - test.That(t, webcam.Properties[0].WidthPx, test.ShouldEqual, 640) - test.That(t, webcam.Properties[0].HeightPx, test.ShouldEqual, 480) -} - -func newWebcamConfig(name, path string) resource.Config { - conf := resource.NewEmptyConfig( - resource.NewName(camera.API, name), - resource.DefaultModelFamily.WithModel("webcam"), - ) - conf.ConvertedAttributes = &videosource.WebcamConfig{Path: path} - return conf -} - -func TestWebcamGetImage(t *testing.T) { - logger := logging.NewTestLogger(t) - config, err := vcamera.Builder(logger). - NewCamera(62, "Lo Res Webcam", vcamera.Resolution{Width: 640, Height: 480}). - NewCamera(63, "Hi Res Webcam", vcamera.Resolution{Width: 1280, Height: 720}). - Stream() - - test.That(t, err, test.ShouldBeNil) - defer config.Shutdown() - - cancelCtx, cancelFn := context.WithCancel(context.Background()) - defer cancelFn() - - conf := newWebcamConfig("cam1", "video62") - cam1, err := videosource.NewWebcam(cancelCtx, nil, conf, logger) - test.That(t, err, test.ShouldBeNil) - defer cam1.Close(cancelCtx) - - stream, err := cam1.Stream(cancelCtx) - test.That(t, err, test.ShouldBeNil) - - img, rel, err := stream.Next(cancelCtx) - test.That(t, err, test.ShouldBeNil) - defer rel() - - test.That(t, img.Bounds().Dx(), test.ShouldEqual, 640) - test.That(t, img.Bounds().Dy(), test.ShouldEqual, 480) - - conf = newWebcamConfig("cam2", "video63") - cam2, err := videosource.NewWebcam(cancelCtx, nil, conf, logger) - test.That(t, err, test.ShouldBeNil) - defer cam2.Close(cancelCtx) - - stream, err = cam2.Stream(cancelCtx) - test.That(t, err, test.ShouldBeNil) - - img, rel, err = stream.Next(cancelCtx) - test.That(t, err, test.ShouldBeNil) - defer rel() - - test.That(t, img.Bounds().Dx(), test.ShouldEqual, 1280) - test.That(t, img.Bounds().Dy(), test.ShouldEqual, 720) -} diff --git a/components/camera/videosource/webcam_test.go b/components/camera/videosource/webcam_test.go deleted file mode 100644 index 4e8afb1ddea..00000000000 --- a/components/camera/videosource/webcam_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package videosource_test - -import ( - "context" - "testing" - - "github.com/pion/mediadevices/pkg/driver" - "github.com/pion/mediadevices/pkg/prop" - "go.viam.com/test" - - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/logging" -) - -// fakeDriver is a driver has a label and media properties. -type fakeDriver struct { - label string - props []prop.Media -} - -func (d *fakeDriver) Open() error { return nil } -func (d *fakeDriver) Properties() []prop.Media { return d.props } -func (d *fakeDriver) ID() string { return d.label } -func (d *fakeDriver) Info() driver.Info { return driver.Info{Label: d.label} } -func (d *fakeDriver) Status() driver.State { return "some state" } -func (d *fakeDriver) Close() error { return nil } - -func newFakeDriver(label string, props []prop.Media) driver.Driver { - return &fakeDriver{label: label, props: props} -} - -func testGetDrivers() []driver.Driver { - props := prop.Media{ - Video: prop.Video{Width: 320, Height: 240, FrameFormat: "some format", FrameRate: 30.0}, - } - withProps := newFakeDriver("some label", []prop.Media{props}) - withoutProps := newFakeDriver("another label", []prop.Media{}) - return []driver.Driver{withProps, withoutProps} -} - -func TestDiscoveryWebcam(t *testing.T) { - logger := logging.NewTestLogger(t) - resp, err := videosource.Discover(context.Background(), testGetDrivers, logger) - - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Webcams, test.ShouldHaveLength, 1) - test.That(t, resp.Webcams[0].Label, test.ShouldResemble, "some label") - test.That(t, resp.Webcams[0].Status, test.ShouldResemble, "some state") - - respProps := resp.Webcams[0].Properties - test.That(t, respProps, test.ShouldHaveLength, 1) - test.That(t, respProps[0].WidthPx, test.ShouldResemble, int32(320)) - test.That(t, respProps[0].HeightPx, test.ShouldResemble, int32(240)) - test.That(t, respProps[0].FrameFormat, test.ShouldResemble, "some format") - test.That(t, respProps[0].FrameRate, test.ShouldResemble, float32(30)) -} diff --git a/components/encoder/ams/ams_as5048.go b/components/encoder/ams/ams_as5048.go deleted file mode 100644 index 8aa119c5931..00000000000 --- a/components/encoder/ams/ams_as5048.go +++ /dev/null @@ -1,346 +0,0 @@ -//go:build linux - -// Package ams implements the AMS_AS5048 encoder -package ams - -import ( - "context" - "fmt" - "math" - "sync" - "time" - - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -const ( - i2cConn = "i2c" - transitionEpsilon = 90 -) - -var ( - model = resource.DefaultModelFamily.WithModel("AMS-AS5048") - scalingFactor = 360.0 / math.Pow(2, 14) - supportedConnections = utils.NewStringSet(i2cConn) -) - -// the wait time necessary to operate the position updating -// loop at 50 Hz. -var waitTimeNano = (1.0 / 50.0) * 1000000000.0 - -func init() { - resource.RegisterComponent( - encoder.API, - model, - resource.Registration[encoder.Encoder, *Config]{ - Constructor: newAS5048Encoder, - }, - ) -} - -// Config contains the connection information for -// configuring an AS5048 encoder. -type Config struct { - // We include connection type here in anticipation for - // future SPI support - ConnectionType string `json:"connection_type"` - *I2CConfig `json:"i2c_attributes,omitempty"` -} - -// Validate checks the attributes of an initialized config -// for proper values. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - - connType := conf.ConnectionType - if len(connType) == 0 { - // TODO: stop defaulting to I2C when SPI support is implemented - conf.ConnectionType = i2cConn - // return nil, errors.New("must specify connection type") - } - _, isSupported := supportedConnections[connType] - if !isSupported { - return nil, errors.Errorf("%s is not a supported connection type", connType) - } - if connType == i2cConn { - if conf.I2CConfig == nil { - return nil, errors.New("i2c selected as connection type, but no attributes supplied") - } - err := conf.I2CConfig.ValidateI2C(path) - if err != nil { - return nil, err - } - } - - return deps, nil -} - -// I2CConfig stores the configuration information for I2C connection. -type I2CConfig struct { - I2CBus string `json:"i2c_bus"` - I2CAddr int `json:"i2c_addr"` -} - -// ValidateI2C ensures all parts of the config are valid. -func (cfg *I2CConfig) ValidateI2C(path string) error { - if cfg.I2CBus == "" { - return resource.NewConfigValidationFieldRequiredError(path, "i2c_bus") - } - if cfg.I2CAddr == 0 { - return resource.NewConfigValidationFieldRequiredError(path, "i2c_addr") - } - - return nil -} - -// Encoder is a struct representing an instance of a hardware unit -// in AMS's AS5048 series of Hall-effect encoders. -type Encoder struct { - resource.Named - mu sync.RWMutex - logger logging.Logger - position float64 - positionOffset float64 - rotations int - positionType encoder.PositionType - i2cBus buses.I2C - i2cAddr byte - i2cBusName string // This is nessesary to check whether we need to create a new i2cBus during reconfigure. - cancelCtx context.Context - cancel context.CancelFunc - activeBackgroundWorkers sync.WaitGroup -} - -func newAS5048Encoder( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (encoder.Encoder, error) { - return makeAS5048Encoder(ctx, deps, conf, logger, nil) -} - -// This function is separated to inject a mock i2c bus during tests. -func makeAS5048Encoder( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - bus buses.I2C, -) (encoder.Encoder, error) { - cfg, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - cancelCtx, cancel := context.WithCancel(context.Background()) - - res := &Encoder{ - Named: conf.ResourceName().AsNamed(), - cancelCtx: cancelCtx, - cancel: cancel, - logger: logger, - positionType: encoder.PositionTypeTicks, - i2cBus: bus, - i2cBusName: cfg.I2CBus, - } - - if err := res.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - if err := res.startPositionLoop(ctx); err != nil { - return nil, err - } - return res, nil -} - -// Reconfigure reconfigures the encoder atomically. -func (enc *Encoder) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - enc.mu.Lock() - defer enc.mu.Unlock() - - if enc.i2cBusName != newConf.I2CBus || enc.i2cBus == nil { - bus, err := buses.NewI2cBus(newConf.I2CBus) - if err != nil { - msg := fmt.Sprintf("can't find I2C bus '%q' for AMS encoder", newConf.I2CBus) - return errors.Wrap(err, msg) - } - enc.i2cBusName = newConf.I2CBus - enc.i2cBus = bus - } - if enc.i2cAddr != byte(newConf.I2CAddr) { - enc.i2cAddr = byte(newConf.I2CAddr) - } - return nil -} - -func (enc *Encoder) startPositionLoop(ctx context.Context) error { - if err := enc.ResetPosition(ctx, map[string]interface{}{}); err != nil { - return err - } - enc.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - for { - if enc.cancelCtx.Err() != nil { - return - } - if err := enc.updatePosition(enc.cancelCtx); err != nil { - enc.logger.CErrorf(ctx, - "error in position loop (skipping update): %s", err.Error(), - ) - } - time.Sleep(time.Duration(waitTimeNano)) - } - }, enc.activeBackgroundWorkers.Done) - return nil -} - -func (enc *Encoder) readPosition(ctx context.Context) (float64, error) { - i2cHandle, err := enc.i2cBus.OpenHandle(enc.i2cAddr) - if err != nil { - return 0, err - } - defer utils.UncheckedErrorFunc(i2cHandle.Close) - - // retrieve the 8 most significant bits of the 14-bit resolution - // position - msB, err := i2cHandle.ReadByteData(ctx, byte(0xFE)) - if err != nil { - return 0, err - } - // retrieve the 6 least significant bits of as a byte (where - // the front two bits are irrelevant) - lsB, err := i2cHandle.ReadByteData(ctx, byte(0xFF)) - if err != nil { - return 0, err - } - return convertBytesToAngle(msB, lsB), nil -} - -func convertBytesToAngle(msB, lsB byte) float64 { - // obtain the 14-bit resolution position, which represents a - // portion of a full rotation. We then scale appropriately - // by (360 / 2^14) to get degrees - byteData := (int(msB) << 6) | int(lsB) - return (float64(byteData) * scalingFactor) -} - -func (enc *Encoder) updatePosition(ctx context.Context) error { - enc.mu.Lock() - defer enc.mu.Unlock() - angleDeg, err := enc.readPosition(ctx) - if err != nil { - return err - } - angleDeg += enc.positionOffset - // in order to keep track of multiple rotations, we increment / decrement - // a rotations counter whenever two subsequent positions are on either side - // of 0 (or 360) within a window of 2 * transitionEpsilon - forwardsTransition := (angleDeg <= transitionEpsilon) && ((360.0 - enc.position) <= transitionEpsilon) - backwardsTransition := (enc.position <= transitionEpsilon) && ((360.0 - angleDeg) <= transitionEpsilon) - if forwardsTransition { - enc.rotations++ - } else if backwardsTransition { - enc.rotations-- - } - enc.position = angleDeg - return nil -} - -// Position returns the total number of rotations detected -// by the encoder (rather than a number of pulse state transitions) -// because this encoder is absolute and not incremental. As a result -// a user MUST set ticks_per_rotation on the config of the corresponding -// motor to 1. Any other value will result in completely incorrect -// position measurements by the motor. -func (enc *Encoder) Position( - ctx context.Context, positionType encoder.PositionType, extra map[string]interface{}, -) (float64, encoder.PositionType, error) { - enc.mu.RLock() - defer enc.mu.RUnlock() - if positionType == encoder.PositionTypeDegrees { - enc.positionType = encoder.PositionTypeDegrees - return enc.position, enc.positionType, nil - } - ticks := float64(enc.rotations) + enc.position/360.0 - enc.positionType = encoder.PositionTypeTicks - return ticks, enc.positionType, nil -} - -// ResetPosition sets the current position measured by the encoder to be -// considered its new zero position. -func (enc *Encoder) ResetPosition( - ctx context.Context, extra map[string]interface{}, -) error { - enc.mu.Lock() - defer enc.mu.Unlock() - // NOTE (GV): potential improvement could be writing the offset position - // to the zero register of the encoder rather than keeping track - // on the struct - enc.position = 0 - enc.rotations = 0 - - i2cHandle, err := enc.i2cBus.OpenHandle(enc.i2cAddr) - if err != nil { - return err - } - defer utils.UncheckedErrorFunc(i2cHandle.Close) - - // clear current zero position - if err := i2cHandle.WriteByteData(ctx, byte(0x16), byte(0)); err != nil { - return err - } - if err := i2cHandle.WriteByteData(ctx, byte(0x17), byte(0)); err != nil { - return err - } - - // read current position - currentMSB, err := i2cHandle.ReadByteData(ctx, byte(0xFE)) - if err != nil { - return err - } - currentLSB, err := i2cHandle.ReadByteData(ctx, byte(0xFF)) - if err != nil { - return err - } - - // write current position to zero register - if err := i2cHandle.WriteByteData(ctx, byte(0x16), currentMSB); err != nil { - return err - } - if err := i2cHandle.WriteByteData(ctx, byte(0x17), currentLSB); err != nil { - return err - } - - return nil -} - -// Properties returns a list of all the position types that are supported by a given encoder. -func (enc *Encoder) Properties(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{ - TicksCountSupported: true, - AngleDegreesSupported: true, - }, nil -} - -// Close stops the position loop of the encoder when the component -// is closed. -func (enc *Encoder) Close(ctx context.Context) error { - enc.cancel() - enc.activeBackgroundWorkers.Wait() - return nil -} diff --git a/components/encoder/ams/ams_as5048_nonlinux.go b/components/encoder/ams/ams_as5048_nonlinux.go deleted file mode 100644 index 4412b51ffb0..00000000000 --- a/components/encoder/ams/ams_as5048_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package ams is Linux-only -package ams diff --git a/components/encoder/ams/ams_as5048_test.go b/components/encoder/ams/ams_as5048_test.go deleted file mode 100644 index df11080ec44..00000000000 --- a/components/encoder/ams/ams_as5048_test.go +++ /dev/null @@ -1,175 +0,0 @@ -//go:build linux - -package ams - -import ( - "context" - "math" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func TestConvertBytesToAngle(t *testing.T) { - // 180 degrees - msB := byte(math.Pow(2.0, 7.0)) - lsB := byte(0) - deg := convertBytesToAngle(msB, lsB) - test.That(t, deg, test.ShouldEqual, 180.0) - - // 270 degrees - msB = byte(math.Pow(2.0, 6.0) + math.Pow(2.0, 7.0)) - lsB = byte(0) - deg = convertBytesToAngle(msB, lsB) - test.That(t, deg, test.ShouldEqual, 270.0) - - // 219.990234 degrees - // 10011100011100 in binary, msB = 10011100, lsB = 00011100 - msB = byte(156) - lsB = byte(28) - deg = convertBytesToAngle(msB, lsB) - test.That(t, deg, test.ShouldAlmostEqual, 219.990234, 1e-6) -} - -func setupDependencies(mockData []byte) (resource.Config, resource.Dependencies, buses.I2C) { - i2cConf := &I2CConfig{ - I2CBus: "1", - I2CAddr: 64, - } - - cfg := resource.Config{ - Name: "encoder", - Model: model, - API: encoder.API, - ConvertedAttributes: &Config{ - ConnectionType: "i2c", - I2CConfig: i2cConf, - }, - } - - i2cHandle := &inject.I2CHandle{} - i2cHandle.ReadByteDataFunc = func(ctx context.Context, register byte) (byte, error) { - return mockData[register], nil - } - i2cHandle.WriteByteDataFunc = func(ctx context.Context, b1, b2 byte) error { - return nil - } - i2cHandle.CloseFunc = func() error { return nil } - - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - - return cfg, resource.Dependencies{}, i2c -} - -func TestAMSEncoder(t *testing.T) { - ctx := context.Background() - - positionMockData := make([]byte, 256) - positionMockData[0xFE] = 100 - positionMockData[0xFF] = 60 - - logger := logging.NewTestLogger(t) - cfg, deps, bus := setupDependencies(positionMockData) - enc, err := makeAS5048Encoder(ctx, deps, cfg, logger, bus) - test.That(t, err, test.ShouldBeNil) - defer enc.Close(ctx) - - t.Run("test automatically set to type ticks", func(t *testing.T) { - testutils.WaitForAssertion(t, func(tb testing.TB) { - pos, _, _ := enc.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(tb, pos, test.ShouldNotEqual, 0.0) - }) - pos, posType, _ := enc.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(t, pos, test.ShouldAlmostEqual, 0.4, 0.1) - test.That(t, posType, test.ShouldEqual, 1) - }) - t.Run("test ticks type from input", func(t *testing.T) { - testutils.WaitForAssertion(t, func(tb testing.TB) { - pos, _, _ := enc.Position(ctx, encoder.PositionTypeTicks, nil) - test.That(tb, pos, test.ShouldNotEqual, 0.0) - }) - pos, posType, _ := enc.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(t, pos, test.ShouldAlmostEqual, 0.4, 0.1) - test.That(t, posType, test.ShouldEqual, 1) - }) - t.Run("test degrees type from input", func(t *testing.T) { - testutils.WaitForAssertion(t, func(tb testing.TB) { - pos, _, _ := enc.Position(ctx, encoder.PositionTypeTicks, nil) - test.That(tb, pos, test.ShouldNotEqual, 0.0) - }) - pos, posType, _ := enc.Position(ctx, encoder.PositionTypeDegrees, nil) - test.That(t, pos, test.ShouldAlmostEqual, 142, 0.1) - test.That(t, posType, test.ShouldEqual, 2) - }) -} - -func setupDependenciesWithWrite(mockData []byte, writeData map[byte]byte) (resource.Config, resource.Dependencies, buses.I2C) { - i2cConf := &I2CConfig{ - I2CBus: "1", - I2CAddr: 64, - } - - cfg := resource.Config{ - Name: "encoder", - Model: model, - API: encoder.API, - ConvertedAttributes: &Config{ - ConnectionType: "i2c", - I2CConfig: i2cConf, - }, - } - - i2cHandle := &inject.I2CHandle{} - i2cHandle.ReadByteDataFunc = func(ctx context.Context, register byte) (byte, error) { - return mockData[register], nil - } - i2cHandle.WriteByteDataFunc = func(ctx context.Context, b1, b2 byte) error { - writeData[b1] = b2 - return nil - } - i2cHandle.CloseFunc = func() error { return nil } - - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - return cfg, resource.Dependencies{}, i2c -} - -func TestAMSEncoderReset(t *testing.T) { - ctx := context.Background() - - positionMockData := make([]byte, 256) - positionMockData[0xFE] = 100 - positionMockData[0xFF] = 60 - - writeData := make(map[byte]byte) - - logger := logging.NewTestLogger(t) - cfg, deps, bus := setupDependenciesWithWrite(positionMockData, writeData) - enc, err := makeAS5048Encoder(ctx, deps, cfg, logger, bus) - test.That(t, err, test.ShouldBeNil) - defer enc.Close(ctx) - - t.Run("test reset", func(t *testing.T) { - testutils.WaitForAssertion(t, func(tb testing.TB) { - enc.ResetPosition(ctx, nil) - pos, posType, _ := enc.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(tb, pos, test.ShouldAlmostEqual, 0, 0.1) - test.That(tb, posType, test.ShouldEqual, 1) - }) - - test.That(t, writeData[0x16], test.ShouldEqual, byte(100)) - test.That(t, writeData[0x17], test.ShouldEqual, byte(60)) - }) -} diff --git a/components/encoder/client.go b/components/encoder/client.go deleted file mode 100644 index 6cb802116c0..00000000000 --- a/components/encoder/client.go +++ /dev/null @@ -1,93 +0,0 @@ -package encoder - -import ( - "context" - - pb "go.viam.com/api/component/encoder/v1" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// client implements EncoderServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - conn rpc.ClientConn - client pb.EncoderServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Encoder, error) { - c := pb.NewEncoderServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - conn: conn, - client: c, - logger: logger, - }, nil -} - -// Position returns the current position in terms of ticks or -// degrees, and whether it is a relative or absolute position. -func (c *client) Position( - ctx context.Context, - positionType PositionType, - extra map[string]interface{}, -) (float64, PositionType, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return 0, PositionTypeUnspecified, err - } - posType := ToProtoPositionType(positionType) - req := &pb.GetPositionRequest{Name: c.name, PositionType: &posType, Extra: ext} - resp, err := c.client.GetPosition(ctx, req) - if err != nil { - return 0, PositionTypeUnspecified, err - } - posType1 := ToEncoderPositionType(&resp.PositionType) - return float64(resp.Value), posType1, nil -} - -// ResetPosition sets the current position of -// the encoder to be its new zero position. -func (c *client) ResetPosition(ctx context.Context, extra map[string]interface{}) error { - ext, err := structpb.NewStruct(extra) - if err != nil { - return err - } - req := &pb.ResetPositionRequest{Name: c.name, Extra: ext} - _, err = c.client.ResetPosition(ctx, req) - return err -} - -// Properties returns a list of all the position types that are supported by a given encoder. -func (c *client) Properties(ctx context.Context, extra map[string]interface{}) (Properties, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return Properties{}, err - } - req := &pb.GetPropertiesRequest{Name: c.name, Extra: ext} - resp, err := c.client.GetProperties(ctx, req) - if err != nil { - return Properties{}, err - } - return ProtoFeaturesToProperties(resp), nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return protoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/components/encoder/client_test.go b/components/encoder/client_test.go deleted file mode 100644 index 7a879181ca8..00000000000 --- a/components/encoder/client_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package encoder_test - -import ( - "context" - "net" - "testing" - - pb "go.viam.com/api/component/encoder/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/encoder" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testEncoderName = "encoder1" - failEncoderName = "encoder2" - fakeEncoderName = "encoder3" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - workingEncoder := &inject.Encoder{} - failingEncoder := &inject.Encoder{} - - var actualExtra map[string]interface{} - - workingEncoder.ResetPositionFunc = func(ctx context.Context, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - workingEncoder.PositionFunc = func( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, - ) (float64, encoder.PositionType, error) { - actualExtra = extra - return 42.0, encoder.PositionTypeUnspecified, nil - } - workingEncoder.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - actualExtra = extra - return encoder.Properties{ - TicksCountSupported: true, - AngleDegreesSupported: false, - }, nil - } - - failingEncoder.ResetPositionFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errSetToZeroFailed - } - failingEncoder.PositionFunc = func( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, - ) (float64, encoder.PositionType, error) { - return 0, encoder.PositionTypeUnspecified, errPositionUnavailable - } - failingEncoder.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{}, errGetPropertiesFailed - } - - resourceMap := map[resource.Name]encoder.Encoder{ - encoder.Named(testEncoderName): workingEncoder, - encoder.Named(failEncoderName): failingEncoder, - } - encoderSvc, err := resource.NewAPIResourceCollection(encoder.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[encoder.Encoder](encoder.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, encoderSvc), test.ShouldBeNil) - - workingEncoder.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingEncoderClient, err := encoder.NewClientFromConn(context.Background(), conn, "", encoder.Named(testEncoderName), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests for working encoder", func(t *testing.T) { - // DoCommand - resp, err := workingEncoderClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - err = workingEncoderClient.ResetPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - pos, positionType, err := workingEncoderClient.Position( - context.Background(), - encoder.PositionTypeUnspecified, - map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 42.0) - test.That(t, positionType, test.ShouldEqual, pb.PositionType_POSITION_TYPE_UNSPECIFIED) - - test.That(t, actualExtra, test.ShouldResemble, map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}}) - - test.That(t, workingEncoderClient.Close(context.Background()), test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - conn, err = viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingEncoderClient, err := encoder.NewClientFromConn(context.Background(), conn, "", encoder.Named(failEncoderName), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests for failing encoder", func(t *testing.T) { - err = failingEncoderClient.ResetPosition(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errSetToZeroFailed.Error()) - - pos, _, err := failingEncoderClient.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPositionUnavailable.Error()) - test.That(t, pos, test.ShouldEqual, 0.0) - - test.That(t, failingEncoderClient.Close(context.Background()), test.ShouldBeNil) - }) - - t.Run("dialed client tests for working encoder", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingEncoderDialedClient, err := encoder.NewClientFromConn(context.Background(), conn, "", encoder.Named(testEncoderName), logger) - test.That(t, err, test.ShouldBeNil) - - pos, _, err := workingEncoderDialedClient.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 42.0) - - err = workingEncoderDialedClient.ResetPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, workingEncoderDialedClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client tests for failing encoder", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingEncoderDialedClient, err := encoder.NewClientFromConn(context.Background(), conn, "", encoder.Named(failEncoderName), logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, failingEncoderDialedClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - test.That(t, conn.Close(), test.ShouldBeNil) -} diff --git a/components/encoder/collectors.go b/components/encoder/collectors.go deleted file mode 100644 index 9ad643bc721..00000000000 --- a/components/encoder/collectors.go +++ /dev/null @@ -1,58 +0,0 @@ -package encoder - -import ( - "context" - "errors" - - pb "go.viam.com/api/component/encoder/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" -) - -type method int64 - -const ( - ticksCount method = iota -) - -func (m method) String() string { - if m == ticksCount { - return "TicksCount" - } - return "Unknown" -} - -// newTicksCountCollector returns a collector to register a ticks count method. If one is already registered -// with the same MethodMetadata it will panic. -func newTicksCountCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - encoder, err := assertEncoder(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - v, positionType, err := encoder.Position(ctx, PositionTypeUnspecified, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, ticksCount.String(), err) - } - return pb.GetPositionResponse{ - Value: float32(v), - PositionType: pb.PositionType(positionType), - }, nil - }) - return data.NewCollector(cFunc, params) -} - -func assertEncoder(resource interface{}) (Encoder, error) { - encoder, ok := resource.(Encoder) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - return encoder, nil -} diff --git a/components/encoder/collectors_test.go b/components/encoder/collectors_test.go deleted file mode 100644 index 05e1de16532..00000000000 --- a/components/encoder/collectors_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package encoder_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - pb "go.viam.com/api/component/encoder/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/data" - "go.viam.com/rdk/logging" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - captureInterval = time.Second - numRetries = 5 -) - -func TestEncoderCollector(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: "encoder", - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Target: &buf, - Clock: mockClock, - } - - enc := newEncoder() - col, err := encoder.NewTicksCountCollector(enc, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, - tu.ToProtoMapIgnoreOmitEmpty(pb.GetPositionResponse{ - Value: 1.0, - PositionType: pb.PositionType_POSITION_TYPE_TICKS_COUNT, - })) -} - -func newEncoder() encoder.Encoder { - e := &inject.Encoder{} - e.PositionFunc = func(ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, - ) (float64, encoder.PositionType, error) { - return 1.0, encoder.PositionTypeTicks, nil - } - return e -} diff --git a/components/encoder/encoder.go b/components/encoder/encoder.go deleted file mode 100644 index bbf5d4708e9..00000000000 --- a/components/encoder/encoder.go +++ /dev/null @@ -1,140 +0,0 @@ -// Package encoder implements the encoder component -package encoder - -import ( - "context" - - pb "go.viam.com/api/component/encoder/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Encoder]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterEncoderServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.EncoderService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: ticksCount.String(), - }, newTicksCountCollector) -} - -// SubtypeName is a constant that identifies the component resource API string "encoder". -const SubtypeName = "encoder" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// PositionType is an enum representing the encoder's position. -type PositionType byte - -// Known encoder position types. -const ( - PositionTypeUnspecified PositionType = iota - // PositionTypeTicks is for relative encoders - // that report how far they've gone from a start position. - PositionTypeTicks - // PositionTypeDegrees is for absolute encoders - // that report their position in degrees along the radial axis. - PositionTypeDegrees -) - -func (t PositionType) String() string { - switch t { - case PositionTypeTicks: - return "ticks" - case PositionTypeDegrees: - return "degrees" - case PositionTypeUnspecified: - fallthrough - default: - return "unspecified" - } -} - -// A Encoder turns a position into a signal. -type Encoder interface { - resource.Resource - - // Position returns the current position in terms of ticks or degrees, and whether it is a relative or absolute position. - // - // myEncoder, err := encoder.FromRobot(machine, "my_encoder") - // if err != nil { - // logger.Fatalf("cannot get encoder: %v", err) - // } - // - // // Get the position of the encoder in ticks - // position, posType, err := myEncoder.Position(context.Background(), encoder.PositionTypeTicks, nil) - Position(ctx context.Context, positionType PositionType, extra map[string]interface{}) (float64, PositionType, error) - - // ResetPosition sets the current position of the motor to be its new zero position. - // - // myEncoder, err := encoder.FromRobot(machine, "my_encoder") - // if err != nil { - // logger.Fatalf("cannot get encoder: %v", err) - // } - // - // err = myEncoder.ResetPosition(context.Background(), nil) - ResetPosition(ctx context.Context, extra map[string]interface{}) error - - // Properties returns a list of all the position types that are supported by a given encoder - // - // myEncoder, err := encoder.FromRobot(machine, "my_encoder") - // - // // Get whether the encoder returns position in ticks or degrees. - // properties, err := myEncoder.Properties(context.Background(), nil) - Properties(ctx context.Context, extra map[string]interface{}) (Properties, error) -} - -// Named is a helper for getting the named Encoder's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromDependencies is a helper for getting the named encoder from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Encoder, error) { - return resource.FromDependencies[Encoder](deps, Named(name)) -} - -// FromRobot is a helper for getting the named encoder from the given Robot. -func FromRobot(r robot.Robot, name string) (Encoder, error) { - return robot.ResourceFromRobot[Encoder](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all encoder names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// ToEncoderPositionType takes a GetPositionResponse and returns -// an equivalent PositionType-to-int map. -func ToEncoderPositionType(positionType *pb.PositionType) PositionType { - if positionType == nil { - return PositionTypeUnspecified - } - if *positionType == pb.PositionType_POSITION_TYPE_ANGLE_DEGREES { - return PositionTypeDegrees - } - if *positionType == pb.PositionType_POSITION_TYPE_TICKS_COUNT { - return PositionTypeTicks - } - return PositionTypeUnspecified -} - -// ToProtoPositionType takes a map of PositionType-to-int (indicating -// the PositionType) and converts it to a GetPositionResponse. -func ToProtoPositionType(positionType PositionType) pb.PositionType { - if positionType == PositionTypeDegrees { - return pb.PositionType_POSITION_TYPE_ANGLE_DEGREES - } - if positionType == PositionTypeTicks { - return pb.PositionType_POSITION_TYPE_TICKS_COUNT - } - return pb.PositionType_POSITION_TYPE_UNSPECIFIED -} diff --git a/components/encoder/errors.go b/components/encoder/errors.go deleted file mode 100644 index bf12299422f..00000000000 --- a/components/encoder/errors.go +++ /dev/null @@ -1,19 +0,0 @@ -package encoder - -import "github.com/pkg/errors" - -// NewPositionTypeUnsupportedError returns a standard error for when -// an encoder does not support the given PositionType. -func NewPositionTypeUnsupportedError(positionType PositionType) error { - return errors.Errorf("encoder does not support %q; use a different PositionType", positionType) -} - -// NewEncodedMotorPositionTypeUnsupportedError returns a standard error for when -// an encoded motor tries to use an encoder that doesn't support Ticks. -func NewEncodedMotorPositionTypeUnsupportedError(props Properties) error { - if props.AngleDegreesSupported { - return errors.New( - "encoder position type is Angle Degrees, need an encoder that supports Ticks") - } - return errors.New("need an encoder that supports Ticks") -} diff --git a/components/encoder/export_collectors_test.go b/components/encoder/export_collectors_test.go deleted file mode 100644 index 4b3b650240b..00000000000 --- a/components/encoder/export_collectors_test.go +++ /dev/null @@ -1,5 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package encoder - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var NewTicksCountCollector = newTicksCountCollector diff --git a/components/encoder/fake/encoder.go b/components/encoder/fake/encoder.go deleted file mode 100644 index 605842376bb..00000000000 --- a/components/encoder/fake/encoder.go +++ /dev/null @@ -1,173 +0,0 @@ -// Package fake implements a fake encoder. -package fake - -import ( - "context" - "math" - "sync" - "time" - - "go.viam.com/utils" - - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var fakeModel = resource.DefaultModelFamily.WithModel("fake") - -func init() { - resource.RegisterComponent(encoder.API, fakeModel, resource.Registration[encoder.Encoder, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (encoder.Encoder, error) { - return NewEncoder(ctx, conf, logger) - }, - }) -} - -// NewEncoder creates a new Encoder. -func NewEncoder( - ctx context.Context, - cfg resource.Config, - logger logging.Logger, -) (encoder.Encoder, error) { - e := &fakeEncoder{ - Named: cfg.ResourceName().AsNamed(), - position: 0, - positionType: encoder.PositionTypeTicks, - logger: logger, - } - if err := e.Reconfigure(ctx, nil, cfg); err != nil { - return nil, err - } - - e.start(ctx) - return e, nil -} - -func (e *fakeEncoder) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - e.mu.Lock() - e.updateRate = newConf.UpdateRate - if e.updateRate == 0 { - e.updateRate = 100 - } - e.mu.Unlock() - return nil -} - -// Config describes the configuration of a fake encoder. -type Config struct { - UpdateRate int64 `json:"update_rate_msec,omitempty"` -} - -// Validate ensures all parts of a config is valid. -func (cfg *Config) Validate(path string) ([]string, error) { - return nil, nil -} - -// fakeEncoder keeps track of a fake motor position. -type fakeEncoder struct { - resource.Named - resource.TriviallyCloseable - - positionType encoder.PositionType - activeBackgroundWorkers sync.WaitGroup - logger logging.Logger - - mu sync.RWMutex - position int64 - speed float64 // ticks per minute - updateRate int64 // update position in start every updateRate ms -} - -// Position returns the current position in terms of ticks or -// degrees, and whether it is a relative or absolute position. -func (e *fakeEncoder) Position( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, -) (float64, encoder.PositionType, error) { - if positionType == encoder.PositionTypeDegrees { - return math.NaN(), encoder.PositionTypeUnspecified, encoder.NewPositionTypeUnsupportedError(positionType) - } - e.mu.RLock() - defer e.mu.RUnlock() - return float64(e.position), e.positionType, nil -} - -// Start starts a background thread to run the encoder. -func (e *fakeEncoder) start(cancelCtx context.Context) { - e.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - for { - select { - case <-cancelCtx.Done(): - return - default: - } - - e.mu.RLock() - updateRate := e.updateRate - e.mu.RUnlock() - if !utils.SelectContextOrWait(cancelCtx, time.Duration(updateRate)*time.Millisecond) { - return - } - - e.mu.Lock() - e.position += int64(e.speed / float64(60*1000/updateRate)) - e.mu.Unlock() - } - }, e.activeBackgroundWorkers.Done) -} - -// ResetPosition sets the current position of the motor (adjusted by a given offset) -// to be its new zero position. -func (e *fakeEncoder) ResetPosition(ctx context.Context, extra map[string]interface{}) error { - e.mu.Lock() - defer e.mu.Unlock() - e.position = int64(0) - return nil -} - -// Properties returns a list of all the position types that are supported by a given encoder. -func (e *fakeEncoder) Properties(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{ - TicksCountSupported: true, - AngleDegreesSupported: false, - }, nil -} - -// Encoder is a fake encoder used for testing. -type Encoder interface { - encoder.Encoder - SetSpeed(ctx context.Context, speed float64) error - SetPosition(ctx context.Context, position int64) error -} - -// SetSpeed sets the speed of the fake motor the encoder is measuring. -func (e *fakeEncoder) SetSpeed(ctx context.Context, speed float64) error { - e.mu.Lock() - defer e.mu.Unlock() - e.speed = speed - return nil -} - -// SetPosition sets the position of the encoder. -func (e *fakeEncoder) SetPosition(ctx context.Context, position int64) error { - e.mu.Lock() - defer e.mu.Unlock() - e.position = position - return nil -} diff --git a/components/encoder/fake/encoder_test.go b/components/encoder/fake/encoder_test.go deleted file mode 100644 index a36adeea1bf..00000000000 --- a/components/encoder/fake/encoder_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package fake - -import ( - "context" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestEncoder(t *testing.T) { - ctx := context.Background() - ic := Config{ - UpdateRate: 100, - } - cfg := resource.Config{Name: "enc1", ConvertedAttributes: &ic} - logger := logging.NewTestLogger(t) - e, _ := NewEncoder(ctx, cfg, logger) - - // Get and set position - t.Run("get and set position", func(t *testing.T) { - pos, positionType, err := e.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(t, pos, test.ShouldEqual, 0) - test.That(t, err, test.ShouldBeNil) - test.That(t, positionType, test.ShouldEqual, encoder.PositionTypeTicks) - - e1 := e.(Encoder) - - err = e1.SetPosition(ctx, 1) - test.That(t, err, test.ShouldBeNil) - - pos, _, err = e.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(t, pos, test.ShouldEqual, 1) - test.That(t, err, test.ShouldBeNil) - }) - - // Reset - t.Run("reset to zero", func(t *testing.T) { - err := e.ResetPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - pos, _, err := e.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(t, pos, test.ShouldEqual, 0) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("specify a type", func(t *testing.T) { - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, positionType, err := e.Position(context.Background(), encoder.PositionTypeTicks, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, 0) - test.That(tb, positionType, test.ShouldEqual, encoder.PositionTypeTicks) - }) - }) - t.Run("get properties", func(t *testing.T) { - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - props, err := e.Properties(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, props.TicksCountSupported, test.ShouldBeTrue) - test.That(tb, props.AngleDegreesSupported, test.ShouldBeFalse) - }) - }) - - // Set Speed - t.Run("set speed", func(t *testing.T) { - e1 := e.(*fakeEncoder) - err := e1.SetSpeed(ctx, 1) - test.That(t, err, test.ShouldBeNil) - test.That(t, e1.speed, test.ShouldEqual, 1) - }) - - // Start with default update rate - t.Run("start default update rate", func(t *testing.T) { - e1 := e.(*fakeEncoder) - err := e1.SetSpeed(ctx, 0) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(t, e1.updateRate, test.ShouldEqual, 100) - }) - - err = e1.SetSpeed(ctx, 600) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - pos, _, err := e.Position(ctx, encoder.PositionTypeUnspecified, nil) - test.That(tb, pos, test.ShouldBeGreaterThan, 0) - test.That(tb, err, test.ShouldBeNil) - }) - }) -} diff --git a/components/encoder/incremental/incremental_encoder.go b/components/encoder/incremental/incremental_encoder.go deleted file mode 100644 index 93b5a760c21..00000000000 --- a/components/encoder/incremental/incremental_encoder.go +++ /dev/null @@ -1,331 +0,0 @@ -// Package incremental implements an incremental encoder -package incremental - -import ( - "context" - "math" - "sync" - "sync/atomic" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var incrModel = resource.DefaultModelFamily.WithModel("incremental") - -func init() { - resource.RegisterComponent( - encoder.API, - incrModel, - resource.Registration[encoder.Encoder, *Config]{ - Constructor: NewIncrementalEncoder, - }) -} - -// Encoder keeps track of a motor position using a rotary incremental encoder. -type Encoder struct { - resource.Named - mu sync.Mutex - A, B board.DigitalInterrupt - // The position is pRaw with the least significant bit chopped off. - position int64 - // pRaw is the number of half-ticks we've gone through: it increments or decrements whenever - // either pin on the encoder changes. - pRaw int64 - // pState is the previous state: the least significant bit is the value of pin A, and the - // second-least-significant bit is pin B. It is used to determine whether to increment or - // decrement pRaw. - pState int64 - boardName string - encAName string - encBName string - - logger logging.Logger - - cancelCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup - positionType encoder.PositionType -} - -// Pins describes the configuration of Pins for a quadrature encoder. -type Pins struct { - A string `json:"a"` - B string `json:"b"` -} - -// Config describes the configuration of a quadrature encoder. -type Config struct { - Pins Pins `json:"pins"` - BoardName string `json:"board"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - - if conf.Pins.A == "" { - return nil, errors.New("expected nonempty string for a") - } - if conf.Pins.B == "" { - return nil, errors.New("expected nonempty string for b") - } - - if len(conf.BoardName) == 0 { - return nil, errors.New("expected nonempty board") - } - deps = append(deps, conf.BoardName) - - return deps, nil -} - -// NewIncrementalEncoder creates a new Encoder. -func NewIncrementalEncoder( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (encoder.Encoder, error) { - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - e := &Encoder{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - position: 0, - positionType: encoder.PositionTypeTicks, - pRaw: 0, - pState: 0, - } - if err := e.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return e, nil -} - -// Reconfigure atomically reconfigures this encoder in place based on the new config. -func (e *Encoder) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - e.mu.Lock() - existingBoardName := e.boardName - existingEncAName := e.encAName - existingEncBName := e.encBName - e.mu.Unlock() - - needRestart := existingBoardName != newConf.BoardName || - existingEncAName != newConf.Pins.A || - existingEncBName != newConf.Pins.B - - board, err := board.FromDependencies(deps, newConf.BoardName) - if err != nil { - return err - } - - encA, err := board.DigitalInterruptByName(newConf.Pins.A) - if err != nil { - return multierr.Combine(errors.Errorf("cannot find pin (%s) for incremental Encoder", newConf.Pins.A), err) - } - encB, err := board.DigitalInterruptByName(newConf.Pins.B) - if err != nil { - return multierr.Combine(errors.Errorf("cannot find pin (%s) for incremental Encoder", newConf.Pins.B), err) - } - - if !needRestart { - return nil - } - utils.UncheckedError(e.Close(ctx)) - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - e.cancelCtx = cancelCtx - e.cancelFunc = cancelFunc - - e.mu.Lock() - e.A = encA - e.B = encB - e.boardName = newConf.BoardName - e.encAName = newConf.Pins.A - e.encBName = newConf.Pins.B - // state is not really valid anymore - atomic.StoreInt64(&e.position, 0) - atomic.StoreInt64(&e.pRaw, 0) - atomic.StoreInt64(&e.pState, 0) - e.mu.Unlock() - - e.Start(ctx, board) - - return nil -} - -// Start starts the Encoder background thread. -func (e *Encoder) Start(ctx context.Context, b board.Board) { - /** - a rotary encoder looks like - - picture from https://github.com/joan2937/pigpio/blob/master/EXAMPLES/C/ROTARY_ENCODER/rotary_encoder.c - 1 2 3 4 1 2 3 4 1 - - +---------+ +---------+ 0 - | | | | - A | | | | - | | | | - +---------+ +---------+ +----- 1 - - +---------+ +---------+ 0 - | | | | - B | | | | - | | | | - ----+ +---------+ +---------+ 1 - - */ - - // State Transition Table - // +---------------+----+----+----+----+ - // | pState/nState | 00 | 01 | 10 | 11 | - // +---------------+----+----+----+----+ - // | 00 | 0 | -1 | +1 | x | - // +---------------+----+----+----+----+ - // | 01 | +1 | 0 | x | -1 | - // +---------------+----+----+----+----+ - // | 10 | -1 | x | 0 | +1 | - // +---------------+----+----+----+----+ - // | 11 | x | +1 | -1 | 0 | - // +---------------+----+----+----+----+ - // 0 -> same state - // x -> impossible state - - ch := make(chan board.Tick) - err := b.StreamTicks(e.cancelCtx, []board.DigitalInterrupt{e.A, e.B}, ch, nil) - if err != nil { - utils.Logger.Errorw("error getting digital interrupt ticks", "error", err) - return - } - - aLevel, err := e.A.Value(ctx, nil) - if err != nil { - utils.Logger.Errorw("error reading a level", "error", err) - } - bLevel, err := e.B.Value(ctx, nil) - if err != nil { - utils.Logger.Errorw("error reading b level", "error", err) - } - e.pState = aLevel | (bLevel << 1) - - e.activeBackgroundWorkers.Add(1) - - utils.ManagedGo(func() { - for { - // This looks redundant with the other select statement below, but it's not: if we're - // supposed to return, we need to do that even if chanA and chanB are full of data, and - // the other select statement will pick random cases in that situation. This select - // statement guarantees that we'll return if we're supposed to, regardless of whether - // there's data in the other channels. - select { - case <-e.cancelCtx.Done(): - return - default: - } - - var tick board.Tick - - select { - case <-e.cancelCtx.Done(): - return - case tick = <-ch: - if tick.Name == e.encAName { - aLevel = 0 - if tick.High { - aLevel = 1 - } - } - if tick.Name == e.encBName { - bLevel = 0 - if tick.High { - bLevel = 1 - } - } - } - nState := aLevel | (bLevel << 1) - if e.pState == nState { - continue - } - switch (e.pState << 2) | nState { - case 0b0001: - fallthrough - case 0b0111: - fallthrough - case 0b1000: - fallthrough - case 0b1110: - atomic.AddInt64(&e.pRaw, -1) - case 0b0010: - fallthrough - case 0b0100: - fallthrough - case 0b1011: - fallthrough - case 0b1101: - atomic.AddInt64(&e.pRaw, 1) - } - atomic.StoreInt64(&e.position, atomic.LoadInt64(&e.pRaw)>>1) - e.pState = nState - } - }, e.activeBackgroundWorkers.Done) -} - -// Position returns the current position in terms of ticks or -// degrees, and whether it is a relative or absolute position. -func (e *Encoder) Position( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, -) (float64, encoder.PositionType, error) { - if positionType == encoder.PositionTypeDegrees { - return math.NaN(), encoder.PositionTypeUnspecified, encoder.NewPositionTypeUnsupportedError(positionType) - } - res := atomic.LoadInt64(&e.position) - return float64(res), e.positionType, nil -} - -// ResetPosition sets the current position of the motor (adjusted by a given offset) -// to be its new zero position. -func (e *Encoder) ResetPosition(ctx context.Context, extra map[string]interface{}) error { - atomic.StoreInt64(&e.position, 0) - atomic.StoreInt64(&e.pRaw, atomic.LoadInt64(&e.pRaw)&0x1) - return nil -} - -// Properties returns a list of all the position types that are supported by a given encoder. -func (e *Encoder) Properties(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{ - TicksCountSupported: true, - AngleDegreesSupported: false, - }, nil -} - -// RawPosition returns the raw position of the encoder. -func (e *Encoder) RawPosition() int64 { - return atomic.LoadInt64(&e.pRaw) -} - -// Close shuts down the Encoder. -func (e *Encoder) Close(ctx context.Context) error { - e.logger.Info("closing encoder") - e.cancelFunc() - e.logger.Info("cancelled context") - e.activeBackgroundWorkers.Wait() - e.logger.Info("background workers done") - return nil -} diff --git a/components/encoder/incremental/incremental_test.go b/components/encoder/incremental/incremental_test.go deleted file mode 100644 index 978ed773491..00000000000 --- a/components/encoder/incremental/incremental_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package incremental - -import ( - "context" - "fmt" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func TestConfig(t *testing.T) { - ctx := context.Background() - - b := MakeBoard(t) - - deps := make(resource.Dependencies) - deps[board.Named("main")] = b - - t.Run("valid config", func(t *testing.T) { - ic := Config{ - BoardName: "main", - Pins: Pins{A: "11", B: "13"}, - } - - rawcfg := resource.Config{Name: "enc1", ConvertedAttributes: &ic} - - _, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - - test.That(t, err, test.ShouldBeNil) - }) - t.Run("invalid config", func(t *testing.T) { - ic := Config{ - BoardName: "pi", - // Pins intentionally missing - } - - rawcfg := resource.Config{Name: "enc1", ConvertedAttributes: &ic} - - _, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldNotBeNil) - }) -} - -func TestEncoder(t *testing.T) { - ctx := context.Background() - - b := MakeBoard(t) - - deps := make(resource.Dependencies) - deps[board.Named("main")] = b - - i1, err := b.DigitalInterruptByName("11") - test.That(t, err, test.ShouldBeNil) - i2, err := b.DigitalInterruptByName("13") - test.That(t, err, test.ShouldBeNil) - - ic := Config{ - BoardName: "main", - Pins: Pins{A: "11", B: "13"}, - } - - rawcfg := resource.Config{Name: "enc1", ConvertedAttributes: &ic} - - t.Run("run forward", func(t *testing.T) { - enc, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - err = i2.(*inject.DigitalInterrupt).Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - err = i1.(*inject.DigitalInterrupt).Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, 1) - }) - }) - - t.Run("run backward", func(t *testing.T) { - enc, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - err = i1.(*inject.DigitalInterrupt).Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - err = i2.(*inject.DigitalInterrupt).Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, -1) - }) - }) - - t.Run("reset position", func(t *testing.T) { - enc, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - // reset position to 0 - err = enc.ResetPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, ticks, test.ShouldEqual, 0) - }) - - t.Run("specify correct position type", func(t *testing.T) { - enc, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, positionType, err := enc.Position(context.Background(), encoder.PositionTypeTicks, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, 0) - test.That(tb, positionType, test.ShouldEqual, encoder.PositionTypeTicks) - }) - }) - t.Run("specify wrong position type", func(t *testing.T) { - enc, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - _, _, err := enc.Position(context.Background(), encoder.PositionTypeDegrees, nil) - test.That(tb, err, test.ShouldNotBeNil) - test.That(tb, err.Error(), test.ShouldContainSubstring, "encoder does not support") - test.That(tb, err.Error(), test.ShouldContainSubstring, "degrees") - }) - }) - - t.Run("get properties", func(t *testing.T) { - enc, err := NewIncrementalEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - props, err := enc.Properties(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, props.TicksCountSupported, test.ShouldBeTrue) - test.That(tb, props.AngleDegreesSupported, test.ShouldBeFalse) - }) - }) -} - -func MakeBoard(t *testing.T) board.Board { - b := inject.NewBoard("test-board") - i1 := &inject.DigitalInterrupt{} - i2 := &inject.DigitalInterrupt{} - callbacks := make(map[board.DigitalInterrupt]chan board.Tick) - i1.NameFunc = func() string { - return "11" - } - i2.NameFunc = func() string { - return "13" - } - i1.TickFunc = func(ctx context.Context, high bool, nanoseconds uint64) error { - ch, ok := callbacks[i1] - test.That(t, ok, test.ShouldBeTrue) - ch <- board.Tick{Name: i1.Name(), High: high, TimestampNanosec: nanoseconds} - return nil - } - i2.TickFunc = func(ctx context.Context, high bool, nanoseconds uint64) error { - ch, ok := callbacks[i2] - test.That(t, ok, test.ShouldBeTrue) - ch <- board.Tick{Name: i2.Name(), High: high, TimestampNanosec: nanoseconds} - return nil - } - i1.ValueFunc = func(ctx context.Context, extra map[string]interface{}) (int64, error) { - return 0, nil - } - i2.ValueFunc = func(ctx context.Context, extra map[string]interface{}) (int64, error) { - return 0, nil - } - - b.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - if name == "11" { - return i1, nil - } else if name == "13" { - return i2, nil - } - return nil, fmt.Errorf("unknown digital interrupt: %s", name) - } - b.StreamTicksFunc = func( - ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, extra map[string]interface{}, - ) error { - for _, i := range interrupts { - callbacks[i] = ch - } - - return nil - } - - return b -} diff --git a/components/encoder/properties.go b/components/encoder/properties.go deleted file mode 100644 index 5962f7606ea..00000000000 --- a/components/encoder/properties.go +++ /dev/null @@ -1,29 +0,0 @@ -package encoder - -import pb "go.viam.com/api/component/encoder/v1" - -// Properties holds the properties of the encoder. -type Properties struct { - TicksCountSupported bool - AngleDegreesSupported bool -} - -// ProtoFeaturesToProperties takes a GetPropertiesResponse and returns -// an equivalent Properties struct. -func ProtoFeaturesToProperties(resp *pb.GetPropertiesResponse) Properties { - return Properties{ - TicksCountSupported: resp.TicksCountSupported, - AngleDegreesSupported: resp.AngleDegreesSupported, - } -} - -// PropertiesToProtoResponse takes a properties struct and converts it -// to a GetPropertiesResponse. -func PropertiesToProtoResponse( - props Properties, -) (*pb.GetPropertiesResponse, error) { - return &pb.GetPropertiesResponse{ - TicksCountSupported: props.TicksCountSupported, - AngleDegreesSupported: props.AngleDegreesSupported, - }, nil -} diff --git a/components/encoder/register/register.go b/components/encoder/register/register.go deleted file mode 100644 index d164db20859..00000000000 --- a/components/encoder/register/register.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package register registers all relevant MovementSensors -package register - -import ( - // Load all encoders. - _ "go.viam.com/rdk/components/encoder/ams" - _ "go.viam.com/rdk/components/encoder/incremental" - _ "go.viam.com/rdk/components/encoder/single" -) diff --git a/components/encoder/server.go b/components/encoder/server.go deleted file mode 100644 index a7e1ab5be10..00000000000 --- a/components/encoder/server.go +++ /dev/null @@ -1,85 +0,0 @@ -package encoder - -import ( - "context" - - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/encoder/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -type serviceServer struct { - pb.UnimplementedEncoderServiceServer - coll resource.APIResourceCollection[Encoder] -} - -// NewRPCServiceServer constructs an Encoder gRPC service serviceServer. -func NewRPCServiceServer(coll resource.APIResourceCollection[Encoder]) interface{} { - return &serviceServer{coll: coll} -} - -// GetPosition returns the current position in terms of ticks or -// degrees, and whether it is a relative or absolute position. -func (s *serviceServer) GetPosition( - ctx context.Context, - req *pb.GetPositionRequest, -) (*pb.GetPositionResponse, error) { - enc, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - position, positionType, err := enc.Position(ctx, ToEncoderPositionType(req.PositionType), req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetPositionResponse{ - Value: float32(position), - PositionType: ToProtoPositionType(positionType), - }, nil -} - -// ResetPosition sets the current position of the encoder -// specified by the request to be its new zero position. -func (s *serviceServer) ResetPosition( - ctx context.Context, - req *pb.ResetPositionRequest, -) (*pb.ResetPositionResponse, error) { - encName := req.GetName() - enc, err := s.coll.Resource(encName) - if err != nil { - return nil, errors.Errorf("no encoder (%s) found", encName) - } - - return &pb.ResetPositionResponse{}, enc.ResetPosition(ctx, req.Extra.AsMap()) -} - -// GetProperties returns a message of booleans indicating which optional features the robot's encoder supports. -func (s *serviceServer) GetProperties( - ctx context.Context, - req *pb.GetPropertiesRequest, -) (*pb.GetPropertiesResponse, error) { - encoderName := req.GetName() - enc, err := s.coll.Resource(encoderName) - if err != nil { - return nil, errors.Errorf("no encoder (%s) found", encoderName) - } - features, err := enc.Properties(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return PropertiesToProtoResponse(features) -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - enc, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, enc, req) -} diff --git a/components/encoder/server_test.go b/components/encoder/server_test.go deleted file mode 100644 index 74927e564cf..00000000000 --- a/components/encoder/server_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package encoder_test - -import ( - "context" - "errors" - "testing" - - pb "go.viam.com/api/component/encoder/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errPositionUnavailable = errors.New("position unavailable") - errSetToZeroFailed = errors.New("set to zero failed") - errPropertiesNotFound = errors.New("properties not found") - errGetPropertiesFailed = errors.New("get properties failed") -) - -func newServer() (pb.EncoderServiceServer, *inject.Encoder, *inject.Encoder, error) { - injectEncoder1 := &inject.Encoder{} - injectEncoder2 := &inject.Encoder{} - - resourceMap := map[resource.Name]encoder.Encoder{ - encoder.Named(testEncoderName): injectEncoder1, - encoder.Named(failEncoderName): injectEncoder2, - } - - injectSvc, err := resource.NewAPIResourceCollection(encoder.API, resourceMap) - if err != nil { - return nil, nil, nil, err - } - return encoder.NewRPCServiceServer(injectSvc).(pb.EncoderServiceServer), injectEncoder1, injectEncoder2, nil -} - -func TestServerGetPosition(t *testing.T) { - encoderServer, workingEncoder, failingEncoder, _ := newServer() - - // fails on a bad encoder - req := pb.GetPositionRequest{Name: fakeEncoderName} - resp, err := encoderServer.GetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - - failingEncoder.PositionFunc = func( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, - ) (float64, encoder.PositionType, error) { - return 0, encoder.PositionTypeUnspecified, errPositionUnavailable - } - - // Position unavailable test - req = pb.GetPositionRequest{Name: failEncoderName} - resp, err = encoderServer.GetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errPositionUnavailable) - - workingEncoder.PositionFunc = func( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, - ) (float64, encoder.PositionType, error) { - return 42.0, encoder.PositionTypeUnspecified, nil - } - req = pb.GetPositionRequest{Name: testEncoderName} - resp, err = encoderServer.GetPosition(context.Background(), &req) - test.That(t, float64(resp.Value), test.ShouldEqual, 42.0) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerResetPosition(t *testing.T) { - encoderServer, workingEncoder, failingEncoder, _ := newServer() - - // fails on a bad encoder - req := pb.ResetPositionRequest{Name: fakeEncoderName} - resp, err := encoderServer.ResetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingEncoder.ResetPositionFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errSetToZeroFailed - } - req = pb.ResetPositionRequest{Name: failEncoderName} - resp, err = encoderServer.ResetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errSetToZeroFailed) - - workingEncoder.ResetPositionFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - req = pb.ResetPositionRequest{Name: testEncoderName} - resp, err = encoderServer.ResetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerGetProperties(t *testing.T) { - encoderServer, workingEncoder, failingEncoder, _ := newServer() - - // fails on a bad encoder - req := pb.GetPropertiesRequest{Name: fakeEncoderName} - resp, err := encoderServer.GetProperties(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingEncoder.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{}, errPropertiesNotFound - } - req = pb.GetPropertiesRequest{Name: failEncoderName} - resp, err = encoderServer.GetProperties(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errPropertiesNotFound) - - workingEncoder.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{ - TicksCountSupported: true, - AngleDegreesSupported: false, - }, nil - } - req = pb.GetPropertiesRequest{Name: testEncoderName} - resp, err = encoderServer.GetProperties(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerExtraParams(t *testing.T) { - encoderServer, workingEncoder, _, _ := newServer() - - var actualExtra map[string]interface{} - workingEncoder.ResetPositionFunc = func(ctx context.Context, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - - ext, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - req := pb.ResetPositionRequest{Name: testEncoderName, Extra: ext} - resp, err := encoderServer.ResetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, actualExtra["foo"], test.ShouldEqual, expectedExtra["foo"]) - test.That(t, actualExtra["baz"], test.ShouldResemble, expectedExtra["baz"]) -} diff --git a/components/encoder/single/single_encoder.go b/components/encoder/single/single_encoder.go deleted file mode 100644 index 8227a25ed38..00000000000 --- a/components/encoder/single/single_encoder.go +++ /dev/null @@ -1,256 +0,0 @@ -/* -Package single implements a single-wire odometer, such as LM393, as an encoder. -This allows the attached motor to determine its relative position. -This class of encoders requires a single digital interrupt pin. - -This encoder must be connected to a motor (or another component that supports encoders -and reports the direction it is moving) in order to record readings. -The motor indicates in which direction it is spinning, thus indicating if the encoder -should increment or decrement reading value. - -Resetting a position must set the position to an int64. A floating point input will be rounded. - -Sample configuration: - - { - "pins" : { - "i": 10 - }, - "board": "pi" - } -*/ -package single - -import ( - "context" - "math" - "sync" - "sync/atomic" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var singleModel = resource.DefaultModelFamily.WithModel("single") - -func init() { - resource.RegisterComponent( - encoder.API, - singleModel, - resource.Registration[encoder.Encoder, *Config]{ - Constructor: NewSingleEncoder, - }) -} - -// DirectionAware lets you ask what direction something is moving. Only used for Encoder for now, unclear future. -// DirectionMoving returns -1 if the motor is currently turning backwards, 1 if forwards and 0 if off. -type DirectionAware interface { - DirectionMoving() int64 -} - -// Encoder keeps track of a motor position using a rotary encoder.s. -type Encoder struct { - resource.Named - - position int64 - - mu sync.Mutex - I board.DigitalInterrupt - m DirectionAware - boardName string - diPinName string - - positionType encoder.PositionType - logger logging.Logger - - cancelCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup -} - -// Pin describes the configuration of Pins for a Single encoder. -type Pin struct { - I string `json:"i"` -} - -// Config describes the configuration of a single encoder. -type Config struct { - Pins Pin `json:"pins"` - BoardName string `json:"board"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - - if conf.Pins.I == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "i") - } - - if len(conf.BoardName) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - deps = append(deps, conf.BoardName) - - return deps, nil -} - -// AttachDirectionalAwareness to pre-created encoder. -func (e *Encoder) AttachDirectionalAwareness(da DirectionAware) { - e.mu.Lock() - e.m = da - e.mu.Unlock() -} - -// NewSingleEncoder creates a new Encoder. -func NewSingleEncoder( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (encoder.Encoder, error) { - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - e := &Encoder{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - position: 0, - positionType: encoder.PositionTypeTicks, - } - if err := e.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return e, nil -} - -// Reconfigure atomically reconfigures this encoder in place based on the new config. -func (e *Encoder) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - e.mu.Lock() - existingBoardName := e.boardName - existingDIPinName := e.diPinName - e.mu.Unlock() - - needRestart := existingBoardName != newConf.BoardName || - existingDIPinName != newConf.Pins.I - - board, err := board.FromDependencies(deps, newConf.BoardName) - if err != nil { - return err - } - - di, err := board.DigitalInterruptByName(newConf.Pins.I) - if err != nil { - return multierr.Combine(errors.Errorf("cannot find pin (%s) for Encoder", newConf.Pins.I), err) - } - - if !needRestart { - return nil - } - utils.UncheckedError(e.Close(ctx)) - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - e.cancelCtx = cancelCtx - e.cancelFunc = cancelFunc - - e.mu.Lock() - e.I = di - e.boardName = newConf.BoardName - e.diPinName = newConf.Pins.I - // state is not really valid anymore - atomic.StoreInt64(&e.position, 0) - e.mu.Unlock() - - e.Start(ctx, board) - - return nil -} - -// Start starts the Encoder background thread. -func (e *Encoder) Start(ctx context.Context, b board.Board) { - encoderChannel := make(chan board.Tick) - err := b.StreamTicks(e.cancelCtx, []board.DigitalInterrupt{e.I}, encoderChannel, nil) - if err != nil { - utils.Logger.Errorw("error getting interrupt ticks", "error", err) - return - } - e.activeBackgroundWorkers.Add(1) - - utils.ManagedGo(func() { - for { - select { - case <-e.cancelCtx.Done(): - return - default: - } - - select { - case <-e.cancelCtx.Done(): - return - case <-encoderChannel: - } - - if e.m != nil { - // There is a minor race condition here. Delays in interrupt processing may result in a - // DirectionMoving() value that is *currently* different from one that was used to drive - // the motor. This may result in ticks being lost or applied in the wrong direction. - dir := e.m.DirectionMoving() - if dir == 1 || dir == -1 { - atomic.AddInt64(&e.position, dir) - } - } else { - e.logger.CDebug(ctx, "received tick for encoder that isn't connected to a motor; ignoring") - } - } - }, e.activeBackgroundWorkers.Done) -} - -// Position returns the current position in terms of ticks or -// degrees, and whether it is a relative or absolute position. -func (e *Encoder) Position( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, -) (float64, encoder.PositionType, error) { - if positionType == encoder.PositionTypeDegrees { - return math.NaN(), encoder.PositionTypeUnspecified, encoder.NewPositionTypeUnsupportedError(positionType) - } - res := atomic.LoadInt64(&e.position) - return float64(res), e.positionType, nil -} - -// ResetPosition sets the current position of the motor (adjusted by a given offset). -func (e *Encoder) ResetPosition(ctx context.Context, extra map[string]interface{}) error { - offsetInt := int64(math.Round(0)) - atomic.StoreInt64(&e.position, offsetInt) - return nil -} - -// Properties returns a list of all the position types that are supported by a given encoder. -func (e *Encoder) Properties(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{ - TicksCountSupported: true, - AngleDegreesSupported: false, - }, nil -} - -// Close shuts down the Encoder. -func (e *Encoder) Close(ctx context.Context) error { - e.cancelFunc() - e.activeBackgroundWorkers.Wait() - return nil -} diff --git a/components/encoder/single/single_encoder_test.go b/components/encoder/single/single_encoder_test.go deleted file mode 100644 index f0d9df88d4b..00000000000 --- a/components/encoder/single/single_encoder_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package single - -import ( - "context" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testBoardName = "main" - testPinName = "10" -) - -func TestConfig(t *testing.T) { - ctx := context.Background() - - b := MakeBoard(t, testBoardName, testPinName) - - deps := make(resource.Dependencies) - deps[board.Named(testBoardName)] = b - - t.Run("valid config", func(t *testing.T) { - ic := Config{ - BoardName: testBoardName, - Pins: Pin{I: testPinName}, - } - - rawcfg := resource.Config{Name: "enc1", ConvertedAttributes: &ic} - - _, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - - test.That(t, err, test.ShouldBeNil) - }) - t.Run("invalid config", func(t *testing.T) { - ic := Config{ - BoardName: "pi", - // Pins intentionally missing - } - - rawcfg := resource.Config{Name: "enc1", ConvertedAttributes: &ic} - - _, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldNotBeNil) - }) -} - -func TestEncoder(t *testing.T) { - ctx := context.Background() - - b := MakeBoard(t, testBoardName, testPinName) - i, err := b.DigitalInterruptByName(testPinName) - test.That(t, err, test.ShouldBeNil) - ii, ok := i.(*inject.DigitalInterrupt) - test.That(t, ok, test.ShouldBeTrue) - - deps := make(resource.Dependencies) - deps[board.Named(testBoardName)] = b - - ic := Config{ - BoardName: "main", - Pins: Pin{I: testPinName}, - } - - rawcfg := resource.Config{Name: "enc1", ConvertedAttributes: &ic} - - t.Run("run forward", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - m := &FakeDir{1} // forward - enc2.AttachDirectionalAwareness(m) - - err = ii.Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, 1) - }) - }) - - t.Run("run backward", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - m := &FakeDir{-1} // backward - enc2.AttachDirectionalAwareness(m) - - err = ii.Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, -1) - }) - }) - - // this test ensures that digital interrupts are ignored if AttachDirectionalAwareness - // is never called - t.Run("run no direction", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - err = ii.Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - // Give the tick time to propagate to encoder - // Warning: theres a race condition if the tick has not been processed - // by the encoder worker - time.Sleep(50 * time.Millisecond) - - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, ticks, test.ShouldEqual, 0) - }) - - t.Run("reset position", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - // reset position to 0 - err = enc.ResetPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, ticks, test.ShouldEqual, 0) - }) - - t.Run("reset position and tick", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - m := &FakeDir{1} // forward - enc2.AttachDirectionalAwareness(m) - - // move forward - err = ii.Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, 1) - }) - - // reset tick - err = enc.ResetPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, ticks, test.ShouldEqual, 0) - - // now tick up again - err = ii.Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, _, err := enc.Position(context.Background(), encoder.PositionTypeUnspecified, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, 1) - }) - }) - t.Run("specify correct position type", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - m := &FakeDir{1} // forward - enc2.AttachDirectionalAwareness(m) - - err = ii.Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - ticks, positionType, err := enc.Position(context.Background(), encoder.PositionTypeTicks, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, ticks, test.ShouldEqual, 1) - test.That(tb, positionType, test.ShouldEqual, encoder.PositionTypeTicks) - }) - }) - t.Run("specify wrong position type", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - m := &FakeDir{-1} // backward - enc2.AttachDirectionalAwareness(m) - - err = ii.Tick(context.Background(), true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - _, _, err := enc.Position(context.Background(), encoder.PositionTypeDegrees, nil) - test.That(tb, err.Error(), test.ShouldContainSubstring, "encoder does not support") - test.That(tb, err.Error(), test.ShouldContainSubstring, "degrees") - }) - }) - t.Run("get properties", func(t *testing.T) { - enc, err := NewSingleEncoder(ctx, deps, rawcfg, logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - enc2 := enc.(*Encoder) - defer enc2.Close(context.Background()) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - props, err := enc.Properties(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, props.TicksCountSupported, test.ShouldBeTrue) - test.That(tb, props.AngleDegreesSupported, test.ShouldBeFalse) - }) - }) -} - -func MakeBoard(t *testing.T, name, pinname string) board.Board { - b := inject.NewBoard(name) - i := &inject.DigitalInterrupt{} - callbacks := make(map[board.DigitalInterrupt]chan board.Tick) - i.NameFunc = func() string { - return testPinName - } - i.TickFunc = func(ctx context.Context, high bool, nanoseconds uint64) error { - ch, ok := callbacks[i] - test.That(t, ok, test.ShouldBeTrue) - ch <- board.Tick{Name: i.Name(), High: high, TimestampNanosec: nanoseconds} - return nil - } - - b.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - return i, nil - } - b.StreamTicksFunc = func( - ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, extra map[string]interface{}, - ) error { - for _, i := range interrupts { - callbacks[i] = ch - } - - return nil - } - - return b -} - -type FakeDir struct { - dir int -} - -func (f *FakeDir) DirectionMoving() int64 { - return int64(f.dir) -} diff --git a/components/gantry/client.go b/components/gantry/client.go deleted file mode 100644 index 1774462308e..00000000000 --- a/components/gantry/client.go +++ /dev/null @@ -1,146 +0,0 @@ -// Package gantry contains a gRPC based gantry client. -package gantry - -import ( - "context" - - pb "go.viam.com/api/component/gantry/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" -) - -// client implements GantryServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.GantryServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Gantry, error) { - c := pb.NewGantryServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - }, nil -} - -func (c *client) Position(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetPosition(ctx, &pb.GetPositionRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - return resp.PositionsMm, nil -} - -func (c *client) Lengths(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - lengths, err := c.client.GetLengths(ctx, &pb.GetLengthsRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - return lengths.LengthsMm, nil -} - -func (c *client) Home(ctx context.Context, extra map[string]interface{}) (bool, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return false, err - } - homed, err := c.client.Home(ctx, &pb.HomeRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return false, err - } - return homed.Homed, nil -} - -func (c *client) MoveToPosition(ctx context.Context, positionsMm, speedsMmPerSec []float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.MoveToPosition(ctx, &pb.MoveToPositionRequest{ - Name: c.name, - PositionsMm: positionsMm, - SpeedsMmPerSec: speedsMmPerSec, - Extra: ext, - }) - return err -} - -func (c *client) Stop(ctx context.Context, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.Stop(ctx, &pb.StopRequest{Name: c.name, Extra: ext}) - return err -} - -func (c *client) ModelFrame() referenceframe.Model { - // TODO(erh): this feels wrong - return nil -} - -func (c *client) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - res, err := c.Position(ctx, nil) - if err != nil { - return nil, err - } - return referenceframe.FloatsToInputs(res), nil -} - -func (c *client) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - for _, goal := range inputSteps { - speeds := []float64{} - err := c.MoveToPosition(ctx, referenceframe.InputsToFloats(goal), speeds, nil) - if err != nil { - return err - } - } - return nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} - -func (c *client) IsMoving(ctx context.Context) (bool, error) { - resp, err := c.client.IsMoving(ctx, &pb.IsMovingRequest{Name: c.name}) - if err != nil { - return false, err - } - return resp.IsMoving, nil -} diff --git a/components/gantry/client_test.go b/components/gantry/client_test.go deleted file mode 100644 index ade2e1388d2..00000000000 --- a/components/gantry/client_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package gantry_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/gantry" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - var gantryPos []float64 - var gantrySpeed []float64 - - pos1 := []float64{1.0, 2.0, 3.0} - len1 := []float64{2.0, 3.0, 4.0} - var extra1 map[string]interface{} - injectGantry := &inject.Gantry{} - injectGantry.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - extra1 = extra - return pos1, nil - } - injectGantry.MoveToPositionFunc = func(ctx context.Context, pos, speed []float64, extra map[string]interface{}) error { - gantryPos = pos - gantrySpeed = speed - extra1 = extra - return nil - } - injectGantry.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - extra1 = extra - return len1, nil - } - injectGantry.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - extra1 = extra - return errStopFailed - } - injectGantry.HomeFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - extra1 = extra - return true, nil - } - - pos2 := []float64{4.0, 5.0, 6.0} - speed2 := []float64{100.0, 80.0, 120.0} - len2 := []float64{5.0, 6.0, 7.0} - var extra2 map[string]interface{} - injectGantry2 := &inject.Gantry{} - injectGantry2.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - extra2 = extra - return pos2, nil - } - injectGantry2.MoveToPositionFunc = func(ctx context.Context, pos, speed []float64, extra map[string]interface{}) error { - gantryPos = pos - gantrySpeed = speed - extra2 = extra - return nil - } - injectGantry2.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - extra2 = extra - return len2, nil - } - injectGantry2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - extra2 = extra - return nil - } - injectGantry2.HomeFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - extra2 = extra - return false, errHomingFailed - } - - gantrySvc, err := resource.NewAPIResourceCollection( - gantry.API, - (map[resource.Name]gantry.Gantry{gantry.Named(testGantryName): injectGantry, gantry.Named(testGantryName2): injectGantry2}), - ) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[gantry.Gantry](gantry.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, gantrySvc), test.ShouldBeNil) - - injectGantry.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - // working - t.Run("gantry client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - gantry1Client, err := gantry.NewClientFromConn(context.Background(), conn, "", gantry.Named(testGantryName), logger) - test.That(t, err, test.ShouldBeNil) - - // DoCommand - resp, err := gantry1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - pos, err := gantry1Client.Position(context.Background(), map[string]interface{}{"foo": 123, "bar": "234"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldResemble, pos1) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 123., "bar": "234"}) - - err = gantry1Client.MoveToPosition(context.Background(), pos2, speed2, map[string]interface{}{"foo": 234, "bar": "345"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, gantryPos, test.ShouldResemble, pos2) - test.That(t, gantrySpeed, test.ShouldResemble, speed2) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 234., "bar": "345"}) - - lens, err := gantry1Client.Lengths(context.Background(), map[string]interface{}{"foo": 345, "bar": "456"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, lens, test.ShouldResemble, len1) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 345., "bar": "456"}) - - homed, err := gantry1Client.Home(context.Background(), map[string]interface{}{"foo": 345, "bar": "456"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, homed, test.ShouldBeTrue) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 345., "bar": "456"}) - - err = gantry1Client.Stop(context.Background(), map[string]interface{}{"foo": 456, "bar": "567"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopFailed.Error()) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 456., "bar": "567"}) - - test.That(t, gantry1Client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("gantry client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", gantry.Named(testGantryName2), logger) - test.That(t, err, test.ShouldBeNil) - - pos, err := client2.Position(context.Background(), map[string]interface{}{"foo": "123", "bar": 234}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldResemble, pos2) - test.That(t, extra2, test.ShouldResemble, map[string]interface{}{"foo": "123", "bar": 234.}) - - homed, err := client2.Home(context.Background(), map[string]interface{}{"foo": 345, "bar": "456"}) - test.That(t, err.Error(), test.ShouldContainSubstring, errHomingFailed.Error()) - test.That(t, homed, test.ShouldBeFalse) - test.That(t, extra2, test.ShouldResemble, map[string]interface{}{"foo": 345., "bar": "456"}) - - err = client2.Stop(context.Background(), map[string]interface{}{"foo": "234", "bar": 345}) - test.That(t, err, test.ShouldBeNil) - test.That(t, extra2, test.ShouldResemble, map[string]interface{}{"foo": "234", "bar": 345.}) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/gantry/collectors.go b/components/gantry/collectors.go deleted file mode 100644 index a66758a42bf..00000000000 --- a/components/gantry/collectors.go +++ /dev/null @@ -1,94 +0,0 @@ -package gantry - -import ( - "context" - "errors" - - pb "go.viam.com/api/component/gantry/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" -) - -type method int64 - -const ( - position method = iota - lengths -) - -func (m method) String() string { - switch m { - case position: - return "Position" - case lengths: - return "Lengths" - } - return "Unknown" -} - -// newPositionCollector returns a collector to register a position method. If one is already registered -// with the same MethodMetadata it will panic. -func newPositionCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - gantry, err := assertGantry(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - v, err := gantry.Position(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, position.String(), err) - } - return pb.GetPositionResponse{ - PositionsMm: scaleMetersToMm(v), - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newLengthsCollector returns a collector to register a lengths method. If one is already registered -// with the same MethodMetadata it will panic. -func newLengthsCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - gantry, err := assertGantry(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - v, err := gantry.Lengths(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, lengths.String(), err) - } - return pb.GetLengthsResponse{ - LengthsMm: scaleMetersToMm(v), - }, nil - }) - return data.NewCollector(cFunc, params) -} - -func scaleMetersToMm(meters []float64) []float64 { - ret := make([]float64, len(meters)) - for i := range ret { - ret[i] = meters[i] * 1000 - } - return ret -} - -func assertGantry(resource interface{}) (Gantry, error) { - gantry, ok := resource.(Gantry) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - return gantry, nil -} diff --git a/components/gantry/collectors_test.go b/components/gantry/collectors_test.go deleted file mode 100644 index 0025aada4c2..00000000000 --- a/components/gantry/collectors_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package gantry_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - pb "go.viam.com/api/component/gantry/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/data" - "go.viam.com/rdk/logging" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - componentName = "gantry" - captureInterval = time.Second - numRetries = 5 -) - -var floatList = []float64{1.0, 2.0, 3.0} - -func TestGantryCollectors(t *testing.T) { - tests := []struct { - name string - collector data.CollectorConstructor - expected map[string]any - }{ - { - name: "Length collector should write a lengths response", - collector: gantry.NewLengthsCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetLengthsResponse{ - LengthsMm: scaleMetersToMm(floatList), - }), - }, - { - name: "Position collector should write a list of positions", - collector: gantry.NewPositionCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetPositionResponse{ - PositionsMm: scaleMetersToMm(floatList), - }), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: componentName, - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Clock: mockClock, - Target: &buf, - } - - gantry := newGantry() - col, err := tc.collector(gantry, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, tc.expected) - }) - } -} - -func newGantry() gantry.Gantry { - g := &inject.Gantry{} - g.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return floatList, nil - } - g.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return floatList, nil - } - return g -} - -func scaleMetersToMm(meters []float64) []float64 { - ret := make([]float64, len(meters)) - for i := range ret { - ret[i] = meters[i] * 1000 - } - return ret -} diff --git a/components/gantry/export_collectors_test.go b/components/gantry/export_collectors_test.go deleted file mode 100644 index a514398e5d6..00000000000 --- a/components/gantry/export_collectors_test.go +++ /dev/null @@ -1,8 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package gantry - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var ( - NewPositionCollector = newPositionCollector - NewLengthsCollector = newLengthsCollector -) diff --git a/components/gantry/fake/gantry.go b/components/gantry/fake/gantry.go deleted file mode 100644 index 7346f2e19e0..00000000000 --- a/components/gantry/fake/gantry.go +++ /dev/null @@ -1,123 +0,0 @@ -// Package fake implements a fake gantry. -package fake - -import ( - "context" - "fmt" - - "github.com/golang/geo/r3" - - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" -) - -func init() { - resource.RegisterComponent( - gantry.API, - resource.DefaultModelFamily.WithModel("fake"), - resource.Registration[gantry.Gantry, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (gantry.Gantry, error) { - return NewGantry(conf.ResourceName(), logger), nil - }, - }) -} - -// NewGantry returns a new fake gantry. -func NewGantry(name resource.Name, logger logging.Logger) gantry.Gantry { - return &Gantry{ - testutils.NewUnimplementedResource(name), - resource.TriviallyReconfigurable{}, - resource.TriviallyCloseable{}, - []float64{1.2}, - []float64{120}, - []float64{5}, - 2, - r3.Vector{X: 1, Y: 0, Z: 0}, - logger, - } -} - -// Gantry is a fake gantry that can simply read and set properties. -type Gantry struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - positionsMm []float64 - speedsMmPerSec []float64 - lengths []float64 - lengthMeters float64 - frame r3.Vector - logger logging.Logger -} - -// Position returns the position in meters. -func (g *Gantry) Position(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return g.positionsMm, nil -} - -// Lengths returns the position in meters. -func (g *Gantry) Lengths(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return g.lengths, nil -} - -// Home runs the homing sequence of the gantry and returns true once completed. -func (g *Gantry) Home(ctx context.Context, extra map[string]interface{}) (bool, error) { - g.logger.CInfo(ctx, "homing") - return true, nil -} - -// MoveToPosition is in meters. -func (g *Gantry) MoveToPosition(ctx context.Context, positionsMm, speedsMmPerSec []float64, extra map[string]interface{}) error { - g.positionsMm = positionsMm - g.speedsMmPerSec = speedsMmPerSec - return nil -} - -// Stop doesn't do anything for a fake gantry. -func (g *Gantry) Stop(ctx context.Context, extra map[string]interface{}) error { - return nil -} - -// IsMoving is always false for a fake gantry. -func (g *Gantry) IsMoving(ctx context.Context) (bool, error) { - return false, nil -} - -// ModelFrame returns a Gantry frame. -func (g *Gantry) ModelFrame() referenceframe.Model { - m := referenceframe.NewSimpleModel("") - f, err := referenceframe.NewTranslationalFrame(g.Name().ShortName(), g.frame, referenceframe.Limit{0, g.lengthMeters}) - if err != nil { - panic(fmt.Errorf("error creating frame: %w", err)) - } - m.OrdTransforms = append(m.OrdTransforms, f) - return m -} - -// CurrentInputs returns positions in the Gantry frame model.. -func (g *Gantry) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - res, err := g.Position(ctx, nil) - if err != nil { - return nil, err - } - return referenceframe.FloatsToInputs(res), nil -} - -// GoToInputs moves using the Gantry frames.. -func (g *Gantry) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - for _, goal := range inputSteps { - err := g.MoveToPosition(ctx, referenceframe.InputsToFloats(goal), g.speedsMmPerSec, nil) - if err != nil { - return err - } - } - return nil -} diff --git a/components/gantry/gantry.go b/components/gantry/gantry.go deleted file mode 100644 index 2eba1026638..00000000000 --- a/components/gantry/gantry.go +++ /dev/null @@ -1,121 +0,0 @@ -package gantry - -import ( - "context" - - pb "go.viam.com/api/component/gantry/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Gantry]{ - Status: resource.StatusFunc(CreateStatus), - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterGantryServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.GantryService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: position.String(), - }, newPositionCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: lengths.String(), - }, newLengthsCollector) -} - -// SubtypeName is a constant that identifies the component resource API string "gantry". -const SubtypeName = "gantry" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named Gantry's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// Gantry is used for controlling gantries of N axis. -type Gantry interface { - resource.Resource - resource.Actuator - referenceframe.ModelFramer - referenceframe.InputEnabled - - // Position returns the position in meters - // - // myGantry, err := gantry.FromRobot(machine, "my_gantry") - // - // // Get the current positions of the axes of the gantry in millimeters. - // position, err := myGantry.Position(context.Background(), nil) - Position(ctx context.Context, extra map[string]interface{}) ([]float64, error) - - // MoveToPosition is in meters - // This will block until done or a new operation cancels this one - // - // myGantry, err := gantry.FromRobot(machine, "my_gantry") - // - // // Create a list of positions for the axes of the gantry to move to. - // // Assume in this example that the gantry is multi-axis, with 3 axes. - // examplePositions := []float64{1, 2, 3} - // - // exampleSpeeds := []float64{3, 9, 12} - // - // // Move the axes of the gantry to the positions specified. - // myGantry.MoveToPosition(context.Background(), examplePositions, exampleSpeeds, nil) - MoveToPosition(ctx context.Context, positionsMm, speedsMmPerSec []float64, extra map[string]interface{}) error - - // Lengths is the length of gantries in meters - // - // myGantry, err := gantry.FromRobot(machine, "my_gantry") - // - // // Get the lengths of the axes of the gantry in millimeters. - // lengths_mm, err := myGantry.Lengths(context.Background(), nil) - Lengths(ctx context.Context, extra map[string]interface{}) ([]float64, error) - - // Home runs the homing sequence of the gantry and returns true once completed - // - // myGantry, err := gantry.FromRobot(machine, "my_gantry") - // - // myGantry.Home(context.Background(), nil) - Home(ctx context.Context, extra map[string]interface{}) (bool, error) -} - -// FromDependencies is a helper for getting the named gantry from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Gantry, error) { - return resource.FromDependencies[Gantry](deps, Named(name)) -} - -// FromRobot is a helper for getting the named gantry from the given Robot. -func FromRobot(r robot.Robot, name string) (Gantry, error) { - return robot.ResourceFromRobot[Gantry](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all gantry names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// CreateStatus creates a status from the gantry. -func CreateStatus(ctx context.Context, g Gantry) (*pb.Status, error) { - positions, err := g.Position(ctx, nil) - if err != nil { - return nil, err - } - - lengths, err := g.Lengths(ctx, nil) - if err != nil { - return nil, err - } - isMoving, err := g.IsMoving(ctx) - if err != nil { - return nil, err - } - return &pb.Status{PositionsMm: positions, LengthsMm: lengths, IsMoving: isMoving}, nil -} diff --git a/components/gantry/gantry_test.go b/components/gantry/gantry_test.go deleted file mode 100644 index 57b7382255b..00000000000 --- a/components/gantry/gantry_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package gantry_test - -import ( - "context" - "testing" - - "github.com/go-viper/mapstructure/v2" - "github.com/pkg/errors" - pb "go.viam.com/api/component/gantry/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testGantryName = "gantry1" - testGantryName2 = "gantry2" - failGantryName = "gantry3" - missingGantryName = "gantry4" -) - -func TestStatusValid(t *testing.T) { - status := &pb.Status{ - PositionsMm: []float64{1.1, 2.2, 3.3}, - LengthsMm: []float64{4.4, 5.5, 6.6}, - IsMoving: true, - } - newStruct, err := protoutils.StructToStructPb(status) - test.That(t, err, test.ShouldBeNil) - test.That( - t, - newStruct.AsMap(), - test.ShouldResemble, - map[string]interface{}{ - "lengths_mm": []interface{}{4.4, 5.5, 6.6}, - "positions_mm": []interface{}{1.1, 2.2, 3.3}, - "is_moving": true, - }, - ) - - convMap := &pb.Status{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(newStruct.AsMap()) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, status) -} - -func TestCreateStatus(t *testing.T) { - status := &pb.Status{ - PositionsMm: []float64{1.1, 2.2, 3.3}, - LengthsMm: []float64{4.4, 5.5, 6.6}, - IsMoving: true, - } - - injectGantry := &inject.Gantry{} - injectGantry.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return status.PositionsMm, nil - } - injectGantry.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return status.LengthsMm, nil - } - injectGantry.IsMovingFunc = func(context.Context) (bool, error) { - return true, nil - } - - t.Run("working", func(t *testing.T) { - status1, err := gantry.CreateStatus(context.Background(), injectGantry) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - - resourceAPI, ok, err := resource.LookupAPIRegistration[gantry.Gantry](gantry.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - status2, err := resourceAPI.Status(context.Background(), injectGantry) - test.That(t, err, test.ShouldBeNil) - test.That(t, status2, test.ShouldResemble, status) - }) - - t.Run("not moving", func(t *testing.T) { - injectGantry.IsMovingFunc = func(context.Context) (bool, error) { - return false, nil - } - - status2 := &pb.Status{ - PositionsMm: []float64{1.1, 2.2, 3.3}, - LengthsMm: []float64{4.4, 5.5, 6.6}, - IsMoving: false, - } - status1, err := gantry.CreateStatus(context.Background(), injectGantry) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status2) - }) - - t.Run("fail on Lengths", func(t *testing.T) { - errFail := errors.New("can't get lengths") - injectGantry.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return nil, errFail - } - _, err := gantry.CreateStatus(context.Background(), injectGantry) - test.That(t, err, test.ShouldBeError, errFail) - }) - - t.Run("fail on Positions", func(t *testing.T) { - errFail := errors.New("can't get positions") - injectGantry.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return nil, errFail - } - _, err := gantry.CreateStatus(context.Background(), injectGantry) - test.That(t, err, test.ShouldBeError, errFail) - }) -} diff --git a/components/gantry/multiaxis/multiaxis.go b/components/gantry/multiaxis/multiaxis.go deleted file mode 100644 index dd82e88e24c..00000000000 --- a/components/gantry/multiaxis/multiaxis.go +++ /dev/null @@ -1,255 +0,0 @@ -// Package multiaxis implements a multi-axis gantry. -package multiaxis - -import ( - "context" - "sync" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - rdkutils "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("multi-axis") - -// Config is used for converting multiAxis config attributes. -type Config struct { - SubAxes []string `json:"subaxes_list"` - MoveSimultaneously *bool `json:"move_simultaneously,omitempty"` -} - -type multiAxis struct { - resource.Named - resource.AlwaysRebuild - subAxes []gantry.Gantry - lengthsMm []float64 - logger logging.Logger - moveSimultaneously bool - model referenceframe.Model - opMgr *operation.SingleOperationManager - workers sync.WaitGroup -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - - if len(conf.SubAxes) == 0 { - return nil, resource.NewConfigValidationError(path, errors.New("need at least one axis")) - } - - deps = append(deps, conf.SubAxes...) - return deps, nil -} - -func init() { - resource.RegisterComponent(gantry.API, model, resource.Registration[gantry.Gantry, *Config]{ - Constructor: newMultiAxis, - }) -} - -// NewMultiAxis creates a new-multi axis gantry. -func newMultiAxis( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (gantry.Gantry, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - mAx := &multiAxis{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - - for _, s := range newConf.SubAxes { - subAx, err := gantry.FromDependencies(deps, s) - if err != nil { - return nil, errors.Wrapf(err, "no axes named [%s]", s) - } - mAx.subAxes = append(mAx.subAxes, subAx) - } - - mAx.moveSimultaneously = false - if newConf.MoveSimultaneously != nil { - mAx.moveSimultaneously = *newConf.MoveSimultaneously - } - - mAx.lengthsMm, err = mAx.Lengths(ctx, nil) - if err != nil { - return nil, err - } - - return mAx, nil -} - -// Home runs the homing sequence of the gantry and returns true once completed. -func (g *multiAxis) Home(ctx context.Context, extra map[string]interface{}) (bool, error) { - for _, subAx := range g.subAxes { - homed, err := subAx.Home(ctx, nil) - if err != nil { - return false, err - } - if !homed { - return false, nil - } - } - return true, nil -} - -// MoveToPosition moves along an axis using inputs in millimeters. -func (g *multiAxis) MoveToPosition(ctx context.Context, positions, speeds []float64, extra map[string]interface{}) error { - ctx, done := g.opMgr.New(ctx) - defer done() - - if len(positions) == 0 { - return errors.Errorf("need position inputs for %v-axis gantry, have %v positions", len(g.subAxes), len(positions)) - } - - if len(positions) != len(g.lengthsMm) { - return errors.Errorf( - "number of input positions %v does not match total gantry axes count %v", - len(positions), len(g.lengthsMm), - ) - } - - fs := []rdkutils.SimpleFunc{} - idx := 0 - for _, subAx := range g.subAxes { - subAxNum, err := subAx.Lengths(ctx, extra) - if err != nil { - return err - } - - pos := positions[idx : idx+len(subAxNum)] - var speed []float64 - // if speeds is an empty list, speed will be set to the default in the subAx MoveToPosition call - if len(speeds) == 0 { - speed = []float64{} - } else { - speed = speeds[idx : idx+len(subAxNum)] - } - idx += len(subAxNum) - - if g.moveSimultaneously { - singleGantry := subAx - fs = append(fs, func(ctx context.Context) error { return singleGantry.MoveToPosition(ctx, pos, speed, nil) }) - } else { - err = subAx.MoveToPosition(ctx, pos, speed, extra) - if err != nil && !errors.Is(err, context.Canceled) { - return err - } - } - } - if g.moveSimultaneously { - if _, err := rdkutils.RunInParallel(ctx, fs); err != nil { - return multierr.Combine(err, g.Stop(ctx, nil)) - } - } - return nil -} - -// GoToInputs moves the gantry to a goal position in the Gantry frame. -func (g *multiAxis) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - for _, goal := range inputSteps { - if len(g.subAxes) == 0 { - return errors.New("no subaxes found for inputs") - } - - // MoveToPosition will use the default gantry speed when an empty float is passed in - speeds := []float64{} - err := g.MoveToPosition(ctx, referenceframe.InputsToFloats(goal), speeds, nil) - if err != nil { - return err - } - } - return nil -} - -// Position returns the position in millimeters. -func (g *multiAxis) Position(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - positions := []float64{} - for _, subAx := range g.subAxes { - pos, err := subAx.Position(ctx, extra) - if err != nil { - return nil, err - } - positions = append(positions, pos...) - } - return positions, nil -} - -// Lengths returns the physical lengths of all axes of a multi-axis Gantry. -func (g *multiAxis) Lengths(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - lengths := []float64{} - for _, subAx := range g.subAxes { - lng, err := subAx.Lengths(ctx, extra) - if err != nil { - return nil, err - } - lengths = append(lengths, lng...) - } - return lengths, nil -} - -// Stop stops the subaxes of the gantry simultaneously. -func (g *multiAxis) Stop(ctx context.Context, extra map[string]interface{}) error { - ctx, done := g.opMgr.New(ctx) - defer done() - for _, subAx := range g.subAxes { - currG := subAx - g.workers.Add(1) - utils.ManagedGo(func() { - if err := currG.Stop(ctx, extra); err != nil { - g.logger.CErrorw(ctx, "failed to stop subaxis", "error", err) - } - }, g.workers.Done) - } - return nil -} - -// Close calls stop. -func (g *multiAxis) Close(ctx context.Context) error { - return g.Stop(ctx, nil) -} - -// IsMoving returns whether the gantry is moving. -func (g *multiAxis) IsMoving(ctx context.Context) (bool, error) { - return g.opMgr.OpRunning(), nil -} - -// CurrentInputs returns the current inputs of the Gantry frame. -func (g *multiAxis) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - if len(g.subAxes) == 0 { - return nil, errors.New("no subaxes found for inputs") - } - positions, err := g.Position(ctx, nil) - if err != nil { - return nil, err - } - - return referenceframe.FloatsToInputs(positions), nil -} - -// ModelFrame returns the frame model of the Gantry. -func (g *multiAxis) ModelFrame() referenceframe.Model { - if g.model == nil { - model := referenceframe.NewSimpleModel("") - for _, subAx := range g.subAxes { - model.OrdTransforms = append(model.OrdTransforms, subAx.ModelFrame()) - } - g.model = model - } - return g.model -} diff --git a/components/gantry/multiaxis/multiaxis_test.go b/components/gantry/multiaxis/multiaxis_test.go deleted file mode 100644 index 9347918842e..00000000000 --- a/components/gantry/multiaxis/multiaxis_test.go +++ /dev/null @@ -1,436 +0,0 @@ -package multiaxis - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/components/motor" - fm "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" - rutils "go.viam.com/rdk/utils" -) - -func createFakeOneaAxis(length float64, positions []float64) *inject.Gantry { - fakesingleaxis := inject.NewGantry("fake") - fakesingleaxis.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return positions, nil - } - fakesingleaxis.MoveToPositionFunc = func(ctx context.Context, pos, speed []float64, extra map[string]interface{}) error { - return nil - } - fakesingleaxis.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return []float64{length}, nil - } - fakesingleaxis.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - fakesingleaxis.HomeFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return true, nil - } - fakesingleaxis.CloseFunc = func(ctx context.Context) error { - return nil - } - fakesingleaxis.ModelFrameFunc = func() referenceframe.Model { - return nil - } - return fakesingleaxis -} - -func createFakeDeps() resource.Dependencies { - fakeGantry1 := inject.NewGantry("1") - fakeGantry1.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return []float64{1}, nil - } - fakeGantry2 := inject.NewGantry("2") - fakeGantry2.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return []float64{1}, nil - } - fakeGantry3 := inject.NewGantry("3") - fakeGantry3.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return []float64{1}, nil - } - fakeMotor := &fm.Motor{ - Named: motor.Named("fm1").AsNamed(), - } - - deps := make(resource.Dependencies) - deps[fakeGantry1.Name()] = fakeGantry1 - deps[fakeGantry2.Name()] = fakeGantry2 - deps[fakeGantry3.Name()] = fakeGantry3 - deps[fakeMotor.Name()] = fakeMotor - return deps -} - -var threeAxes = []gantry.Gantry{ - createFakeOneaAxis(1, []float64{1}), - createFakeOneaAxis(2, []float64{5}), - createFakeOneaAxis(3, []float64{9}), -} - -var twoAxes = []gantry.Gantry{ - createFakeOneaAxis(5, []float64{1}), - createFakeOneaAxis(6, []float64{5}), -} - -func TestValidate(t *testing.T) { - fakecfg := &Config{SubAxes: []string{}} - _, err := fakecfg.Validate("path") - test.That(t, err.Error(), test.ShouldContainSubstring, "need at least one axis") - - fakecfg = &Config{SubAxes: []string{"singleaxis"}} - _, err = fakecfg.Validate("path") - test.That(t, err, test.ShouldBeNil) -} - -func TestNewMultiAxis(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - deps := createFakeDeps() - - fakeMultAxcfg := resource.Config{ - Name: "gantry", - ConvertedAttributes: &Config{ - SubAxes: []string{"1", "2", "3"}, - }, - } - fmag, err := newMultiAxis(ctx, deps, fakeMultAxcfg, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, fmag, test.ShouldNotBeNil) - fakemulax, ok := fmag.(*multiAxis) - test.That(t, ok, test.ShouldBeTrue) - lenfloat := []float64{1, 1, 1} - test.That(t, fakemulax.lengthsMm, test.ShouldResemble, lenfloat) - - fakeMultAxcfg = resource.Config{ - Name: "gantry", - Attributes: rutils.AttributeMap{ - "subaxes_list": []string{}, - }, - } - _, err = newMultiAxis(ctx, deps, fakeMultAxcfg, logger) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestMoveToPosition(t *testing.T) { - ctx := context.Background() - positions := []float64{} - speeds := []float64{} - - fakemultiaxis := &multiAxis{ - opMgr: operation.NewSingleOperationManager(), - } - err := fakemultiaxis.MoveToPosition(ctx, positions, speeds, nil) - test.That(t, err, test.ShouldNotBeNil) - - fakemultiaxis = &multiAxis{ - subAxes: threeAxes, - lengthsMm: []float64{1, 2, 3}, - opMgr: operation.NewSingleOperationManager(), - } - positions = []float64{1, 2, 3} - speeds = []float64{100, 200, 300} - err = fakemultiaxis.MoveToPosition(ctx, positions, speeds, nil) - test.That(t, err, test.ShouldBeNil) - - fakemultiaxis = &multiAxis{ - subAxes: twoAxes, - lengthsMm: []float64{1, 2}, - opMgr: operation.NewSingleOperationManager(), - } - positions = []float64{1, 2} - speeds = []float64{100, 200} - err = fakemultiaxis.MoveToPosition(ctx, positions, speeds, nil) - test.That(t, err, test.ShouldBeNil) -} - -func TestGoToInputs(t *testing.T) { - ctx := context.Background() - inputs := []referenceframe.Input{} - - fakemultiaxis := &multiAxis{ - opMgr: operation.NewSingleOperationManager(), - } - err := fakemultiaxis.GoToInputs(ctx, inputs) - test.That(t, err, test.ShouldNotBeNil) - - fakemultiaxis = &multiAxis{ - subAxes: threeAxes, - lengthsMm: []float64{1, 2, 3}, - opMgr: operation.NewSingleOperationManager(), - } - inputs = []referenceframe.Input{{Value: 1}, {Value: 2}, {Value: 3}} - err = fakemultiaxis.GoToInputs(ctx, inputs) - test.That(t, err, test.ShouldBeNil) - - fakemultiaxis = &multiAxis{ - subAxes: twoAxes, - lengthsMm: []float64{1, 2}, - opMgr: operation.NewSingleOperationManager(), - } - inputs = []referenceframe.Input{{Value: 1}, {Value: 2}} - err = fakemultiaxis.GoToInputs(ctx, inputs) - test.That(t, err, test.ShouldBeNil) -} - -func TestPosition(t *testing.T) { - ctx := context.Background() - - fakemultiaxis := &multiAxis{ - subAxes: threeAxes, - opMgr: operation.NewSingleOperationManager(), - } - pos, err := fakemultiaxis.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldResemble, []float64{1, 5, 9}) - - fakemultiaxis = &multiAxis{ - subAxes: twoAxes, - opMgr: operation.NewSingleOperationManager(), - } - pos, err = fakemultiaxis.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldResemble, []float64{1, 5}) -} - -func TestLengths(t *testing.T) { - ctx := context.Background() - fakemultiaxis := &multiAxis{ - opMgr: operation.NewSingleOperationManager(), - } - lengths, err := fakemultiaxis.Lengths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, lengths, test.ShouldResemble, []float64{}) - - fakemultiaxis = &multiAxis{ - subAxes: threeAxes, - opMgr: operation.NewSingleOperationManager(), - } - lengths, err = fakemultiaxis.Lengths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, lengths, test.ShouldResemble, []float64{1, 2, 3}) - - fakemultiaxis = &multiAxis{ - subAxes: twoAxes, - opMgr: operation.NewSingleOperationManager(), - } - - lengths, err = fakemultiaxis.Lengths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, lengths, test.ShouldResemble, []float64{5, 6}) -} - -func TestHome(t *testing.T) { - ctx := context.Background() - fakemultiaxis := &multiAxis{ - opMgr: operation.NewSingleOperationManager(), - } - homed, err := fakemultiaxis.Home(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, homed, test.ShouldBeTrue) - - fakemultiaxis = &multiAxis{ - subAxes: threeAxes, - opMgr: operation.NewSingleOperationManager(), - } - homed, err = fakemultiaxis.Home(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, homed, test.ShouldBeTrue) - - fakemultiaxis = &multiAxis{ - subAxes: twoAxes, - opMgr: operation.NewSingleOperationManager(), - } - - homed, err = fakemultiaxis.Home(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, homed, test.ShouldBeTrue) -} - -func TestStop(t *testing.T) { - ctx := context.Background() - fakemultiaxis := &multiAxis{ - opMgr: operation.NewSingleOperationManager(), - } - test.That(t, fakemultiaxis.Stop(ctx, nil), test.ShouldBeNil) - - fakemultiaxis = &multiAxis{ - subAxes: threeAxes, - opMgr: operation.NewSingleOperationManager(), - } - test.That(t, fakemultiaxis.Stop(ctx, nil), test.ShouldBeNil) - - fakemultiaxis = &multiAxis{ - subAxes: twoAxes, - opMgr: operation.NewSingleOperationManager(), - } - test.That(t, fakemultiaxis.Stop(ctx, nil), test.ShouldBeNil) -} - -func TestCurrentInputs(t *testing.T) { - ctx := context.Background() - fakemultiaxis := &multiAxis{ - opMgr: operation.NewSingleOperationManager(), - } - inputs, err := fakemultiaxis.CurrentInputs(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, inputs, test.ShouldResemble, []referenceframe.Input(nil)) - - fakemultiaxis = &multiAxis{ - subAxes: threeAxes, - opMgr: operation.NewSingleOperationManager(), - } - inputs, err = fakemultiaxis.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, inputs, test.ShouldResemble, []referenceframe.Input{{Value: 1}, {Value: 5}, {Value: 9}}) - - fakemultiaxis = &multiAxis{ - subAxes: twoAxes, - opMgr: operation.NewSingleOperationManager(), - } - inputs, err = fakemultiaxis.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, inputs, test.ShouldResemble, []referenceframe.Input{{Value: 1}, {Value: 5}}) -} - -func TestModelFrame(t *testing.T) { - fakemultiaxis := &multiAxis{ - Named: gantry.Named("foo").AsNamed(), - subAxes: twoAxes, - lengthsMm: []float64{1, 1}, - opMgr: operation.NewSingleOperationManager(), - } - model := fakemultiaxis.ModelFrame() - test.That(t, model, test.ShouldNotBeNil) - - fakemultiaxis = &multiAxis{ - Named: gantry.Named("foo").AsNamed(), - subAxes: threeAxes, - lengthsMm: []float64{1, 1, 1}, - opMgr: operation.NewSingleOperationManager(), - } - model = fakemultiaxis.ModelFrame() - test.That(t, model, test.ShouldNotBeNil) -} - -func createComplexDeps() resource.Dependencies { - position1 := []float64{6, 5} - mAx1 := inject.NewGantry("1") - mAx1.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return position1, nil - } - mAx1.MoveToPositionFunc = func(ctx context.Context, pos, speeds []float64, extra map[string]interface{}) error { - if move, _ := extra["move"].(bool); move { - position1[0] += pos[0] - position1[1] += pos[1] - } - - return nil - } - mAx1.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return []float64{100, 101}, nil - } - mAx1.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - - position2 := []float64{9, 8, 7} - mAx2 := inject.NewGantry("2") - mAx2.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return position2, nil - } - mAx2.MoveToPositionFunc = func(ctx context.Context, pos, speeds []float64, extra map[string]interface{}) error { - if move, _ := extra["move"].(bool); move { - position2[0] += pos[0] - position2[1] += pos[1] - position2[2] += pos[2] - } - return nil - } - mAx2.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return []float64{102, 103, 104}, nil - } - mAx2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - - fakeMotor := &fm.Motor{ - Named: motor.Named("foo").AsNamed(), - } - - deps := make(resource.Dependencies) - deps[mAx1.Name()] = mAx1 - deps[mAx2.Name()] = mAx2 - deps[fakeMotor.Name()] = fakeMotor - return deps -} - -func TestComplexMultiAxis(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - cfg := resource.Config{ - Name: "complexGantry", - ConvertedAttributes: &Config{ - SubAxes: []string{"1", "2"}, - }, - } - deps := createComplexDeps() - - g, err := newMultiAxis(ctx, deps, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("too many inputs", func(t *testing.T) { - err = g.MoveToPosition(ctx, []float64{1, 2, 3, 4, 5, 6}, []float64{100, 200, 300, 300, 200, 100}, nil) - test.That(t, err, test.ShouldNotBeNil) - }) - - t.Run("too few inputs", func(t *testing.T) { - err = g.MoveToPosition(ctx, []float64{1, 2, 3, 4}, []float64{100, 200, 300, 300, 200, 100}, nil) - test.That(t, err, test.ShouldNotBeNil) - }) - - t.Run("just right inputs", func(t *testing.T) { - pos, err := g.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldResemble, []float64{6, 5, 9, 8, 7}) - }) - - t.Run( - "test that multiaxis moves and each subaxes moves correctly", - func(t *testing.T) { - extra := map[string]interface{}{"move": true} - err = g.MoveToPosition(ctx, []float64{1, 2, 3, 4, 5}, []float64{100, 200, 300, 200, 100}, extra) - test.That(t, err, test.ShouldBeNil) - - pos, err := g.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldNotResemble, []float64{6, 5, 9, 8, 7}) - - // This section tests out that each subaxes has moved, and moved the correct amount - // according to it's input lengths - currG, ok := g.(*multiAxis) - test.That(t, ok, test.ShouldBeTrue) - - // This loop mimics the loop in MoveToposition to check that the correct - // positions are sent to each subaxis - idx := 0 - for _, subAx := range currG.subAxes { - lengths, err := subAx.Lengths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - subAxPos, err := subAx.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(subAxPos), test.ShouldEqual, len(lengths)) - - test.That(t, subAxPos, test.ShouldResemble, pos[idx:idx+len(lengths)]) - idx += len(lengths) - } - }) -} diff --git a/components/gantry/register/register.go b/components/gantry/register/register.go deleted file mode 100644 index 44e872cd3e7..00000000000 --- a/components/gantry/register/register.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package register registers all relevant gantries -package register - -import ( - // for gantries. - _ "go.viam.com/rdk/components/gantry/fake" - _ "go.viam.com/rdk/components/gantry/multiaxis" - _ "go.viam.com/rdk/components/gantry/singleaxis" -) diff --git a/components/gantry/server.go b/components/gantry/server.go deleted file mode 100644 index 82542634a22..00000000000 --- a/components/gantry/server.go +++ /dev/null @@ -1,120 +0,0 @@ -// Package gantry contains a gRPC based gantry service server. -package gantry - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/gantry/v1" - - "go.viam.com/rdk/operation" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// serviceServer implements the GantryService from gantry.proto. -type serviceServer struct { - pb.UnimplementedGantryServiceServer - coll resource.APIResourceCollection[Gantry] -} - -// NewRPCServiceServer constructs an gantry gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Gantry]) interface{} { - return &serviceServer{coll: coll} -} - -// GetPosition returns the position of the gantry specified. -func (s *serviceServer) GetPosition( - ctx context.Context, - req *pb.GetPositionRequest, -) (*pb.GetPositionResponse, error) { - gantry, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - pos, err := gantry.Position(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetPositionResponse{PositionsMm: pos}, nil -} - -// GetLengths gets the lengths of a gantry of the underlying robot. -func (s *serviceServer) GetLengths( - ctx context.Context, - req *pb.GetLengthsRequest, -) (*pb.GetLengthsResponse, error) { - gantry, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - lengthsMm, err := gantry.Lengths(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetLengthsResponse{LengthsMm: lengthsMm}, nil -} - -// Home runs the homing sequence of the gantry and returns true once completed. -func (s *serviceServer) Home( - ctx context.Context, - req *pb.HomeRequest, -) (*pb.HomeResponse, error) { - gantry, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - homed, err := gantry.Home(ctx, req.Extra.AsMap()) - if err != nil { - return &pb.HomeResponse{Homed: homed}, err - } - return &pb.HomeResponse{Homed: homed}, nil -} - -// MoveToPosition moves the gantry to the position specified. -func (s *serviceServer) MoveToPosition( - ctx context.Context, - req *pb.MoveToPositionRequest, -) (*pb.MoveToPositionResponse, error) { - operation.CancelOtherWithLabel(ctx, req.Name) - gantry, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return &pb.MoveToPositionResponse{}, gantry.MoveToPosition(ctx, req.PositionsMm, req.SpeedsMmPerSec, req.Extra.AsMap()) -} - -// Stop stops the gantry specified. -func (s *serviceServer) Stop(ctx context.Context, req *pb.StopRequest) (*pb.StopResponse, error) { - operation.CancelOtherWithLabel(ctx, req.Name) - gantry, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return &pb.StopResponse{}, gantry.Stop(ctx, req.Extra.AsMap()) -} - -// IsMoving queries of a component is in motion. -func (s *serviceServer) IsMoving(ctx context.Context, req *pb.IsMovingRequest) (*pb.IsMovingResponse, error) { - gantry, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - moving, err := gantry.IsMoving(ctx) - if err != nil { - return nil, err - } - return &pb.IsMovingResponse{IsMoving: moving}, nil -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - gantry, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, gantry, req) -} diff --git a/components/gantry/server_test.go b/components/gantry/server_test.go deleted file mode 100644 index 8f246813df1..00000000000 --- a/components/gantry/server_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package gantry_test - -import ( - "context" - "testing" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/gantry/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errPositionFailed = errors.New("couldn't get position") - errHomingFailed = errors.New("homing unsuccessful") - errMoveToPositionFailed = errors.New("couldn't move to position") - errLengthsFailed = errors.New("couldn't get lengths") - errStopFailed = errors.New("couldn't stop") - errGantryNotFound = errors.New("not found") -) - -func newServer() (pb.GantryServiceServer, *inject.Gantry, *inject.Gantry, error) { - injectGantry := &inject.Gantry{} - injectGantry2 := &inject.Gantry{} - gantries := map[resource.Name]gantry.Gantry{ - gantry.Named(testGantryName): injectGantry, - gantry.Named(failGantryName): injectGantry2, - } - gantrySvc, err := resource.NewAPIResourceCollection(gantry.API, gantries) - if err != nil { - return nil, nil, nil, err - } - return gantry.NewRPCServiceServer(gantrySvc).(pb.GantryServiceServer), injectGantry, injectGantry2, nil -} - -func TestServer(t *testing.T) { - gantryServer, injectGantry, injectGantry2, err := newServer() - test.That(t, err, test.ShouldBeNil) - - var gantryPos []float64 - var gantrySpeed []float64 - - pos1 := []float64{1.0, 2.0, 3.0} - speed1 := []float64{100.0, 200.0, 300.0} - len1 := []float64{2.0, 3.0, 4.0} - extra1 := map[string]interface{}{} - injectGantry.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - extra1 = extra - return pos1, nil - } - injectGantry.HomeFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - extra1 = extra - return true, nil - } - injectGantry.MoveToPositionFunc = func(ctx context.Context, pos, speed []float64, extra map[string]interface{}) error { - gantryPos = pos - gantrySpeed = speed - extra1 = extra - return nil - } - injectGantry.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - extra1 = extra - return len1, nil - } - injectGantry.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - extra1 = extra - return nil - } - - pos2 := []float64{4.0, 5.0, 6.0} - speed2 := []float64{100.0, 80.0, 120.0} - injectGantry2.PositionFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return nil, errPositionFailed - } - injectGantry2.HomeFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - extra1 = extra - return false, errHomingFailed - } - injectGantry2.MoveToPositionFunc = func(ctx context.Context, pos, speed []float64, extra map[string]interface{}) error { - gantryPos = pos - gantrySpeed = speed - return errMoveToPositionFailed - } - injectGantry2.LengthsFunc = func(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - return nil, errLengthsFailed - } - injectGantry2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopFailed - } - - //nolint:dupl - t.Run("gantry position", func(t *testing.T) { - _, err := gantryServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: missingGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGantryNotFound.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "123", "bar": 234}) - test.That(t, err, test.ShouldBeNil) - resp, err := gantryServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: testGantryName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.PositionsMm, test.ShouldResemble, pos1) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": "123", "bar": 234.}) - - _, err = gantryServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: failGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPositionFailed.Error()) - }) - - t.Run("move to position", func(t *testing.T) { - _, err := gantryServer.MoveToPosition( - context.Background(), - &pb.MoveToPositionRequest{Name: missingGantryName, PositionsMm: pos2, SpeedsMmPerSec: speed2}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGantryNotFound.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "234", "bar": 345}) - test.That(t, err, test.ShouldBeNil) - _, err = gantryServer.MoveToPosition( - context.Background(), - &pb.MoveToPositionRequest{Name: testGantryName, PositionsMm: pos2, SpeedsMmPerSec: speed2, Extra: ext}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, gantryPos, test.ShouldResemble, pos2) - test.That(t, gantrySpeed, test.ShouldResemble, speed2) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": "234", "bar": 345.}) - - _, err = gantryServer.MoveToPosition( - context.Background(), - &pb.MoveToPositionRequest{Name: failGantryName, PositionsMm: pos1, SpeedsMmPerSec: speed1}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errMoveToPositionFailed.Error()) - test.That(t, gantryPos, test.ShouldResemble, pos1) - test.That(t, gantrySpeed, test.ShouldResemble, speed1) - }) - - //nolint:dupl - t.Run("lengths", func(t *testing.T) { - _, err := gantryServer.GetLengths(context.Background(), &pb.GetLengthsRequest{Name: missingGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGantryNotFound.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": 123, "bar": "234"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gantryServer.GetLengths(context.Background(), &pb.GetLengthsRequest{Name: testGantryName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.LengthsMm, test.ShouldResemble, len1) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 123., "bar": "234"}) - - _, err = gantryServer.GetLengths(context.Background(), &pb.GetLengthsRequest{Name: failGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errLengthsFailed.Error()) - }) - - t.Run("home", func(t *testing.T) { - _, err := gantryServer.Home(context.Background(), &pb.HomeRequest{Name: missingGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGantryNotFound.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": 123, "bar": "234"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gantryServer.Home(context.Background(), &pb.HomeRequest{Name: testGantryName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Homed, test.ShouldBeTrue) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 123., "bar": "234"}) - - resp, err = gantryServer.Home(context.Background(), &pb.HomeRequest{Name: failGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp.Homed, test.ShouldBeFalse) - test.That(t, err.Error(), test.ShouldContainSubstring, errHomingFailed.Error()) - }) - - t.Run("stop", func(t *testing.T) { - _, err = gantryServer.Stop(context.Background(), &pb.StopRequest{Name: missingGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGantryNotFound.Error()) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": 234, "bar": "123"}) - test.That(t, err, test.ShouldBeNil) - _, err = gantryServer.Stop(context.Background(), &pb.StopRequest{Name: testGantryName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, extra1, test.ShouldResemble, map[string]interface{}{"foo": 234., "bar": "123"}) - - _, err = gantryServer.Stop(context.Background(), &pb.StopRequest{Name: failGantryName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopFailed.Error()) - }) -} diff --git a/components/gantry/singleaxis/singleaxis.go b/components/gantry/singleaxis/singleaxis.go deleted file mode 100644 index 34f01ef5f7e..00000000000 --- a/components/gantry/singleaxis/singleaxis.go +++ /dev/null @@ -1,645 +0,0 @@ -// Package singleaxis implements a single-axis gantry. -package singleaxis - -import ( - "context" - "fmt" - "math" - "sync" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.uber.org/multierr" - utils "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - spatial "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -var ( - model = resource.DefaultModelFamily.WithModel("single-axis") - // homingTimeout (nanoseconds) is calculated using the gantry's rpm, mmPerRevolution, and lengthMm. - homingTimeout = time.Duration(15e9) -) - -// limitErrorMargin is added or subtracted from the location of the limit switch to ensure the switch is not passed. -const limitErrorMargin = 0.25 - -// Config is used for converting singleAxis config attributes. -type Config struct { - Board string `json:"board,omitempty"` // used to read limit switch pins and control motor with gpio pins - Motor string `json:"motor"` - LimitSwitchPins []string `json:"limit_pins,omitempty"` - LimitPinEnabled *bool `json:"limit_pin_enabled_high,omitempty"` - LengthMm float64 `json:"length_mm"` - MmPerRevolution float64 `json:"mm_per_rev"` - GantryMmPerSec float64 `json:"gantry_mm_per_sec,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - - if len(cfg.Motor) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "motor") - } - deps = append(deps, cfg.Motor) - - if cfg.LengthMm <= 0 { - err := resource.NewConfigValidationFieldRequiredError(path, "length_mm") - return nil, errors.Wrap(err, "length must be non-zero and positive") - } - - if cfg.MmPerRevolution <= 0 { - err := resource.NewConfigValidationFieldRequiredError(path, "mm_per_rev") - return nil, errors.Wrap(err, "mm_per_rev must be non-zero and positive") - } - - if cfg.Board == "" && len(cfg.LimitSwitchPins) > 0 { - return nil, errors.New("gantries with limit_pins require a board to sense limit hits") - } - - if cfg.Board != "" { - deps = append(deps, cfg.Board) - } - - if len(cfg.LimitSwitchPins) == 1 && cfg.MmPerRevolution == 0 { - return nil, errors.New("the single-axis gantry has one limit switch axis, needs pulley radius to set position limits") - } - - if len(cfg.LimitSwitchPins) > 0 && cfg.LimitPinEnabled == nil { - return nil, errors.New("limit pin enabled must be set to true or false") - } - return deps, nil -} - -func init() { - resource.RegisterComponent(gantry.API, model, resource.Registration[gantry.Gantry, *Config]{ - Constructor: newSingleAxis, - }) -} - -type singleAxis struct { - resource.Named - - board board.Board - motor motor.Motor - mu sync.Mutex - - limitSwitchPins []string - limitHigh bool - positionLimits []float64 - positionRange float64 - - lengthMm float64 - mmPerRevolution float64 - rpm float64 - - model referenceframe.Model - frame r3.Vector - - cancelFunc func() - logger logging.Logger - opMgr *operation.SingleOperationManager - activeBackgroundWorkers sync.WaitGroup -} - -// newSingleAxis creates a new single axis gantry. -func newSingleAxis( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (gantry.Gantry, error) { - sAx := &singleAxis{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - - if err := sAx.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return sAx, nil -} - -func (g *singleAxis) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - if g.motor != nil { - if err := g.motor.Stop(ctx, nil); err != nil { - return err - } - } - - if g.cancelFunc != nil { - g.cancelFunc() - g.activeBackgroundWorkers.Wait() - } - - g.mu.Lock() - defer g.mu.Unlock() - - needsToReHome := false - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - // Changing these attributes does not rerun homing - g.lengthMm = newConf.LengthMm - g.mmPerRevolution = newConf.MmPerRevolution - if g.mmPerRevolution <= 0 && len(newConf.LimitSwitchPins) == 1 { - return errors.New("gantry with one limit switch per axis needs a mm_per_length ratio defined") - } - - // Add a default frame, then overwrite with the config frame if that is supplied - g.frame = r3.Vector{X: 1.0, Y: 0, Z: 0} - if conf.Frame != nil { - g.frame = conf.Frame.Translation - } - - rpm := g.gantryToMotorSpeeds(newConf.GantryMmPerSec) - g.rpm = rpm - if g.rpm == 0 { - g.logger.CWarn(ctx, "gantry_mm_per_sec not provided, defaulting to 100 motor rpm") - g.rpm = 100 - } - - // Rerun homing if the board has changed - if newConf.Board != "" { - if g.board == nil || g.board.Name().ShortName() != newConf.Board { - board, err := board.FromDependencies(deps, newConf.Board) - if err != nil { - return err - } - g.board = board - needsToReHome = true - } - } - - // Rerun homing if the motor changes - if g.motor == nil || g.motor.Name().ShortName() != newConf.Motor { - needsToReHome = true - motorDep, err := motor.FromDependencies(deps, newConf.Motor) - if err != nil { - return err - } - properties, err := motorDep.Properties(ctx, nil) - if err != nil { - return err - } - ok := properties.PositionReporting - if !ok { - return motor.NewPropertyUnsupportedError(properties, newConf.Motor) - } - g.motor = motorDep - } - - // Rerun homing if anything with the limit switch pins changes - if newConf.LimitPinEnabled != nil && len(newConf.LimitSwitchPins) != 0 { - if (len(g.limitSwitchPins) != len(newConf.LimitSwitchPins)) || (g.limitHigh != *newConf.LimitPinEnabled) { - g.limitHigh = *newConf.LimitPinEnabled - needsToReHome = true - g.limitSwitchPins = newConf.LimitSwitchPins - } else { - for i, pin := range newConf.LimitSwitchPins { - if pin != g.limitSwitchPins[i] { - g.limitSwitchPins[i] = pin - needsToReHome = true - } - } - } - } - if len(newConf.LimitSwitchPins) > 2 { - return errors.Errorf("invalid gantry type: need 1, 2 or 0 pins per axis, have %v pins", len(newConf.LimitSwitchPins)) - } - - if needsToReHome { - g.logger.CInfof(ctx, "single-axis gantry '%v' needs to re-home", g.Named.Name().ShortName()) - g.positionRange = 0 - g.positionLimits = []float64{0, 0} - } - ctx, cancelFunc := context.WithCancel(context.Background()) - g.cancelFunc = cancelFunc - g.checkHit(ctx) - - return nil -} - -// Home runs the homing sequence of the gantry, starts checkHit in the background, and returns true once completed. -func (g *singleAxis) Home(ctx context.Context, extra map[string]interface{}) (bool, error) { - if g.cancelFunc != nil { - g.cancelFunc() - g.activeBackgroundWorkers.Wait() - } - g.mu.Lock() - defer g.mu.Unlock() - homed, err := g.doHome(ctx) - if err != nil { - return homed, err - } - ctx, cancelFunc := context.WithCancel(context.Background()) - g.cancelFunc = cancelFunc - g.checkHit(ctx) - return true, nil -} - -func (g *singleAxis) checkHit(ctx context.Context) { - g.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer utils.UncheckedErrorFunc(func() error { - g.mu.Lock() - defer g.mu.Unlock() - return g.motor.Stop(ctx, nil) - }) - defer g.activeBackgroundWorkers.Done() - for { - select { - case <-ctx.Done(): - return - default: - } - - for i := 0; i < len(g.limitSwitchPins); i++ { - hit, err := g.limitHit(ctx, i) - if err != nil { - g.logger.CError(ctx, err) - } - - if hit { - child, cancel := context.WithTimeout(ctx, 10*time.Millisecond) - g.mu.Lock() - if err := g.motor.Stop(ctx, nil); err != nil { - g.logger.CError(ctx, err) - } - g.mu.Unlock() - <-child.Done() - cancel() - g.mu.Lock() - if err := g.moveAway(ctx, i); err != nil { - g.logger.CError(ctx, err) - } - g.mu.Unlock() - } - } - } - }) -} - -// Once a limit switch is hit in any move call (from the motor or the gantry component), -// this function stops the motor, and reverses the direction of movement until the limit -// switch is no longer activated. -func (g *singleAxis) moveAway(ctx context.Context, pin int) error { - dir := 1.0 - if pin != 0 { - dir = -1.0 - } - if err := g.motor.GoFor(ctx, dir*g.rpm, 0, nil); err != nil { - return err - } - defer utils.UncheckedErrorFunc(func() error { - return g.motor.Stop(ctx, nil) - }) - for { - if ctx.Err() != nil { - return ctx.Err() - } - hit, err := g.limitHit(ctx, pin) - if err != nil { - return err - } - if !hit { - if err := g.motor.Stop(ctx, nil); err != nil { - return err - } - return nil - } - } -} - -// doHome is a helper function that runs the actual homing sequence. -func (g *singleAxis) doHome(ctx context.Context) (bool, error) { - np := len(g.limitSwitchPins) - ctx, done := g.opMgr.New(ctx) - defer done() - - switch np { - // An axis with an encoder will encode the zero position, and add the second position limit - // based on the steps per length - case 0: - if err := g.homeEncoder(ctx); err != nil { - return false, err - } - // An axis with one limit switch will go till it hits the limit switch, encode that position as the - // zero position of the singleAxis, and adds a second position limit based on the steps per length. - // An axis with two limit switches will go till it hits the first limit switch, encode that position as the - // zero position of the singleAxis, then go till it hits the second limit switch, then encode that position as the - // at-length position of the singleAxis. - case 1, 2: - if err := g.homeLimSwitch(ctx); err != nil { - return false, err - } - } - - return true, nil -} - -func (g *singleAxis) homeLimSwitch(ctx context.Context) error { - var positionA, positionB float64 - positionA, err := g.testLimit(ctx, 0) - if err != nil { - return err - } - - if len(g.limitSwitchPins) > 1 { - // Multiple limit switches, get positionB from testLimit - positionB, err = g.testLimit(ctx, 1) - if err != nil { - return err - } - } else { - // Only one limit switch, calculate positionB - revPerLength := g.lengthMm / g.mmPerRevolution - positionB = positionA + revPerLength - } - - g.positionLimits = []float64{positionA, positionB} - g.positionRange = positionB - positionA - if g.positionRange == 0 { - g.logger.CError(ctx, "positionRange is 0 or not a valid number") - } else { - g.logger.CDebugf(ctx, "positionA: %0.2f positionB: %0.2f range: %0.2f", positionA, positionB, g.positionRange) - } - - // Go to start position at the middle of the axis. - x := g.gantryToMotorPosition(0.5 * g.lengthMm) - if err := g.motor.GoTo(ctx, g.rpm, x, nil); err != nil { - return err - } - - return nil -} - -// home encoder assumes that you have places one of the stepper motors where you -// want your zero position to be, you need to know which way is "forward" -// on your motor. -func (g *singleAxis) homeEncoder(ctx context.Context) error { - revPerLength := g.lengthMm / g.mmPerRevolution - - positionA, err := g.motor.Position(ctx, nil) - if err != nil { - return err - } - - positionB := positionA + revPerLength - - g.positionLimits = []float64{positionA, positionB} - return nil -} - -func (g *singleAxis) gantryToMotorPosition(positions float64) float64 { - x := positions / g.lengthMm - x = g.positionLimits[0] + (x * g.positionRange) - return x -} - -func (g *singleAxis) gantryToMotorSpeeds(speeds float64) float64 { - r := (speeds / g.mmPerRevolution) * 60 - return r -} - -func (g *singleAxis) testLimit(ctx context.Context, pin int) (float64, error) { - defer utils.UncheckedErrorFunc(func() error { - return g.motor.Stop(ctx, nil) - }) - wrongPin := 1 - d := -1.0 - if pin != 0 { - d = 1 - wrongPin = 0 - } - - err := g.motor.GoFor(ctx, d*g.rpm, 0, nil) - if err != nil { - return 0, err - } - - // short sleep to allow pin number to switch correctly - time.Sleep(100 * time.Millisecond) - - start := time.Now() - for { - hit, err := g.limitHit(ctx, pin) - if err != nil { - return 0, err - } - if hit { - err = g.motor.Stop(ctx, nil) - if err != nil { - return 0, err - } - break - } - - // check if the wrong limit switch was hit - wrongHit, err := g.limitHit(ctx, wrongPin) - if err != nil { - return 0, err - } - if wrongHit { - err = g.motor.Stop(ctx, nil) - if err != nil { - return 0, err - } - return 0, errors.Errorf( - "expected limit switch %v but hit limit switch %v, try switching the order in the config", - pin, - wrongPin) - } - - elapsed := time.Since(start) - // if the parameters checked are non-zero, calculate a timeout with a safety factor of - // 5 to complete the gantry's homing sequence to find the limit switches - if g.mmPerRevolution != 0 && g.rpm != 0 && g.lengthMm != 0 { - homingTimeout = time.Duration((1 / (g.rpm / 60e9 * g.mmPerRevolution / g.lengthMm) * 5)) - } - if elapsed > (homingTimeout) { - return 0, errors.Errorf("gantry timed out testing limit, timeout = %v", homingTimeout) - } - - if !utils.SelectContextOrWait(ctx, time.Millisecond*10) { - return 0, ctx.Err() - } - } - // Short pause after stopping to increase the precision of the position of each limit switch - position, err := g.motor.Position(ctx, nil) - time.Sleep(250 * time.Millisecond) - return position, err -} - -// this function may need to be run in the background upon initialisation of the ganty, -// also may need to use a digital intterupt pin instead of a gpio pin. -func (g *singleAxis) limitHit(ctx context.Context, limitPin int) (bool, error) { - pin, err := g.board.GPIOPinByName(g.limitSwitchPins[limitPin]) - if err != nil { - return false, err - } - high, err := pin.Get(ctx, nil) - - return high == g.limitHigh, err -} - -// Position returns the position in millimeters. -func (g *singleAxis) Position(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - pos, err := g.motor.Position(ctx, extra) - if err != nil { - return []float64{}, err - } - - x := g.lengthMm * ((pos - g.positionLimits[0]) / g.positionRange) - - return []float64{x}, nil -} - -// Lengths returns the physical lengths of an axis of a Gantry. -func (g *singleAxis) Lengths(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - g.mu.Lock() - defer g.mu.Unlock() - return []float64{g.lengthMm}, nil -} - -// MoveToPosition moves along an axis using inputs in millimeters. -func (g *singleAxis) MoveToPosition(ctx context.Context, positions, speeds []float64, extra map[string]interface{}) error { - if g.positionRange == 0 { - return errors.Errorf("cannot move to position until gantry '%v' is homed", g.Named.Name().ShortName()) - } - ctx, done := g.opMgr.New(ctx) - defer done() - - if len(positions) != 1 { - return fmt.Errorf("single-axis MoveToPosition needs 1 position to move, got: %v", len(positions)) - } - - if len(speeds) > 1 { - return fmt.Errorf("single-axis MoveToPosition needs 1 speed to move, got: %v", len(speeds)) - } - - if positions[0] < 0 || positions[0] > g.lengthMm { - return fmt.Errorf("out of range (%.2f) min: 0 max: %.2f", positions[0], g.lengthMm) - } - - if len(speeds) == 0 { - speeds = append(speeds, g.rpm) - g.logger.CDebug(ctx, "single-axis received invalid speed, using default gantry speed") - } else if rdkutils.Float64AlmostEqual(math.Abs(speeds[0]), 0.0, 0.1) { - if err := g.motor.Stop(ctx, nil); err != nil { - return err - } - return fmt.Errorf("speed (%.2f) is too slow, stopping gantry", speeds[0]) - } - - x := g.gantryToMotorPosition(positions[0]) - r := g.gantryToMotorSpeeds(speeds[0]) - // Limit switch errors that stop the motors. - // Currently needs to be moved by underlying gantry motor. - if len(g.limitSwitchPins) > 0 { - // Stops if position x is past the 0 limit switch - if x <= (g.positionLimits[0] + limitErrorMargin) { - g.logger.CError(ctx, "Cannot move past limit switch!") - return g.motor.Stop(ctx, extra) - } - - // Stops if position x is past the at-length limit switch - if x >= (g.positionLimits[1] - limitErrorMargin) { - g.logger.CError(ctx, "Cannot move past limit switch!") - return g.motor.Stop(ctx, extra) - } - } - - g.logger.CDebugf(ctx, "going to %.2f at speed %.2f", x, r) - if err := g.motor.GoTo(ctx, r, x, extra); err != nil { - return err - } - return nil -} - -// Stop stops the motor of the gantry. -func (g *singleAxis) Stop(ctx context.Context, extra map[string]interface{}) error { - ctx, done := g.opMgr.New(ctx) - defer done() - return g.motor.Stop(ctx, extra) -} - -// Close calls stop. -func (g *singleAxis) Close(ctx context.Context) error { - g.mu.Lock() - defer g.mu.Unlock() - if err := g.Stop(ctx, nil); err != nil { - return err - } - g.cancelFunc() - g.activeBackgroundWorkers.Wait() - return nil -} - -// IsMoving returns whether the gantry is moving. -func (g *singleAxis) IsMoving(ctx context.Context) (bool, error) { - g.mu.Lock() - defer g.mu.Unlock() - return g.opMgr.OpRunning(), nil -} - -// ModelFrame returns the frame model of the Gantry. -func (g *singleAxis) ModelFrame() referenceframe.Model { - g.mu.Lock() - defer g.mu.Unlock() - if g.model == nil { - var errs error - m := referenceframe.NewSimpleModel("") - - f, err := referenceframe.NewStaticFrame(g.Name().ShortName(), spatial.NewZeroPose()) - errs = multierr.Combine(errs, err) - m.OrdTransforms = append(m.OrdTransforms, f) - - f, err = referenceframe.NewTranslationalFrame(g.Name().ShortName(), g.frame, referenceframe.Limit{Min: 0, Max: g.lengthMm}) - errs = multierr.Combine(errs, err) - - if errs != nil { - g.logger.Error(errs) - return nil - } - - m.OrdTransforms = append(m.OrdTransforms, f) - g.model = m - } - return g.model -} - -// CurrentInputs returns the current inputs of the Gantry frame. -func (g *singleAxis) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - g.mu.Lock() - defer g.mu.Unlock() - res, err := g.Position(ctx, nil) - if err != nil { - return nil, err - } - return referenceframe.FloatsToInputs(res), nil -} - -// GoToInputs moves the gantry to a goal position in the Gantry frame. -func (g *singleAxis) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - g.mu.Lock() - defer g.mu.Unlock() - for _, goal := range inputSteps { - speed := []float64{} - err := g.MoveToPosition(ctx, referenceframe.InputsToFloats(goal), speed, nil) - if err != nil { - return err - } - } - return nil -} diff --git a/components/gantry/singleaxis/singleaxis_test.go b/components/gantry/singleaxis/singleaxis_test.go deleted file mode 100644 index 1ffc6da2514..00000000000 --- a/components/gantry/singleaxis/singleaxis_test.go +++ /dev/null @@ -1,878 +0,0 @@ -package singleaxis - -import ( - "context" - "math" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - motorName = "x" - testGName = "test" - boardName = "board" -) - -var fakeFrame = &referenceframe.LinkConfig{ - Translation: r3.Vector{X: 0, Y: 1.0, Z: 0}, -} - -var badFrame = &referenceframe.LinkConfig{} - -var ( - count = 0 - pinValues = []int{1, 1, 0} -) - -func createFakeMotor() motor.Motor { - return &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{PositionReporting: true}, nil - }, - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return float64(count + 1), nil - }, - ResetZeroPositionFunc: func(ctx context.Context, offset float64, extra map[string]interface{}) error { return nil }, - GoToFunc: func(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { return nil }, - GoForFunc: func(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { return nil }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return nil }, - SetPowerFunc: func(ctx context.Context, powerPct float64, extra map[string]interface{}) error { return nil }, - } -} - -func createLimitBoard() board.Board { - injectGPIOPin := &inject.GPIOPin{ - GetFunc: func(ctx context.Context, extra map[string]interface{}) (bool, error) { return true, nil }, - SetFunc: func(ctx context.Context, high bool, extra map[string]interface{}) error { return nil }, - } - return &inject.Board{GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { return injectGPIOPin, nil }} -} - -func createFakeBoard() board.Board { - pinCount := 0 - injectGPIOPin := &inject.GPIOPin{ - GetFunc: func(ctx context.Context, extra map[string]interface{}) (bool, error) { - if pinValues[pinCount] == 1 { - return true, nil - } - pinCount++ - if pinCount == len(pinValues) { - pinCount = 0 - } - return false, nil - }, - SetFunc: func(ctx context.Context, high bool, extra map[string]interface{}) error { return nil }, - } - return &inject.Board{GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { return injectGPIOPin, nil }} -} - -func createFakeDepsForTestNewSingleAxis(t *testing.T) resource.Dependencies { - t.Helper() - deps := make(resource.Dependencies) - deps[board.Named(boardName)] = createFakeBoard() - deps[motor.Named(motorName)] = createFakeMotor() - return deps -} - -var setTrue = true - -func TestValidate(t *testing.T) { - fakecfg := &Config{} - deps, err := fakecfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "motor") - - fakecfg.Motor = motorName - deps, err = fakecfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "length_mm") - - fakecfg.LengthMm = 1.0 - fakecfg.MmPerRevolution = 1.0 - fakecfg.LimitSwitchPins = []string{"1"} - deps, err = fakecfg.Validate("path") - test.That(t, deps, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "board") - - fakecfg.Board = boardName - fakecfg.MmPerRevolution = 1 - fakecfg.LimitPinEnabled = &setTrue - deps, err = fakecfg.Validate("path") - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldResemble, []string{fakecfg.Motor, fakecfg.Board}) - test.That(t, fakecfg.GantryMmPerSec, test.ShouldEqual, float64(0)) -} - -func TestNewSingleAxis(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps := createFakeDepsForTestNewSingleAxis(t) - fakecfg := resource.Config{Name: testGName} - _, err := newSingleAxis(ctx, deps, fakecfg, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "expected *singleaxis.Config but got ") - - deps = createFakeDepsForTestNewSingleAxis(t) - fakecfg = resource.Config{ - Name: testGName, - Frame: badFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LimitSwitchPins: []string{"1", "2"}, - LengthMm: 1.0, - Board: boardName, - LimitPinEnabled: &setTrue, - GantryMmPerSec: float64(300), - }, - } - fakegantry, err := newSingleAxis(ctx, deps, fakecfg, logger) - test.That(t, err, test.ShouldBeNil) - _, ok := fakegantry.(*singleAxis) - test.That(t, ok, test.ShouldBeTrue) - - deps = createFakeDepsForTestNewSingleAxis(t) - fakecfg = resource.Config{ - Name: testGName, - Frame: fakeFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LimitSwitchPins: []string{"1"}, - LengthMm: 1.0, - Board: boardName, - LimitPinEnabled: &setTrue, - MmPerRevolution: 0.1, - GantryMmPerSec: float64(300), - }, - } - fakegantry, err = newSingleAxis(ctx, deps, fakecfg, logger) - _, ok = fakegantry.(*singleAxis) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - - deps = createFakeDepsForTestNewSingleAxis(t) - fakecfg = resource.Config{ - Name: testGName, - Frame: fakeFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LimitSwitchPins: []string{"1"}, - LengthMm: 1.0, - Board: boardName, - LimitPinEnabled: &setTrue, - GantryMmPerSec: float64(300), - }, - } - fakegantry, err = newSingleAxis(ctx, deps, fakecfg, logger) - _, ok = fakegantry.(*singleAxis) - test.That(t, ok, test.ShouldBeFalse) - test.That(t, err.Error(), test.ShouldContainSubstring, "gantry with one limit switch per axis needs a mm_per_length ratio defined") - - deps = createFakeDepsForTestNewSingleAxis(t) - fakecfg = resource.Config{ - Name: testGName, - Frame: fakeFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LimitSwitchPins: []string{"1", "2", "3"}, - LengthMm: 1.0, - Board: boardName, - LimitPinEnabled: &setTrue, - }, - } - - _, err = newSingleAxis(ctx, deps, fakecfg, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid gantry type: need 1, 2 or 0 pins per axis, have 3 pins") - - deps = make(resource.Dependencies) - _, err = newSingleAxis(ctx, deps, fakecfg, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "missing from dependencies") - - injectMotor := &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil - }, - } - deps = make(resource.Dependencies) - deps[motor.Named(motorName)] = injectMotor - deps[board.Named(boardName)] = createFakeBoard() - - _, err = newSingleAxis(ctx, deps, fakecfg, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid gantry type") - - injectMotor = &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: false, - }, nil - }, - } - - deps = make(resource.Dependencies) - deps[motor.Named(motorName)] = injectMotor - deps[board.Named(boardName)] = createFakeBoard() - properties, _ := injectMotor.Properties(ctx, nil) - _, err = newSingleAxis(ctx, deps, fakecfg, logger) - expectedErr := motor.NewPropertyUnsupportedError(properties, motorName) - test.That(t, err, test.ShouldBeError, expectedErr) -} - -func TestReconfigure(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps := createFakeDepsForTestNewSingleAxis(t) - fakecfg := resource.Config{ - Name: testGName, - Frame: fakeFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LimitSwitchPins: []string{"1", "2"}, - LengthMm: 1.0, - Board: boardName, - LimitPinEnabled: &setTrue, - GantryMmPerSec: float64(300), - }, - } - fakegantry, err := newSingleAxis(ctx, deps, fakecfg, logger) - test.That(t, err, test.ShouldBeNil) - g := fakegantry.(*singleAxis) - - deps = createFakeDepsForTestNewSingleAxis(t) - newconf := resource.Config{ - Name: testGName, - Frame: fakeFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LimitSwitchPins: []string{"1", "3"}, - LengthMm: 5.0, - Board: boardName, - LimitPinEnabled: &setTrue, - GantryMmPerSec: float64(400), - MmPerRevolution: 10, - }, - } - err = fakegantry.Reconfigure(ctx, deps, newconf) - test.That(t, err, test.ShouldBeNil) - - test.That(t, g.limitSwitchPins, test.ShouldResemble, []string{"1", "3"}) - test.That(t, g.lengthMm, test.ShouldEqual, 5.0) - test.That(t, g.rpm, test.ShouldEqual, float64(2400)) - test.That(t, g.mmPerRevolution, test.ShouldEqual, 10) -} - -func TestHome(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - fakegantry := &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1"}, - opMgr: operation.NewSingleOperationManager(), - } - homed, err := fakegantry.Home(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, homed, test.ShouldBeTrue) - - goForErr := errors.New("GoFor failed") - posErr := errors.New("Position fail") - fakeMotor := &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: false, - }, nil - }, - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return goForErr - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { - return nil - }, - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return math.NaN(), posErr - }, - } - fakegantry = &singleAxis{ - motor: fakeMotor, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - homed, err = fakegantry.Home(ctx, nil) - test.That(t, err, test.ShouldBeError, posErr) - test.That(t, homed, test.ShouldBeFalse) - - fakegantry = &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1", "2"}, - opMgr: operation.NewSingleOperationManager(), - } - homed, err = fakegantry.Home(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, homed, test.ShouldBeTrue) - - fakegantry = &singleAxis{ - motor: fakeMotor, - opMgr: operation.NewSingleOperationManager(), - } - homed, err = fakegantry.Home(ctx, nil) - test.That(t, err, test.ShouldBeError, posErr) - test.That(t, homed, test.ShouldBeFalse) - - fakegantry = &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1", "2"}, - opMgr: operation.NewSingleOperationManager(), - } - homed, err = fakegantry.Home(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, homed, test.ShouldBeTrue) -} - -func TestHomeLimitSwitch(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - fakegantry := &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1", "2"}, - opMgr: operation.NewSingleOperationManager(), - } - - err := fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakegantry.positionLimits, test.ShouldResemble, []float64{1, 1}) - - getPosErr := errors.New("failed to get position") - fakegantry.motor = &inject.Motor{ - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { return nil }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return nil }, - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { return 0, getPosErr }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldBeError, getPosErr) - - fakegantry.motor = &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil - }, - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return errors.New("err") - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return nil }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldNotBeNil) - - fakegantry.motor = &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil - }, - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return nil - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return errors.New("err") }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldNotBeNil) - - fakegantry.motor = &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil - }, - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return errors.New("err") - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return nil }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldNotBeNil) - - injectGPIOPin := &inject.GPIOPin{} - injectGPIOPin.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return true, errors.New("not supported") - } - injectGPIOPinGood := &inject.GPIOPin{} - injectGPIOPinGood.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return true, nil - } - - fakegantry.board = &inject.Board{ - GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { - return injectGPIOPin, nil - }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldNotBeNil) - - fakegantry.board = &inject.Board{ - GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { - if pin == "1" { - return injectGPIOPinGood, nil - } - return injectGPIOPin, nil - }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestHomeLimitSwitch2(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - fakegantry := &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1"}, - lengthMm: float64(1), - mmPerRevolution: float64(.1), - opMgr: operation.NewSingleOperationManager(), - } - - err := fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakegantry.positionLimits, test.ShouldResemble, []float64{1, 11}) - - getPosErr := errors.New("failed to get position") - fakegantry.motor = &inject.Motor{ - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return nil - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { - return nil - }, - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, getPosErr - }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldBeError, getPosErr) - - fakegantry.motor = &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil - }, - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return errors.New("not supported") - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return nil }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldNotBeNil) - - injectGPIOPin := &inject.GPIOPin{} - injectGPIOPin.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return true, errors.New("not supported") - } - - fakegantry.board = &inject.Board{ - GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { - return injectGPIOPin, nil - }, - } - err = fakegantry.homeLimSwitch(ctx) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestHomeEncoder(t *testing.T) { - fakegantry := &singleAxis{opMgr: operation.NewSingleOperationManager()} - - resetZeroErr := errors.New("failed to set zero") - injMotor := &inject.Motor{ - GoForFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { return nil }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return nil }, - ResetZeroPositionFunc: func(ctx context.Context, offset float64, extra map[string]interface{}) error { return resetZeroErr }, - } - fakegantry.motor = injMotor - ctx := context.Background() - - getPosErr := errors.New("failed to get position") - injMotor.ResetZeroPositionFunc = func(ctx context.Context, offset float64, extra map[string]interface{}) error { return nil } - injMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { return 0, getPosErr } - err := fakegantry.homeEncoder(ctx) - test.That(t, err.Error(), test.ShouldContainSubstring, "get position") - - injMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { return 0, nil } - err = fakegantry.homeEncoder(ctx) - test.That(t, err, test.ShouldBeNil) -} - -func TestTestLimit(t *testing.T) { - ctx := context.Background() - fakegantry := &singleAxis{ - limitSwitchPins: []string{"1", "2"}, - motor: createFakeMotor(), - board: createLimitBoard(), - rpm: float64(300), - limitHigh: true, - opMgr: operation.NewSingleOperationManager(), - } - pos, err := fakegantry.testLimit(ctx, 0) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, float64(1)) -} - -func TestTestLimitTimeout(t *testing.T) { - ctx := context.Background() - fakegantry := &singleAxis{ - limitSwitchPins: []string{"1", "2"}, - motor: createFakeMotor(), - board: createLimitBoard(), - rpm: float64(3000), - limitHigh: true, - opMgr: operation.NewSingleOperationManager(), - mmPerRevolution: 10, - lengthMm: 100, - } - - injectGPIOPin := &inject.GPIOPin{} - injectGPIOPin.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return false, nil - } - - fakegantry.board = &inject.Board{ - GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { - return injectGPIOPin, nil - }, - } - - pos, err := fakegantry.testLimit(ctx, 0) - test.That(t, err.Error(), test.ShouldContainSubstring, "timeout = 1s") - test.That(t, pos, test.ShouldEqual, 0.0) -} - -func TestLimitHit(t *testing.T) { - ctx := context.Background() - fakegantry := &singleAxis{ - limitSwitchPins: []string{"1", "2", "3"}, - board: createLimitBoard(), - limitHigh: true, - opMgr: operation.NewSingleOperationManager(), - } - - hit, err := fakegantry.limitHit(ctx, 0) - test.That(t, err, test.ShouldBeNil) - test.That(t, hit, test.ShouldEqual, true) -} - -func TestPosition(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - fakegantry := &singleAxis{ - motor: &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: false, - }, nil - }, - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { return 1, nil }, - }, - board: createFakeBoard(), - positionLimits: []float64{0, 1}, - positionRange: 1.0, - limitHigh: true, - limitSwitchPins: []string{"1", "2"}, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - positions, err := fakegantry.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, positions, test.ShouldResemble, []float64{0}) - - fakegantry = &singleAxis{ - motor: &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, errors.New("not supported") - }, - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 1, errors.New("not supported") - }, - }, - board: createFakeBoard(), - limitHigh: true, - limitSwitchPins: []string{"1", "2"}, - positionLimits: []float64{0, 1}, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - positions, err = fakegantry.Position(ctx, nil) - test.That(t, positions, test.ShouldResemble, []float64{}) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestLengths(t *testing.T) { - fakegantry := &singleAxis{ - lengthMm: float64(1.0), - opMgr: operation.NewSingleOperationManager(), - } - ctx := context.Background() - fakelengths, err := fakegantry.Lengths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.ShouldHaveLength(t, fakelengths, test.ShouldEqual(float64(1.0))) -} - -func TestMoveToPosition(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - fakegantry := &singleAxis{ - logger: logger, - board: createFakeBoard(), - motor: createFakeMotor(), - limitHigh: true, - positionRange: 10, - opMgr: operation.NewSingleOperationManager(), - } - pos := []float64{1, 2} - speed := []float64{100, 200} - err := fakegantry.MoveToPosition(ctx, pos, speed, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "needs 1 position to move") - - pos = []float64{1} - speed = []float64{100} - err = fakegantry.MoveToPosition(ctx, pos, speed, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "out of range") - - fakegantry.lengthMm = float64(4) - fakegantry.positionLimits = []float64{0, 4} - fakegantry.limitSwitchPins = []string{"1", "2"} - err = fakegantry.MoveToPosition(ctx, pos, speed, nil) - test.That(t, err, test.ShouldBeNil) - - fakegantry.lengthMm = float64(4) - fakegantry.positionLimits = []float64{0.01, .01} - fakegantry.limitSwitchPins = []string{"1", "2"} - fakegantry.motor = &inject.Motor{StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return errors.New("err") }} - err = fakegantry.MoveToPosition(ctx, pos, speed, nil) - test.That(t, err, test.ShouldNotBeNil) - - injectGPIOPin := &inject.GPIOPin{} - injectGPIOPin.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return true, errors.New("err") - } - injectGPIOPinGood := &inject.GPIOPin{} - injectGPIOPinGood.GetFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return false, nil - } - - fakegantry.board = &inject.Board{ - GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { - return injectGPIOPin, nil - }, - } - - fakegantry.board = &inject.Board{GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { return injectGPIOPin, nil }} - err = fakegantry.MoveToPosition(ctx, pos, speed, nil) - test.That(t, err, test.ShouldNotBeNil) - - fakegantry.board = &inject.Board{GPIOPinByNameFunc: func(pin string) (board.GPIOPin, error) { return injectGPIOPinGood, nil }} - fakegantry.motor = &inject.Motor{ - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { return nil }, - GoToFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return errors.New("err") - }, - } - fakegantry.positionLimits = []float64{0, 4} - err = fakegantry.MoveToPosition(ctx, pos, speed, nil) - test.That(t, err, test.ShouldNotBeNil) - - fakegantry.motor = &inject.Motor{GoToFunc: func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return nil - }} - err = fakegantry.MoveToPosition(ctx, pos, speed, nil) - test.That(t, err, test.ShouldBeNil) -} - -func TestModelFrame(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps := createFakeDepsForTestNewSingleAxis(t) - fakecfg := resource.Config{ - Name: testGName, - Frame: fakeFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LimitSwitchPins: []string{"1", "2"}, - LengthMm: 1.0, - Board: boardName, - LimitPinEnabled: &setTrue, - GantryMmPerSec: float64(300), - }, - } - fakegantry, _ := newSingleAxis(ctx, deps, fakecfg, logger) - m := fakegantry.ModelFrame() - test.That(t, m, test.ShouldNotBeNil) -} - -func TestStop(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - fakegantry := &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1", "2"}, - lengthMm: float64(200), - positionLimits: []float64{0, 2}, - opMgr: operation.NewSingleOperationManager(), - } - - test.That(t, fakegantry.Stop(ctx, nil), test.ShouldBeNil) -} - -func TestCurrentInputs(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - fakegantry := &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1", "2"}, - lengthMm: float64(200), - positionLimits: []float64{0, 2}, - positionRange: 2.0, - opMgr: operation.NewSingleOperationManager(), - } - - input, err := fakegantry.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, input[0].Value, test.ShouldEqual, 100) - - fakegantry = &singleAxis{ - motor: createFakeMotor(), - board: createFakeBoard(), - limitHigh: true, - logger: logger, - rpm: float64(300), - limitSwitchPins: []string{"1"}, - lengthMm: float64(200), - positionLimits: []float64{0, 2}, - positionRange: 2.0, - opMgr: operation.NewSingleOperationManager(), - } - - input, err = fakegantry.CurrentInputs(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, input[0].Value, test.ShouldEqual, 100) - - // out of bounds position - fakegantry = &singleAxis{ - motor: &inject.Motor{ - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 5, errors.New("nope") - }, - }, - board: createFakeBoard(), - limitHigh: false, - logger: logger, - rpm: float64(300), - lengthMm: float64(200), - positionLimits: []float64{0, 0.5}, - opMgr: operation.NewSingleOperationManager(), - } - - input, err = fakegantry.CurrentInputs(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, input, test.ShouldBeNil) -} - -func TestGoToInputs(t *testing.T) { - ctx := context.Background() - inputs := []referenceframe.Input{} - logger := logging.NewTestLogger(t) - - fakecfg := resource.Config{ - Name: "fakeGantry", - Frame: fakeFrame, - ConvertedAttributes: &Config{ - Motor: motorName, - LengthMm: 1.0, - MmPerRevolution: 10, - }, - } - deps := createFakeDepsForTestNewSingleAxis(t) - g, err := newSingleAxis(ctx, deps, fakecfg, logger) - test.That(t, err, test.ShouldBeNil) - - err = g.GoToInputs(ctx, inputs) - test.That(t, err.Error(), test.ShouldContainSubstring, "is homed") - - fakegantry := &singleAxis{ - board: createFakeBoard(), - limitSwitchPins: []string{"1", "2"}, - limitHigh: true, - motor: createFakeMotor(), - lengthMm: 1.0, - mmPerRevolution: 0.1, - rpm: 10, - positionLimits: []float64{1, 2}, - model: nil, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - - fakegantry.positionRange = 10 - err = fakegantry.GoToInputs(ctx, inputs) - test.That(t, err.Error(), test.ShouldContainSubstring, "needs 1 position to move") - - inputs = []referenceframe.Input{{Value: 1.0}, {Value: 2.0}} - err = fakegantry.GoToInputs(ctx, inputs) - test.That(t, err.Error(), test.ShouldContainSubstring, "needs 1 position to move") - - inputs = []referenceframe.Input{{Value: -1.0}} - err = fakegantry.GoToInputs(ctx, inputs) - test.That(t, err.Error(), test.ShouldContainSubstring, "out of range") - - inputs = []referenceframe.Input{{Value: 4.0}} - err = fakegantry.GoToInputs(ctx, inputs) - test.That(t, err.Error(), test.ShouldContainSubstring, "out of range") - - inputs = []referenceframe.Input{{Value: 1.0}} - err = fakegantry.GoToInputs(ctx, inputs) - test.That(t, err, test.ShouldBeNil) - - err = fakegantry.GoToInputs(ctx, inputs, inputs) - test.That(t, err, test.ShouldBeNil) - - err = fakegantry.GoToInputs(ctx) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/gantry/verify_main_test.go b/components/gantry/verify_main_test.go deleted file mode 100644 index 958ae36e41e..00000000000 --- a/components/gantry/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package gantry - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/generic/client_test.go b/components/generic/client_test.go deleted file mode 100644 index 238e44f7474..00000000000 --- a/components/generic/client_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package generic_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/generic" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testGenericName = "gen1" - failGenericName = "gen2" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - workingGeneric := &inject.GenericComponent{} - failingGeneric := &inject.GenericComponent{} - - workingGeneric.DoFunc = testutils.EchoFunc - failingGeneric.DoFunc = func( - ctx context.Context, - cmd map[string]interface{}, - ) ( - map[string]interface{}, - error, - ) { - return nil, errDoFailed - } - - resourceMap := map[resource.Name]resource.Resource{ - generic.Named(testGenericName): workingGeneric, - generic.Named(failGenericName): failingGeneric, - } - genericSvc, err := resource.NewAPIResourceCollection(generic.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[resource.Resource](generic.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, genericSvc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("client tests for working generic", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingGenericClient, err := generic.NewClientFromConn(context.Background(), conn, "", generic.Named(testGenericName), logger) - test.That(t, err, test.ShouldBeNil) - - resp, err := workingGenericClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, workingGenericClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("client tests for failing generic", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingGenericClient, err := generic.NewClientFromConn(context.Background(), conn, "", generic.Named(failGenericName), logger) - test.That(t, err, test.ShouldBeNil) - - _, err = failingGenericClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errDoFailed.Error()) - - test.That(t, failingGenericClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client tests for working generic", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := resourceAPI.RPCClient(context.Background(), conn, "", generic.Named(testGenericName), logger) - test.That(t, err, test.ShouldBeNil) - - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/generic/fake/fake_test.go b/components/generic/fake/fake_test.go deleted file mode 100644 index 306cf5f4e64..00000000000 --- a/components/generic/fake/fake_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package fake - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/logging" -) - -func TestDoCommand(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - gen := newGeneric(generic.Named("foo"), logger) - cmd := map[string]interface{}{"bar": "baz"} - resp, err := gen.DoCommand(ctx, cmd) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, cmd) -} diff --git a/components/generic/server_test.go b/components/generic/server_test.go deleted file mode 100644 index 4c48781cadd..00000000000 --- a/components/generic/server_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package generic_test - -import ( - "context" - "errors" - "testing" - - commonpb "go.viam.com/api/common/v1" - genericpb "go.viam.com/api/component/generic/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var errDoFailed = errors.New("do failed") - -func newServer() (genericpb.GenericServiceServer, *inject.GenericComponent, *inject.GenericComponent, error) { - injectGeneric := &inject.GenericComponent{} - injectGeneric2 := &inject.GenericComponent{} - resourceMap := map[resource.Name]resource.Resource{ - generic.Named(testGenericName): injectGeneric, - generic.Named(failGenericName): injectGeneric2, - } - injectSvc, err := resource.NewAPIResourceCollection(generic.API, resourceMap) - if err != nil { - return nil, nil, nil, err - } - return generic.NewRPCServiceServer(injectSvc).(genericpb.GenericServiceServer), injectGeneric, injectGeneric2, nil -} - -func TestGenericDo(t *testing.T) { - genericServer, workingGeneric, failingGeneric, err := newServer() - test.That(t, err, test.ShouldBeNil) - - workingGeneric.DoFunc = func( - ctx context.Context, - cmd map[string]interface{}, - ) ( - map[string]interface{}, - error, - ) { - return cmd, nil - } - failingGeneric.DoFunc = func( - ctx context.Context, - cmd map[string]interface{}, - ) ( - map[string]interface{}, - error, - ) { - return nil, errDoFailed - } - - commandStruct, err := protoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - - req := commonpb.DoCommandRequest{Name: testGenericName, Command: commandStruct} - resp, err := genericServer.DoCommand(context.Background(), &req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp.Result.AsMap()["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"]) - test.That(t, resp.Result.AsMap()["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - req = commonpb.DoCommandRequest{Name: failGenericName, Command: commandStruct} - resp, err = genericServer.DoCommand(context.Background(), &req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errDoFailed.Error()) - test.That(t, resp, test.ShouldBeNil) -} diff --git a/components/gripper/client_test.go b/components/gripper/client_test.go deleted file mode 100644 index 9f67cf6a516..00000000000 --- a/components/gripper/client_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package gripper_test - -import ( - "context" - "net" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/gripper" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - var gripperOpen string - var extraOptions map[string]interface{} - expectedGeometries := []spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{1, 2, 3}, "")} - - grabbed1 := true - injectGripper := &inject.Gripper{} - injectGripper.OpenFunc = func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - gripperOpen = testGripperName - return nil - } - injectGripper.GrabFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - extraOptions = extra - return grabbed1, nil - } - injectGripper.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - return nil - } - injectGripper.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { - return expectedGeometries, nil - } - - injectGripper2 := &inject.Gripper{} - injectGripper2.OpenFunc = func(ctx context.Context, extra map[string]interface{}) error { - gripperOpen = failGripperName - return errCantOpen - } - injectGripper2.GrabFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return false, errCantGrab - } - injectGripper2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopUnimplemented - } - - gripperSvc, err := resource.NewAPIResourceCollection( - gripper.API, - map[resource.Name]gripper.Gripper{gripper.Named(testGripperName): injectGripper, gripper.Named(failGripperName): injectGripper2}) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[gripper.Gripper](gripper.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, gripperSvc), test.ShouldBeNil) - - injectGripper.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - // working - t.Run("gripper client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - gripper1Client, err := gripper.NewClientFromConn(context.Background(), conn, "", gripper.Named(testGripperName), logger) - test.That(t, err, test.ShouldBeNil) - - // DoCommand - resp, err := gripper1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - extra := map[string]interface{}{"foo": "Open"} - err = gripper1Client.Open(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, extra) - test.That(t, gripperOpen, test.ShouldEqual, testGripperName) - - extra = map[string]interface{}{"foo": "Grab"} - grabbed, err := gripper1Client.Grab(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, extra) - test.That(t, grabbed, test.ShouldEqual, grabbed1) - - extra = map[string]interface{}{"foo": "Stop"} - test.That(t, gripper1Client.Stop(context.Background(), extra), test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, extra) - - extra = map[string]interface{}{"foo": "Geometries"} - geometries, err := gripper1Client.Geometries(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - for i, geometry := range geometries { - test.That(t, spatialmath.GeometriesAlmostEqual(expectedGeometries[i], geometry), test.ShouldBeTrue) - } - - test.That(t, gripper1Client.Close(context.Background()), test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("gripper client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", gripper.Named(failGripperName), logger) - test.That(t, err, test.ShouldBeNil) - - extra := map[string]interface{}{} - err = client2.Open(context.Background(), extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCantOpen.Error()) - test.That(t, gripperOpen, test.ShouldEqual, failGripperName) - - grabbed, err := client2.Grab(context.Background(), extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCantGrab.Error()) - test.That(t, grabbed, test.ShouldEqual, false) - - err = client2.Stop(context.Background(), extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopUnimplemented.Error()) - - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/gripper/gripper_test.go b/components/gripper/gripper_test.go deleted file mode 100644 index b65e69073ba..00000000000 --- a/components/gripper/gripper_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package gripper_test - -import ( - "context" - "testing" - - "github.com/golang/geo/r3" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/components/gripper/fake" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testGripperName = "gripper1" - testGripperName2 = "gripper2" - failGripperName = "gripper3" - missingGripperName = "gripper4" -) - -func TestCreateStatus(t *testing.T) { - t.Run("is moving", func(t *testing.T) { - status := &commonpb.ActuatorStatus{ - IsMoving: true, - } - - injectGripper := &inject.Gripper{} - injectGripper.IsMovingFunc = func(context.Context) (bool, error) { - return true, nil - } - status1, err := gripper.CreateStatus(context.Background(), injectGripper) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - - resourceAPI, ok, err := resource.LookupAPIRegistration[gripper.Gripper](gripper.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - status2, err := resourceAPI.Status(context.Background(), injectGripper) - test.That(t, err, test.ShouldBeNil) - test.That(t, status2, test.ShouldResemble, status) - }) - - t.Run("is not moving", func(t *testing.T) { - status := &commonpb.ActuatorStatus{ - IsMoving: false, - } - - injectGripper := &inject.Gripper{} - injectGripper.IsMovingFunc = func(context.Context) (bool, error) { - return false, nil - } - status1, err := gripper.CreateStatus(context.Background(), injectGripper) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - }) -} - -func TestGetGeometries(t *testing.T) { - cfg := resource.Config{ - Name: "fakeGripper", - API: gripper.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{X: 10, Y: 5, Z: 10}}, - } - gripper, err := fake.NewGripper(context.Background(), nil, cfg, nil) - test.That(t, err, test.ShouldBeNil) - - geometries, err := gripper.Geometries(context.Background(), nil) - expected, _ := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{X: 10, Y: 5, Z: 10}, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, geometries, test.ShouldResemble, []spatialmath.Geometry{expected}) -} diff --git a/components/gripper/register/register.go b/components/gripper/register/register.go index 958d9f20898..b2d34482727 100644 --- a/components/gripper/register/register.go +++ b/components/gripper/register/register.go @@ -5,5 +5,4 @@ import ( // for grippers. _ "go.viam.com/rdk/components/gripper/fake" _ "go.viam.com/rdk/components/gripper/robotiq" - _ "go.viam.com/rdk/components/gripper/softrobotics" ) diff --git a/components/gripper/server_test.go b/components/gripper/server_test.go deleted file mode 100644 index 248b22ac24e..00000000000 --- a/components/gripper/server_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package gripper_test - -import ( - "context" - "errors" - "testing" - - pb "go.viam.com/api/component/gripper/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errCantOpen = errors.New("can't open") - errCantGrab = errors.New("can't grab") - errStopUnimplemented = errors.New("stop unimplemented") - errGripperNotFound = errors.New("not found") -) - -func newServer() (pb.GripperServiceServer, *inject.Gripper, *inject.Gripper, error) { - injectGripper := &inject.Gripper{} - injectGripper2 := &inject.Gripper{} - grippers := map[resource.Name]gripper.Gripper{ - gripper.Named(testGripperName): injectGripper, - gripper.Named(testGripperName2): injectGripper2, - } - gripperSvc, err := resource.NewAPIResourceCollection(gripper.API, grippers) - if err != nil { - return nil, nil, nil, err - } - return gripper.NewRPCServiceServer(gripperSvc).(pb.GripperServiceServer), injectGripper, injectGripper2, nil -} - -func TestServer(t *testing.T) { - gripperServer, injectGripper, injectGripper2, err := newServer() - test.That(t, err, test.ShouldBeNil) - - var gripperOpen string - var extraOptions map[string]interface{} - - success1 := true - injectGripper.OpenFunc = func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - gripperOpen = testGripperName - return nil - } - injectGripper.GrabFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - extraOptions = extra - return success1, nil - } - injectGripper.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - return nil - } - - injectGripper2.OpenFunc = func(ctx context.Context, extra map[string]interface{}) error { - gripperOpen = testGripperName2 - return errCantOpen - } - injectGripper2.GrabFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - return false, errCantGrab - } - injectGripper2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopUnimplemented - } - - t.Run("open", func(t *testing.T) { - _, err := gripperServer.Open(context.Background(), &pb.OpenRequest{Name: missingGripperName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGripperNotFound.Error()) - - extra := map[string]interface{}{"foo": "Open"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - _, err = gripperServer.Open(context.Background(), &pb.OpenRequest{Name: testGripperName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, gripperOpen, test.ShouldEqual, testGripperName) - test.That(t, extraOptions, test.ShouldResemble, extra) - - _, err = gripperServer.Open(context.Background(), &pb.OpenRequest{Name: testGripperName2}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCantOpen.Error()) - test.That(t, gripperOpen, test.ShouldEqual, testGripperName2) - }) - - t.Run("grab", func(t *testing.T) { - _, err := gripperServer.Grab(context.Background(), &pb.GrabRequest{Name: missingGripperName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGripperNotFound.Error()) - - extra := map[string]interface{}{"foo": "Grab"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - resp, err := gripperServer.Grab(context.Background(), &pb.GrabRequest{Name: testGripperName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Success, test.ShouldEqual, success1) - test.That(t, extraOptions, test.ShouldResemble, extra) - - resp, err = gripperServer.Grab(context.Background(), &pb.GrabRequest{Name: testGripperName2}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCantGrab.Error()) - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("stop", func(t *testing.T) { - _, err = gripperServer.Stop(context.Background(), &pb.StopRequest{Name: missingGripperName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGripperNotFound.Error()) - - extra := map[string]interface{}{"foo": "Stop"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - _, err = gripperServer.Stop(context.Background(), &pb.StopRequest{Name: testGripperName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, extra) - - _, err = gripperServer.Stop(context.Background(), &pb.StopRequest{Name: testGripperName2}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errStopUnimplemented) - }) -} diff --git a/components/gripper/softrobotics/gripper.go b/components/gripper/softrobotics/gripper.go deleted file mode 100644 index 4394c2f6849..00000000000 --- a/components/gripper/softrobotics/gripper.go +++ /dev/null @@ -1,237 +0,0 @@ -// Package softrobotics implements the vacuum gripper from Soft Robotics. -package softrobotics - -import ( - "context" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -var model = resource.DefaultModelFamily.WithModel("softrobotics") - -// Config is the config for a trossen gripper. -type Config struct { - Board string `json:"board"` - Open string `json:"open"` - Close string `json:"close"` - Power string `json:"power"` - AnalogReader string `json:"analog_reader"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - if cfg.Board == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - if cfg.Open == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "open") - } - if cfg.Close == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "close") - } - if cfg.Power == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "power") - } - - if cfg.AnalogReader != "psi" { - return nil, resource.NewConfigValidationError(path, - errors.Errorf("analog_reader %s on board must be created and called 'psi'", cfg.AnalogReader)) - } - deps = append(deps, cfg.Board) - return deps, nil -} - -func init() { - resource.RegisterComponent(gripper.API, model, resource.Registration[gripper.Gripper, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (gripper.Gripper, error) { - b, err := board.FromDependencies(deps, "local") - if err != nil { - return nil, err - } - return newGripper(b, conf, logger) - }, - }) -} - -// softGripper TODO -// -// open is 5 -// close is 6. -type softGripper struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - - theBoard board.Board - - psi board.Analog - - pinOpen, pinClose, pinPower board.GPIOPin - - logger logging.Logger - opMgr *operation.SingleOperationManager - geometries []spatialmath.Geometry -} - -// newGripper instantiates a new Gripper of softGripper type. -func newGripper(b board.Board, conf resource.Config, logger logging.Logger) (gripper.Gripper, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - psi, err := b.AnalogByName("psi") - if err != nil { - return nil, err - } - pinOpen, err := b.GPIOPinByName(newConf.Open) - if err != nil { - return nil, err - } - pinClose, err := b.GPIOPinByName(newConf.Close) - if err != nil { - return nil, err - } - pinPower, err := b.GPIOPinByName(newConf.Power) - if err != nil { - return nil, err - } - - theGripper := &softGripper{ - Named: conf.ResourceName().AsNamed(), - theBoard: b, - psi: psi, - pinOpen: pinOpen, - pinClose: pinClose, - pinPower: pinPower, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - - if theGripper.psi == nil { - return nil, errors.New("no psi analog reader") - } - - if conf.Frame != nil && conf.Frame.Geometry != nil { - geometry, err := conf.Frame.Geometry.ParseConfig() - if err != nil { - return nil, err - } - theGripper.geometries = []spatialmath.Geometry{geometry} - } - - return theGripper, nil -} - -// Stop TODO. -func (g *softGripper) Stop(ctx context.Context, extra map[string]interface{}) error { - ctx, done := g.opMgr.New(ctx) - defer done() - return multierr.Combine( - g.pinOpen.Set(ctx, false, nil), - g.pinClose.Set(ctx, false, nil), - g.pinPower.Set(ctx, false, nil), - ) -} - -// Open TODO. -func (g *softGripper) Open(ctx context.Context, extra map[string]interface{}) error { - ctx, done := g.opMgr.New(ctx) - defer done() - - err := multierr.Combine( - g.pinOpen.Set(ctx, true, nil), - g.pinPower.Set(ctx, true, nil), - ) - if err != nil { - return err - } - - for { - if !utils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return ctx.Err() - } // REMOVE - - val, err := g.psi.Read(ctx, nil) - if err != nil { - return multierr.Combine(err, g.Stop(ctx, extra)) - } - - if val > 500 { - break - } - - if !utils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return ctx.Err() - } - } - - return g.Stop(ctx, extra) -} - -// Grab TODO. -func (g *softGripper) Grab(ctx context.Context, extra map[string]interface{}) (bool, error) { - ctx, done := g.opMgr.New(ctx) - defer done() - - err := multierr.Combine( - g.pinClose.Set(ctx, true, nil), - g.pinPower.Set(ctx, true, nil), - ) - if err != nil { - return false, err - } - - for { - if !utils.SelectContextOrWait(ctx, 100*time.Millisecond) { - return false, ctx.Err() - } // REMOVE - - val, err := g.psi.Read(ctx, nil) - if err != nil { - return false, multierr.Combine(err, g.Stop(ctx, extra)) - } - - if val <= 200 { - break - } - - if !utils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return false, ctx.Err() - } - } - - return false, g.Stop(ctx, extra) -} - -// IsMoving returns whether the gripper is moving. -func (g *softGripper) IsMoving(ctx context.Context) (bool, error) { - return g.opMgr.OpRunning(), nil -} - -// ModelFrame is unimplemented for softGripper. -func (g *softGripper) ModelFrame() referenceframe.Model { - return nil -} - -// Geometries returns the geometries associated with the softGripper. -func (g *softGripper) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - return g.geometries, nil -} diff --git a/components/gripper/verify_main_test.go b/components/gripper/verify_main_test.go deleted file mode 100644 index 85b2ea46454..00000000000 --- a/components/gripper/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package gripper - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/input/client.go b/components/input/client.go deleted file mode 100644 index f2b4db12237..00000000000 --- a/components/input/client.go +++ /dev/null @@ -1,384 +0,0 @@ -// Package input contains a gRPC based input controller client. -package input - -import ( - "context" - "sync" - "time" - - pb "go.viam.com/api/component/inputcontroller/v1" - "go.viam.com/utils" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// client implements InputControllerServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - client pb.InputControllerServiceClient - logger logging.Logger - - name string - streamCancel context.CancelFunc - streamHUP bool - streamRunning bool - streamReady bool - - // streamMu ensures that only one stream at most is active at any given time - streamMu sync.Mutex - - // mu guards access to other members of the struct - mu sync.RWMutex - - closeContext context.Context - activeBackgroundWorkers sync.WaitGroup - cancelBackgroundWorkers context.CancelFunc - callbackWait sync.WaitGroup - callbacks map[Control]map[EventType]ControlFunction - extra *structpb.Struct -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Controller, error) { - c := pb.NewInputControllerServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - closeContext: ctx, - }, nil -} - -func (c *client) Controls(ctx context.Context, extra map[string]interface{}) ([]Control, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetControls(ctx, &pb.GetControlsRequest{ - Controller: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - var controls []Control - for _, control := range resp.Controls { - controls = append(controls, Control(control)) - } - return controls, nil -} - -func (c *client) Events(ctx context.Context, extra map[string]interface{}) (map[Control]Event, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetEvents(ctx, &pb.GetEventsRequest{ - Controller: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - - eventsOut := make(map[Control]Event) - for _, eventIn := range resp.Events { - eventsOut[Control(eventIn.Control)] = Event{ - Time: eventIn.Time.AsTime(), - Event: EventType(eventIn.Event), - Control: Control(eventIn.Control), - Value: eventIn.Value, - } - } - return eventsOut, nil -} - -// TriggerEvent allows directly sending an Event (such as a button press) from external code. -func (c *client) TriggerEvent(ctx context.Context, event Event, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - eventMsg := &pb.Event{ - Time: timestamppb.New(event.Time), - Event: string(event.Event), - Control: string(event.Control), - Value: event.Value, - } - - _, err = c.client.TriggerEvent(ctx, &pb.TriggerEventRequest{ - Controller: c.name, - Event: eventMsg, - Extra: ext, - }) - - return err -} - -func (c *client) checkReady(ctx context.Context) error { - c.mu.RLock() - ready := c.streamReady - c.mu.RUnlock() - - for !ready { - c.mu.RLock() - ready = c.streamReady - c.mu.RUnlock() - if !utils.SelectContextOrWait(ctx, 50*time.Millisecond) { - return ctx.Err() - } - } - return nil -} - -func (c *client) RegisterControlCallback( - ctx context.Context, - control Control, - triggers []EventType, - ctrlFunc ControlFunction, - extra map[string]interface{}, -) error { - c.mu.Lock() - if c.callbacks == nil { - c.callbacks = make(map[Control]map[EventType]ControlFunction) - } - - if _, ok := c.callbacks[control]; !ok { - c.callbacks[control] = make(map[EventType]ControlFunction) - } - - for _, trigger := range triggers { - if trigger == ButtonChange { - c.callbacks[control][ButtonRelease] = ctrlFunc - c.callbacks[control][ButtonPress] = ctrlFunc - } else { - c.callbacks[control][trigger] = ctrlFunc - } - } - c.mu.Unlock() - - // We want to start one and only one connectStream() - c.streamMu.Lock() - defer c.streamMu.Unlock() - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - c.mu.Lock() - c.extra = ext - c.mu.Unlock() - if c.streamRunning { - if err := c.checkReady(ctx); err != nil { - return err - } - c.mu.Lock() - c.streamHUP = true - c.streamReady = false - c.streamCancel() - c.mu.Unlock() - } else { - c.streamRunning = true - c.activeBackgroundWorkers.Add(1) - closeContext, cancel := context.WithCancel(c.closeContext) - c.cancelBackgroundWorkers = cancel - utils.PanicCapturingGo(func() { - defer c.activeBackgroundWorkers.Done() - c.connectStream(closeContext) - }) - if err := c.checkReady(ctx); err != nil { - return err - } - } - - return nil -} - -func (c *client) connectStream(ctx context.Context) { - defer func() { - c.streamMu.Lock() - defer c.streamMu.Unlock() - c.mu.Lock() - defer c.mu.Unlock() - c.streamCancel = nil - c.streamRunning = false - c.streamHUP = false - c.streamReady = false - c.callbackWait.Wait() - }() - - // Will retry on connection errors and disconnects - for { - c.mu.Lock() - c.streamReady = false - c.mu.Unlock() - select { - case <-ctx.Done(): - return - default: - } - - var haveCallbacks bool - c.mu.RLock() - req := &pb.StreamEventsRequest{ - Controller: c.name, - Extra: c.extra, - } - - for control, v := range c.callbacks { - outEvent := &pb.StreamEventsRequest_Events{ - Control: string(control), - } - - for event, ctrlFunc := range v { - if ctrlFunc != nil { - haveCallbacks = true - outEvent.Events = append(outEvent.Events, string(event)) - } else { - outEvent.CancelledEvents = append(outEvent.CancelledEvents, string(event)) - } - } - req.Events = append(req.Events, outEvent) - } - c.mu.RUnlock() - - if !haveCallbacks { - return - } - c.mu.Lock() - streamCtx, cancel := context.WithCancel(ctx) - c.streamCancel = cancel - c.mu.Unlock() - - stream, err := c.client.StreamEvents(streamCtx, req) - if err != nil { - c.logger.CError(ctx, err) - if utils.SelectContextOrWait(ctx, 3*time.Second) { - continue - } - return - } - - c.mu.RLock() - hup := c.streamHUP - c.mu.RUnlock() - if !hup { - c.sendConnectionStatus(ctx, true) - } - - // Handle the rest of the stream - for { - select { - case <-ctx.Done(): - return - default: - } - - c.mu.Lock() - c.streamHUP = false - c.streamReady = true - c.mu.Unlock() - streamResp, err := stream.Recv() - - if err != nil && streamResp == nil { - c.mu.RLock() - hup := c.streamHUP - c.mu.RUnlock() - if hup { - break - } - c.sendConnectionStatus(ctx, false) - if utils.SelectContextOrWait(ctx, 3*time.Second) { - c.logger.CError(ctx, err) - break - } - return - } - if err != nil { - c.logger.CError(ctx, err) - } - eventIn := streamResp.Event - eventOut := Event{ - Time: eventIn.Time.AsTime(), - Event: EventType(eventIn.Event), - Control: Control(eventIn.Control), - Value: eventIn.Value, - } - c.execCallback(ctx, eventOut) - } - } -} - -func (c *client) sendConnectionStatus(ctx context.Context, connected bool) { - c.mu.RLock() - defer c.mu.RUnlock() - evType := Disconnect - now := time.Now() - if connected { - evType = Connect - } - - for control := range c.callbacks { - eventOut := Event{ - Time: now, - Event: evType, - Control: control, - Value: 0, - } - c.execCallback(ctx, eventOut) - } -} - -func (c *client) execCallback(ctx context.Context, event Event) { - c.mu.RLock() - defer c.mu.RUnlock() - callbackMap, ok := c.callbacks[event.Control] - if !ok { - return - } - - callback, ok := callbackMap[event.Event] - if ok && callback != nil { - c.callbackWait.Add(1) - utils.PanicCapturingGo(func() { - defer c.callbackWait.Done() - callback(ctx, event) - }) - } - callbackAll, ok := callbackMap[AllEvents] - if ok && callbackAll != nil { - c.callbackWait.Add(1) - utils.PanicCapturingGo(func() { - defer c.callbackWait.Done() - callbackAll(ctx, event) - }) - } -} - -// Close cleanly closes the underlying connections. -func (c *client) Close(ctx context.Context) error { - if c.cancelBackgroundWorkers != nil { - c.cancelBackgroundWorkers() - c.cancelBackgroundWorkers = nil - } - c.activeBackgroundWorkers.Wait() - return nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/components/input/client_test.go b/components/input/client_test.go deleted file mode 100644 index 9d817ab45d6..00000000000 --- a/components/input/client_test.go +++ /dev/null @@ -1,344 +0,0 @@ -package input_test - -import ( - "context" - "net" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/input" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - var extraOptions map[string]interface{} - - injectInputController := &inject.TriggerableInputController{} - injectInputController.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - extraOptions = extra - return []input.Control{input.AbsoluteX, input.ButtonStart}, nil - } - injectInputController.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - extraOptions = extra - eventsOut := make(map[input.Control]input.Event) - eventsOut[input.AbsoluteX] = input.Event{Time: time.Now(), Event: input.PositionChangeAbs, Control: input.AbsoluteX, Value: 0.7} - eventsOut[input.ButtonStart] = input.Event{Time: time.Now(), Event: input.ButtonPress, Control: input.ButtonStart, Value: 1.0} - return eventsOut, nil - } - evStream := make(chan input.Event) - injectInputController.RegisterControlCallbackFunc = func( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, - ) error { - extraOptions = extra - if ctrlFunc != nil { - outEvent := input.Event{Time: time.Now(), Event: triggers[0], Control: input.ButtonStart, Value: 0.0} - if control == input.AbsoluteX { - outEvent = input.Event{Time: time.Now(), Event: input.PositionChangeAbs, Control: input.AbsoluteX, Value: 0.75} - } - ctrlFunc(ctx, outEvent) - } else { - evStream <- input.Event{} - } - return nil - } - - injectInputController2 := &inject.InputController{} - injectInputController2.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - return nil, errControlsFailed - } - injectInputController2.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - return nil, errEventsFailed - } - - resources := map[resource.Name]input.Controller{ - input.Named(testInputControllerName): injectInputController, - input.Named(failInputControllerName): injectInputController2, - } - inputControllerSvc, err := resource.NewAPIResourceCollection(input.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[input.Controller](input.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, inputControllerSvc), test.ShouldBeNil) - - injectInputController.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("input controller client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - inputController1Client, err := input.NewClientFromConn(context.Background(), conn, "", input.Named(testInputControllerName), logger) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, inputController1Client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }() - - // DoCommand - resp, err := inputController1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - extra := map[string]interface{}{"foo": "Controls"} - controlList, err := inputController1Client.Controls(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, controlList, test.ShouldResemble, []input.Control{input.AbsoluteX, input.ButtonStart}) - test.That(t, extraOptions, test.ShouldResemble, extra) - - extra = map[string]interface{}{"foo": "Events"} - startTime := time.Now() - outState, err := inputController1Client.Events(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, outState[input.ButtonStart].Event, test.ShouldEqual, input.ButtonPress) - test.That(t, outState[input.ButtonStart].Control, test.ShouldEqual, input.ButtonStart) - test.That(t, outState[input.ButtonStart].Value, test.ShouldEqual, 1) - test.That(t, outState[input.ButtonStart].Time.After(startTime), test.ShouldBeTrue) - test.That(t, outState[input.ButtonStart].Time.Before(time.Now()), test.ShouldBeTrue) - - test.That(t, outState[input.AbsoluteX].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(t, outState[input.AbsoluteX].Control, test.ShouldEqual, input.AbsoluteX) - test.That(t, outState[input.AbsoluteX].Value, test.ShouldEqual, 0.7) - test.That(t, outState[input.AbsoluteX].Time.After(startTime), test.ShouldBeTrue) - test.That(t, outState[input.AbsoluteX].Time.Before(time.Now()), test.ShouldBeTrue) - test.That(t, extraOptions, test.ShouldResemble, extra) - - extra = map[string]interface{}{"foo": "RegisterControlCallback"} - ctrlFuncIn := func(ctx context.Context, event input.Event) { evStream <- event } - err = inputController1Client.RegisterControlCallback( - context.Background(), - input.ButtonStart, - []input.EventType{input.ButtonRelease}, - ctrlFuncIn, - extra, - ) - test.That(t, err, test.ShouldBeNil) - ev := <-evStream - test.That(t, ev.Event, test.ShouldEqual, input.ButtonRelease) - test.That(t, ev.Control, test.ShouldEqual, input.ButtonStart) - test.That(t, ev.Value, test.ShouldEqual, 0.0) - test.That(t, ev.Time.After(startTime), test.ShouldBeTrue) - test.That(t, ev.Time.Before(time.Now()), test.ShouldBeTrue) - test.That(t, extraOptions, test.ShouldResemble, extra) - - err = inputController1Client.RegisterControlCallback( - context.Background(), - input.AbsoluteX, - []input.EventType{input.PositionChangeAbs}, - ctrlFuncIn, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - ev1 := <-evStream - ev2 := <-evStream - - var btnEv, posEv input.Event - if ev1.Control == input.ButtonStart { - btnEv = ev1 - posEv = ev2 - } else { - btnEv = ev2 - posEv = ev1 - } - - test.That(t, btnEv.Event, test.ShouldEqual, input.ButtonRelease) - test.That(t, btnEv.Control, test.ShouldEqual, input.ButtonStart) - test.That(t, btnEv.Value, test.ShouldEqual, 0.0) - test.That(t, btnEv.Time.After(startTime), test.ShouldBeTrue) - test.That(t, btnEv.Time.Before(time.Now()), test.ShouldBeTrue) - - test.That(t, posEv.Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(t, posEv.Control, test.ShouldEqual, input.AbsoluteX) - test.That(t, posEv.Value, test.ShouldEqual, 0.75) - test.That(t, posEv.Time.After(startTime), test.ShouldBeTrue) - test.That(t, posEv.Time.Before(time.Now()), test.ShouldBeTrue) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{}) - - err = inputController1Client.RegisterControlCallback( - context.Background(), - input.AbsoluteX, - []input.EventType{input.PositionChangeAbs}, - nil, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - - ev1 = <-evStream - ev2 = <-evStream - - if ev1.Control == input.ButtonStart { - btnEv = ev1 - posEv = ev2 - } else { - btnEv = ev2 - posEv = ev1 - } - - test.That(t, posEv, test.ShouldResemble, input.Event{}) - - test.That(t, btnEv.Event, test.ShouldEqual, input.ButtonRelease) - test.That(t, btnEv.Control, test.ShouldEqual, input.ButtonStart) - test.That(t, btnEv.Value, test.ShouldEqual, 0.0) - test.That(t, btnEv.Time.After(startTime), test.ShouldBeTrue) - test.That(t, btnEv.Time.Before(time.Now()), test.ShouldBeTrue) - - injectInputController.TriggerEventFunc = func(ctx context.Context, event input.Event, extra map[string]interface{}) error { - return errTriggerEvent - } - event1 := input.Event{ - Time: time.Now().UTC(), - Event: input.PositionChangeAbs, - Control: input.AbsoluteX, - Value: 0.7, - } - injectable, ok := inputController1Client.(input.Triggerable) - test.That(t, ok, test.ShouldBeTrue) - err = injectable.TriggerEvent(context.Background(), event1, map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errTriggerEvent.Error()) - - var injectedEvent input.Event - extra = map[string]interface{}{"foo": "TriggerEvent"} - injectInputController.TriggerEventFunc = func(ctx context.Context, event input.Event, extra map[string]interface{}) error { - injectedEvent = event - extraOptions = extra - return nil - } - err = injectable.TriggerEvent(context.Background(), event1, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, injectedEvent, test.ShouldResemble, event1) - test.That(t, extraOptions, test.ShouldResemble, extra) - injectInputController.TriggerEventFunc = nil - }) - - t.Run("input controller client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", input.Named(failInputControllerName), logger) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }() - - _, err = client2.Controls(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errControlsFailed.Error()) - - _, err = client2.Events(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errEventsFailed.Error()) - - event1 := input.Event{ - Time: time.Now().UTC(), - Event: input.PositionChangeAbs, - Control: input.AbsoluteX, - Value: 0.7, - } - injectable, ok := client2.(input.Triggerable) - test.That(t, ok, test.ShouldBeTrue) - err = injectable.TriggerEvent(context.Background(), event1, map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not of type Triggerable") - }) -} - -func TestClientRace(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectInputController := &inject.TriggerableInputController{} - injectInputController.RegisterControlCallbackFunc = func( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, - ) error { - return nil - } - - resources := map[resource.Name]input.Controller{ - input.Named(testInputControllerName): injectInputController, - } - inputControllerSvc, err := resource.NewAPIResourceCollection(input.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[input.Controller](input.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, inputControllerSvc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - inputController1Client, err := input.NewClientFromConn(context.Background(), conn, "", input.Named(testInputControllerName), logger) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, inputController1Client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }() - - extra := map[string]interface{}{"foo": "RegisterControlCallback"} - ctrlFuncIn := func(ctx context.Context, event input.Event) {} - err = inputController1Client.RegisterControlCallback( - context.Background(), - input.ButtonStart, - []input.EventType{input.ButtonRelease}, - ctrlFuncIn, - extra, - ) - test.That(t, err, test.ShouldBeNil) - - err = inputController1Client.RegisterControlCallback( - context.Background(), - input.AbsoluteX, - []input.EventType{input.PositionChangeAbs}, - ctrlFuncIn, - extra, - ) - test.That(t, err, test.ShouldBeNil) - - err = inputController1Client.RegisterControlCallback( - context.Background(), - input.AbsoluteX, - []input.EventType{input.PositionChangeAbs}, - ctrlFuncIn, - extra, - ) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/input/fake/input.go b/components/input/fake/input.go deleted file mode 100644 index 590711d203e..00000000000 --- a/components/input/fake/input.go +++ /dev/null @@ -1,213 +0,0 @@ -// Package fake implements a fake input controller. -package fake - -import ( - "context" - "math/rand" - "sync" - "time" - - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("fake") - -func init() { - resource.RegisterComponent( - input.API, - model, - resource.Registration[input.Controller, *Config]{ - Constructor: func( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (input.Controller, error) { - return NewInputController(ctx, conf, logger) - }, - }, - ) -} - -// Config can list input structs (with their states), define event values and callback delays. -type Config struct { - resource.TriviallyValidateConfig - controls []input.Control - - // EventValue will dictate the value of the events returned. Random between -1 to 1 if unset. - EventValue *float64 `json:"event_value,omitempty"` - - // CallbackDelaySec is the amount of time between callbacks getting triggered. Random between (1-2] sec if unset. - // 0 is not valid and will be overwritten by a random delay. - CallbackDelaySec float64 `json:"callback_delay_sec"` -} - -type callback struct { - control input.Control - triggers []input.EventType - ctrlFunc input.ControlFunction -} - -// NewInputController returns a fake input.Controller. -func NewInputController(ctx context.Context, conf resource.Config, logger logging.Logger) (input.Controller, error) { - closeCtx, cancelFunc := context.WithCancel(context.Background()) - - c := &InputController{ - Named: conf.ResourceName().AsNamed(), - closeCtx: closeCtx, - cancelFunc: cancelFunc, - callbacks: make([]callback, 0), - logger: logger, - } - - if err := c.Reconfigure(ctx, nil, conf); err != nil { - return nil, err - } - - // start callback thread - c.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - c.startCallbackLoop() - }, c.activeBackgroundWorkers.Done) - - return c, nil -} - -// An InputController fakes an input.Controller. -type InputController struct { - resource.Named - - closeCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup - - mu sync.Mutex - controls []input.Control - eventValue *float64 - callbackDelay *time.Duration - callbacks []callback - logger logging.Logger -} - -// Reconfigure updates the config of the controller. -func (c *InputController) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - c.mu.Lock() - defer c.mu.Unlock() - - c.controls = newConf.controls - c.eventValue = newConf.EventValue - if newConf.CallbackDelaySec != 0 { - // convert to milliseconds to avoid any issues with float to int conversions - delay := time.Duration(newConf.CallbackDelaySec*1000) * time.Millisecond - c.callbackDelay = &delay - } - return nil -} - -// Controls lists the inputs of the gamepad. -func (c *InputController) Controls(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - c.mu.Lock() - defer c.mu.Unlock() - if len(c.controls) == 0 { - return []input.Control{input.AbsoluteX, input.ButtonStart}, nil - } - return c.controls, nil -} - -func (c *InputController) eventVal() float64 { - if c.eventValue != nil { - return *c.eventValue - } - //nolint:gosec - return rand.Float64() -} - -// Events returns the a specified or random input.Event (the current state) for AbsoluteX. -func (c *InputController) Events(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - c.mu.Lock() - defer c.mu.Unlock() - eventsOut := make(map[input.Control]input.Event) - - eventsOut[input.AbsoluteX] = input.Event{Time: time.Now(), Event: input.PositionChangeAbs, Control: input.AbsoluteX, Value: c.eventVal()} - return eventsOut, nil -} - -// RegisterControlCallback registers a callback function to be executed on the specified trigger Event. The fake implementation will -// trigger the callback at a random or user-specified interval with a random or user-specified value. -func (c *InputController) RegisterControlCallback( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, -) error { - c.mu.Lock() - defer c.mu.Unlock() - - c.callbacks = append(c.callbacks, callback{control: control, triggers: triggers, ctrlFunc: ctrlFunc}) - return nil -} - -func (c *InputController) startCallbackLoop() { - for { - var callbackDelay time.Duration - - if c.closeCtx.Err() != nil { - return - } - - c.mu.Lock() - if c.callbackDelay != nil { - callbackDelay = *c.callbackDelay - } else { - //nolint:gosec - callbackDelay = 1*time.Second + time.Duration(rand.Float64()*1000)*time.Millisecond - } - c.mu.Unlock() - - if !utils.SelectContextOrWait(c.closeCtx, callbackDelay) { - return - } - - select { - case <-c.closeCtx.Done(): - return - default: - c.mu.Lock() - evValue := c.eventVal() - for _, callback := range c.callbacks { - for _, t := range callback.triggers { - event := input.Event{Time: time.Now(), Event: t, Control: callback.control, Value: evValue} - callback.ctrlFunc(c.closeCtx, event) - } - } - c.mu.Unlock() - } - } -} - -// TriggerEvent allows directly sending an Event (such as a button press) from external code. -func (c *InputController) TriggerEvent(ctx context.Context, event input.Event, extra map[string]interface{}) error { - return errors.New("unsupported") -} - -// Close attempts to cleanly close the input controller. -func (c *InputController) Close(ctx context.Context) error { - c.mu.Lock() - var err error - if c.cancelFunc != nil { - c.cancelFunc() - c.cancelFunc = nil - } - c.mu.Unlock() - - c.activeBackgroundWorkers.Wait() - return err -} diff --git a/components/input/fake/input_test.go b/components/input/fake/input_test.go deleted file mode 100644 index ebcb5f5463e..00000000000 --- a/components/input/fake/input_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package fake - -import ( - "context" - "testing" - "time" - - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func setupDefaultInput(t *testing.T) *InputController { - t.Helper() - return setupInputWithCfg(t, Config{}) -} - -var ( - controls = []input.Control{input.AbsoluteHat0X} - value = 0.7 - delay = 200 * time.Millisecond -) - -func setupDefinedInput(t *testing.T) *InputController { - t.Helper() - conf := Config{ - controls: controls, - EventValue: &value, - CallbackDelaySec: float64(delay/time.Millisecond) / 1000, - } - return setupInputWithCfg(t, conf) -} - -func setupInputWithCfg(t *testing.T, conf Config) *InputController { - t.Helper() - logger := logging.NewTestLogger(t) - input, err := NewInputController(context.Background(), resource.Config{ConvertedAttributes: &conf}, logger) - test.That(t, err, test.ShouldBeNil) - return input.(*InputController) -} - -func TestControl(t *testing.T) { - for _, tc := range []struct { - TestName string - Default bool - Expected []input.Control - }{ - { - "default", - true, - []input.Control{input.AbsoluteX, input.ButtonStart}, - }, - { - "defined", - false, - controls, - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - var i input.Controller - if tc.Default { - i = setupDefaultInput(t) - } else { - i = setupDefinedInput(t) - } - defer func() { - test.That(t, i.Close(context.Background()), test.ShouldBeNil) - }() - actual, err := i.Controls(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, actual, test.ShouldResemble, tc.Expected) - }) - } -} - -func TestEvents(t *testing.T) { - for _, useDefaultInput := range []bool{true, false} { - var tName string - if useDefaultInput { - tName = "default" - } else { - tName = "defined" - } - - t.Run(tName, func(t *testing.T) { - var i input.Controller - if useDefaultInput { - i = setupDefaultInput(t) - } else { - i = setupDefinedInput(t) - } - defer func() { - test.That(t, i.Close(context.Background()), test.ShouldBeNil) - }() - actual, err := i.Events(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(actual), test.ShouldEqual, 1) - - event, ok := actual[input.AbsoluteX] - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, event.Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(t, event.Control, test.ShouldEqual, input.AbsoluteX) - - if useDefaultInput { - test.That(t, event.Value, test.ShouldBeBetween, 0, 1) - } else { - test.That(t, event.Value, test.ShouldAlmostEqual, value) - } - }) - } -} - -func TestRegisterControlCallback(t *testing.T) { - i := setupDefinedInput(t) - defer func() { - test.That(t, i.Close(context.Background()), test.ShouldBeNil) - }() - calledEnough := make(chan struct{}) - var ( - callCount int - v float64 - ) - - ctrlFunc := func(ctx context.Context, event input.Event) { - callCount++ - if callCount == 5 { - v = event.Value - close(calledEnough) - } - } - - start := time.Now() - err := i.RegisterControlCallback(context.Background(), input.AbsoluteHat0X, []input.EventType{input.ButtonPress}, ctrlFunc, nil) - test.That(t, err, test.ShouldBeNil) - <-calledEnough - test.That(t, time.Since(start), test.ShouldBeGreaterThanOrEqualTo, 5*delay) - test.That(t, time.Since(start), test.ShouldBeLessThanOrEqualTo, 7*delay) - test.That(t, callCount, test.ShouldEqual, 5) - test.That(t, v, test.ShouldAlmostEqual, value) -} - -func TestTriggerEvent(t *testing.T) { - i := setupDefaultInput(t) - defer func() { - test.That(t, i.Close(context.Background()), test.ShouldBeNil) - }() - err := i.TriggerEvent(context.Background(), input.Event{}, nil) - test.That(t, err, test.ShouldBeError, errors.New("unsupported")) -} diff --git a/components/input/gamepad/gamepad.go b/components/input/gamepad/gamepad.go deleted file mode 100644 index cd13fc066ad..00000000000 --- a/components/input/gamepad/gamepad.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !linux -// +build !linux - -// Package gamepad implements a linux gamepad as an input controller. -package gamepad - -import ( - "context" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("gamepad") - -func init() { - resource.RegisterComponent(input.API, model, resource.Registration[input.Controller, resource.NoNativeConfig]{ - Constructor: func(ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger) (input.Controller, error) { - return nil, errors.New("gamepad input currently only supported on linux") - }, - }) -} diff --git a/components/input/gamepad/gamepad_linux.go b/components/input/gamepad/gamepad_linux.go deleted file mode 100644 index 637d21f42ce..00000000000 --- a/components/input/gamepad/gamepad_linux.go +++ /dev/null @@ -1,425 +0,0 @@ -//go:build linux -// +build linux - -// Package gamepad implements a linux gamepad as an input controller. -package gamepad - -import ( - "context" - "math" - "path/filepath" - "strings" - "sync" - "syscall" - "time" - - "github.com/pkg/errors" - "github.com/viamrobotics/evdev" - "go.viam.com/utils" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -const defaultMapping = "Microsoft X-Box 360 pad" - -var model = resource.DefaultModelFamily.WithModel("gamepad") - -// Config is used for converting config attributes. -type Config struct { - resource.TriviallyValidateConfig - DevFile string `json:"dev_file,omitempty"` - AutoReconnect bool `json:"auto_reconnect,omitempty"` -} - -func init() { - resource.RegisterComponent(input.API, model, resource.Registration[input.Controller, *Config]{ - Constructor: NewController, - }) -} - -func createController(_ context.Context, name resource.Name, logger logging.Logger, devFile string, reconnect bool) input.Controller { - ctxWithCancel, cancel := context.WithCancel(context.Background()) - g := gamepad{ - Named: name.AsNamed(), - logger: logger, - reconnect: reconnect, - devFile: devFile, - cancelFunc: cancel, - callbacks: map[input.Control]map[input.EventType]input.ControlFunction{}, - lastEvents: map[input.Control]input.Event{}, - } - - g.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer g.activeBackgroundWorkers.Done() - for { - if !utils.SelectContextOrWait(ctxWithCancel, 250*time.Millisecond) { - return - } - err := g.connectDev(ctxWithCancel) - if err != nil { - if g.reconnect { - if !strings.Contains(err.Error(), "no gamepad found") { - g.logger.Error(err) - } - continue - } - g.logger.Error(err) - return - } - g.eventDispatcher(ctxWithCancel) - } - }) - return &g -} - -// NewController creates a new gamepad. -func NewController( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, -) (input.Controller, error) { - return createController( - ctx, - conf.ResourceName(), - logger, - conf.ConvertedAttributes.(*Config).DevFile, - conf.ConvertedAttributes.(*Config).AutoReconnect, - ), nil -} - -// gamepad is an input.Controller. -type gamepad struct { - resource.Named - resource.AlwaysRebuild - dev *evdev.Evdev - Model string - Mapping Mapping - controls []input.Control - lastEvents map[input.Control]input.Event - logger logging.Logger - mu sync.RWMutex - activeBackgroundWorkers sync.WaitGroup - cancelFunc func() - callbacks map[input.Control]map[input.EventType]input.ControlFunction - devFile string - reconnect bool -} - -// Mapping represents the evdev code to input.Control mapping for a given gamepad model. -type Mapping struct { - Buttons map[evdev.KeyType]input.Control - Axes map[evdev.AbsoluteType]input.Control -} - -func timevaltoTime(timeVal syscall.Timeval) time.Time { - //nolint:unconvert - return time.Unix(int64(timeVal.Sec), int64(timeVal.Usec*1000)) -} - -func scaleAxis(x, inMin, inMax int32, outMin, outMax float64) float64 { - return float64(x-inMin)*(outMax-outMin)/float64(inMax-inMin) + outMin -} - -func (g *gamepad) eventDispatcher(ctx context.Context) { - evChan := g.dev.Poll(ctx) - for { - select { - case <-ctx.Done(): - err := g.dev.Close() - if err != nil { - g.logger.CError(ctx, err) - } - return - case eventIn := <-evChan: - if eventIn == nil || eventIn.Event.Type == evdev.EventMisc || - (eventIn.Event.Type == evdev.EventSync && eventIn.Event.Code == 0) { - continue - } - // Use debug line below when developing new controller mappings - // g.logger.Debugf( - // "%s: Type: %d, Code: %d, Value: %d\n", - // timevaltoTime(eventIn.Event.Time), eventIn.Event.Type, eventIn.Event.Control, eventIn.Event.Value) - - var eventOut input.Event - switch eventIn.Event.Type { - case evdev.EventSync: - if evdev.SyncType(eventIn.Event.Code) == 4 { - g.sendConnectionStatus(ctx, false) - err := g.dev.Close() - if err != nil { - g.logger.CError(ctx, err) - } - g.dev = nil - return - } - g.logger.CDebugf(ctx, "unhandled event: %+v", eventIn) - - case evdev.EventAbsolute: - thisAxis, ok := g.Mapping.Axes[eventIn.Type.(evdev.AbsoluteType)] - if !ok { - // Unmapped axis - continue - } - - info := g.dev.AbsoluteTypes()[eventIn.Type.(evdev.AbsoluteType)] - - var scaledPos float64 - //nolint:exhaustive - switch thisAxis { - case input.AbsolutePedalAccelerator, input.AbsolutePedalBrake, input.AbsolutePedalClutch: - // Scale pedals 1.0 to 0 - // We invert the values because resting state is the high value and we'd like it to be zero. - scaledPos = 1 - scaleAxis(eventIn.Event.Value, info.Min, info.Max, 0, 1.0) - case input.AbsoluteZ, input.AbsoluteRZ: - // Scale triggers 0.0 to 1.0 - scaledPos = scaleAxis(eventIn.Event.Value, info.Min, info.Max, 0, 1.0) - default: - // Scale normal axes -1.0 to 1.0 - scaledPos = scaleAxis(eventIn.Event.Value, info.Min, info.Max, -1.0, 1.0) - } - - // Use evDev provided deadzone - if math.Abs(scaledPos) <= float64(info.Flat)/float64(info.Max-info.Min) { - scaledPos = 0.0 - } - - eventOut = input.Event{ - Time: timevaltoTime(eventIn.Event.Time), - Event: input.PositionChangeAbs, - Control: thisAxis, - Value: scaledPos, - } - - case evdev.EventKey: - thisButton, ok := g.Mapping.Buttons[eventIn.Type.(evdev.KeyType)] - if !ok { - // Unmapped button - continue - } - - eventOut = input.Event{ - Time: timevaltoTime(eventIn.Event.Time), - Event: input.ButtonChange, - Control: thisButton, - Value: float64(eventIn.Event.Value), - } - - switch eventIn.Event.Value { - case 0: - eventOut.Event = input.ButtonRelease - case 1: - eventOut.Event = input.ButtonPress - case 2: - eventOut.Event = input.ButtonHold - } - case evdev.EventEffect, evdev.EventEffectStatus, evdev.EventLED, evdev.EventMisc, - evdev.EventPower, evdev.EventRelative, evdev.EventRepeat, evdev.EventSound, - evdev.EventSwitch: - fallthrough - default: - g.logger.CDebugf(ctx, "unhandled event: %+v", eventIn) - } - - g.makeCallbacks(ctx, eventOut) - } - } -} - -func (g *gamepad) sendConnectionStatus(ctx context.Context, connected bool) { - evType := input.Disconnect - now := time.Now() - if connected { - evType = input.Connect - } - - for _, control := range g.controls { - if g.lastEvents[control].Event != evType { - eventOut := input.Event{ - Time: now, - Event: evType, - Control: control, - Value: 0, - } - g.makeCallbacks(ctx, eventOut) - } - } -} - -func (g *gamepad) makeCallbacks(ctx context.Context, eventOut input.Event) { - g.mu.Lock() - g.lastEvents[eventOut.Control] = eventOut - g.mu.Unlock() - - g.mu.RLock() - _, ok := g.callbacks[eventOut.Control] - g.mu.RUnlock() - if !ok { - g.mu.Lock() - g.callbacks[eventOut.Control] = make(map[input.EventType]input.ControlFunction) - g.mu.Unlock() - } - g.mu.RLock() - defer g.mu.RUnlock() - - ctrlFunc, ok := g.callbacks[eventOut.Control][eventOut.Event] - if ok && ctrlFunc != nil { - g.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer g.activeBackgroundWorkers.Done() - ctrlFunc(ctx, eventOut) - }) - } - - ctrlFuncAll, ok := g.callbacks[eventOut.Control][input.AllEvents] - if ok && ctrlFuncAll != nil { - g.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer g.activeBackgroundWorkers.Done() - ctrlFuncAll(ctx, eventOut) - }) - } -} - -func (g *gamepad) connectDev(ctx context.Context) error { - g.mu.Lock() - var devs []string - devs = []string{g.devFile} - - if len(devs) != 1 || devs[0] == "" { - var err error - devs, err = filepath.Glob("/dev/input/event*") - if err != nil { - g.mu.Unlock() - return err - } - } - - for _, n := range devs { - dev, err := evdev.OpenFile(n) - if err != nil { - continue - } - name := dev.Name() - name = strings.TrimSpace(name) - mapping, ok := MappingForModel(name) - if ok { - g.logger.CInfof(ctx, "found known gamepad: '%s' at %s", name, n) - g.dev = dev - g.Model = g.dev.Name() - g.Mapping = mapping - break - } - if err := dev.Close(); err != nil { - return err - } - } - - // Fallback to a default mapping - if g.dev == nil { - for _, n := range devs { - dev, err := evdev.OpenFile(n) - if err != nil { - continue - } - if isGamepad(dev) { - name := dev.Name() - g.logger.CInfof(ctx, "found gamepad: '%s' at %s", name, n) - g.logger.CInfof(ctx, "no button mapping for '%s', using default: '%s'", name, defaultMapping) - g.dev = dev - g.Model = g.dev.Name() - g.Mapping, _ = MappingForModel(defaultMapping) - break - } - if err := dev.Close(); err != nil { - return err - } - } - } - - if g.dev == nil { - g.mu.Unlock() - return errors.New("no gamepad found (check /dev/input/eventXX permissions)") - } - - for _, v := range g.Mapping.Axes { - g.controls = append(g.controls, v) - } - for _, v := range g.Mapping.Buttons { - g.controls = append(g.controls, v) - } - - g.mu.Unlock() - g.sendConnectionStatus(ctx, true) - - return nil -} - -// Close terminates background worker threads. -func (g *gamepad) Close(ctx context.Context) error { - g.cancelFunc() - g.activeBackgroundWorkers.Wait() - if g.dev != nil { - if err := g.dev.Close(); err != nil { - g.logger.CError(ctx, err) - } - } - return nil -} - -// Controls lists the inputs of the gamepad. -func (g *gamepad) Controls(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - g.mu.RLock() - defer g.mu.RUnlock() - if g.dev == nil && len(g.controls) == 0 { - return nil, errors.New("no controller connected") - } - out := append([]input.Control(nil), g.controls...) - return out, nil -} - -// Events returns the last input.Event (the current state). -func (g *gamepad) Events(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - g.mu.RLock() - defer g.mu.RUnlock() - out := make(map[input.Control]input.Event) - for key, value := range g.lastEvents { - out[key] = value - } - return out, nil -} - -// RegisterControlCallback registers a callback function to be executed on the specified control's trigger Events. -func (g *gamepad) RegisterControlCallback( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, -) error { - g.mu.Lock() - defer g.mu.Unlock() - if g.callbacks[control] == nil { - g.callbacks[control] = make(map[input.EventType]input.ControlFunction) - } - - for _, trigger := range triggers { - if trigger == input.ButtonChange { - g.callbacks[control][input.ButtonRelease] = ctrlFunc - g.callbacks[control][input.ButtonPress] = ctrlFunc - } else { - g.callbacks[control][trigger] = ctrlFunc - } - } - return nil -} - -func isGamepad(dev *evdev.Evdev) bool { - if dev.IsJoystick() { - axes := dev.AbsoluteTypes() - _, x := axes[evdev.AbsoluteX] - _, y := axes[evdev.AbsoluteY] - return x && y - } - return false -} diff --git a/components/input/gamepad/gamepad_mappings_linux.go b/components/input/gamepad/gamepad_mappings_linux.go deleted file mode 100644 index fb75a4a0b77..00000000000 --- a/components/input/gamepad/gamepad_mappings_linux.go +++ /dev/null @@ -1,284 +0,0 @@ -//go:build linux -// +build linux - -package gamepad - -import ( - "strings" - - "github.com/viamrobotics/evdev" - - "go.viam.com/rdk/components/input" -) - -const stadiaPrefix = "Stadia" - -// gamepadMappings contains all the axes/button translations for each model. -// use evtest on linux figure out what maps to what. -var gamepadMappings = map[string]Mapping{ - // 8BitDo Pro 2 Wireless, S-input mode - // Also the Nintendo Switch Pro Controller - "Pro Controller": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 3: input.AbsoluteRX, - 4: input.AbsoluteRY, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 304: input.ButtonSouth, - 305: input.ButtonEast, - 306: input.ButtonWest, - 307: input.ButtonNorth, - 308: input.ButtonLT, - 309: input.ButtonRT, - 310: input.ButtonLT2, - 311: input.ButtonRT2, - 312: input.ButtonSelect, - 313: input.ButtonStart, - 314: input.ButtonLThumb, - 315: input.ButtonRThumb, - 316: input.ButtonMenu, - 317: input.ButtonRecord, - }, - }, - // Wireless, X-input mode - "8BitDo Pro 2": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 2: input.AbsoluteZ, - 3: input.AbsoluteRX, - 4: input.AbsoluteRY, - 5: input.AbsoluteRZ, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 304: input.ButtonSouth, - 305: input.ButtonEast, - 306: input.ButtonWest, - 307: input.ButtonNorth, - 308: input.ButtonLT, - 309: input.ButtonRT, - 310: input.ButtonSelect, - 311: input.ButtonStart, - 312: input.ButtonLThumb, - 313: input.ButtonRThumb, - 139: input.ButtonMenu, - }, - }, - // Wired, X-input mode 8BitDo Pro 2 - "Microsoft X-Box 360 pad": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 2: input.AbsoluteZ, - 3: input.AbsoluteRX, - 4: input.AbsoluteRY, - 5: input.AbsoluteRZ, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 304: input.ButtonSouth, - 305: input.ButtonEast, - 307: input.ButtonWest, - 308: input.ButtonNorth, - 310: input.ButtonLT, - 311: input.ButtonRT, - 314: input.ButtonSelect, - 315: input.ButtonStart, - 317: input.ButtonLThumb, - 318: input.ButtonRThumb, - 316: input.ButtonMenu, - }, - }, - // Xbox Series X|S, wireless mode - "Xbox Wireless Controller": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 10: input.AbsoluteZ, - 2: input.AbsoluteRX, - 5: input.AbsoluteRY, - 9: input.AbsoluteRZ, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 304: input.ButtonSouth, - 305: input.ButtonEast, - 307: input.ButtonWest, - 308: input.ButtonNorth, - 310: input.ButtonLT, - 311: input.ButtonRT, - 314: input.ButtonSelect, - 315: input.ButtonStart, - 317: input.ButtonLThumb, - 318: input.ButtonRThumb, - 316: input.ButtonMenu, - 167: input.ButtonRecord, - }, - }, - // Xbox Series X|S, wired mode - "Microsoft Xbox One X pad": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 2: input.AbsoluteZ, - 3: input.AbsoluteRX, - 4: input.AbsoluteRY, - 5: input.AbsoluteRZ, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 304: input.ButtonSouth, - 305: input.ButtonEast, - 307: input.ButtonWest, - 308: input.ButtonNorth, - 310: input.ButtonLT, - 311: input.ButtonRT, - 314: input.ButtonSelect, - 315: input.ButtonStart, - 317: input.ButtonLThumb, - 318: input.ButtonRThumb, - 316: input.ButtonMenu, - }, - }, - // Wireless industrial controller - "FORT Robotics nVSC Application": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 2: input.AbsoluteZ, - 3: input.AbsoluteRX, - 4: input.AbsoluteRY, - 5: input.AbsoluteRZ, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 288: input.ButtonSouth, - 289: input.ButtonEast, - 291: input.ButtonWest, - 290: input.ButtonNorth, - 292: input.ButtonEStop, - }, - }, - - // https://www.amazon.com/SQDeal-Joystick-Controller-Vibration-Feedback/dp/B01GR9ZZTS - "USB Gamepad": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 2: input.AbsoluteRY, - 5: input.AbsoluteRX, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 288: input.ButtonNorth, - 289: input.ButtonEast, - 291: input.ButtonWest, - 290: input.ButtonSouth, - - 292: input.ButtonLT, - 293: input.ButtonRT, - 294: input.ButtonLT2, - 295: input.ButtonRT2, - - 296: input.ButtonSelect, - 297: input.ButtonStart, - }, - }, - // PS4 Controller - "Wireless Controller": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 2: input.AbsoluteZ, - 3: input.AbsoluteRX, - 4: input.AbsoluteRY, - 5: input.AbsoluteRZ, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 304: input.ButtonSouth, - 305: input.ButtonEast, - 307: input.ButtonNorth, - 308: input.ButtonWest, - 310: input.ButtonLT, - 311: input.ButtonRT, - 314: input.ButtonSelect, - 315: input.ButtonStart, - 317: input.ButtonLThumb, - 318: input.ButtonRThumb, - 316: input.ButtonMenu, - }, - }, - // Logitech G920/G29 Wheel - "Logitech G920 Driving Force Racing Wheel": { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsolutePedalAccelerator, - 2: input.AbsolutePedalBrake, - 5: input.AbsolutePedalClutch, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 288: input.ButtonSouth, - 289: input.ButtonEast, - 291: input.ButtonNorth, - 290: input.ButtonWest, - 293: input.ButtonLT, - 292: input.ButtonRT, - 295: input.ButtonSelect, - 294: input.ButtonStart, - 297: input.ButtonLThumb, - 296: input.ButtonRThumb, - 298: input.ButtonMenu, - }, - }, - // Stadia Controller - stadiaPrefix: { - Axes: map[evdev.AbsoluteType]input.Control{ - 0: input.AbsoluteX, - 1: input.AbsoluteY, - 10: input.AbsoluteZ, - 2: input.AbsoluteRX, - 5: input.AbsoluteRY, - 9: input.AbsoluteRZ, - 16: input.AbsoluteHat0X, - 17: input.AbsoluteHat0Y, - }, - Buttons: map[evdev.KeyType]input.Control{ - 304: input.ButtonSouth, - 305: input.ButtonEast, - 307: input.ButtonWest, - 308: input.ButtonNorth, - 310: input.ButtonLT, - 311: input.ButtonRT, - 314: input.ButtonSelect, - 315: input.ButtonStart, - 316: input.ButtonMenu, - 317: input.ButtonLThumb, - 318: input.ButtonRThumb, - }, - }, -} - -// MappingForModel returns the mapping for a given model. -func MappingForModel(model string) (Mapping, bool) { - // Stadia controller device names are unique of the form "StadiaXXXX-XXXX" - if strings.HasPrefix(model, stadiaPrefix) { - model = stadiaPrefix - } - mapping, ok := gamepadMappings[model] - return mapping, ok -} diff --git a/components/input/gpio/gpio.go b/components/input/gpio/gpio.go deleted file mode 100644 index 4d7b8d065bc..00000000000 --- a/components/input/gpio/gpio.go +++ /dev/null @@ -1,383 +0,0 @@ -// Package gpio implements a gpio/adc based input.Controller. -package gpio - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/bep/debounce" - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("gpio") - -// Config is the overall config. -type Config struct { - Board string `json:"board"` - Buttons map[string]*ButtonConfig `json:"buttons"` - Axes map[string]*AxisConfig `json:"axes"` -} - -// AxisConfig is a subconfig for axes. -type AxisConfig struct { - Control input.Control `json:"control"` - Min int `json:"min"` - Max int `json:"max"` - Bidirectional bool `json:"bidirectional"` - Deadzone int `json:"deadzone"` - MinChange int `json:"min_change"` - PollHz float64 `json:"poll_hz"` - Invert bool `json:"invert"` -} - -// ButtonConfig is a subconfig for buttons. -type ButtonConfig struct { - Control input.Control `json:"control"` - Invert bool `json:"invert"` - DebounceMs int `json:"debounce_msec"` // set to -1 to disable, default=5 -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - if conf.Board == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - if len(conf.Axes) == 0 && len(conf.Buttons) == 0 { - return nil, resource.NewConfigValidationError(path, errors.New("buttons and axes cannot be both empty")) - } - deps = append(deps, conf.Board) - return deps, nil -} - -func (conf *Config) validateValues() error { - for _, control := range conf.Buttons { - if control.DebounceMs == 0 { - control.DebounceMs = 5 - } - } - - for _, axis := range conf.Axes { - if axis.MinChange < 1 { - axis.MinChange = 1 - } - if axis.PollHz <= 0 { - axis.PollHz = 10 - } - if axis.Min >= axis.Max { - return fmt.Errorf("min (%d) must be less than max (%d)", axis.Min, axis.Max) - } - } - - return nil -} - -func init() { - resource.RegisterComponent(input.API, model, resource.Registration[input.Controller, *Config]{ - Constructor: NewGPIOController, - }) -} - -// NewGPIOController returns a new input.Controller. -func NewGPIOController( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (input.Controller, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - cancelCtx, cancel := context.WithCancel(context.Background()) - c := Controller{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - cancelFunc: cancel, - callbacks: map[input.Control]map[input.EventType]input.ControlFunction{}, - lastEvents: map[input.Control]input.Event{}, - } - - if err := newConf.validateValues(); err != nil { - return nil, err - } - - brd, err := board.FromDependencies(deps, newConf.Board) - if err != nil { - return nil, err - } - - for interruptName, control := range newConf.Buttons { - interrupt, err := brd.DigitalInterruptByName(interruptName) - if err != nil { - return nil, err - } - err = c.newButton(cancelCtx, brd, interrupt, *control) - if err != nil { - return nil, err - } - } - - for reader, axis := range newConf.Axes { - err := c.newAxis(cancelCtx, brd, reader, *axis) - if err != nil { - return nil, err - } - } - - c.sendConnectionStatus(ctx, true) - - return &c, nil -} - -// A Controller creates an input.Controller from DigitalInterrupts and AnalogReaders. -type Controller struct { - resource.Named - resource.AlwaysRebuild - mu sync.RWMutex - controls []input.Control - lastEvents map[input.Control]input.Event - logger logging.Logger - activeBackgroundWorkers sync.WaitGroup - cancelFunc func() - callbacks map[input.Control]map[input.EventType]input.ControlFunction -} - -// Controls lists the inputs. -func (c *Controller) Controls(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - c.mu.RLock() - defer c.mu.RUnlock() - out := append([]input.Control(nil), c.controls...) - return out, nil -} - -// Events returns the last input.Event (the current state) of each control. -func (c *Controller) Events(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - c.mu.RLock() - defer c.mu.RUnlock() - out := make(map[input.Control]input.Event) - for key, value := range c.lastEvents { - out[key] = value - } - return out, nil -} - -// RegisterControlCallback registers a callback function to be executed on the specified trigger Event. -func (c *Controller) RegisterControlCallback( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, -) error { - c.mu.Lock() - defer c.mu.Unlock() - if c.callbacks[control] == nil { - c.callbacks[control] = make(map[input.EventType]input.ControlFunction) - } - - for _, trigger := range triggers { - if trigger == input.ButtonChange { - c.callbacks[control][input.ButtonRelease] = ctrlFunc - c.callbacks[control][input.ButtonPress] = ctrlFunc - } else { - c.callbacks[control][trigger] = ctrlFunc - } - } - return nil -} - -// Close terminates background worker threads. -func (c *Controller) Close(ctx context.Context) error { - c.cancelFunc() - c.activeBackgroundWorkers.Wait() - return nil -} - -func (c *Controller) makeCallbacks(ctx context.Context, eventOut input.Event) { - c.mu.Lock() - c.lastEvents[eventOut.Control] = eventOut - c.mu.Unlock() - - c.mu.RLock() - _, ok := c.callbacks[eventOut.Control] - c.mu.RUnlock() - if !ok { - c.mu.Lock() - c.callbacks[eventOut.Control] = make(map[input.EventType]input.ControlFunction) - c.mu.Unlock() - } - c.mu.RLock() - defer c.mu.RUnlock() - - ctrlFunc, ok := c.callbacks[eventOut.Control][eventOut.Event] - if ok && ctrlFunc != nil { - c.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer c.activeBackgroundWorkers.Done() - ctrlFunc(ctx, eventOut) - }) - } - - ctrlFuncAll, ok := c.callbacks[eventOut.Control][input.AllEvents] - if ok && ctrlFuncAll != nil { - c.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer c.activeBackgroundWorkers.Done() - ctrlFuncAll(ctx, eventOut) - }) - } -} - -func (c *Controller) sendConnectionStatus(ctx context.Context, connected bool) { - evType := input.Disconnect - now := time.Now() - if connected { - evType = input.Connect - } - - for _, control := range c.controls { - if c.lastEvents[control].Event != evType { - eventOut := input.Event{ - Time: now, - Event: evType, - Control: control, - Value: 0, - } - c.makeCallbacks(ctx, eventOut) - } - } -} - -func (c *Controller) newButton(ctx context.Context, brd board.Board, interrupt board.DigitalInterrupt, cfg ButtonConfig) error { - tickChan := make(chan board.Tick) - err := brd.StreamTicks(ctx, []board.DigitalInterrupt{interrupt}, tickChan, nil) - if err != nil { - return errors.Wrap(err, "error getting digital interrupt ticks") - } - - c.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - debounced := debounce.New(time.Millisecond * time.Duration(cfg.DebounceMs)) - for { - var val bool - select { - case <-ctx.Done(): - return - case tick := <-tickChan: - val = tick.High - } - - if cfg.Invert { - val = !val - } - - evt := input.ButtonPress - outVal := 1.0 - if !val { - evt = input.ButtonRelease - outVal = 0 - } - - eventOut := input.Event{ - Time: time.Now(), - Event: evt, - Control: cfg.Control, - Value: outVal, - } - if cfg.DebounceMs < 0 { - c.makeCallbacks(ctx, eventOut) - } else { - debounced(func() { c.makeCallbacks(ctx, eventOut) }) - } - } - }, c.activeBackgroundWorkers.Done) - c.controls = append(c.controls, cfg.Control) - return nil -} - -func (c *Controller) newAxis(ctx context.Context, brd board.Board, analogName string, cfg AxisConfig) error { - reader, err := brd.AnalogByName(analogName) - if err != nil { - return err - } - - c.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - var prevVal int - ticker := time.NewTicker(time.Second / time.Duration(cfg.PollHz)) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - } - rawVal, err := reader.Read(ctx, nil) - if err != nil { - c.logger.CError(ctx, err) - } - - if rawVal > cfg.Max { - rawVal = cfg.Max - } else if rawVal < cfg.Min { - rawVal = cfg.Min - } - - var outVal float64 - if cfg.Bidirectional { - center := (cfg.Min + cfg.Max) / 2 - if abs(rawVal-center) < cfg.Deadzone { - rawVal = center - outVal = 0.0 - } else { - outVal = scaleAxis(rawVal, cfg.Min, cfg.Max, -1, 1) - } - } else { - if abs(rawVal-cfg.Min) < cfg.Deadzone { - rawVal = cfg.Min - } - outVal = scaleAxis(rawVal, cfg.Min, cfg.Max, 0, 1) - } - - if abs(rawVal-prevVal) < cfg.MinChange { - continue - } - - if cfg.Invert { - outVal *= -1 - } - - prevVal = rawVal - eventOut := input.Event{ - Time: time.Now(), - Event: input.PositionChangeAbs, - Control: cfg.Control, - Value: outVal, - } - c.makeCallbacks(ctx, eventOut) - } - }, c.activeBackgroundWorkers.Done) - c.controls = append(c.controls, cfg.Control) - return nil -} - -func abs(x int) int { - if x < 0 { - return -x - } - return x -} - -func scaleAxis(x, inMin, inMax int, outMin, outMax float64) float64 { - return float64(x-inMin)*(outMax-outMin)/float64(inMax-inMin) + outMin -} diff --git a/components/input/gpio/gpio_test.go b/components/input/gpio/gpio_test.go deleted file mode 100644 index ef7d3f64377..00000000000 --- a/components/input/gpio/gpio_test.go +++ /dev/null @@ -1,656 +0,0 @@ -package gpio - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -type setupResult struct { - btn1Callbacks, btn2Callbacks int64 - axis1Callbacks, axis2Callbacks, axis3Callbacks int64 - ctx context.Context - logger logging.Logger - b *inject.Board - dev input.Controller - axis1Time, axis2Time time.Time - axisMu sync.RWMutex - interrupt1, interrupt2 *inject.DigitalInterrupt - analog1, analog2, analog3 *inject.Analog - mu sync.Mutex -} - -func setup(t *testing.T) *setupResult { - t.Helper() - s := setupResult{} - - s.ctx = context.Background() - s.logger = logging.NewTestLogger(t) - - b := inject.NewBoard("test-board") - s.b = b - s.interrupt1 = &inject.DigitalInterrupt{} - s.interrupt2 = &inject.DigitalInterrupt{} - - callbacks := make(map[board.DigitalInterrupt]chan board.Tick) - - s.interrupt1.NameFunc = func() string { - return "interrupt1" - } - s.interrupt1.TickFunc = func(ctx context.Context, high bool, nanoseconds uint64) error { - ch, ok := callbacks[s.interrupt1] - test.That(t, ok, test.ShouldBeTrue) - ch <- board.Tick{Name: s.interrupt1.Name(), High: high, TimestampNanosec: nanoseconds} - return nil - } - - // interrupt2 funcs - s.interrupt2.NameFunc = func() string { - return "interrupt2" - } - s.interrupt2.TickFunc = func(ctx context.Context, high bool, nanoseconds uint64) error { - ch, ok := callbacks[s.interrupt2] - test.That(t, ok, test.ShouldBeTrue) - ch <- board.Tick{Name: s.interrupt2.Name(), High: high, TimestampNanosec: nanoseconds} - return nil - } - - b.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - if name == "interrupt1" { - return s.interrupt1, nil - } else if name == "interrupt2" { - return s.interrupt2, nil - } - return nil, fmt.Errorf("unknown digital interrupt: %s", name) - } - b.StreamTicksFunc = func( - ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, extra map[string]interface{}, - ) error { - for _, i := range interrupts { - callbacks[i] = ch - } - return nil - } - - s.analog1 = &inject.Analog{} - s.analog2 = &inject.Analog{} - s.analog3 = &inject.Analog{} - analog1Val, analog2Val, analog3Val := 0, 0, 0 - - s.analog1.WriteFunc = func(ctx context.Context, value int, extra map[string]interface{}) error { - s.mu.Lock() - defer s.mu.Unlock() - analog1Val = value - return nil - } - - s.analog1.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { - s.mu.Lock() - defer s.mu.Unlock() - return analog1Val, nil - } - s.analog2.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { - s.mu.Lock() - defer s.mu.Unlock() - return analog2Val, nil - } - s.analog3.ReadFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) { - s.mu.Lock() - defer s.mu.Unlock() - return analog3Val, nil - } - - s.analog2.WriteFunc = func(ctx context.Context, value int, extra map[string]interface{}) error { - s.mu.Lock() - defer s.mu.Unlock() - analog2Val = value - return nil - } - - s.analog3.WriteFunc = func(ctx context.Context, value int, extra map[string]interface{}) error { - s.mu.Lock() - defer s.mu.Unlock() - analog3Val = value - return nil - } - - b.AnalogByNameFunc = func(name string) (board.Analog, error) { - switch name { - case "analog1": - return s.analog1, nil - case "analog2": - return s.analog2, nil - case "analog3": - return s.analog3, nil - default: - return nil, fmt.Errorf("unknown analog: %s", name) - } - } - - deps := make(resource.Dependencies) - deps[board.Named("main")] = s.b - - ic := Config{ - Board: "main", - Buttons: map[string]*ButtonConfig{ - "interrupt1": { - Control: input.ButtonNorth, - Invert: false, - DebounceMs: 20, - }, - "interrupt2": { - Control: input.ButtonSouth, - Invert: true, - DebounceMs: -1, - }, - }, - Axes: map[string]*AxisConfig{ - "analog1": { - Control: input.AbsoluteX, - Min: 0, - Max: 1023, - Bidirectional: false, - Deadzone: 0, - MinChange: 0, - PollHz: 0, - Invert: false, - }, - "analog2": { - Control: input.AbsoluteY, - Min: 0, - Max: 1023, - Bidirectional: true, - Deadzone: 20, - MinChange: 15, - PollHz: 50, - Invert: false, - }, - "analog3": { - Control: input.AbsoluteRX, - Min: -5000, - Max: 5000, - Bidirectional: true, - Deadzone: 0, - MinChange: 0, - PollHz: 0, - Invert: true, - }, - }, - } - - inputReg, ok := resource.LookupRegistration(input.API, resource.DefaultModelFamily.WithModel("gpio")) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, inputReg, test.ShouldNotBeNil) - - res, err := inputReg.Constructor(context.Background(), deps, resource.Config{Name: "input1", ConvertedAttributes: &ic}, s.logger) - test.That(t, err, test.ShouldBeNil) - - s.dev, ok = res.(input.Controller) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, s.dev, test.ShouldNotBeNil) - - err = s.dev.RegisterControlCallback(s.ctx, input.ButtonNorth, []input.EventType{input.ButtonChange}, - func(ctx context.Context, event input.Event) { - atomic.AddInt64(&s.btn1Callbacks, 1) - }, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - - err = s.dev.RegisterControlCallback(s.ctx, input.ButtonSouth, []input.EventType{input.ButtonChange}, - func(ctx context.Context, event input.Event) { - atomic.AddInt64(&s.btn2Callbacks, 1) - }, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - - err = s.dev.RegisterControlCallback(s.ctx, input.AbsoluteX, []input.EventType{input.PositionChangeAbs}, - func(ctx context.Context, event input.Event) { - s.axisMu.Lock() - s.axis1Time = time.Now() - s.axisMu.Unlock() - atomic.AddInt64(&s.axis1Callbacks, 1) - }, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - - err = s.dev.RegisterControlCallback(s.ctx, input.AbsoluteY, []input.EventType{input.PositionChangeAbs}, - func(ctx context.Context, event input.Event) { - s.axisMu.Lock() - s.axis2Time = time.Now() - s.axisMu.Unlock() - atomic.AddInt64(&s.axis2Callbacks, 1) - }, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - - err = s.dev.RegisterControlCallback(s.ctx, input.AbsoluteRX, []input.EventType{input.PositionChangeAbs}, - func(ctx context.Context, event input.Event) { - atomic.AddInt64(&s.axis3Callbacks, 1) - }, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - - return &s -} - -func teardown(t *testing.T, s *setupResult) { - t.Helper() - test.That(t, s.dev.Close(context.Background()), test.ShouldBeNil) -} - -func TestGPIOInput(t *testing.T) { - t.Run("config defaults", func(t *testing.T) { - c := &Config{ - Board: "main", - Buttons: map[string]*ButtonConfig{ - "interrupt1": { - Control: input.ButtonNorth, - Invert: false, - DebounceMs: 20, - }, - "interrupt2": { - Control: input.ButtonSouth, - Invert: true, - DebounceMs: -1, - }, - "interrupt3": { - Control: input.ButtonWest, - Invert: false, - DebounceMs: 0, // default - }, - }, - Axes: map[string]*AxisConfig{ - "analog1": { - Control: input.AbsoluteX, - Min: 0, - Max: 1023, - Bidirectional: false, - Deadzone: 0, - MinChange: 0, - PollHz: 0, - Invert: false, - }, - "analog2": { - Control: input.AbsoluteY, - Min: 0, - Max: 1023, - Bidirectional: true, - Deadzone: 20, - MinChange: 15, - PollHz: 50, - Invert: false, - }, - }, - } - err := c.validateValues() - - test.That(t, err, test.ShouldBeNil) - test.That(t, c.Buttons["interrupt1"].DebounceMs, test.ShouldEqual, 20) // unchanged - test.That(t, c.Buttons["interrupt2"].DebounceMs, test.ShouldEqual, -1) // unchanged - test.That(t, c.Buttons["interrupt3"].DebounceMs, test.ShouldEqual, 5) // default - - test.That(t, c.Axes["analog1"].PollHz, test.ShouldEqual, 10) // default - test.That(t, c.Axes["analog2"].PollHz, test.ShouldEqual, 50) // default - }) - - t.Run("config axis min > max", func(t *testing.T) { - c := &Config{ - Board: "main", - Axes: map[string]*AxisConfig{ - "analog1": { - Control: input.AbsoluteX, - Min: 1023, - Max: 0, - Bidirectional: false, - Deadzone: 0, - MinChange: 0, - PollHz: 0, - Invert: false, - }, - }, - } - err := c.validateValues() - - test.That(t, err, test.ShouldNotBeNil) - }) - - t.Run("initial button state", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := (s.dev).Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["ButtonNorth"].Value, test.ShouldEqual, 0) - test.That(tb, state["ButtonNorth"].Event, test.ShouldEqual, input.Connect) - test.That(tb, state["ButtonSouth"].Value, test.ShouldEqual, 0) - test.That(tb, state["ButtonSouth"].Event, test.ShouldEqual, input.Connect) - }) - }) - - //nolint:dupl - t.Run("button press and release", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - err := s.interrupt1.Tick(s.ctx, true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["ButtonNorth"].Value, test.ShouldEqual, 1) - test.That(tb, state["ButtonNorth"].Event, test.ShouldEqual, input.ButtonPress) - test.That(tb, atomic.LoadInt64(&s.btn1Callbacks), test.ShouldEqual, 1) - }) - - err = s.interrupt1.Tick(s.ctx, false, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["ButtonNorth"].Value, test.ShouldEqual, 0) - test.That(tb, state["ButtonNorth"].Event, test.ShouldEqual, input.ButtonRelease) - test.That(tb, atomic.LoadInt64(&s.btn1Callbacks), test.ShouldEqual, 2) - }) - }) - - // Testing methodology: Issue many events within the debounce time and confirm that only one is registered - // Note: This is a time-sensitive test and is prone to flakiness. - t.Run("button press debounce", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - // this loop must complete within the debounce time - for i := 0; i < 20; i++ { - err := s.interrupt1.Tick(s.ctx, false, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - err = s.interrupt1.Tick(s.ctx, true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - } - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["ButtonNorth"].Value, test.ShouldEqual, 1) - test.That(tb, state["ButtonNorth"].Event, test.ShouldEqual, input.ButtonPress) - test.That(tb, atomic.LoadInt64(&s.btn1Callbacks), test.ShouldEqual, 1) - }) - - time.Sleep(time.Millisecond * 10) - test.That(t, atomic.LoadInt64(&s.btn1Callbacks), test.ShouldEqual, 1) - }) - - //nolint:dupl - t.Run("inverted button press and release", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - err := s.interrupt2.Tick(s.ctx, true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["ButtonSouth"].Value, test.ShouldEqual, 0) - test.That(tb, state["ButtonSouth"].Event, test.ShouldEqual, input.ButtonRelease) - test.That(tb, atomic.LoadInt64(&s.btn2Callbacks), test.ShouldEqual, 1) - }) - - err = s.interrupt2.Tick(s.ctx, false, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["ButtonSouth"].Value, test.ShouldEqual, 1) - test.That(tb, state["ButtonSouth"].Event, test.ShouldEqual, input.ButtonPress) - test.That(tb, atomic.LoadInt64(&s.btn2Callbacks), test.ShouldEqual, 2) - }) - }) - - t.Run("inverted button press with debounce disabled", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - iterations := 50 - - for i := 0; i < iterations; i++ { - err := s.interrupt2.Tick(s.ctx, true, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - err = s.interrupt2.Tick(s.ctx, false, uint64(time.Now().UnixNano())) - test.That(t, err, test.ShouldBeNil) - } - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["ButtonSouth"].Value, test.ShouldEqual, 1) - test.That(tb, state["ButtonSouth"].Event, test.ShouldEqual, input.ButtonPress) - test.That(tb, atomic.LoadInt64(&s.btn2Callbacks), test.ShouldEqual, iterations*2) - }) - - time.Sleep(time.Millisecond * 10) - test.That(t, atomic.LoadInt64(&s.btn2Callbacks), test.ShouldEqual, iterations*2) - }) - - t.Run("axis1 (default)", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteX"].Value, test.ShouldAlmostEqual, 0, 0.005) - test.That(tb, state["AbsoluteX"].Event, test.ShouldEqual, input.Connect) - test.That(tb, atomic.LoadInt64(&s.axis1Callbacks), test.ShouldEqual, 0) - }) - - s.analog1.Write(s.ctx, 1023, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteX"].Value, test.ShouldAlmostEqual, 1, 0.005) - test.That(tb, state["AbsoluteX"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis1Callbacks), test.ShouldEqual, 1) - }) - - s.analog1.Write(s.ctx, 511, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteX"].Value, test.ShouldAlmostEqual, 0.5, 0.005) - test.That(tb, state["AbsoluteX"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis1Callbacks), test.ShouldEqual, 2) - }) - }) - - t.Run("axis deadzone", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - s.analog2.Write(s.ctx, 511, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 1) - }) - - s.analog2.Write(s.ctx, 511+20, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0.04, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 2) - }) - - s.analog2.Write(s.ctx, 511-20, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, -0.04, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 3) - }) - - s.analog2.Write(s.ctx, 511+19, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 4) - }) - - s.analog2.Write(s.ctx, 511-19, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 4) - }) - }) - - t.Run("axis min change (default)", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - s.analog2.Write(s.ctx, 600, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0.17, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 1) - }) - - s.analog2.Write(s.ctx, 600+14, nil) - time.Sleep(time.Millisecond * 30) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0.17, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 1) - }) - - s.analog2.Write(s.ctx, 600-14, nil) - time.Sleep(time.Millisecond * 30) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0.17, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 1) - }) - - s.analog2.Write(s.ctx, 600-15, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteY"].Value, test.ShouldAlmostEqual, 0.14, 0.005) - test.That(tb, state["AbsoluteY"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis2Callbacks), test.ShouldEqual, 2) - }) - }) - - t.Run("axis negative input and inversion", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - s.analog3.Write(s.ctx, 5000, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteRX"].Value, test.ShouldAlmostEqual, -1, 0.005) - test.That(tb, state["AbsoluteRX"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis3Callbacks), test.ShouldEqual, 1) - }) - - s.analog3.Write(s.ctx, -1000, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteRX"].Value, test.ShouldAlmostEqual, 0.2, 0.005) - test.That(tb, state["AbsoluteRX"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis3Callbacks), test.ShouldEqual, 2) - }) - }) - - t.Run("axis range capping", func(t *testing.T) { - s := setup(t) - defer teardown(t, s) - - s.analog3.Write(s.ctx, -6000, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteRX"].Value, test.ShouldAlmostEqual, 1, 0.005) - test.That(tb, state["AbsoluteRX"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis3Callbacks), test.ShouldEqual, 1) - }) - - s.analog3.Write(s.ctx, 6000, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteRX"].Value, test.ShouldAlmostEqual, -1, 0.005) - test.That(tb, state["AbsoluteRX"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis3Callbacks), test.ShouldEqual, 2) - }) - - s.analog3.Write(s.ctx, 0, nil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - state, err := s.dev.Events(s.ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, state["AbsoluteRX"].Value, test.ShouldAlmostEqual, 0, 0.005) - test.That(tb, state["AbsoluteRX"].Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(tb, atomic.LoadInt64(&s.axis3Callbacks), test.ShouldEqual, 3) - }) - }) -} diff --git a/components/input/input.go b/components/input/input.go deleted file mode 100644 index eb37a8798b3..00000000000 --- a/components/input/input.go +++ /dev/null @@ -1,172 +0,0 @@ -// Package input provides human input, such as buttons, switches, knobs, gamepads, joysticks, keyboards, mice, etc. -package input - -import ( - "context" - "time" - - pb "go.viam.com/api/component/inputcontroller/v1" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Controller]{ - Status: resource.StatusFunc(CreateStatus), - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterInputControllerServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.InputControllerService_ServiceDesc, - RPCClient: NewClientFromConn, - }) -} - -// SubtypeName is a constant that identifies the component resource API string input. -const SubtypeName = "input_controller" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named input's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// Controller is a logical "container" more than an actual device -// Could be a single gamepad, or a collection of digitalInterrupts and analogReaders, a keyboard, etc. -type Controller interface { - resource.Resource - - // Controls returns a list of Controls provided by the Controller - Controls(ctx context.Context, extra map[string]interface{}) ([]Control, error) - - // Events returns most recent Event for each input (which should be the current state) - Events(ctx context.Context, extra map[string]interface{}) (map[Control]Event, error) - - // RegisterCallback registers a callback that will fire on given EventTypes for a given Control. - // The callback is called on the same goroutine as the firer and if any long operation is to occur, - // the callback should start a goroutine. - RegisterControlCallback( - ctx context.Context, - control Control, - triggers []EventType, - ctrlFunc ControlFunction, - extra map[string]interface{}, - ) error -} - -// ControlFunction is a callback passed to RegisterControlCallback. -type ControlFunction func(ctx context.Context, ev Event) - -// EventType represents the type of input event, and is returned by LastEvent() or passed to ControlFunction callbacks. -type EventType string - -// EventType list, to be expanded as new input devices are developed. -const ( - // Callbacks registered for this event will be called in ADDITION to other registered event callbacks. - AllEvents EventType = "AllEvents" - // Sent at controller initialization, and on reconnects. - Connect EventType = "Connect" - // If unplugged, or wireless/network times out. - Disconnect EventType = "Disconnect" - // Typical key press. - ButtonPress EventType = "ButtonPress" - // Key release. - ButtonRelease EventType = "ButtonRelease" - // Key is held down. This will likely be a repeated event. - ButtonHold EventType = "ButtonHold" - // Both up and down for convenience during registration, not typically emitted. - ButtonChange EventType = "ButtonChange" - // Absolute position is reported via Value, a la joysticks. - PositionChangeAbs EventType = "PositionChangeAbs" - // Relative position is reported via Value, a la mice, or simulating axes with up/down buttons. - PositionChangeRel EventType = "PositionChangeRel" -) - -// Control identifies the input (specific Axis or Button) of a controller. -type Control string - -// Controls, to be expanded as new input devices are developed. -const ( - // Axes. - AbsoluteX Control = "AbsoluteX" - AbsoluteY Control = "AbsoluteY" - AbsoluteZ Control = "AbsoluteZ" - AbsoluteRX Control = "AbsoluteRX" - AbsoluteRY Control = "AbsoluteRY" - AbsoluteRZ Control = "AbsoluteRZ" - AbsoluteHat0X Control = "AbsoluteHat0X" - AbsoluteHat0Y Control = "AbsoluteHat0Y" - - // Buttons. - ButtonSouth Control = "ButtonSouth" - ButtonEast Control = "ButtonEast" - ButtonWest Control = "ButtonWest" - ButtonNorth Control = "ButtonNorth" - ButtonLT Control = "ButtonLT" - ButtonRT Control = "ButtonRT" - ButtonLT2 Control = "ButtonLT2" - ButtonRT2 Control = "ButtonRT2" - ButtonLThumb Control = "ButtonLThumb" - ButtonRThumb Control = "ButtonRThumb" - ButtonSelect Control = "ButtonSelect" - ButtonStart Control = "ButtonStart" - ButtonMenu Control = "ButtonMenu" - ButtonRecord Control = "ButtonRecord" - ButtonEStop Control = "ButtonEStop" - - // Pedals. - AbsolutePedalAccelerator Control = "AbsolutePedalAccelerator" - AbsolutePedalBrake Control = "AbsolutePedalBrake" - AbsolutePedalClutch Control = "AbsolutePedalClutch" -) - -// Event is passed to the registered ControlFunction or returned by State(). -type Event struct { - Time time.Time - Event EventType - Control Control // Key or Axis - Value float64 // 0 or 1 for buttons, -1.0 to +1.0 for axes -} - -// Triggerable is used by the WebGamepad interface to inject events. -type Triggerable interface { - // TriggerEvent allows directly sending an Event (such as a button press) from external code - TriggerEvent(ctx context.Context, event Event, extra map[string]interface{}) error -} - -// FromDependencies is a helper for getting the named input controller from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Controller, error) { - return resource.FromDependencies[Controller](deps, Named(name)) -} - -// FromRobot is a helper for getting the named input controller from the given Robot. -func FromRobot(r robot.Robot, name string) (Controller, error) { - return robot.ResourceFromRobot[Controller](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all input controller names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// CreateStatus creates a status from the input controller. -func CreateStatus(ctx context.Context, c Controller) (*pb.Status, error) { - eventsIn, err := c.Events(ctx, map[string]interface{}{}) - if err != nil { - return nil, err - } - events := make([]*pb.Event, 0, len(eventsIn)) - for _, eventIn := range eventsIn { - events = append(events, &pb.Event{ - Time: timestamppb.New(eventIn.Time), - Event: string(eventIn.Event), - Control: string(eventIn.Control), - Value: eventIn.Value, - }) - } - - return &pb.Status{Events: events}, nil -} diff --git a/components/input/input_test.go b/components/input/input_test.go deleted file mode 100644 index 2ee493461df..00000000000 --- a/components/input/input_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package input_test - -import ( - "context" - "testing" - "time" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/inputcontroller/v1" - "go.viam.com/test" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testInputControllerName = "inputController1" - failInputControllerName = "inputController2" - missingInputControllerName = "inputController3" -) - -func TestCreateStatus(t *testing.T) { - timestamp := time.Now() - event := input.Event{Time: timestamp, Event: input.PositionChangeAbs, Control: input.AbsoluteX, Value: 0.7} - status := &pb.Status{ - Events: []*pb.Event{ - {Time: timestamppb.New(timestamp), Event: string(event.Event), Control: string(event.Control), Value: event.Value}, - }, - } - injectInputController := &inject.InputController{} - injectInputController.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - eventsOut := make(map[input.Control]input.Event) - eventsOut[input.AbsoluteX] = event - return eventsOut, nil - } - - t.Run("working", func(t *testing.T) { - status1, err := input.CreateStatus(context.Background(), injectInputController) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - }) - - t.Run("fail on Events", func(t *testing.T) { - errFail := errors.New("can't get events") - injectInputController.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - return nil, errFail - } - _, err := input.CreateStatus(context.Background(), injectInputController) - test.That(t, err, test.ShouldBeError, errFail) - }) -} diff --git a/components/input/mux/mux.go b/components/input/mux/mux.go deleted file mode 100644 index ddc662c6894..00000000000 --- a/components/input/mux/mux.go +++ /dev/null @@ -1,225 +0,0 @@ -// Package mux implements a multiplexed input controller. -package mux - -import ( - "context" - "sync" - - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("mux") - -func init() { - resource.RegisterComponent(input.API, model, resource.Registration[input.Controller, *Config]{ - Constructor: NewController, - }) -} - -// Config is used for converting config attributes. -type Config struct { - resource.TriviallyValidateConfig - Sources []string `json:"sources"` -} - -// NewController returns a new multiplexed input.Controller. -func NewController( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (input.Controller, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - ctxWithCancel, cancel := context.WithCancel(ctx) - m := mux{ - Named: conf.ResourceName().AsNamed(), - callbacks: map[input.Control]map[input.EventType]input.ControlFunction{}, - cancelFunc: cancel, - ctxWithCancel: ctxWithCancel, - eventsChan: make(chan input.Event, 1024), - logger: logger, - } - - for _, s := range newConf.Sources { - c, err := input.FromDependencies(deps, s) - if err != nil { - return nil, err - } - m.sources = append(m.sources, c) - } - - m.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer m.activeBackgroundWorkers.Done() - for { - select { - case eventIn := <-m.eventsChan: - m.makeCallbacks(eventIn) - case <-ctxWithCancel.Done(): - return - } - } - }) - - return &m, nil -} - -// mux is an input.Controller. -type mux struct { - resource.Named - resource.AlwaysRebuild - - sources []input.Controller - mu sync.RWMutex - activeBackgroundWorkers sync.WaitGroup - ctxWithCancel context.Context - cancelFunc func() - callbacks map[input.Control]map[input.EventType]input.ControlFunction - eventsChan chan input.Event - logger logging.Logger -} - -func (m *mux) makeCallbacks(eventOut input.Event) { - m.mu.RLock() - _, ok := m.callbacks[eventOut.Control] - m.mu.RUnlock() - if !ok { - m.mu.Lock() - m.callbacks[eventOut.Control] = make(map[input.EventType]input.ControlFunction) - m.mu.Unlock() - } - m.mu.RLock() - defer m.mu.RUnlock() - - ctrlFunc, ok := m.callbacks[eventOut.Control][eventOut.Event] - if ok && ctrlFunc != nil { - m.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer m.activeBackgroundWorkers.Done() - ctrlFunc(m.ctxWithCancel, eventOut) - }) - } - - ctrlFuncAll, ok := m.callbacks[eventOut.Control][input.AllEvents] - if ok && ctrlFuncAll != nil { - m.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer m.activeBackgroundWorkers.Done() - ctrlFuncAll(m.ctxWithCancel, eventOut) - }) - } -} - -// Close terminates background worker threads. -func (m *mux) Close(ctx context.Context) error { - m.cancelFunc() - m.activeBackgroundWorkers.Wait() - return nil -} - -// Controls lists the unique input.Controls for the combined sources. -func (m *mux) Controls(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - controlMap := make(map[input.Control]bool) - var ok bool - var errs error - for _, c := range m.sources { - controls, err := c.Controls(ctx, extra) - if err != nil { - errs = multierr.Combine(errs, err) - continue - } - ok = true - for _, ctrl := range controls { - controlMap[ctrl] = true - } - } - if !ok { - return nil, errs - } - var controlsOut []input.Control - for c := range controlMap { - controlsOut = append(controlsOut, c) - } - - return controlsOut, nil -} - -// Events returns the last input.Event (the current state). -func (m *mux) Events(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - eventsOut := make(map[input.Control]input.Event) - var ok bool - var errs error - for _, c := range m.sources { - eventList, err := c.Events(ctx, extra) - if err != nil { - errs = multierr.Combine(errs, err) - continue - } - ok = true - for ctrl, eventA := range eventList { - eventB, ok := eventsOut[ctrl] - if !ok || eventA.Time.After(eventB.Time) { - eventsOut[ctrl] = eventA - } - } - } - if !ok { - return nil, errs - } - return eventsOut, nil -} - -// RegisterControlCallback registers a callback function to be executed on the specified control's trigger Events. -func (m *mux) RegisterControlCallback( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, -) error { - m.mu.Lock() - defer m.mu.Unlock() - if m.callbacks[control] == nil { - m.callbacks[control] = make(map[input.EventType]input.ControlFunction) - } - - for _, trigger := range triggers { - if trigger == input.ButtonChange { - m.callbacks[control][input.ButtonRelease] = ctrlFunc - m.callbacks[control][input.ButtonPress] = ctrlFunc - } else { - m.callbacks[control][trigger] = ctrlFunc - } - } - - relayFunc := func(ctx context.Context, eventIn input.Event) { - select { - case m.eventsChan <- eventIn: - case <-ctx.Done(): - } - } - - var ok bool - var errs error - for _, c := range m.sources { - err := c.RegisterControlCallback(ctx, control, triggers, relayFunc, extra) - if err != nil { - errs = multierr.Combine(errs, err) - continue - } - ok = true - } - if !ok { - return errs - } - return nil -} diff --git a/components/input/register/register.go b/components/input/register/register.go deleted file mode 100644 index 6dcc2d14075..00000000000 --- a/components/input/register/register.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package register registers all relevant inputs -package register - -import ( - // for inputs. - _ "go.viam.com/rdk/components/input/fake" - _ "go.viam.com/rdk/components/input/gamepad" - _ "go.viam.com/rdk/components/input/gpio" - _ "go.viam.com/rdk/components/input/mux" - _ "go.viam.com/rdk/components/input/webgamepad" -) diff --git a/components/input/server.go b/components/input/server.go deleted file mode 100644 index 3bc81b7ee77..00000000000 --- a/components/input/server.go +++ /dev/null @@ -1,180 +0,0 @@ -// Package input contains a gRPC based input controller service server. -package input - -import ( - "context" - - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/inputcontroller/v1" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// serviceServer implements the InputControllerService from proto. -type serviceServer struct { - pb.UnimplementedInputControllerServiceServer - coll resource.APIResourceCollection[Controller] -} - -// NewRPCServiceServer constructs an input controller gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Controller]) interface{} { - return &serviceServer{coll: coll} -} - -// GetControls lists the inputs of an Controller. -func (s *serviceServer) GetControls( - ctx context.Context, - req *pb.GetControlsRequest, -) (*pb.GetControlsResponse, error) { - controller, err := s.coll.Resource(req.Controller) - if err != nil { - return nil, err - } - - controlList, err := controller.Controls(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - - resp := &pb.GetControlsResponse{} - - for _, control := range controlList { - resp.Controls = append(resp.Controls, string(control)) - } - return resp, nil -} - -// GetEvents returns the last Event (current state) of each control. -func (s *serviceServer) GetEvents( - ctx context.Context, - req *pb.GetEventsRequest, -) (*pb.GetEventsResponse, error) { - controller, err := s.coll.Resource(req.Controller) - if err != nil { - return nil, err - } - - eventsIn, err := controller.Events(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - - resp := &pb.GetEventsResponse{} - - for _, eventIn := range eventsIn { - resp.Events = append(resp.Events, &pb.Event{ - Time: timestamppb.New(eventIn.Time), - Event: string(eventIn.Event), - Control: string(eventIn.Control), - Value: eventIn.Value, - }) - } - - return resp, nil -} - -// TriggerEvent allows directly sending an Event (such as a button press) from external code. -func (s *serviceServer) TriggerEvent( - ctx context.Context, - req *pb.TriggerEventRequest, -) (*pb.TriggerEventResponse, error) { - controller, err := s.coll.Resource(req.Controller) - if err != nil { - return nil, err - } - injectController, ok := controller.(Triggerable) - if !ok { - return nil, errors.Errorf("input controller is not of type Triggerable (%s)", req.Controller) - } - - err = injectController.TriggerEvent( - ctx, - Event{ - Time: req.Event.Time.AsTime(), - Event: EventType(req.Event.Event), - Control: Control(req.Event.Control), - Value: req.Event.Value, - }, - req.Extra.AsMap(), - ) - if err != nil { - return nil, err - } - - return &pb.TriggerEventResponse{}, nil -} - -// StreamEvents returns a stream of Event. -func (s *serviceServer) StreamEvents( - req *pb.StreamEventsRequest, - server pb.InputControllerService_StreamEventsServer, -) error { - controller, err := s.coll.Resource(req.Controller) - if err != nil { - return err - } - eventsChan := make(chan *pb.Event, 1024) - - ctrlFunc := func(ctx context.Context, eventIn Event) { - resp := &pb.Event{ - Time: timestamppb.New(eventIn.Time), - Event: string(eventIn.Event), - Control: string(eventIn.Control), - Value: eventIn.Value, - } - select { - case eventsChan <- resp: - case <-ctx.Done(): - } - } - for _, ev := range req.Events { - var triggers []EventType - for _, v := range ev.Events { - triggers = append(triggers, EventType(v)) - } - if len(triggers) > 0 { - err := controller.RegisterControlCallback(server.Context(), Control(ev.Control), triggers, ctrlFunc, req.Extra.AsMap()) - if err != nil { - return err - } - } - - var cancelledTriggers []EventType - for _, v := range ev.CancelledEvents { - cancelledTriggers = append(cancelledTriggers, EventType(v)) - } - if len(cancelledTriggers) > 0 { - err := controller.RegisterControlCallback(server.Context(), Control(ev.Control), cancelledTriggers, nil, req.Extra.AsMap()) - if err != nil { - return err - } - } - } - - for { - select { - case <-server.Context().Done(): - return server.Context().Err() - case msg := <-eventsChan: - err := server.Send(&pb.StreamEventsResponse{Event: msg}) - if err != nil { - return err - } - } - } -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - controller, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, controller, req) -} diff --git a/components/input/server_test.go b/components/input/server_test.go deleted file mode 100644 index f2955c7af5b..00000000000 --- a/components/input/server_test.go +++ /dev/null @@ -1,331 +0,0 @@ -package input_test - -import ( - "context" - "testing" - "time" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/inputcontroller/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errControlsFailed = errors.New("can't get controls") - errEventsFailed = errors.New("can't get last events") - errTriggerEvent = errors.New("can't inject event") - errSendFailed = errors.New("send fail") - errRegisterFailed = errors.New("can't register callbacks") - errNotFound = errors.New("not found") -) - -type streamServer struct { - grpc.ServerStream - ctx context.Context - messageCh chan<- *pb.StreamEventsResponse - fail bool -} - -func (x *streamServer) Context() context.Context { - return x.ctx -} - -func (x *streamServer) Send(m *pb.StreamEventsResponse) error { - if x.fail { - return errSendFailed - } - if x.messageCh == nil { - return nil - } - x.messageCh <- m - return nil -} - -func newServer() (pb.InputControllerServiceServer, *inject.TriggerableInputController, *inject.InputController, error) { - injectInputController := &inject.TriggerableInputController{} - injectInputController2 := &inject.InputController{} - inputControllers := map[resource.Name]input.Controller{ - input.Named(testInputControllerName): injectInputController, - input.Named(failInputControllerName): injectInputController2, - } - inputControllerSvc, err := resource.NewAPIResourceCollection(input.API, inputControllers) - if err != nil { - return nil, nil, nil, err - } - return input.NewRPCServiceServer(inputControllerSvc).(pb.InputControllerServiceServer), injectInputController, injectInputController2, nil -} - -func TestServer(t *testing.T) { - inputControllerServer, injectInputController, injectInputController2, err := newServer() - test.That(t, err, test.ShouldBeNil) - - var extraOptions map[string]interface{} - injectInputController.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - extraOptions = extra - return []input.Control{input.AbsoluteX, input.ButtonStart}, nil - } - injectInputController.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - extraOptions = extra - eventsOut := make(map[input.Control]input.Event) - eventsOut[input.AbsoluteX] = input.Event{Time: time.Now(), Event: input.PositionChangeAbs, Control: input.AbsoluteX, Value: 0.7} - eventsOut[input.ButtonStart] = input.Event{Time: time.Now(), Event: input.ButtonPress, Control: input.ButtonStart, Value: 1.0} - return eventsOut, nil - } - injectInputController.RegisterControlCallbackFunc = func( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, - ) error { - extraOptions = extra - outEvent := input.Event{Time: time.Now(), Event: triggers[0], Control: input.ButtonStart, Value: 0.0} - ctrlFunc(ctx, outEvent) - return nil - } - - injectInputController2.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - return nil, errControlsFailed - } - injectInputController2.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - return nil, errEventsFailed - } - injectInputController2.RegisterControlCallbackFunc = func( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, - ) error { - return errRegisterFailed - } - - t.Run("GetControls", func(t *testing.T) { - _, err := inputControllerServer.GetControls( - context.Background(), - &pb.GetControlsRequest{Controller: missingInputControllerName}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errNotFound.Error()) - - extra := map[string]interface{}{"foo": "Controls"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - resp, err := inputControllerServer.GetControls( - context.Background(), - &pb.GetControlsRequest{Controller: testInputControllerName, Extra: ext}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Controls, test.ShouldResemble, []string{"AbsoluteX", "ButtonStart"}) - test.That(t, extraOptions, test.ShouldResemble, extra) - - _, err = inputControllerServer.GetControls( - context.Background(), - &pb.GetControlsRequest{Controller: failInputControllerName}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errControlsFailed.Error()) - }) - - t.Run("GetEvents", func(t *testing.T) { - _, err := inputControllerServer.GetEvents( - context.Background(), - &pb.GetEventsRequest{Controller: missingInputControllerName}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errNotFound.Error()) - - extra := map[string]interface{}{"foo": "Events"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - startTime := time.Now() - time.Sleep(time.Millisecond) - resp, err := inputControllerServer.GetEvents( - context.Background(), - &pb.GetEventsRequest{Controller: testInputControllerName, Extra: ext}, - ) - test.That(t, err, test.ShouldBeNil) - var absEv, buttonEv *pb.Event - if resp.Events[0].Control == "AbsoluteX" { - absEv = resp.Events[0] - buttonEv = resp.Events[1] - } else { - absEv = resp.Events[1] - buttonEv = resp.Events[0] - } - - test.That(t, absEv.Event, test.ShouldEqual, input.PositionChangeAbs) - test.That(t, absEv.Control, test.ShouldEqual, input.AbsoluteX) - test.That(t, absEv.Value, test.ShouldEqual, 0.7) - test.That(t, absEv.Time.AsTime().After(startTime), test.ShouldBeTrue) - test.That(t, absEv.Time.AsTime().Before(time.Now()), test.ShouldBeTrue) - - test.That(t, buttonEv.Event, test.ShouldEqual, input.ButtonPress) - test.That(t, buttonEv.Control, test.ShouldEqual, input.ButtonStart) - test.That(t, buttonEv.Value, test.ShouldEqual, 1) - test.That(t, buttonEv.Time.AsTime().After(startTime), test.ShouldBeTrue) - test.That(t, buttonEv.Time.AsTime().Before(time.Now()), test.ShouldBeTrue) - - test.That(t, extraOptions, test.ShouldResemble, extra) - - _, err = inputControllerServer.GetEvents( - context.Background(), - &pb.GetEventsRequest{Controller: failInputControllerName}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errEventsFailed.Error()) - }) - - t.Run("StreamEvents", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - messageCh := make(chan *pb.StreamEventsResponse, 1024) - s := &streamServer{ - ctx: cancelCtx, - messageCh: messageCh, - } - - startTime := time.Now() - err := inputControllerServer.StreamEvents(&pb.StreamEventsRequest{Controller: missingInputControllerName}, s) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errNotFound.Error()) - - extra := map[string]interface{}{"foo": "StreamEvents"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - eventReqList := &pb.StreamEventsRequest{ - Controller: testInputControllerName, - Events: []*pb.StreamEventsRequest_Events{ - { - Control: string(input.ButtonStart), - Events: []string{ - string(input.ButtonRelease), - }, - }, - }, - Extra: ext, - } - relayFunc := func(ctx context.Context, event input.Event) { - messageCh <- &pb.StreamEventsResponse{ - Event: &pb.Event{ - Time: timestamppb.New(event.Time), - Event: string(event.Event), - Control: string(event.Control), - Value: event.Value, - }, - } - } - - err = injectInputController.RegisterControlCallback( - cancelCtx, - input.ButtonStart, - []input.EventType{input.ButtonRelease}, - relayFunc, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{}) - - s.fail = true - - err = inputControllerServer.StreamEvents(eventReqList, s) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errSendFailed.Error()) - - var streamErr error - done := make(chan struct{}) - s.fail = false - - go func() { - streamErr = inputControllerServer.StreamEvents(eventReqList, s) - close(done) - }() - - resp := <-messageCh - event := resp.Event - test.That(t, event.Control, test.ShouldEqual, string(input.ButtonStart)) - test.That(t, event.Event, test.ShouldEqual, input.ButtonRelease) - test.That(t, event.Value, test.ShouldEqual, 0) - test.That(t, event.Time.AsTime().After(startTime), test.ShouldBeTrue) - test.That(t, event.Time.AsTime().Before(time.Now()), test.ShouldBeTrue) - - cancel() - <-done - test.That(t, extraOptions, test.ShouldResemble, extra) - test.That(t, streamErr, test.ShouldEqual, context.Canceled) - - eventReqList.Controller = failInputControllerName - err = inputControllerServer.StreamEvents(eventReqList, s) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errRegisterFailed.Error()) - }) - - t.Run("TriggerEvent", func(t *testing.T) { - _, err := inputControllerServer.TriggerEvent( - context.Background(), - &pb.TriggerEventRequest{Controller: missingInputControllerName}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errNotFound.Error()) - - injectInputController.TriggerEventFunc = func(ctx context.Context, event input.Event, extra map[string]interface{}) error { - return errors.New("can't inject event") - } - - event1 := input.Event{ - Time: time.Now().UTC(), - Event: input.PositionChangeAbs, - Control: input.AbsoluteX, - Value: 0.7, - } - pbEvent := &pb.Event{ - Time: timestamppb.New(event1.Time), - Event: string(event1.Event), - Control: string(event1.Control), - Value: event1.Value, - } - _, err = inputControllerServer.TriggerEvent( - context.Background(), - &pb.TriggerEventRequest{ - Controller: testInputControllerName, - Event: pbEvent, - }, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errTriggerEvent.Error()) - - var injectedEvent input.Event - - injectInputController.TriggerEventFunc = func(ctx context.Context, event input.Event, extra map[string]interface{}) error { - extraOptions = extra - injectedEvent = event - return nil - } - extra := map[string]interface{}{"foo": "TriggerEvent"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - _, err = inputControllerServer.TriggerEvent( - context.Background(), - &pb.TriggerEventRequest{Controller: testInputControllerName, Event: pbEvent, Extra: ext}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, injectedEvent, test.ShouldResemble, event1) - test.That(t, extraOptions, test.ShouldResemble, extra) - - injectInputController.TriggerEventFunc = nil - - _, err = inputControllerServer.TriggerEvent( - context.Background(), - &pb.TriggerEventRequest{Controller: failInputControllerName, Event: pbEvent}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "is not of type Triggerable") - }) -} diff --git a/components/input/verify_main_test.go b/components/input/verify_main_test.go deleted file mode 100644 index 6fef7ecdbda..00000000000 --- a/components/input/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package input - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/input/webgamepad/webgamepad.go b/components/input/webgamepad/webgamepad.go deleted file mode 100644 index f25b54d7d3d..00000000000 --- a/components/input/webgamepad/webgamepad.go +++ /dev/null @@ -1,127 +0,0 @@ -// Package webgamepad implements a web based input controller. -package webgamepad - -import ( - "context" - "sync" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// NOTE: Component NAME (in config file) must be set to "WebGamepad" exactly -// This is because there's no way to get a component's model from a robot.Robot. -var model = resource.DefaultModelFamily.WithModel("webgamepad") - -func init() { - resource.RegisterComponent(input.API, model, resource.Registration[input.Controller, resource.NoNativeConfig]{ - Constructor: NewController, - }) -} - -// NewController creates a new gamepad. -func NewController( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, -) (input.Controller, error) { - return &webGamepad{ - Named: conf.ResourceName().AsNamed(), - callbacks: map[input.Control]map[input.EventType]input.ControlFunction{}, - lastEvents: map[input.Control]input.Event{}, - controls: []input.Control{ - input.AbsoluteX, input.AbsoluteY, input.AbsoluteRX, input.AbsoluteRY, - input.AbsoluteZ, input.AbsoluteRZ, input.AbsoluteHat0X, input.AbsoluteHat0Y, - input.ButtonSouth, input.ButtonEast, input.ButtonWest, input.ButtonNorth, - input.ButtonLT, input.ButtonRT, input.ButtonLThumb, input.ButtonRThumb, - input.ButtonSelect, input.ButtonStart, input.ButtonMenu, - }, - logger: logger, - }, nil -} - -// webGamepad is an input.Controller. -type webGamepad struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - controls []input.Control - lastEvents map[input.Control]input.Event - mu sync.RWMutex - callbacks map[input.Control]map[input.EventType]input.ControlFunction - logger logging.Logger -} - -func (w *webGamepad) makeCallbacks(ctx context.Context, eventOut input.Event) { - w.mu.Lock() - w.lastEvents[eventOut.Control] = eventOut - w.mu.Unlock() - - w.mu.RLock() - _, ok := w.callbacks[eventOut.Control] - w.mu.RUnlock() - if !ok { - w.mu.Lock() - w.callbacks[eventOut.Control] = make(map[input.EventType]input.ControlFunction) - w.mu.Unlock() - } - w.mu.RLock() - defer w.mu.RUnlock() - - ctrlFunc, ok := w.callbacks[eventOut.Control][eventOut.Event] - if ok && ctrlFunc != nil { - ctrlFunc(ctx, eventOut) - } - - ctrlFuncAll, ok := w.callbacks[eventOut.Control][input.AllEvents] - if ok && ctrlFuncAll != nil { - ctrlFuncAll(ctx, eventOut) - } -} - -// Controls lists the inputs of the gamepad. -func (w *webGamepad) Controls(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - out := append([]input.Control(nil), w.controls...) - return out, nil -} - -// Events returns the last input.Event (the current state). -func (w *webGamepad) Events(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - w.mu.RLock() - defer w.mu.RUnlock() - out := make(map[input.Control]input.Event) - for key, value := range w.lastEvents { - out[key] = value - } - return out, nil -} - -// RegisterControlCallback registers a callback function to be executed on the specified control's trigger Events. -func (w *webGamepad) RegisterControlCallback( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, -) error { - w.mu.Lock() - defer w.mu.Unlock() - if w.callbacks[control] == nil { - w.callbacks[control] = make(map[input.EventType]input.ControlFunction) - } - - for _, trigger := range triggers { - if trigger == input.ButtonChange { - w.callbacks[control][input.ButtonRelease] = ctrlFunc - w.callbacks[control][input.ButtonPress] = ctrlFunc - } else { - w.callbacks[control][trigger] = ctrlFunc - } - } - return nil -} - -// TriggerEvent allows directly sending an Event (such as a button press) from external code. -func (w *webGamepad) TriggerEvent(ctx context.Context, event input.Event, extra map[string]interface{}) error { - w.makeCallbacks(ctx, event) - return nil -} diff --git a/components/motor/client.go b/components/motor/client.go deleted file mode 100644 index 9a623656a7c..00000000000 --- a/components/motor/client.go +++ /dev/null @@ -1,160 +0,0 @@ -// Package motor contains a gRPC bases motor client -package motor - -import ( - "context" - - pb "go.viam.com/api/component/motor/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// client implements MotorServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.MotorServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Motor, error) { - c := pb.NewMotorServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - }, nil -} - -func (c *client) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - req := &pb.SetPowerRequest{ - Name: c.name, - PowerPct: powerPct, - Extra: ext, - } - _, err = c.client.SetPower(ctx, req) - return err -} - -func (c *client) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - req := &pb.GoForRequest{ - Name: c.name, - Rpm: rpm, - Revolutions: revolutions, - Extra: ext, - } - _, err = c.client.GoFor(ctx, req) - return err -} - -func (c *client) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - req := &pb.GoToRequest{ - Name: c.name, - Rpm: rpm, - PositionRevolutions: positionRevolutions, - Extra: ext, - } - _, err = c.client.GoTo(ctx, req) - return err -} - -func (c *client) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - req := &pb.ResetZeroPositionRequest{ - Name: c.name, - Offset: offset, - Extra: ext, - } - _, err = c.client.ResetZeroPosition(ctx, req) - return err -} - -func (c *client) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return 0, err - } - req := &pb.GetPositionRequest{Name: c.name, Extra: ext} - resp, err := c.client.GetPosition(ctx, req) - if err != nil { - return 0, err - } - return resp.GetPosition(), nil -} - -func (c *client) Properties(ctx context.Context, extra map[string]interface{}) (Properties, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return Properties{}, err - } - req := &pb.GetPropertiesRequest{Name: c.name, Extra: ext} - resp, err := c.client.GetProperties(ctx, req) - if err != nil { - return Properties{}, err - } - return ProtoFeaturesToProperties(resp), nil -} - -func (c *client) Stop(ctx context.Context, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - req := &pb.StopRequest{Name: c.name, Extra: ext} - _, err = c.client.Stop(ctx, req) - return err -} - -func (c *client) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return false, 0.0, err - } - req := &pb.IsPoweredRequest{Name: c.name, Extra: ext} - resp, err := c.client.IsPowered(ctx, req) - if err != nil { - return false, 0.0, err - } - return resp.GetIsOn(), resp.GetPowerPct(), nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} - -func (c *client) IsMoving(ctx context.Context) (bool, error) { - resp, err := c.client.IsMoving(ctx, &pb.IsMovingRequest{Name: c.name}) - if err != nil { - return false, err - } - return resp.IsMoving, nil -} diff --git a/components/motor/client_test.go b/components/motor/client_test.go deleted file mode 100644 index 7ce13593570..00000000000 --- a/components/motor/client_test.go +++ /dev/null @@ -1,267 +0,0 @@ -package motor_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/motor" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - workingMotor := &inject.Motor{} - failingMotor := &inject.Motor{} - - var actualExtra map[string]interface{} - var actualPowerPct float64 - - workingMotor.SetPowerFunc = func(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - actualExtra = extra - actualPowerPct = powerPct - return nil - } - workingMotor.GoForFunc = func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - workingMotor.GoToFunc = func(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - workingMotor.ResetZeroPositionFunc = func(ctx context.Context, offset float64, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - workingMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - actualExtra = extra - return 42.0, nil - } - workingMotor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - actualExtra = extra - return motor.Properties{ - PositionReporting: true, - }, nil - } - workingMotor.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - workingMotor.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - actualExtra = extra - return true, actualPowerPct, nil - } - - failingMotor.SetPowerFunc = func(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - return errSetPowerFailed - } - failingMotor.GoForFunc = func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return errGoForFailed - } - failingMotor.GoToFunc = func(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { - return errGoToFailed - } - failingMotor.ResetZeroPositionFunc = func(ctx context.Context, offset float64, extra map[string]interface{}) error { - return errResetZeroFailed - } - failingMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, errPositionUnavailable - } - failingMotor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, errPropertiesNotFound - } - failingMotor.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopFailed - } - failingMotor.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return false, 0.0, errIsPoweredFailed - } - - resourceMap := map[resource.Name]motor.Motor{ - motor.Named(testMotorName): workingMotor, - motor.Named(failMotorName): failingMotor, - } - motorSvc, err := resource.NewAPIResourceCollection(motor.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[motor.Motor](motor.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, motorSvc), test.ShouldBeNil) - - workingMotor.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingMotorClient, err := motor.NewClientFromConn(context.Background(), conn, "", motor.Named(testMotorName), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests for working motor", func(t *testing.T) { - // DoCommand - resp, err := workingMotorClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - err = workingMotorClient.SetPower(context.Background(), 42.0, nil) - test.That(t, err, test.ShouldBeNil) - - err = workingMotorClient.GoFor(context.Background(), 42.0, 42.0, nil) - test.That(t, err, test.ShouldBeNil) - - err = workingMotorClient.GoTo(context.Background(), 42.0, 42.0, nil) - test.That(t, err, test.ShouldBeNil) - - err = workingMotorClient.ResetZeroPosition(context.Background(), 0.5, nil) - test.That(t, err, test.ShouldBeNil) - - pos, err := workingMotorClient.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 42.0) - - properties, err := workingMotorClient.Properties(context.Background(), nil) - test.That(t, properties.PositionReporting, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - - err = workingMotorClient.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - isOn, powerPct, err := workingMotorClient.IsPowered( - context.Background(), - map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}}) - test.That(t, isOn, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 42.0) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}}) - - test.That(t, workingMotorClient.Close(context.Background()), test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - conn, err = viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingMotorClient, err := motor.NewClientFromConn(context.Background(), conn, "", motor.Named(failMotorName), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests for failing motor", func(t *testing.T) { - err := failingMotorClient.GoTo(context.Background(), 42.0, 42.0, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGoToFailed.Error()) - - err = failingMotorClient.ResetZeroPosition(context.Background(), 0.5, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errResetZeroFailed.Error()) - - pos, err := failingMotorClient.Position(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPositionUnavailable.Error()) - test.That(t, pos, test.ShouldEqual, 0.0) - - err = failingMotorClient.SetPower(context.Background(), 42.0, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errSetPowerFailed.Error()) - - err = failingMotorClient.GoFor(context.Background(), 42.0, 42.0, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errGoForFailed.Error()) - - properties, err := failingMotorClient.Properties(context.Background(), nil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPropertiesNotFound.Error()) - - isOn, powerPct, err := failingMotorClient.IsPowered(context.Background(), nil) - test.That(t, isOn, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0.0) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errIsPoweredFailed.Error()) - - err = failingMotorClient.Stop(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopFailed.Error()) - - test.That(t, failingMotorClient.Close(context.Background()), test.ShouldBeNil) - }) - - t.Run("dialed client tests for working motor", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingMotorDialedClient, err := motor.NewClientFromConn(context.Background(), conn, "", motor.Named(testMotorName), logger) - test.That(t, err, test.ShouldBeNil) - - pos, err := workingMotorDialedClient.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 42.0) - - properties, err := workingMotorDialedClient.Properties(context.Background(), nil) - test.That(t, properties.PositionReporting, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - - err = workingMotorDialedClient.GoTo(context.Background(), 42.0, 42.0, nil) - test.That(t, err, test.ShouldBeNil) - - err = workingMotorDialedClient.ResetZeroPosition(context.Background(), 0.5, nil) - test.That(t, err, test.ShouldBeNil) - - err = workingMotorDialedClient.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - isOn, powerPct, err := workingMotorDialedClient.IsPowered(context.Background(), nil) - test.That(t, isOn, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 42.0) - test.That(t, err, test.ShouldBeNil) - - test.That(t, workingMotorDialedClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client tests for failing motor", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingMotorDialedClient, err := motor.NewClientFromConn(context.Background(), conn, "", motor.Named(failMotorName), logger) - test.That(t, err, test.ShouldBeNil) - - err = failingMotorDialedClient.SetPower(context.Background(), 39.2, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errSetPowerFailed.Error()) - - properties, err := failingMotorDialedClient.Properties(context.Background(), nil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPropertiesNotFound.Error()) - - isOn, powerPct, err := failingMotorDialedClient.IsPowered(context.Background(), nil) - test.That(t, isOn, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0.0) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errIsPoweredFailed.Error()) - - test.That(t, failingMotorDialedClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - test.That(t, conn.Close(), test.ShouldBeNil) -} diff --git a/components/motor/collectors.go b/components/motor/collectors.go deleted file mode 100644 index 1d328d7b36c..00000000000 --- a/components/motor/collectors.go +++ /dev/null @@ -1,87 +0,0 @@ -package motor - -import ( - "context" - "errors" - - pb "go.viam.com/api/component/motor/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" -) - -type method int64 - -const ( - position method = iota - isPowered -) - -func (m method) String() string { - switch m { - case position: - return "Position" - case isPowered: - return "IsPowered" - } - return "Unknown" -} - -// newPositionCollector returns a collector to register a position method. If one is already registered -// with the same MethodMetadata it will panic. -func newPositionCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - motor, err := assertMotor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - v, err := motor.Position(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, position.String(), err) - } - return pb.GetPositionResponse{ - Position: v, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newIsPoweredCollector returns a collector to register an is powered method. If one is already registered -// with the same MethodMetadata it will panic. -func newIsPoweredCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - motor, err := assertMotor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - v, powerPct, err := motor.IsPowered(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, isPowered.String(), err) - } - return pb.IsPoweredResponse{ - IsOn: v, - PowerPct: powerPct, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -func assertMotor(resource interface{}) (Motor, error) { - motor, ok := resource.(Motor) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - return motor, nil -} diff --git a/components/motor/collectors_test.go b/components/motor/collectors_test.go deleted file mode 100644 index f80baf75095..00000000000 --- a/components/motor/collectors_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package motor_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - pb "go.viam.com/api/component/motor/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/data" - "go.viam.com/rdk/logging" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - componentName = "motor" - captureInterval = time.Second - numRetries = 5 -) - -func TestMotorCollectors(t *testing.T) { - tests := []struct { - name string - collector data.CollectorConstructor - expected map[string]any - }{ - { - name: "Motor position collector should write a position response", - collector: motor.NewPositionCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetPositionResponse{ - Position: 1.0, - }), - }, - { - name: "Motor isPowered collector should write an isPowered response", - collector: motor.NewIsPoweredCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.IsPoweredResponse{ - IsOn: false, - PowerPct: .5, - }), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: componentName, - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Clock: mockClock, - Target: &buf, - } - - motor := newMotor() - col, err := tc.collector(motor, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, tc.expected) - }) - } -} - -func newMotor() motor.Motor { - m := &inject.Motor{} - m.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return false, .5, nil - } - m.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 1.0, nil - } - return m -} diff --git a/components/motor/dimensionengineering/common.go b/components/motor/dimensionengineering/common.go deleted file mode 100644 index c7221b95003..00000000000 --- a/components/motor/dimensionengineering/common.go +++ /dev/null @@ -1,44 +0,0 @@ -package dimensionengineering - -type ( - commandCode byte - opCode byte -) - -const ( - minSpeed = 0x0 - maxSpeed = 0x7f - - // Commands. - singleForward commandCode = 0 - singleBackwards commandCode = 1 - singleDrive commandCode = 2 - multiForward commandCode = 3 - multiBackward commandCode = 4 - multiDrive commandCode = 5 - multiTurnLeft commandCode = 6 - multiTurnRight commandCode = 7 - multiTurn commandCode = 8 - setRamping commandCode = 20 - setDeadband commandCode = 21 - - // Serial level op-codes. - opMotor1Forward opCode = 0x00 - opMotor1Backwards opCode = 0x01 - opMinVoltage opCode = 0x02 - opMaxVoltage opCode = 0x03 - opMotor2Forward opCode = 0x04 - opMotor2Backwards opCode = 0x05 - opMotor1Drive opCode = 0x06 - opMotor2Drive opCode = 0x07 - opMultiDriveForward opCode = 0x08 - opMultiDriveBackwards opCode = 0x09 - opMultiDriveRight opCode = 0x0a - opMultiDriveLeft opCode = 0x0b - opMultiDrive opCode = 0x0c - opMultiTurn opCode = 0x0d - opSerialTimeout opCode = 0x0e - opSerialBaudRate opCode = 0x0f - opRamping opCode = 0x10 - opDeadband opCode = 0x11 -) diff --git a/components/motor/dimensionengineering/sabertooth.go b/components/motor/dimensionengineering/sabertooth.go deleted file mode 100644 index c09d9e9e625..00000000000 --- a/components/motor/dimensionengineering/sabertooth.go +++ /dev/null @@ -1,557 +0,0 @@ -// Package dimensionengineering contains implementations of the dimensionengineering motor controls -package dimensionengineering - -import ( - "context" - "fmt" - "io" - "math" - "strings" - "sync" - "time" - - "github.com/jacobsa/go-serial/serial" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - rutils "go.viam.com/rdk/utils" -) - -// https://www.dimensionengineering.com/datasheets/Sabertooth2x60.pdf -var model = resource.DefaultModelFamily.WithModel("de-sabertooth") - -// controllers is global to all instances, mapped by serial device. -var ( - globalMu sync.Mutex - controllers map[string]*controller - validBaudRates = []uint{115200, 38400, 19200, 9600, 2400} -) - -// controller is common across all Sabertooth motor instances sharing a controller. -type controller struct { - mu sync.Mutex - port io.ReadWriteCloser - serialDevice string - logger logging.Logger - activeAxes map[int]bool - testChan chan []byte - address int // 128-135 -} - -// Motor is a single axis/motor/component instance. -type Motor struct { - resource.Named - resource.AlwaysRebuild - - logger logging.Logger - // A reference to the actual controller that needs to be commanded for the motor to run - c *controller - // which channel the motor is connected to on the controller - Channel int - // Simply indicates if the RDK _thinks_ the motor is moving, because this controller has no feedback, this may not reflect reality - isOn bool - // The current power setting the RDK _thinks_ the motor is running, because this controller has no feedback, this may not reflect reality - currentPowerPct float64 - // dirFlip means that the motor is wired "backwards" from what we expect forward/backward to mean, - // so we need to "flip" the direction sent by control - dirFlip bool - // the minimum power that can be set for the motor to prevent stalls - minPowerPct float64 - // the maximum power that can be set for the motor - maxPowerPct float64 - // the freewheel RPM of the motor - maxRPM float64 - - // A manager to ensure only a single operation is happening at any given time since commands could overlap on the serial port - opMgr *operation.SingleOperationManager -} - -// Config adds DimensionEngineering-specific config options. -type Config struct { - // path to /dev/ttyXXXX file - SerialPath string `json:"serial_path"` - - // The baud rate of the controller - BaudRate int `json:"serial_baud_rate,omitempty"` - - // Valid values are 128-135 - SerialAddress int `json:"serial_address"` - - // Valid values are 1/2 - MotorChannel int `json:"motor_channel"` - - // Flip the direction of the signal sent to the controller. - // Due to wiring/motor orientation, "forward" on the controller may not represent "forward" on the robot - DirectionFlip bool `json:"dir_flip,omitempty"` - - // A value to control how quickly the controller ramps to a particular setpoint - RampValue int `json:"controller_ramp_value,omitempty"` - - // The maximum freewheel rotational velocity of the motor after the final drive (maximum effective wheel speed) - MaxRPM float64 `json:"max_rpm,omitempty"` - - // The name of the encoder used for this motor - Encoder string `json:"encoder,omitempty"` - - // The lowest power percentage to allow for this motor. This is used to prevent motor stalls and overheating. Default is 0.0 - MinPowerPct float64 `json:"min_power_pct,omitempty"` - - // The max power percentage to allow for this motor. Default is 0.0 - MaxPowerPct float64 `json:"max_power_pct,omitempty"` - - // The number of ticks per rotation of this motor from the encoder - TicksPerRotation int `json:"ticks_per_rotation,omitempty"` - - // TestChan is a fake "serial" path for test use only - TestChan chan []byte `json:"-,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.SerialPath == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "serial_path") - } - - return nil, nil -} - -func init() { - controllers = make(map[string]*controller) - - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *Config]{ - Constructor: func( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (motor.Motor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - return NewMotor(ctx, newConf, conf.ResourceName(), logger) - }, - }) -} - -func newController(c *Config, logger logging.Logger) (*controller, error) { - ctrl := new(controller) - ctrl.activeAxes = make(map[int]bool) - ctrl.serialDevice = c.SerialPath - ctrl.logger = logger - ctrl.address = c.SerialAddress - - if c.TestChan != nil { - ctrl.testChan = c.TestChan - } else { - serialOptions := serial.OpenOptions{ - PortName: c.SerialPath, - BaudRate: uint(c.BaudRate), - DataBits: 8, - StopBits: 1, - MinimumReadSize: 1, - RTSCTSFlowControl: true, - } - - port, err := serial.Open(serialOptions) - if err != nil { - return nil, err - } - ctrl.port = port - } - - ctrl.activeAxes[1] = false - ctrl.activeAxes[2] = false - - return ctrl, nil -} - -func (cfg *Config) populateDefaults() { - if cfg.BaudRate == 0 { - cfg.BaudRate = 9600 - } - - if cfg.MaxPowerPct == 0.0 { - cfg.MaxPowerPct = 1.0 - } -} - -func (cfg *Config) validateValues() error { - errs := make([]string, 0) - if cfg.MotorChannel != 1 && cfg.MotorChannel != 2 { - errs = append(errs, fmt.Sprintf("invalid channel %v, acceptable values are 1 and 2", cfg.MotorChannel)) - } - if cfg.SerialAddress < 128 || cfg.SerialAddress > 135 { - errs = append(errs, "invalid address, acceptable values are 128 thru 135") - } - if !rutils.ValidateBaudRate(validBaudRates, cfg.BaudRate) { - errs = append(errs, fmt.Sprintf("invalid baud_rate, acceptable values are %v", validBaudRates)) - } - if cfg.BaudRate != 2400 && cfg.BaudRate != 9600 && cfg.BaudRate != 19200 && cfg.BaudRate != 38400 && cfg.BaudRate != 115200 { - errs = append(errs, "invalid baud_rate, acceptable values are 2400, 9600, 19200, 38400, 115200") - } - if cfg.MinPowerPct < 0.0 || cfg.MinPowerPct > cfg.MaxPowerPct { - errs = append(errs, "invalid min_power_pct, acceptable values are 0 to max_power_pct") - } - if cfg.MaxPowerPct > 1.0 { - errs = append(errs, "invalid max_power_pct, acceptable values are min_power_pct to 100.0") - } - if len(errs) > 0 { - return fmt.Errorf("error validating sabertooth controller config: %s", strings.Join(errs, "\r\n")) - } - return nil -} - -// NewMotor returns a Sabertooth driven motor. -func NewMotor(ctx context.Context, c *Config, name resource.Name, logger logging.Logger) (motor.Motor, error) { - globalMu.Lock() - defer globalMu.Unlock() - - // populate the default values into the config - c.populateDefaults() - - // Validate the actual config values make sense - err := c.validateValues() - if err != nil { - return nil, err - } - ctrl, ok := controllers[c.SerialPath] - if !ok { - newCtrl, err := newController(c, logger) - if err != nil { - return nil, err - } - controllers[c.SerialPath] = newCtrl - ctrl = newCtrl - } - - ctrl.mu.Lock() - defer ctrl.mu.Unlock() - - // is on a known/supported amplifier only when map entry exists - claimed, ok := ctrl.activeAxes[c.MotorChannel] - if !ok { - return nil, fmt.Errorf("invalid Sabertooth motor axis: %d", c.MotorChannel) - } - if claimed { - return nil, fmt.Errorf("axis %d is already in use", c.MotorChannel) - } - ctrl.activeAxes[c.MotorChannel] = true - - m := &Motor{ - Named: name.AsNamed(), - c: ctrl, - Channel: c.MotorChannel, - dirFlip: c.DirectionFlip, - minPowerPct: c.MinPowerPct, - maxPowerPct: c.MaxPowerPct, - maxRPM: c.MaxRPM, - opMgr: operation.NewSingleOperationManager(), - logger: logger, - } - - if err := m.configure(c); err != nil { - return nil, err - } - - if c.RampValue > 0 { - setRampCmd, err := newCommand(c.SerialAddress, setRamping, c.MotorChannel, byte(c.RampValue)) - if err != nil { - return nil, err - } - - err = m.c.sendCmd(setRampCmd) - if err != nil { - return nil, err - } - } - - return m, nil -} - -// IsPowered returns if the motor is currently on or off. -func (m *Motor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return m.isOn, m.currentPowerPct, nil -} - -// Close stops the motor and marks the axis inactive. -func (m *Motor) Close(ctx context.Context) error { - active := m.isAxisActive() - if !active { - return nil - } - - err := m.Stop(context.Background(), nil) - if err != nil { - m.c.logger.CError(ctx, err) - } - - m.c.mu.Lock() - defer m.c.mu.Unlock() - m.c.activeAxes[m.Channel] = false - for _, active = range m.c.activeAxes { - if active { - return nil - } - } - if m.c.port != nil { - err = m.c.port.Close() - if err != nil { - m.c.logger.CError(ctx, fmt.Errorf("error closing serial connection: %w", err)) - } - } - globalMu.Lock() - defer globalMu.Unlock() - delete(controllers, m.c.serialDevice) - return nil -} - -func (m *Motor) isAxisActive() bool { - m.c.mu.Lock() - defer m.c.mu.Unlock() - return m.c.activeAxes[m.Channel] -} - -// Must be run inside a lock. -func (m *Motor) configure(c *Config) error { - // Turn off the motor with opMixedDrive and a value of 64 (stop) - cmd, err := newCommand(m.c.address, singleForward, c.MotorChannel, 0x00) - if err != nil { - return err - } - err = m.c.sendCmd(cmd) - return err -} - -// Must be run inside a lock. -func (c *controller) sendCmd(cmd *command) error { - packet := cmd.ToPacket() - if c.testChan != nil { - c.testChan <- packet - return nil - } - _, err := c.port.Write(packet) - return err -} - -// SetPower instructs the motor to go in a specific direction at a percentage -// of power between -1 and 1. -func (m *Motor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - if math.Abs(powerPct) < m.minPowerPct { - return m.Stop(ctx, extra) - } - if powerPct > 1 { - powerPct = 1 - } else if powerPct < -1 { - powerPct = -1 - } - - m.opMgr.CancelRunning(ctx) - m.c.mu.Lock() - defer m.c.mu.Unlock() - m.isOn = true - m.currentPowerPct = powerPct - - rawSpeed := powerPct * maxSpeed - switch speed := math.Abs(rawSpeed); { - case speed < 0.1: - m.c.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - case m.maxRPM > 0 && speed > m.maxRPM-0.1: - m.c.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.maxRPM) - default: - } - if math.Signbit(rawSpeed) { - rawSpeed *= -1 - } - - // Jog - var cmd commandCode - if powerPct < 0 { - // If dirFlip is set, we actually want to reverse the command - if m.dirFlip { - cmd = singleForward - } else { - cmd = singleBackwards - } - } else { - // If dirFlip is set, we actually want to reverse the command - if m.dirFlip { - cmd = singleBackwards - } else { - cmd = singleForward - } - } - c, err := newCommand(m.c.address, cmd, m.Channel, byte(int(rawSpeed))) - if err != nil { - return errors.Wrap(err, "error in SetPower") - } - err = m.c.sendCmd(c) - return err -} - -// GoFor moves an inputted number of revolutions at the given rpm, no encoder is present -// for this so power is determined via a linear relationship with the maxRPM and the distance -// traveled is a time based estimation based on desired RPM. -func (m *Motor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - if m.maxRPM == 0 { - return motor.NewZeroRPMError() - } - - powerPct, waitDur := goForMath(m.maxRPM, rpm, revolutions) - err := m.SetPower(ctx, powerPct, extra) - if err != nil { - return errors.Wrap(err, "error in GoFor") - } - - if revolutions == 0 { - return nil - } - - if m.opMgr.NewTimedWaitOp(ctx, waitDur) { - return m.Stop(ctx, extra) - } - return nil -} - -// GoTo instructs the motor to go to a specific position (provided in revolutions from home/zero), -// at a specific speed. Regardless of the directionality of the RPM this function will move the motor -// towards the specified target/position. -func (m *Motor) GoTo(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { - return motor.NewGoToUnsupportedError(fmt.Sprintf("Channel %d on Sabertooth %d", m.Channel, m.c.address)) -} - -// ResetZeroPosition defines the current position to be zero (+/- offset). -func (m *Motor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - return motor.NewResetZeroPositionUnsupportedError(fmt.Sprintf("Channel %d on Sabertooth %d", - m.Channel, m.c.address)) -} - -// Position reports the position in revolutions. -func (m *Motor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil -} - -// Stop turns the power to the motor off immediately, without any gradual step down. -func (m *Motor) Stop(ctx context.Context, extra map[string]interface{}) error { - m.c.mu.Lock() - defer m.c.mu.Unlock() - - _, done := m.opMgr.New(ctx) - defer done() - - m.isOn = false - m.currentPowerPct = 0.0 - cmd, err := newCommand(m.c.address, singleForward, m.Channel, 0) - if err != nil { - return err - } - - err = m.c.sendCmd(cmd) - return err -} - -// IsMoving returns whether the motor is currently moving. -func (m *Motor) IsMoving(ctx context.Context) (bool, error) { - return m.isOn, nil -} - -// DoCommand executes additional commands beyond the Motor{} interface. -func (m *Motor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - name, ok := cmd["command"] - if !ok { - return nil, errors.New("missing 'command' value") - } - return nil, fmt.Errorf("no such command: %s", name) -} - -// Properties returns the additional properties supported by this motor. -func (m *Motor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{PositionReporting: false}, nil -} - -type command struct { - Address byte - Op byte - Data byte - Checksum byte -} - -func newCommand(controllerAddress int, motorMode commandCode, channel int, data byte) (*command, error) { - var opcode opCode - switch motorMode { - case singleForward: - switch channel { - case 1: - opcode = opMotor1Forward - case 2: - opcode = opMotor2Forward - default: - return nil, errors.New("invalid motor channel") - } - case singleBackwards: - switch channel { - case 1: - opcode = opMotor1Backwards - case 2: - opcode = opMotor2Backwards - default: - return nil, errors.New("invalid motor channel") - } - case singleDrive: - switch channel { - case 1: - opcode = opMotor1Drive - case 2: - opcode = opMotor2Drive - default: - return nil, errors.New("invalid motor channel") - } - case multiForward: - opcode = opMultiDriveForward - case multiBackward: - opcode = opMultiDriveForward - case multiDrive: - opcode = opMultiDrive - case setRamping: - opcode = opRamping - case setDeadband: - case multiTurnRight: - case multiTurnLeft: - case multiTurn: - default: - return nil, fmt.Errorf("opcode %x not implemented", opcode) - } - sum := byte(controllerAddress) + byte(opcode) + data - checksum := sum & 0x7F - return &command{ - Address: byte(controllerAddress), - Op: byte(opcode), - Data: data, - Checksum: checksum, - }, nil -} - -func (c *command) ToPacket() []byte { - return []byte{c.Address, c.Op, c.Data, c.Checksum} -} - -// If revolutions is 0, the returned wait duration will be 0 representing that -// the motor should run indefinitely. -func goForMath(maxRPM, rpm, revolutions float64) (float64, time.Duration) { - // need to do this so time is reasonable - if rpm > maxRPM { - rpm = maxRPM - } else if rpm < -1*maxRPM { - rpm = -1 * maxRPM - } - - if revolutions == 0 { - powerPct := rpm / maxRPM - return powerPct, 0 - } - - dir := rpm * revolutions / math.Abs(revolutions*rpm) - powerPct := math.Abs(rpm) / maxRPM * dir - waitDur := time.Duration(math.Abs(revolutions/rpm)*60*1000) * time.Millisecond - return powerPct, waitDur -} diff --git a/components/motor/dimensionengineering/sabertooth_test.go b/components/motor/dimensionengineering/sabertooth_test.go deleted file mode 100644 index b23d19f9ee3..00000000000 --- a/components/motor/dimensionengineering/sabertooth_test.go +++ /dev/null @@ -1,473 +0,0 @@ -package dimensionengineering_test - -import ( - "bytes" - "context" - "fmt" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/motor/dimensionengineering" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// var txMu sync.Mutex - -var sabertoothModel = resource.DefaultModelFamily.WithModel("de-sabertooth") - -func checkTx(t *testing.T, resChan chan string, c chan []byte, expects []byte) { - t.Helper() - message := <-c - t.Logf("Expected: %b, Actual %b", expects, message) - test.That(t, bytes.Compare(message, expects), test.ShouldBeZeroValue) - resChan <- "DONE" -} - -//nolint:dupl -func TestSabertoothMotor(t *testing.T) { - ctx := context.Background() - logger, obs := logging.NewObservedTestLogger(t) - c := make(chan []byte, 1024) - resChan := make(chan string, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 128, - DirectionFlip: false, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - m1, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err, test.ShouldBeNil) - defer m1.Close(ctx) - - // This should be the stop command - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x00, 0x00}) - - motor1, ok := m1.(motor.Motor) - test.That(t, ok, test.ShouldBeTrue) - - t.Run("motor supports position reporting", func(t *testing.T) { - properties, err := motor1.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - }) - - t.Run("motor SetPower testing", func(t *testing.T) { - // Test 0 (aka "stop") - test.That(t, motor1.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x00, 0x00}) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - // Test 0.5 of max power - test.That(t, motor1.SetPower(ctx, 0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x3f, 0x3f}) - - // Test -0.5 of max power - test.That(t, motor1.SetPower(ctx, -0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x01, 0x3f, 0x40}) - - // Test max power - test.That(t, motor1.SetPower(ctx, 1, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x7f, 0x7f}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - - // Test 0 (aka "stop") - test.That(t, motor1.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x00, 0x00}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - }) - - mc2 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 2, - TestChan: c, - SerialAddress: 128, - DirectionFlip: false, - MaxRPM: 1, - } - - m2, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor2", ConvertedAttributes: &mc2}, logger) - test.That(t, err, test.ShouldBeNil) - defer m2.Close(ctx) - - checkTx(t, resChan, c, []byte{0x80, 0x04, 0x00, 0x04}) - - motor2, ok := m2.(motor.Motor) - test.That(t, ok, test.ShouldBeTrue) - - t.Run("motor supports position reporting", func(t *testing.T) { - properties, err := motor2.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - }) - - t.Run("motor SetPower testing", func(t *testing.T) { - // Test 0 (aka "stop") - test.That(t, motor2.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x04, 0x00, 0x04}) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - // Test 0.5 of max power - test.That(t, motor2.SetPower(ctx, 0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x04, 0x3f, 0x43}) - - // Test -0.5 of max power - test.That(t, motor2.SetPower(ctx, -0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x05, 0x3f, 0x44}) - - // Test max power - test.That(t, motor2.SetPower(ctx, 1, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x04, 0x7f, 0x03}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - - // Test 0 (aka "stop") - test.That(t, motor2.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x04, 0x00, 0x04}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - }) -} - -//nolint:dupl -func TestSabertoothMotorDirectionFlip(t *testing.T) { - ctx := context.Background() - logger, obs := logging.NewObservedTestLogger(t) - c := make(chan []byte, 1024) - resChan := make(chan string, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 128, - DirectionFlip: true, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - m1, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err, test.ShouldBeNil) - defer m1.Close(ctx) - - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x00, 0x00}) - - motor1, ok := m1.(motor.Motor) - test.That(t, ok, test.ShouldBeTrue) - - t.Run("motor SetPower testing", func(t *testing.T) { - // Test 0 (aka "stop") - test.That(t, motor1.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x01, 0x00, 0x01}) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - // Test 0.5 of max power - test.That(t, motor1.SetPower(ctx, 0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x01, 0x3f, 0x40}) - - // Test -0.5 of max power - test.That(t, motor1.SetPower(ctx, -0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x3f, 0x3f}) - - // Test max power - test.That(t, motor1.SetPower(ctx, 1, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x01, 0x7f, 0x00}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - - // Test 0 (aka "stop") - test.That(t, motor1.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x01, 0x00, 0x01}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - }) - - mc2 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 2, - TestChan: c, - SerialAddress: 128, - DirectionFlip: true, - MaxRPM: 1, - } - - m2, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor2", ConvertedAttributes: &mc2}, logger) - test.That(t, err, test.ShouldBeNil) - defer m2.Close(ctx) - - checkTx(t, resChan, c, []byte{0x80, 0x04, 0x00, 0x04}) - - motor2, ok := m2.(motor.Motor) - test.That(t, ok, test.ShouldBeTrue) - - t.Run("motor supports position reporting", func(t *testing.T) { - properties, err := motor2.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - }) - - t.Run("motor SetPower testing", func(t *testing.T) { - // Test 0 (aka "stop") - test.That(t, motor2.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x05, 0x00, 0x05}) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - // Test 0.5 of max power - test.That(t, motor2.SetPower(ctx, 0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x05, 0x3f, 0x44}) - - // Test -0.5 of max power - test.That(t, motor2.SetPower(ctx, -0.5, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x04, 0x3f, 0x43}) - - // Test max power - test.That(t, motor2.SetPower(ctx, 1, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x05, 0x7f, 0x04}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - - // Test 0 (aka "stop") - test.That(t, motor2.SetPower(ctx, 0, nil), test.ShouldBeNil) - checkTx(t, resChan, c, []byte{0x80, 0x05, 0x00, 0x05}) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - }) -} - -func TestSabertoothRampConfig(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - resChan := make(chan string, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 128, - RampValue: 100, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - m1, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err, test.ShouldBeNil) - defer m1.Close(ctx) - - checkTx(t, resChan, c, []byte{0x80, 0x00, 0x00, 0x00}) - checkTx(t, resChan, c, []byte{0x80, 0x10, 0x64, 0x74}) - - _, ok = m1.(motor.Motor) - test.That(t, ok, test.ShouldBeTrue) -} - -func TestSabertoothAddressMapping(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - resChan := make(chan string, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 129, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - m1, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err, test.ShouldBeNil) - defer m1.Close(ctx) - - checkTx(t, resChan, c, []byte{0x81, 0x00, 0x00, 0x01}) -} - -func TestInvalidMotorChannel(t *testing.T) { - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 3, - TestChan: c, - SerialAddress: 129, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - _, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid channel") -} - -func TestInvalidBaudRate(t *testing.T) { - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 129, - BaudRate: 1, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - _, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid baud_rate") -} - -func TestInvalidSerialAddress(t *testing.T) { - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 140, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - _, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid address") -} - -func TestInvalidMinPowerPct(t *testing.T) { - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 129, - MinPowerPct: 0.7, - MaxPowerPct: 0.5, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - _, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid min_power_pct") -} - -func TestInvalidMaxPowerPct(t *testing.T) { - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 1, - TestChan: c, - SerialAddress: 129, - MinPowerPct: 0.7, - MaxPowerPct: 1.5, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - _, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid max_power_pct") -} - -func TestMultipleInvalidParameters(t *testing.T) { - logger := logging.NewTestLogger(t) - c := make(chan []byte, 1024) - deps := make(resource.Dependencies) - - mc1 := dimensionengineering.Config{ - SerialPath: "testchan", - MotorChannel: 3, - TestChan: c, - BaudRate: 10, - SerialAddress: 140, - MinPowerPct: 1.7, - MaxPowerPct: 1.5, - MaxRPM: 1, - } - - motorReg, ok := resource.LookupRegistration(motor.API, sabertoothModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - _, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc1}, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid channel") - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid address") - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid baud_rate") - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid min_power_pct") - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid max_power_pct") -} diff --git a/components/motor/dmc4000/dmc.go b/components/motor/dmc4000/dmc.go deleted file mode 100644 index ac95f73baec..00000000000 --- a/components/motor/dmc4000/dmc.go +++ /dev/null @@ -1,822 +0,0 @@ -// Package dmc4000 implements stepper motors behind a Galil DMC4000 series motor controller -package dmc4000 - -import ( - "bytes" - "context" - "fmt" - "io" - "math" - "strconv" - "strings" - "sync" - "time" - - "github.com/jacobsa/go-serial/serial" - "github.com/pkg/errors" - "go.viam.com/utils" - "go.viam.com/utils/usb" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -// Timeout for Home(). -const homeTimeout = time.Minute - -var model = resource.DefaultModelFamily.WithModel("DMC4000") - -// controllers is global to all instances, mapped by serial device. -var ( - globalMu sync.Mutex - controllers map[string]*controller - usbFilter = usb.SearchFilter{} -) - -// controller is common across all DMC4000 motor instances sharing a controller. -type controller struct { - mu sync.Mutex - port io.ReadWriteCloser - serialDevice string - logger logging.Logger - activeAxes map[string]bool - ampModel1 string - ampModel2 string - testChan chan string -} - -// Motor is a single axis/motor/component instance. -type Motor struct { - resource.Named - resource.AlwaysRebuild - c *controller - Axis string - TicksPerRotation int - maxRPM float64 - MaxAcceleration float64 - HomeRPM float64 - jogging bool - opMgr *operation.SingleOperationManager - powerPct float64 - logger logging.Logger -} - -// Config adds DMC-specific config options. -type Config struct { - resource.TriviallyValidateConfig - DirectionFlip bool `json:"dir_flip,omitempty"` // Flip the direction of the signal sent if there is a Dir pin - MaxRPM float64 `json:"max_rpm,omitempty"` - MaxAcceleration float64 `json:"max_acceleration_rpm_per_sec,omitempty"` - TicksPerRotation int `json:"ticks_per_rotation"` - SerialDevice string `json:"serial_path"` // path to /dev/ttyXXXX file - Axis string `json:"controller_axis"` // A-H - HomeRPM float64 `json:"home_rpm"` // Speed for Home() - - // Set the per phase current (when using stepper amp) - // https://www.galil.com/download/comref/com4103/index.html#amplifier_gain.html - AmplifierGain int `json:"amplifier_gain"` - // Can reduce current when holding - // https://www.galil.com/download/comref/com4103/index.html#low_current_stepper_mode.html - LowCurrent int `json:"low_current"` - - // TestChan is a fake "serial" path for test use only - TestChan chan string `json:"-"` -} - -func init() { - controllers = make(map[string]*controller) - - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *Config]{ - Constructor: func( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (motor.Motor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - return NewMotor(ctx, newConf, conf.ResourceName(), logger) - }, - }) -} - -// NewMotor returns a DMC4000 driven motor. -func NewMotor(ctx context.Context, c *Config, name resource.Name, logger logging.Logger) (motor.Motor, error) { - if c.SerialDevice == "" { - devs := usb.Search(usbFilter, func(vendorID, productID int) bool { - if vendorID == 0x403 && productID == 0x6001 { - return true - } - return false - }) - - if len(devs) > 0 { - c.SerialDevice = devs[0].Path - } else { - return nil, errors.New("couldn't find DMC4000 serial connection") - } - } - - globalMu.Lock() - ctrl, ok := controllers[c.SerialDevice] - if !ok { - newCtrl, err := newController(c, logger) - if err != nil { - return nil, err - } - controllers[c.SerialDevice] = newCtrl - ctrl = newCtrl - } - globalMu.Unlock() - - ctrl.mu.Lock() - defer ctrl.mu.Unlock() - - // is on a known/supported amplifier only when map entry exists - claimed, ok := ctrl.activeAxes[c.Axis] - if !ok { - return nil, fmt.Errorf("invalid dmc4000 motor axis: %s", c.Axis) - } - if claimed { - return nil, fmt.Errorf("axis %s is already in use", c.Axis) - } - ctrl.activeAxes[c.Axis] = true - - if c.TicksPerRotation == 0 { - return nil, errors.New("expected ticks_per_rotation in config for motor") - } - - m := &Motor{ - Named: name.AsNamed(), - c: ctrl, - Axis: c.Axis, - TicksPerRotation: c.TicksPerRotation, - maxRPM: c.MaxRPM, - MaxAcceleration: c.MaxAcceleration, - HomeRPM: c.HomeRPM, - powerPct: 0.0, - opMgr: operation.NewSingleOperationManager(), - logger: logger, - } - - if m.maxRPM <= 0 { - m.maxRPM = 1000 // arbitrary high value - } - - if m.MaxAcceleration <= 0 { - m.MaxAcceleration = 1000 // rpm/s, arbitrary/safe value (1s to max) - } - - if m.HomeRPM <= 0 { - m.HomeRPM = m.maxRPM / 4 - } - - if err := m.configure(c); err != nil { - return nil, err - } - - return m, nil -} - -// Close stops the motor and marks the axis inactive. -func (m *Motor) Close(ctx context.Context) error { - m.c.mu.Lock() - active := m.c.activeAxes[m.Axis] - m.c.mu.Unlock() - if !active { - return nil - } - err := m.Stop(context.Background(), nil) - if err != nil { - m.c.logger.CError(ctx, err) - } - - m.c.mu.Lock() - defer m.c.mu.Unlock() - m.c.activeAxes[m.Axis] = false - for _, active = range m.c.activeAxes { - if active { - return nil - } - } - if m.c.port != nil { - err = m.c.port.Close() - if err != nil { - m.c.logger.CError(ctx, err) - } - } - globalMu.Lock() - defer globalMu.Unlock() - delete(controllers, m.c.serialDevice) - return nil -} - -func newController(c *Config, logger logging.Logger) (*controller, error) { - ctrl := new(controller) - ctrl.activeAxes = make(map[string]bool) - ctrl.serialDevice = c.SerialDevice - ctrl.logger = logger - - if c.TestChan != nil { - ctrl.testChan = c.TestChan - } else { - serialOptions := serial.OpenOptions{ - PortName: c.SerialDevice, - BaudRate: 115200, - DataBits: 8, - StopBits: 1, - MinimumReadSize: 1, - RTSCTSFlowControl: true, - } - - port, err := serial.Open(serialOptions) - if err != nil { - return nil, err - } - ctrl.port = port - } - - // Set echo off to not scramble our returns - _, err := ctrl.sendCmd("EO 0") - if err != nil && !strings.HasPrefix(err.Error(), "unknown error after cmd") { - return nil, err - } - - ret, err := ctrl.sendCmd("ID") - if err != nil { - return nil, err - } - - var modelNum string - for _, line := range strings.Split(ret, "\n") { - tokens := strings.Split(line, ", ") - switch tokens[0] { - case "DMC": - modelNum = tokens[1] - logger.Infof("Found DMC4000 (%s) on port: %s\n%s", modelNum, c.SerialDevice, ret) - case "AMP1": - ctrl.ampModel1 = tokens[1] - case "AMP2": - ctrl.ampModel2 = tokens[1] - } - } - - if modelNum != "4000" && modelNum != "4103" && modelNum != "4200" { - return nil, fmt.Errorf("unsupported DMC model number: %s", modelNum) - } - - if ctrl.ampModel1 != "44140" && ctrl.ampModel2 != "44140" { - return nil, fmt.Errorf("unsupported amplifier model(s) found, amp1: %s, amp2, %s", ctrl.ampModel1, ctrl.ampModel2) - } - - // Add to the map if it's a known amp - if ctrl.ampModel1 == "44140" { - ctrl.activeAxes["A"] = false - ctrl.activeAxes["B"] = false - ctrl.activeAxes["C"] = false - ctrl.activeAxes["D"] = false - } - - if ctrl.ampModel2 == "44140" { - ctrl.activeAxes["E"] = false - ctrl.activeAxes["F"] = false - ctrl.activeAxes["G"] = false - ctrl.activeAxes["H"] = false - } - - return ctrl, nil -} - -// Must be run inside a lock. -func (m *Motor) configure(c *Config) error { - var amp string - if m.Axis == "A" || m.Axis == "B" || m.Axis == "C" || m.Axis == "D" { - amp = m.c.ampModel1 - } else { - amp = m.c.ampModel2 - } - - switch amp { - case "44140": - m.TicksPerRotation *= 64 // fixed microstepping - - // Stepper type, with optional reversing - motorType := "2" // string because no trailing zeros - if c.DirectionFlip { - motorType = "2.5" - } - - // Turn off the motor - _, err := m.c.sendCmd(fmt.Sprintf("MO%s", m.Axis)) - if err != nil { - return err - } - - // Set motor type to stepper (possibly reversed) - _, err = m.c.sendCmd(fmt.Sprintf("MT%s=%s", m.Axis, motorType)) - if err != nil { - return err - } - - // Set amplifier gain - _, err = m.c.sendCmd(fmt.Sprintf("AG%s=%d", m.Axis, c.AmplifierGain)) - if err != nil { - return err - } - - // Set low current mode - _, err = m.c.sendCmd(fmt.Sprintf("LC%s=%d", m.Axis, c.LowCurrent)) - if err != nil { - return err - } - - // Acceleration - _, err = m.c.sendCmd(fmt.Sprintf("AC%s=%d", m.Axis, m.rpmsToA(m.MaxAcceleration))) - if err != nil { - return err - } - - // Deceleration - _, err = m.c.sendCmd(fmt.Sprintf("DC%s=%d", m.Axis, m.rpmsToA(m.MaxAcceleration))) - if err != nil { - return err - } - - // Enable the motor - _, err = m.c.sendCmd(fmt.Sprintf("SH%s", m.Axis)) - return err - - default: - return fmt.Errorf("unsupported amplifier model: %s", amp) - } -} - -// Must be run inside a lock. -func (c *controller) sendCmd(cmd string) (string, error) { - if c.testChan != nil { - c.testChan <- cmd - } else { - _, err := c.port.Write([]byte(cmd + "\r\n")) - if err != nil { - return "", err - } - } - - var ret []byte - for { - buf := make([]byte, 4096) - if c.testChan != nil { - ret = []byte(<-c.testChan) - break - } - - n, err := c.port.Read(buf) - if err != nil { - return string(ret), err - } - ret = append(ret, buf[:n]...) - if bytes.ContainsAny(buf[:n], ":?") { - break - } - } - if bytes.LastIndexByte(ret, []byte(":")[0]) == len(ret)-1 { - ret := string(bytes.TrimSpace(ret[:len(ret)-1])) - // c.logger.Debugf("CMD (%s) OK: %s", cmd, ret) - return ret, nil - } - - if bytes.LastIndexByte(ret, []byte("?")[0]) == len(ret)-1 { - errorDetail, err := c.sendCmd("TC1") - if err != nil { - return string(ret), fmt.Errorf("error when trying to get error code from previous command (%s): %w", cmd, err) - } - return string(bytes.TrimSpace(ret[:len(ret)-1])), fmt.Errorf("cmd (%s) returned error: %s", cmd, errorDetail) - } - - return string(ret), fmt.Errorf("unknown error after cmd (%s), response: %s", cmd, string(ret)) -} - -// Convert rpm to DMC4000 counts/sec. -func (m *Motor) rpmToV(rpm float64) int { - rpm = math.Abs(rpm) - if rpm > m.maxRPM { - rpm = m.maxRPM - } - speed := rpm * float64(m.TicksPerRotation) / 60 - - // Hard limits from controller - if speed > 3000000 { - speed = 3000000 - } - return int(speed) -} - -// Convert rpm/s to DMC4000 counts/sec^2. -func (m *Motor) rpmsToA(rpms float64) int { - rpms = math.Abs(rpms) - if rpms > m.MaxAcceleration { - rpms = m.MaxAcceleration - } - acc := rpms * float64(m.TicksPerRotation) / 60 - - // Hard limits from controller - if acc > 1073740800 { - acc = 1073740800 - } else if acc < 1024 { - acc = 1024 - } - return int(acc) -} - -// Convert revolutions to steps. -func (m *Motor) posToSteps(pos float64) int32 { - goal := int32(pos * float64(m.TicksPerRotation)) - - // Hard limits from controller - if goal > 2147483647 { - goal = 2147483647 - } else if goal < -2147483648 { - goal = -2147483648 - } - return goal -} - -// SetPower instructs the motor to go in a specific direction at a percentage -// of power between -1 and 1. Scaled to MaxRPM. -func (m *Motor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - powerPct = math.Min(powerPct, 1.0) - powerPct = math.Max(powerPct, -1.0) - - switch pow := math.Abs(powerPct); { - case pow < 0.1: - m.c.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return m.Stop(ctx, extra) - case m.maxRPM > 0 && pow*m.maxRPM > m.maxRPM-0.1: - m.c.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.maxRPM) - default: - } - - m.powerPct = powerPct - return m.Jog(ctx, powerPct*m.maxRPM) -} - -// Jog moves indefinitely at the specified RPM. -func (m *Motor) Jog(ctx context.Context, rpm float64) error { - m.opMgr.CancelRunning(ctx) - m.c.mu.Lock() - defer m.c.mu.Unlock() - m.jogging = true - - rawSpeed := m.rpmToV(rpm) - if math.Signbit(rpm) { - rawSpeed *= -1 - } - - // Jog - _, err := m.c.sendCmd(fmt.Sprintf("JG%s=%d", m.Axis, rawSpeed)) - if err != nil { - return err - } - - // Begin action - _, err = m.c.sendCmd(fmt.Sprintf("BG%s", m.Axis)) - if err != nil { - return err - } - return nil -} - -func (m *Motor) stopJog() error { - if m.jogging { - m.jogging = false - _, err := m.c.sendCmd(fmt.Sprintf("ST%s", m.Axis)) - return err - } - return nil -} - -// GoFor instructs the motor to go in a specific direction for a specific amount of -// revolutions at a given speed in revolutions per minute. Both the RPM and the revolutions -// can be assigned negative values to move in a backwards direction. Note: if both are -// negative the motor will spin in the forward direction. -func (m *Motor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - switch speed := math.Abs(rpm); { - case speed < 0.1: - m.c.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - case m.maxRPM > 0 && speed > m.maxRPM-0.1: - m.c.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.maxRPM) - default: - } - ctx, done := m.opMgr.New(ctx) - defer done() - - m.c.mu.Lock() - defer m.c.mu.Unlock() - curPos, err := m.doPosition() - if err != nil { - return err - } - if math.Signbit(rpm) { - revolutions *= -1 - } - goal := curPos + revolutions - err = m.doGoTo(rpm, goal) - if err != nil { - return err - } - - return m.opMgr.WaitForSuccess( - ctx, - time.Millisecond*10, - m.isStopped, - ) -} - -// GoTo instructs the motor to go to a specific position (provided in revolutions from home/zero), -// at a specific speed. Regardless of the directionality of the RPM this function will move the motor -// towards the specified target/position. -func (m *Motor) GoTo(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { - ctx, done := m.opMgr.New(ctx) - defer done() - - m.c.mu.Lock() - defer m.c.mu.Unlock() - - if err := m.doGoTo(rpm, position); err != nil { - return motor.NewGoToUnsupportedError(m.Name().ShortName()) - } - - return m.opMgr.WaitForSuccess( - ctx, - time.Millisecond*10, - m.isStopped, - ) -} - -// ResetZeroPosition defines the current position to be zero (+/- offset). -func (m *Motor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - m.c.mu.Lock() - defer m.c.mu.Unlock() - _, err := m.c.sendCmd(fmt.Sprintf("DP%s=%d", m.Axis, int(-1*offset*float64(m.TicksPerRotation)))) - if err != nil { - return errors.Wrap(err, "error in ResetZeroPosition") - } - return err -} - -// Position reports the position in revolutions. -func (m *Motor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - m.c.mu.Lock() - defer m.c.mu.Unlock() - return m.doPosition() -} - -// Stop turns the power to the motor off immediately, without any gradual step down. -func (m *Motor) Stop(ctx context.Context, extra map[string]interface{}) error { - m.c.mu.Lock() - defer m.c.mu.Unlock() - - ctx, done := m.opMgr.New(ctx) - defer done() - - m.jogging = false - _, err := m.c.sendCmd(fmt.Sprintf("ST%s", m.Axis)) - if err != nil { - return errors.Wrap(err, "error in Stop function") - } - - return m.opMgr.WaitForSuccess( - ctx, - time.Millisecond*10, - m.isStopped, - ) -} - -// IsMoving returns whether or not the motor is currently moving. -func (m *Motor) IsMoving(ctx context.Context) (bool, error) { - m.c.mu.Lock() - defer m.c.mu.Unlock() - stopped, err := m.isStopped(ctx) - if err != nil { - return false, err - } - return !stopped, nil -} - -// IsPowered returns whether or not the motor is currently moving. -func (m *Motor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - on, err := m.IsMoving(ctx) - if err != nil { - return on, m.powerPct, errors.Wrap(err, "error in IsPowered") - } - return on, m.powerPct, err -} - -// Must be run inside a lock. -func (m *Motor) isStopped(ctx context.Context) (bool, error) { - // check that stop was actually commanded - ret, err := m.c.sendCmd(fmt.Sprintf("SC%s", m.Axis)) - if err != nil { - return false, err - } - sc, err := strconv.Atoi(ret) - if err != nil { - return false, err - } - - // Stop codes that indicate the motor is NOT actually stopped - // https://www.galil.com/download/comref/com4103/index.html#stop_code.html - if sc == 0 || sc == 30 || sc == 50 || sc == 60 || sc == 100 { - return false, nil - } - - // check that total error is zero (not coasting) - ret, err = m.c.sendCmd(fmt.Sprintf("TE%s", m.Axis)) - if err != nil { - return false, err - } - te, err := strconv.Atoi(ret) - if err != nil { - return false, err - } - - if te != 0 { - return false, nil - } - - return true, nil -} - -// Home runs the dmc homing routine. -func (m *Motor) Home(ctx context.Context) error { - ctx, done := m.opMgr.New(ctx) - defer done() - - // start homing (self-locking) - if err := m.startHome(); err != nil { - return err - } - - // wait for routine to finish - defer func() { - if err := m.Stop(ctx, nil); err != nil { - m.c.logger.CError(ctx, err) - } - }() - - startTime := time.Now() - for { - if !utils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return errors.New("context cancelled during Home") - } - - // Wait for - m.c.mu.Lock() - ret, err := m.c.sendCmd(fmt.Sprintf("SC%s", m.Axis)) - m.c.mu.Unlock() - if err != nil { - return err - } - sc, err := strconv.Atoi(ret) - if err != nil { - return err - } - - // stop code 10 indicates homing sequence finished - if sc == 10 { - return nil - } - - if time.Since(startTime) >= homeTimeout { - return errors.New("timed out during Home") - } - } -} - -// Does its own locking. -func (m *Motor) startHome() error { - m.c.mu.Lock() - defer m.c.mu.Unlock() - - // Exit jog mode if in it - err := m.stopJog() - if err != nil { - return err - } - - // Speed (stage 1) - _, err = m.c.sendCmd(fmt.Sprintf("SP%s=%d", m.Axis, m.rpmToV(m.HomeRPM))) - if err != nil { - return err - } - - // Speed (stage 2) - _, err = m.c.sendCmd(fmt.Sprintf("HV%s=%d", m.Axis, m.rpmToV(m.HomeRPM/10))) - if err != nil { - return err - } - - // Homing action - _, err = m.c.sendCmd(fmt.Sprintf("HM%s", m.Axis)) - if err != nil { - return err - } - - // Begin action - _, err = m.c.sendCmd(fmt.Sprintf("BG%s", m.Axis)) - if err != nil { - return err - } - - return nil -} - -// Must be run inside a lock. -func (m *Motor) doGoTo(rpm, position float64) error { - // Exit jog mode if in it - err := m.stopJog() - if err != nil { - return err - } - - // Position tracking mode - _, err = m.c.sendCmd(fmt.Sprintf("PT%s=1", m.Axis)) - if err != nil { - return err - } - - switch speed := math.Abs(rpm); { - case speed < 0.1: - m.c.logger.Warn("motor speed is nearly 0 rev_per_min") - case m.maxRPM > 0 && speed > m.maxRPM-0.1: - m.c.logger.Warnf("motor speed is nearly the max rev_per_min (%f)", m.maxRPM) - default: - } - - // Speed - _, err = m.c.sendCmd(fmt.Sprintf("SP%s=%d", m.Axis, m.rpmToV(rpm))) - if err != nil { - return err - } - - // Position target - _, err = m.c.sendCmd(fmt.Sprintf("PA%s=%d", m.Axis, m.posToSteps(position))) - if err != nil { - return err - } - return nil -} - -// Must be run inside a lock. -func (m *Motor) doPosition() (float64, error) { - ret, err := m.c.sendCmd(fmt.Sprintf("RP%s", m.Axis)) - if err != nil { - return 0, err - } - position, err := strconv.ParseFloat(ret, 64) - if err != nil { - return 0, err - } - return position / float64(m.TicksPerRotation), nil -} - -// DoCommand executes additional commands beyond the Motor{} interface. -func (m *Motor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - name, ok := cmd["command"] - if !ok { - return nil, errors.New("missing 'command' value") - } - switch name { - case "home": - return nil, m.Home(ctx) - case "jog": - rpmRaw, ok := cmd["rpm"] - if !ok { - return nil, errors.New("need rpm value for jog") - } - rpm, ok := rpmRaw.(float64) - if !ok { - return nil, errors.New("rpm value must be floating point") - } - return nil, m.Jog(ctx, rpm) - case "raw": - raw, ok := cmd["raw_input"] - if !ok { - return nil, errors.New("need raw string to send to controller") - } - m.c.mu.Lock() - defer m.c.mu.Unlock() - retVal, err := m.c.sendCmd(raw.(string)) - ret := map[string]interface{}{"return": retVal} - return ret, err - default: - return nil, fmt.Errorf("no such command: %s", name) - } -} - -// Properties returns the additional properties supported by this motor. -func (m *Motor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{PositionReporting: true}, nil -} diff --git a/components/motor/dmc4000/dmc4000_test.go b/components/motor/dmc4000/dmc4000_test.go deleted file mode 100644 index 00bae35f7fa..00000000000 --- a/components/motor/dmc4000/dmc4000_test.go +++ /dev/null @@ -1,713 +0,0 @@ -package dmc4000_test - -import ( - "context" - "fmt" - "sync" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/motor/dmc4000" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// check is essentially test.That with tb.Error instead of tb.Fatal (Fatal exits and leaves the go routines stuck waiting). -func check( - resChan chan string, - actual interface{}, - assert func(actual interface{}, expected ...interface{}) string, expected ...interface{}, -) { - if result := assert(actual, expected...); result != "" { - resChan <- result - } -} - -var txMu sync.Mutex - -func waitTx(tb testing.TB, resChan chan string) { - tb.Helper() - txMu.Lock() - defer txMu.Unlock() - for { - res := <-resChan - if res == "DONE" { - return - } - tb.Error(res) - } -} - -func checkTx(resChan, c chan string, expects []string) { - defer txMu.Unlock() - for _, expected := range expects { - tx := <-c - check(resChan, tx, test.ShouldResemble, expected) - c <- ":" - } - resChan <- "DONE" -} - -func checkRx(resChan, c chan string, expects, sends []string) { - defer txMu.Unlock() - for i, expected := range expects { - tx := <-c - check(resChan, tx, test.ShouldResemble, expected) - c <- sends[i] - } - resChan <- "DONE" -} - -func TestDMC4000Motor(t *testing.T) { - ctx := context.Background() - logger, obs := logging.NewObservedTestLogger(t) - c := make(chan string) - resChan := make(chan string, 1024) - deps := make(resource.Dependencies) - - mc := dmc4000.Config{ - SerialDevice: "testchan", - Axis: "A", - HomeRPM: 50, - AmplifierGain: 3, - LowCurrent: -1, - TestChan: c, - MaxAcceleration: 5000, - MaxRPM: 300, - TicksPerRotation: 200, - } - - motorReg, ok := resource.LookupRegistration(motor.API, resource.DefaultModelFamily.WithModel("DMC4000")) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, motorReg, test.ShouldNotBeNil) - - // These are the setup register writes - txMu.Lock() - go checkRx(resChan, c, - []string{ - "EO 0", - "ID", - "MOA", - "MTA=2", - "AGA=3", - "LCA=-1", - "ACA=1066666", - "DCA=1066666", - "SHA", - }, - []string{ - ":", - "FW, DMC4183 Rev 1.3h\r\nDMC, 4103, Rev 11\r\nAMP1, 44140, Rev 3\r\nAMP2, 44140, Rev 3\r\n:", - ":", - ":", - ":", - ":", - ":", - ":", - ":", - }, - ) - - m, err := motorReg.Constructor(context.Background(), deps, resource.Config{Name: "motor1", ConvertedAttributes: &mc}, logger) - test.That(t, err, test.ShouldBeNil) - defer func() { - txMu.Lock() - go checkRx(resChan, c, - []string{"STA", "SCA", "TEA"}, - []string{" :", " 4\r\n:", " 0\r\n:"}, - ) - test.That(t, m.Close(context.Background()), test.ShouldBeNil) - waitTx(t, resChan) - }() - motorDep, ok := m.(motor.Motor) - test.That(t, ok, test.ShouldBeTrue) - waitTx(t, resChan) - - t.Run("motor supports position reporting", func(t *testing.T) { - properties, err := motorDep.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeTrue) - }) - - t.Run("motor SetPower testing", func(t *testing.T) { - // Test 0 (aka "stop") - txMu.Lock() - go checkRx(resChan, c, - []string{"STA", "SCA", "TEA"}, - []string{" :", "4\r\n:", "0\r\n:"}, - ) - test.That(t, motorDep.SetPower(ctx, 0, nil), test.ShouldBeNil) - - // Test almost 0 - txMu.Lock() - go checkRx(resChan, c, - []string{"STA", "SCA", "TEA"}, - []string{" :", "4\r\n:", "0\r\n:"}, - ) - test.That(t, motorDep.SetPower(ctx, 0.05, nil), test.ShouldBeNil) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - // Test 0.5 of max power - txMu.Lock() - go checkTx(resChan, c, []string{ - "JGA=32000", - "BGA", - }) - test.That(t, motorDep.SetPower(ctx, 0.5, nil), test.ShouldBeNil) - - // Test -0.5 of max power - txMu.Lock() - go checkTx(resChan, c, []string{ - "JGA=-32000", - "BGA", - }) - test.That(t, motorDep.SetPower(ctx, -0.5, nil), test.ShouldBeNil) - waitTx(t, resChan) - - // Test max power - txMu.Lock() - go checkTx(resChan, c, []string{ - "JGA=64000", - "BGA", - }) - test.That(t, motorDep.SetPower(ctx, 1, nil), test.ShouldBeNil) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - }) - - t.Run("motor Stop testing", func(t *testing.T) { - txMu.Lock() - go checkRx(resChan, c, - []string{"STA", "SCA", "TEA"}, - []string{" :", " 4\r\n:", " 0\r\n:"}, - ) - test.That(t, motorDep.Stop(ctx, nil), test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor position testing", func(t *testing.T) { - // Check at 4.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{"RPA"}, - []string{" 51200\r\n:"}, - ) - pos, err := motorDep.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 4.0) - waitTx(t, resChan) - }) - - t.Run("motor GoFor with positive rpm and positive revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=40960", - "SCA", - "TEA", - }, - []string{ - " 0\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=92160", - "SCA", - "TEA", - }, - []string{ - " 51200\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=99840", - "SCA", - "TEA", - }, - []string{ - " 15360\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, 6.6, nil), test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor GoFor with negative rpm and positive revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=-40960", - "SCA", - "TEA", - }, - []string{ - " 0\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=10239", - "SCA", - "TEA", - }, - []string{ - " 51200\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=15360", - "SCA", - "TEA", - }, - []string{ - " 99840\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, 6.6, nil), test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor GoFor with positive rpm and negative revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=-40960", - "SCA", - "TEA", - }, - []string{ - " 0\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=10239", - "SCA", - "TEA", - }, - []string{ - " 51200\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=-69120", - "SCA", - "TEA", - }, - []string{ - " 15360\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, -6.6, nil), test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor GoFor with negative rpm and negative revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=40960", - "SCA", - "TEA", - }, - []string{ - " 0\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=92160", - "SCA", - "TEA", - }, - []string{ - " 51200\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "PTA=1", - "SPA=10666", - "PAA=99840", - "SCA", - "TEA", - }, - []string{ - " 15360\r\n:", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, -6.6, nil), test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor GoFor with 0 RPM", func(t *testing.T) { - test.That(t, motorDep.GoFor(ctx, 0, 1, nil), test.ShouldBeError, motor.NewZeroRPMError()) - }) - - t.Run("motor GoFor with almost 0 RPM", func(t *testing.T) { - test.That(t, motorDep.GoFor(ctx, 0.05, 1, nil), test.ShouldBeError, motor.NewZeroRPMError()) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - }) - - t.Run("motor GoFor after jogging", func(t *testing.T) { - // Test max power - txMu.Lock() - go checkTx(resChan, c, []string{ - "JGA=64000", - "BGA", - }) - test.That(t, motorDep.SetPower(ctx, 1, nil), test.ShouldBeNil) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - - // Test 0.5 of max power - txMu.Lock() - go checkTx(resChan, c, []string{ - "JGA=32000", - "BGA", - }) - test.That(t, motorDep.SetPower(ctx, 0.5, nil), test.ShouldBeNil) - - // Check with position at 0.0 revolutions - txMu.Lock() - go checkRx(resChan, c, - []string{ - "RPA", - "STA", - "PTA=1", - "SPA=10666", - "PAA=40960", - "SCA", - "TEA", - }, - []string{ - " 0\r\n:", - ":", - ":", - ":", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, -3.2, nil), test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor is on testing", func(t *testing.T) { - // Off - StopCode != special cases, TotalError = 0 - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SCA", - "TEA", - }, - []string{ - " 4\r\n:", - " 0\r\n:", - }, - ) - on, powerPct, err := motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.5) - test.That(t, err, test.ShouldBeNil) - - // On - TE != 0 - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SCA", - "TEA", - }, - []string{ - " 4\r\n:", - " 5\r\n:", - }, - ) - on, powerPct, err = motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, 0.5) - test.That(t, err, test.ShouldBeNil) - - // On - StopCodes = sepecial cases - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SCA", - }, - []string{ - " 0\r\n:", - }, - ) - on, powerPct, err = motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, 0.5) - test.That(t, err, test.ShouldBeNil) - - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SCA", - }, - []string{ - " 30\r\n:", - }, - ) - on, powerPct, err = motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, 0.5) - test.That(t, err, test.ShouldBeNil) - - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SCA", - }, - []string{ - " 50\r\n:", - }, - ) - on, powerPct, err = motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, 0.5) - test.That(t, err, test.ShouldBeNil) - - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SCA", - }, - []string{ - " 60\r\n:", - }, - ) - on, powerPct, err = motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, 0.5) - test.That(t, err, test.ShouldBeNil) - - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SCA", - }, - []string{ - " 100\r\n:", - }, - ) - on, powerPct, err = motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, 0.5) - test.That(t, err, test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor zero testing", func(t *testing.T) { - // No offset (and when actually off) - txMu.Lock() - go checkTx(resChan, c, []string{"DPA=0"}) - test.That(t, motorDep.ResetZeroPosition(ctx, 0, nil), test.ShouldBeNil) - - // 3.1 offset (and when actually off) - txMu.Lock() - go checkTx(resChan, c, []string{"DPA=39680"}) - test.That(t, motorDep.ResetZeroPosition(ctx, 3.1, nil), test.ShouldBeNil) - waitTx(t, resChan) - }) - - t.Run("motor do raw command", func(t *testing.T) { - txMu.Lock() - go checkRx(resChan, c, - []string{"testTX"}, - []string{" testRX\r\n:"}, - ) - resp, err := motorDep.DoCommand(ctx, map[string]interface{}{"command": "raw", "raw_input": "testTX"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["return"], test.ShouldEqual, "testRX") - waitTx(t, resChan) - }) - - t.Run("motor do home command", func(t *testing.T) { - txMu.Lock() - go checkRx(resChan, c, - []string{ - "SPA=10666", - "HVA=1066", - "HMA", - "BGA", - "SCA", - "SCA", - "SCA", - "SCA", - "SCA", - "SCA", - "SCA", - "STA", - "SCA", - "TEA", - }, - []string{ - ":", - ":", - ":", - ":", - " 0\r\n:", - " 0\r\n:", - " 0\r\n:", - " 0\r\n:", - " 0\r\n:", - " 0\r\n:", - " 10\r\n:", - ":", - " 4\r\n:", - " 0\r\n:", - }, - ) - resp, err := motorDep.DoCommand(ctx, map[string]interface{}{"command": "home"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldBeNil) - waitTx(t, resChan) - }) -} diff --git a/components/motor/dmc4000/filter_darwin.go b/components/motor/dmc4000/filter_darwin.go deleted file mode 100644 index 6ca5096df81..00000000000 --- a/components/motor/dmc4000/filter_darwin.go +++ /dev/null @@ -1,9 +0,0 @@ -package dmc4000 - -import ( - "go.viam.com/utils/usb" -) - -func init() { - usbFilter = usb.NewSearchFilter("IOUserSerial", "usbserial-") -} diff --git a/components/motor/errors.go b/components/motor/errors.go deleted file mode 100644 index 6820605f360..00000000000 --- a/components/motor/errors.go +++ /dev/null @@ -1,32 +0,0 @@ -package motor - -import "github.com/pkg/errors" - -// NewResetZeroPositionUnsupportedError returns a standard error for when a motor -// is required to support reseting the zero position. -func NewResetZeroPositionUnsupportedError(motorName string) error { - return errors.Errorf("motor with name %s does not support ResetZeroPosition", motorName) -} - -// NewPropertyUnsupportedError returns an error representing the need -// for a motor to support a particular property. -func NewPropertyUnsupportedError(prop Properties, motorName string) error { - return errors.Errorf("motor named %s has wrong support for property %#v", motorName, prop) -} - -// NewZeroRPMError returns an error representing a request to move a motor at -// zero speed (i.e., moving the motor without moving the motor). -func NewZeroRPMError() error { - return errors.New("Cannot move motor at an RPM that is nearly 0") -} - -// NewGoToUnsupportedError returns error when a motor is required to support GoTo feature. -func NewGoToUnsupportedError(motorName string) error { - return errors.Errorf("motor with name %s does not support GoTo", motorName) -} - -// NewControlParametersUnimplementedError returns an error when a control parameters are -// unimplemented in the config being used of a controlledMotor. -func NewControlParametersUnimplementedError() error { - return errors.New("control parameters must be configured to setup a motor with controls") -} diff --git a/components/motor/export_collectors_test.go b/components/motor/export_collectors_test.go deleted file mode 100644 index 516b7e0705e..00000000000 --- a/components/motor/export_collectors_test.go +++ /dev/null @@ -1,8 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package motor - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var ( - NewPositionCollector = newPositionCollector - NewIsPoweredCollector = newIsPoweredCollector -) diff --git a/components/motor/fake/motor.go b/components/motor/fake/motor.go deleted file mode 100644 index 4c928e2d505..00000000000 --- a/components/motor/fake/motor.go +++ /dev/null @@ -1,415 +0,0 @@ -// Package fake implements a fake motor. -package fake - -import ( - "context" - "math" - "sync" - "time" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board" - fakeboard "go.viam.com/rdk/components/board/fake" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/components/encoder/fake" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -var ( - model = resource.DefaultModelFamily.WithModel("fake") - fakeBoardConf = resource.Config{ - Name: "fakeboard", - API: board.API, - ConvertedAttributes: &fakeboard.Config{ - FailNew: false, - }, - } -) - -const defaultMaxRpm = 100 - -// PinConfig defines the mapping of where motor are wired. -type PinConfig struct { - Direction string `json:"dir"` - PWM string `json:"pwm"` -} - -// Config describes the configuration of a motor. -type Config struct { - Pins PinConfig `json:"pins,omitempty"` - BoardName string `json:"board,omitempty"` - MinPowerPct float64 `json:"min_power_pct,omitempty"` - MaxPowerPct float64 `json:"max_power_pct,omitempty"` - PWMFreq uint `json:"pwm_freq,omitempty"` - Encoder string `json:"encoder,omitempty"` - MaxRPM float64 `json:"max_rpm,omitempty"` - TicksPerRotation int `json:"ticks_per_rotation,omitempty"` - DirectionFlip bool `json:"direction_flip,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - if cfg.BoardName != "" { - deps = append(deps, cfg.BoardName) - } - if cfg.Encoder != "" { - if cfg.TicksPerRotation <= 0 { - return nil, resource.NewConfigValidationError(path, errors.New("need nonzero TicksPerRotation for encoded motor")) - } - deps = append(deps, cfg.Encoder) - } - return deps, nil -} - -func init() { - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *Config]{ - Constructor: NewMotor, - }) -} - -// A Motor allows setting and reading a set power percentage and -// direction. -type Motor struct { - resource.Named - resource.TriviallyCloseable - - mu sync.Mutex - powerPct float64 - Board string - PWM board.GPIOPin - PositionReporting bool - Encoder fake.Encoder - MaxRPM float64 - DirFlip bool - TicksPerRotation int - - OpMgr *operation.SingleOperationManager - Logger logging.Logger -} - -// NewMotor creates a new fake motor. -func NewMotor(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (motor.Motor, error) { - m := &Motor{ - Named: conf.ResourceName().AsNamed(), - Logger: logger, - OpMgr: operation.NewSingleOperationManager(), - } - if err := m.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return m, nil -} - -// Reconfigure atomically reconfigures this motor in place based on the new config. -func (m *Motor) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - m.mu.Lock() - defer m.mu.Unlock() - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - var b board.Board - if newConf.BoardName != "" { - m.Board = newConf.BoardName - b, err = board.FromDependencies(deps, m.Board) - if err != nil { - return err - } - } else { - m.Logger.CInfo(ctx, "board not provided, using a fake board") - m.Board = "fakeboard" - b, err = fakeboard.NewBoard(ctx, fakeBoardConf, m.Logger) - if err != nil { - return err - } - } - - pwmPin := "1" - if newConf.Pins.PWM != "" { - pwmPin = newConf.Pins.PWM - } - m.PWM, err = b.GPIOPinByName(pwmPin) - if err != nil { - return err - } - if err = m.PWM.SetPWMFreq(ctx, newConf.PWMFreq, nil); err != nil { - return err - } - - m.MaxRPM = newConf.MaxRPM - - if m.MaxRPM == 0 { - m.Logger.CInfof(ctx, "Max RPM not provided to a fake motor, defaulting to %v", defaultMaxRpm) - m.MaxRPM = defaultMaxRpm - } - - if newConf.Encoder != "" { - m.TicksPerRotation = newConf.TicksPerRotation - - e, err := encoder.FromDependencies(deps, newConf.Encoder) - if err != nil { - return err - } - fakeEncoder, ok := e.(fake.Encoder) - if !ok { - return resource.TypeError[fake.Encoder](e) - } - m.Encoder = fakeEncoder - m.PositionReporting = true - } else { - m.PositionReporting = false - } - m.DirFlip = false - if newConf.DirectionFlip { - m.DirFlip = true - } - return nil -} - -// Position returns motor position in rotations. -func (m *Motor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Encoder == nil { - return 0, errors.New("encoder is not defined") - } - - ticks, _, err := m.Encoder.Position(ctx, encoder.PositionTypeUnspecified, extra) - if err != nil { - return 0, err - } - - if m.TicksPerRotation == 0 { - return 0, errors.New("need nonzero TicksPerRotation for motor") - } - - return ticks / float64(m.TicksPerRotation), nil -} - -// Properties returns the status of whether the motor supports certain optional properties. -func (m *Motor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: m.PositionReporting, - }, nil -} - -// SetPower sets the given power percentage. -func (m *Motor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - m.mu.Lock() - defer m.mu.Unlock() - - m.OpMgr.CancelRunning(ctx) - m.Logger.CDebugf(ctx, "Motor SetPower %f", powerPct) - m.setPowerPct(powerPct) - - if m.Encoder != nil { - if m.TicksPerRotation <= 0 { - return errors.New("need positive nonzero TicksPerRotation") - } - - newSpeed := (m.MaxRPM * m.powerPct) * float64(m.TicksPerRotation) - err := m.Encoder.SetSpeed(ctx, newSpeed) - if err != nil { - return err - } - } - return nil -} - -func (m *Motor) setPowerPct(powerPct float64) { - m.powerPct = powerPct -} - -// PowerPct returns the set power percentage. -func (m *Motor) PowerPct() float64 { - m.mu.Lock() - defer m.mu.Unlock() - if m.DirFlip { - m.powerPct *= -1 - } - return m.powerPct -} - -// Direction returns the set direction. -func (m *Motor) Direction() int { - m.mu.Lock() - defer m.mu.Unlock() - switch { - case m.powerPct > 0: - return 1 - case m.powerPct < 0: - return -1 - } - return 0 -} - -// If revolutions is 0, the returned wait duration will be 0 representing that -// the motor should run indefinitely. -func goForMath(maxRPM, rpm, revolutions float64) (float64, time.Duration, float64) { - // need to do this so time is reasonable - if rpm > maxRPM { - rpm = maxRPM - } else if rpm < -1*maxRPM { - rpm = -1 * maxRPM - } - if rpm == 0 { - return 0, 0, revolutions / math.Abs(revolutions) - } - - if revolutions == 0 { - powerPct := rpm / maxRPM - return powerPct, 0, 1 - } - - dir := rpm * revolutions / math.Abs(revolutions*rpm) - powerPct := math.Abs(rpm) / maxRPM * dir - waitDur := time.Duration(math.Abs(revolutions/rpm)*60*1000) * time.Millisecond - return powerPct, waitDur, dir -} - -// GoFor sets the given direction and an arbitrary power percentage. -// If rpm is 0, the motor should immediately move to the final position. -func (m *Motor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - switch speed := math.Abs(rpm); { - case speed < 0.1: - m.Logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - case m.MaxRPM > 0 && speed > m.MaxRPM-0.1: - m.Logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.MaxRPM) - default: - } - - powerPct, waitDur, dir := goForMath(m.MaxRPM, rpm, revolutions) - - var finalPos float64 - if m.Encoder != nil { - curPos, err := m.Position(ctx, nil) - if err != nil { - return err - } - finalPos = curPos + dir*math.Abs(revolutions) - } - - err := m.SetPower(ctx, powerPct, nil) - if err != nil { - return err - } - - if revolutions == 0 { - return nil - } - - if m.OpMgr.NewTimedWaitOp(ctx, waitDur) { - err = m.Stop(ctx, nil) - if err != nil { - return err - } - - if m.Encoder != nil { - return m.Encoder.SetPosition(ctx, int64(finalPos*float64(m.TicksPerRotation))) - } - } - return nil -} - -// GoTo sets the given direction and an arbitrary power percentage for now. -func (m *Motor) GoTo(ctx context.Context, rpm, pos float64, extra map[string]interface{}) error { - if m.Encoder == nil { - return errors.New("encoder is not defined") - } - - switch speed := math.Abs(rpm); { - case speed < 0.1: - m.Logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - case m.MaxRPM > 0 && speed > m.MaxRPM-0.1: - m.Logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.MaxRPM) - default: - } - - curPos, err := m.Position(ctx, nil) - if err != nil { - return err - } - if curPos == pos { - return nil - } - - revolutions := pos - curPos - - powerPct, waitDur, _ := goForMath(m.MaxRPM, math.Abs(rpm), revolutions) - - err = m.SetPower(ctx, powerPct, nil) - if err != nil { - return err - } - - if revolutions == 0 { - return nil - } - - if m.OpMgr.NewTimedWaitOp(ctx, waitDur) { - err = m.Stop(ctx, nil) - if err != nil { - return err - } - - return m.Encoder.SetPosition(ctx, int64(pos*float64(m.TicksPerRotation))) - } - - return nil -} - -// Stop has the motor pretend to be off. -func (m *Motor) Stop(ctx context.Context, extra map[string]interface{}) error { - m.mu.Lock() - defer m.mu.Unlock() - - m.Logger.CDebug(ctx, "Motor Stopped") - m.setPowerPct(0.0) - if m.Encoder != nil { - err := m.Encoder.SetSpeed(ctx, 0.0) - if err != nil { - return errors.Wrapf(err, "error in Stop from motor (%s)", m.Name()) - } - } - return nil -} - -// ResetZeroPosition resets the zero position. -func (m *Motor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - if m.Encoder == nil { - return errors.New("encoder is not defined") - } - - if m.TicksPerRotation == 0 { - return errors.New("need nonzero TicksPerRotation for motor") - } - - err := m.Encoder.SetPosition(ctx, int64(-1*offset)) - if err != nil { - return errors.Wrapf(err, "error in ResetZeroPosition from motor (%s)", m.Name()) - } - - return nil -} - -// IsPowered returns if the motor is pretending to be on or not, and its power level. -func (m *Motor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - m.mu.Lock() - defer m.mu.Unlock() - return math.Abs(m.powerPct) >= 0.005, m.powerPct, nil -} - -// IsMoving returns if the motor is pretending to be moving or not. -func (m *Motor) IsMoving(ctx context.Context) (bool, error) { - m.mu.Lock() - defer m.mu.Unlock() - return math.Abs(m.powerPct) >= 0.005, nil -} diff --git a/components/motor/fake/motor_test.go b/components/motor/fake/motor_test.go deleted file mode 100644 index f52e4f0fa3b..00000000000 --- a/components/motor/fake/motor_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package fake - -import ( - "context" - "fmt" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/encoder/fake" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -func TestMotorInit(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - enc, err := fake.NewEncoder(context.Background(), resource.Config{ - ConvertedAttributes: &fake.Config{}, - }, logger) - test.That(t, err, test.ShouldBeNil) - m := &Motor{ - Encoder: enc.(fake.Encoder), - Logger: logger, - PositionReporting: true, - MaxRPM: 60, - TicksPerRotation: 1, - OpMgr: operation.NewSingleOperationManager(), - } - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - - properties, err := m.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeTrue) -} - -func TestGoFor(t *testing.T) { - logger, obs := logging.NewObservedTestLogger(t) - ctx := context.Background() - - enc, err := fake.NewEncoder(context.Background(), resource.Config{ - ConvertedAttributes: &fake.Config{}, - }, logger) - test.That(t, err, test.ShouldBeNil) - m := &Motor{ - Encoder: enc.(fake.Encoder), - Logger: logger, - PositionReporting: true, - MaxRPM: 60, - TicksPerRotation: 1, - OpMgr: operation.NewSingleOperationManager(), - } - - err = m.GoFor(ctx, 0, 1, nil) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - test.That(t, err, test.ShouldBeError, motor.NewZeroRPMError()) - - err = m.GoFor(ctx, 60, 1, nil) - test.That(t, err, test.ShouldBeNil) - allObs = obs.All() - latestLoggedEntry = allObs[1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - pos, err := m.Position(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, pos, test.ShouldEqual, 1) - }) -} - -func TestGoTo(t *testing.T) { - logger, obs := logging.NewObservedTestLogger(t) - ctx := context.Background() - - enc, err := fake.NewEncoder(context.Background(), resource.Config{ - ConvertedAttributes: &fake.Config{}, - }, logger) - test.That(t, err, test.ShouldBeNil) - m := &Motor{ - Encoder: enc.(fake.Encoder), - Logger: logger, - PositionReporting: true, - MaxRPM: 60, - TicksPerRotation: 1, - OpMgr: operation.NewSingleOperationManager(), - } - - err = m.GoTo(ctx, 60, 1, nil) - test.That(t, err, test.ShouldBeNil) - allObs := obs.All() - latestLoggedEntry := allObs[0] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - - err = m.GoTo(ctx, 0, 1, nil) - test.That(t, err, test.ShouldBeNil) - allObs = obs.All() - latestLoggedEntry = allObs[3] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - pos, err := m.Position(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, pos, test.ShouldEqual, 1) - }) -} - -func TestResetZeroPosition(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - enc, err := fake.NewEncoder(context.Background(), resource.Config{ - ConvertedAttributes: &fake.Config{}, - }, logger) - test.That(t, err, test.ShouldBeNil) - m := &Motor{ - Encoder: enc.(fake.Encoder), - Logger: logger, - PositionReporting: true, - MaxRPM: 60, - TicksPerRotation: 1, - OpMgr: operation.NewSingleOperationManager(), - } - - err = m.ResetZeroPosition(ctx, 0, nil) - test.That(t, err, test.ShouldBeNil) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) -} - -func TestPower(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - enc, err := fake.NewEncoder(context.Background(), resource.Config{ - ConvertedAttributes: &fake.Config{}, - }, logger) - test.That(t, err, test.ShouldBeNil) - m := &Motor{ - Encoder: enc.(fake.Encoder), - Logger: logger, - PositionReporting: true, - MaxRPM: 60, - TicksPerRotation: 1, - OpMgr: operation.NewSingleOperationManager(), - } - - err = m.SetPower(ctx, 1.0, nil) - test.That(t, err, test.ShouldBeNil) - - powerPct := m.PowerPct() - test.That(t, powerPct, test.ShouldEqual, 1.0) - - dir := m.Direction() - test.That(t, dir, test.ShouldEqual, 1) - - isPowered, powerPctReported, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, isPowered, test.ShouldEqual, true) - test.That(t, powerPctReported, test.ShouldEqual, powerPct) - - isMoving, err := m.IsMoving(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, isMoving, test.ShouldEqual, true) - - err = m.Stop(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - powerPct = m.PowerPct() - test.That(t, powerPct, test.ShouldEqual, 0.0) -} diff --git a/components/motor/gpio/basic.go b/components/motor/gpio/basic.go deleted file mode 100644 index 0e89dc64561..00000000000 --- a/components/motor/gpio/basic.go +++ /dev/null @@ -1,346 +0,0 @@ -package gpio - -import ( - "context" - "math" - "sync" - - "github.com/pkg/errors" - "go.uber.org/multierr" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -// NewMotor constructs a new GPIO based motor on the given board using the -// given configuration. -func NewMotor(b board.Board, mc Config, name resource.Name, logger logging.Logger) (motor.Motor, error) { - if mc.MaxPowerPct == 0 { - mc.MaxPowerPct = 1.0 - } - if mc.MaxPowerPct < 0.06 || mc.MaxPowerPct > 1.0 { - return nil, errors.New("max_power_pct must be between 0.06 and 1.0") - } - - motorType, err := mc.Pins.MotorType("") - if err != nil { - return nil, err - } else if motorType == AB { - logger.Warnf( - "motor %s has been configured with A and B pins, but no PWM. Make sure this is intentional", - name.Name, - ) - } - - if mc.MinPowerPct < 0 { - mc.MinPowerPct = 0 - } else if mc.MinPowerPct > 1.0 { - mc.MinPowerPct = 1.0 - } - - if mc.PWMFreq == 0 { - mc.PWMFreq = 800 - } - - m := &Motor{ - Named: name.AsNamed(), - Board: b, - on: false, - pwmFreq: mc.PWMFreq, - minPowerPct: mc.MinPowerPct, - maxPowerPct: mc.MaxPowerPct, - maxRPM: mc.MaxRPM, - dirFlip: mc.DirectionFlip, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - motorType: motorType, - } - - switch motorType { - case ABPwm, AB: - a, err := b.GPIOPinByName(mc.Pins.A) - if err != nil { - return nil, err - } - m.A = a - - b, err := b.GPIOPinByName(mc.Pins.B) - if err != nil { - return nil, err - } - m.B = b - case DirectionPwm: - direction, err := b.GPIOPinByName(mc.Pins.Direction) - if err != nil { - return nil, err - } - m.Direction = direction - } - - if (motorType == ABPwm) || (motorType == DirectionPwm) { - pwm, err := b.GPIOPinByName(mc.Pins.PWM) - if err != nil { - return nil, err - } - m.PWM = pwm - } - - if mc.Pins.EnablePinHigh != "" { - enablePinHigh, err := b.GPIOPinByName(mc.Pins.EnablePinHigh) - if err != nil { - return nil, err - } - m.EnablePinHigh = enablePinHigh - } - if mc.Pins.EnablePinLow != "" { - enablePinLow, err := b.GPIOPinByName(mc.Pins.EnablePinLow) - if err != nil { - return nil, err - } - m.EnablePinLow = enablePinLow - } - - return m, nil -} - -// A Motor is a GPIO based Motor that resides on a GPIO Board. -type Motor struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - - mu sync.Mutex - opMgr *operation.SingleOperationManager - logger logging.Logger - // config - Board board.Board - A, B, Direction, PWM, En board.GPIOPin - EnablePinLow board.GPIOPin - EnablePinHigh board.GPIOPin - pwmFreq uint - minPowerPct float64 - maxPowerPct float64 - maxRPM float64 - dirFlip bool - // state - on bool - powerPct float64 - motorType MotorType -} - -// Position always returns 0. -func (m *Motor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil -} - -// Properties returns the status of whether the motor supports certain optional properties. -func (m *Motor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: false, - }, nil -} - -// turnOff turns down the motor entirely by setting all the pins accordingly. -func (m *Motor) turnOff(ctx context.Context, extra map[string]interface{}) error { - var errs error - m.powerPct = 0.0 - m.on = false - if m.EnablePinLow != nil { - enLowErr := errors.Wrap(m.EnablePinLow.Set(ctx, true, extra), "unable to disable low signal") - errs = multierr.Combine(errs, enLowErr) - } - if m.EnablePinHigh != nil { - enHighErr := errors.Wrap(m.EnablePinHigh.Set(ctx, false, extra), "unable to disable high signal") - errs = multierr.Combine(errs, enHighErr) - } - - if m.A != nil && m.B != nil { - aErr := errors.Wrap(m.A.Set(ctx, false, extra), "could not set A pin to low") - bErr := errors.Wrap(m.B.Set(ctx, false, extra), "could not set B pin to low") - errs = multierr.Combine(errs, aErr, bErr) - } - - if m.PWM != nil { - pwmErr := errors.Wrap(m.PWM.Set(ctx, false, extra), "could not set PWM pin to low") - errs = multierr.Combine(errs, pwmErr) - } - return errs -} - -// setPWM sets the associated pins (as discovered) and sets PWM to the given power percentage. -// Anything calling setPWM MUST lock the motor's mutex prior. -func (m *Motor) setPWM(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - var errs error - powerPct = fixPowerPct(powerPct, m.maxPowerPct) - if math.Abs(powerPct) < m.minPowerPct && math.Abs(powerPct) > 0 { - powerPct = sign(powerPct) * m.minPowerPct - } - m.powerPct = powerPct - - m.on = true - - if m.EnablePinLow != nil { - errs = multierr.Combine(errs, m.EnablePinLow.Set(ctx, false, extra)) - } - if m.EnablePinHigh != nil { - errs = multierr.Combine(errs, m.EnablePinHigh.Set(ctx, true, extra)) - } - - var pwmPin board.GPIOPin - - switch m.motorType { - case ABPwm, DirectionPwm: - if math.Abs(powerPct) <= 0.001 { - return m.turnOff(ctx, extra) - } - pwmPin = m.PWM - case AB: - switch { - case powerPct >= 0.001: - pwmPin = m.B - if m.dirFlip { - pwmPin = m.A - } - powerPct = 1.0 - math.Abs(powerPct) // Other pin is always high, so only when PWM is LOW are we driving. Thus, we invert here. - case powerPct <= -0.001: - pwmPin = m.A - if m.dirFlip { - pwmPin = m.B - } - powerPct = 1.0 - math.Abs(powerPct) // Other pin is always high, so only when PWM is LOW are we driving. Thus, we invert here. - default: - return m.turnOff(ctx, extra) - } - } - - powerPct = math.Max(math.Abs(powerPct), m.minPowerPct) - return multierr.Combine( - errs, - pwmPin.SetPWMFreq(ctx, m.pwmFreq, extra), - pwmPin.SetPWM(ctx, powerPct, extra), - ) -} - -// SetPower instructs the motor to operate at an rpm, where the sign of the rpm -// indicates direction. -func (m *Motor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - if math.Abs(powerPct) <= 0.01 { - return m.Stop(ctx, extra) - } - // Stop locks/unlocks the mutex as well so in the case that the power ~= 0 - // we want to simply rely on the mutex use in Stop - m.mu.Lock() - defer m.mu.Unlock() - - switch m.motorType { - case DirectionPwm: - x := !math.Signbit(powerPct) - if m.dirFlip { - x = !x - } - return multierr.Combine( - m.Direction.Set(ctx, x, extra), - m.setPWM(ctx, powerPct, extra), - ) - case ABPwm, AB: - a := m.A - b := m.B - if m.dirFlip { - a = m.B - b = m.A - } - return multierr.Combine( - a.Set(ctx, !math.Signbit(powerPct), extra), - b.Set(ctx, math.Signbit(powerPct), extra), - m.setPWM(ctx, powerPct, extra), // Must be last for A/B only drivers - ) - } - - if !math.Signbit(powerPct) { - return m.setPWM(ctx, powerPct, extra) - } - - return errors.New("trying to go backwards but don't have dir or a&b pins") -} - -// GoFor moves an inputted number of revolutions at the given rpm, no encoder is present -// for this so power is determined via a linear relationship with the maxRPM and the distance -// traveled is a time based estimation based on desired RPM. -func (m *Motor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - if m.maxRPM == 0 { - return errors.New("not supported, define max_rpm attribute != 0") - } - - switch speed := math.Abs(rpm); { - case speed < 0.1: - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - case m.maxRPM > 0 && speed > m.maxRPM-0.1: - m.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.maxRPM) - default: - } - - powerPct, waitDur := goForMath(m.maxRPM, rpm, revolutions) - err := m.SetPower(ctx, powerPct, extra) - if err != nil { - return errors.Wrap(err, "error in GoFor") - } - - if revolutions == 0 { - return nil - } - - if m.opMgr.NewTimedWaitOp(ctx, waitDur) { - return m.Stop(ctx, extra) - } - return nil -} - -// IsPowered returns if the motor is currently on or off. -func (m *Motor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - m.mu.Lock() - defer m.mu.Unlock() - return m.on, m.powerPct, nil -} - -// Stop turns the power to the motor off immediately, without any gradual step down, by setting the appropriate pins to low states. -func (m *Motor) Stop(ctx context.Context, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - m.mu.Lock() - defer m.mu.Unlock() - return m.setPWM(ctx, 0, extra) -} - -// IsMoving returns if the motor is currently on or off. -func (m *Motor) IsMoving(ctx context.Context) (bool, error) { - m.mu.Lock() - defer m.mu.Unlock() - return m.on, nil -} - -// GoTo is not supported. -func (m *Motor) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - return motor.NewGoToUnsupportedError(m.Name().ShortName()) -} - -// ResetZeroPosition is not supported. -func (m *Motor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - return motor.NewResetZeroPositionUnsupportedError(m.Name().ShortName()) -} - -// DirectionMoving returns the direction we are currently moving in, with 1 representing -// forward and -1 representing backwards. -func (m *Motor) DirectionMoving() int64 { - move, powerPct, err := m.IsPowered(context.Background(), nil) - if move { - return int64(sign(powerPct)) - } - if err != nil { - m.logger.Error(err) - } - return 0 -} diff --git a/components/motor/gpio/basic_test.go b/components/motor/gpio/basic_test.go deleted file mode 100644 index d523cde0765..00000000000 --- a/components/motor/gpio/basic_test.go +++ /dev/null @@ -1,541 +0,0 @@ -package gpio - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board" - fakeboard "go.viam.com/rdk/components/board/fake" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -const maxRPM = 100 - -// Test the A/B/PWM style IO. -func TestMotorABPWM(t *testing.T) { - ctx := context.Background() - b := &fakeboard.Board{GPIOPins: map[string]*fakeboard.GPIOPin{}} - logger, obs := logging.NewObservedTestLogger(t) - - mc := resource.Config{ - Name: "abc", - } - - m, err := NewMotor(b, Config{ - Pins: PinConfig{A: "1", B: "2", PWM: "3"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("motor (A/B/PWM) initialization errors", func(t *testing.T) { - m, err := NewMotor(b, Config{ - Pins: PinConfig{A: "1", B: "2", PWM: "3"}, MaxPowerPct: 100, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, m, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errors.New("max_power_pct must be between 0.06 and 1.0")) - - m, err = NewMotor(b, Config{ - Pins: PinConfig{A: "1", PWM: "3"}, MaxPowerPct: 0.07, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, m, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationError("", getPinConfigErrorMessage(aNotB))) - - m, err = NewMotor(b, Config{ - Pins: PinConfig{B: "1", PWM: "3"}, MaxPowerPct: 0.07, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, m, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationError("", getPinConfigErrorMessage(bNotA))) - }) - - t.Run("motor (A/B/PWM) Off testing", func(t *testing.T) { - test.That(t, m.Stop(ctx, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, byte(0)) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0) - }) - - t.Run("motor (A/B/PWM) SetPower testing", func(t *testing.T) { - gpioMotor, ok := m.(*Motor) - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, gpioMotor.setPWM(ctx, 0.23, nil), test.ShouldBeNil) - on, powerPct, err := gpioMotor.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 0.23) - - test.That(t, m.SetPower(ctx, 0.43, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .43) - - on, powerPct, err = m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 0.43) - - test.That(t, m.SetPower(ctx, -0.44, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .44) - - on, powerPct, err = m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, -0.44) - - test.That(t, m.SetPower(ctx, 0, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "3").Get(context.Background()), test.ShouldEqual, false) - - on, powerPct, err = m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0) - }) - - t.Run("motor (A/B/PWM) GoFor testing", func(t *testing.T) { - test.That(t, m.GoFor(ctx, 50, 0, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .5) - - test.That(t, m.GoFor(ctx, -50, 0, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .5) - - test.That(t, m.GoFor(ctx, 0, 1, nil), test.ShouldBeError, motor.NewZeroRPMError()) - test.That(t, m.Stop(context.Background(), nil), test.ShouldBeNil) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - test.That(t, m.GoFor(ctx, 100, 1, nil), test.ShouldBeNil) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - }) - - t.Run("motor (A/B/PWM) Power testing", func(t *testing.T) { - test.That(t, m.SetPower(ctx, 0.45, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .45) - }) - - t.Run("motor (A/B/PWM) Position testing", func(t *testing.T) { - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0.0) - - properties, err := m.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - }) - - t.Run("motor (A/B/PWM) Set PWM frequency testing", func(t *testing.T) { - test.That(t, mustGetGPIOPinByName(b, "3").PWMFreq(context.Background()), test.ShouldEqual, 4000) - mustGetGPIOPinByName(b, "3").SetPWMFreq(ctx, 8000) - test.That(t, mustGetGPIOPinByName(b, "3").PWMFreq(context.Background()), test.ShouldEqual, 8000) - }) -} - -// Test the DIR/PWM style IO. -func TestMotorDirPWM(t *testing.T) { - ctx := context.Background() - b := &fakeboard.Board{GPIOPins: map[string]*fakeboard.GPIOPin{}} - logger := logging.NewTestLogger(t) - - mc := resource.Config{ - Name: "fake_motor", - } - - t.Run("motor (DIR/PWM) initialization errors", func(t *testing.T) { - m, err := NewMotor(b, Config{Pins: PinConfig{Direction: "1", EnablePinLow: "2", PWM: "3"}, PWMFreq: 4000}, - mc.ResourceName(), logger) - - test.That(t, err, test.ShouldBeNil) - test.That(t, m.GoFor(ctx, 50, 10, nil), test.ShouldBeError, errors.New("not supported, define max_rpm attribute != 0")) - - m, err = NewMotor(b, Config{Pins: PinConfig{Direction: "1", EnablePinLow: "2"}, PWMFreq: 4000}, - mc.ResourceName(), logger) - test.That(t, m, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationError("", getPinConfigErrorMessage(dirNotPWM))) - - _, err = NewMotor( - b, - Config{Pins: PinConfig{Direction: "1", EnablePinLow: "2", PWM: "3"}, MaxPowerPct: 100, PWMFreq: 4000}, - mc.ResourceName(), logger, - ) - test.That(t, err, test.ShouldBeError, errors.New("max_power_pct must be between 0.06 and 1.0")) - }) - - m, err := NewMotor(b, Config{ - Pins: PinConfig{Direction: "1", EnablePinLow: "2", PWM: "3"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("motor (DIR/PWM) Off testing", func(t *testing.T) { - test.That(t, m.Stop(ctx, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "1").PWM(context.Background()), test.ShouldEqual, 0) - test.That(t, mustGetGPIOPinByName(b, "2").PWM(context.Background()), test.ShouldEqual, 0) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0) - }) - - t.Run("motor (DIR/PWM) GoFor testing", func(t *testing.T) { - test.That(t, m.GoFor(ctx, 50, 0, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .5) - - test.That(t, m.GoFor(ctx, -50, 0, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .5) - - test.That(t, m.Stop(context.Background(), nil), test.ShouldBeNil) - }) - - t.Run("motor (DIR/PWM) Power testing", func(t *testing.T) { - test.That(t, m.SetPower(ctx, 0.45, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, .45) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeTrue) - test.That(t, powerPct, test.ShouldEqual, 0.45) - }) - - t.Run("motor (DIR/PWM) Position testing", func(t *testing.T) { - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0.0) - - properties, err := m.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - }) - - t.Run("motor (DIR/PWM) Set PWM frequency testing", func(t *testing.T) { - test.That(t, mustGetGPIOPinByName(b, "3").PWMFreq(context.Background()), test.ShouldEqual, 4000) - mustGetGPIOPinByName(b, "3").SetPWMFreq(ctx, 8000) - test.That(t, mustGetGPIOPinByName(b, "3").PWMFreq(context.Background()), test.ShouldEqual, 8000) - }) -} - -// Test the A/B only style IO. -func TestMotorAB(t *testing.T) { - ctx := context.Background() - b := &fakeboard.Board{GPIOPins: map[string]*fakeboard.GPIOPin{}} - logger := logging.NewTestLogger(t) - mc := resource.Config{ - Name: "fake_motor", - } - - t.Run("motor (A/B) initialization errors", func(t *testing.T) { - m, err := NewMotor(b, Config{ - Pins: PinConfig{A: "1", EnablePinLow: "3"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, m, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationError("", getPinConfigErrorMessage(aNotB))) - }) - - m, err := NewMotor(b, Config{ - Pins: PinConfig{A: "1", B: "2", EnablePinLow: "3"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("motor (A/B) On testing", func(t *testing.T) { - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0) - }) - - t.Run("motor (A/B) Off testing", func(t *testing.T) { - test.That(t, m.SetPower(ctx, .5, nil), test.ShouldBeNil) - test.That(t, m.Stop(ctx, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "3").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "1").PWM(context.Background()), test.ShouldEqual, 0) - test.That(t, mustGetGPIOPinByName(b, "2").PWM(context.Background()), test.ShouldEqual, 0) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldBeFalse) - test.That(t, powerPct, test.ShouldEqual, 0) - }) - - t.Run("motor (A/B) GoFor testing", func(t *testing.T) { - test.That(t, m.SetPower(ctx, .5, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "1").PWM(context.Background()), test.ShouldEqual, 0) - test.That(t, mustGetGPIOPinByName(b, "2").PWM(context.Background()), test.ShouldEqual, .5) - - test.That(t, m.SetPower(ctx, -.5, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "1").PWM(context.Background()), test.ShouldEqual, .5) - test.That(t, mustGetGPIOPinByName(b, "2").PWM(context.Background()), test.ShouldEqual, 0) - - test.That(t, m.Stop(ctx, nil), test.ShouldBeNil) - }) - - t.Run("motor (A/B) Power testing", func(t *testing.T) { - test.That(t, m.SetPower(ctx, .45, nil), test.ShouldBeNil) - test.That(t, mustGetGPIOPinByName(b, "2").PWM(context.Background()), test.ShouldEqual, .55) - }) - - t.Run("motor (A/B) Position testing", func(t *testing.T) { - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0.0) - - properties, err := m.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeFalse) - }) - - t.Run("motor (A/B) Set PWM frequency testing", func(t *testing.T) { - test.That(t, mustGetGPIOPinByName(b, "2").PWMFreq(context.Background()), test.ShouldEqual, 4000) - mustGetGPIOPinByName(b, "2").SetPWMFreq(ctx, 8000) - test.That(t, mustGetGPIOPinByName(b, "2").PWMFreq(context.Background()), test.ShouldEqual, 8000) - }) -} - -func TestMotorABNoEncoder(t *testing.T) { - ctx := context.Background() - b := &fakeboard.Board{GPIOPins: map[string]*fakeboard.GPIOPin{}} - logger := logging.NewTestLogger(t) - mc := resource.Config{ - Name: "fake_motor", - } - - m, err := NewMotor(b, Config{ - Pins: PinConfig{A: "1", B: "2", EnablePinLow: "3"}, - PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("motor no encoder GoFor testing", func(t *testing.T) { - test.That(t, m.GoFor(ctx, 50, 10, nil), test.ShouldBeError, errors.New("not supported, define max_rpm attribute != 0")) - }) - - t.Run("motor no encoder GoTo testing", func(t *testing.T) { - test.That(t, m.GoTo(ctx, 50, 10, nil), test.ShouldBeError, errors.New("motor with name fake_motor does not support GoTo")) - }) - - t.Run("motor no encoder ResetZeroPosition testing", func(t *testing.T) { - test.That(t, m.ResetZeroPosition(ctx, 5.0001, nil), test.ShouldBeError, - errors.New("motor with name fake_motor does not support ResetZeroPosition")) - }) -} - -func TestGoForInterruptionAB(t *testing.T) { - b := &fakeboard.Board{GPIOPins: map[string]*fakeboard.GPIOPin{}} - logger := logging.NewTestLogger(t) - - mc := resource.Config{ - Name: "abc", - } - - m, err := NewMotor(b, Config{ - Pins: PinConfig{A: "1", B: "2", PWM: "3"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - - _, waitDur := goForMath(maxRPM, -50, 100) - errChan := make(chan error) - startTime := time.Now() - go func() { - goForErr := m.GoFor(context.Background(), -50, 100, nil) - errChan <- goForErr - }() - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - isOn, _, onErr := m.IsPowered(context.Background(), nil) - test.That(tb, isOn, test.ShouldBeTrue) - test.That(tb, onErr, test.ShouldBeNil) - }) - test.That(t, m.Stop(context.Background(), nil), test.ShouldBeNil) - receivedErr := <-errChan - test.That(t, receivedErr, test.ShouldBeNil) - test.That(t, time.Since(startTime), test.ShouldBeLessThan, waitDur) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "2").Get(context.Background()), test.ShouldEqual, false) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, 0) -} - -func TestGoForInterruptionDir(t *testing.T) { - b := &fakeboard.Board{GPIOPins: map[string]*fakeboard.GPIOPin{}} - logger := logging.NewTestLogger(t) - - mc := resource.Config{ - Name: "abc", - } - - m, err := NewMotor(b, Config{ - Pins: PinConfig{Direction: "1", EnablePinLow: "2", PWM: "3"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - - _, waitDur := goForMath(maxRPM, 50, 100) - errChan := make(chan error) - startTime := time.Now() - go func() { - goForErr := m.GoFor(context.Background(), 50, 100, nil) - errChan <- goForErr - }() - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - isOn, _, onErr := m.IsPowered(context.Background(), nil) - test.That(tb, onErr, test.ShouldBeNil) - test.That(tb, isOn, test.ShouldBeTrue) - }) - test.That(t, m.Stop(context.Background(), nil), test.ShouldBeNil) - receivedErr := <-errChan - test.That(t, receivedErr, test.ShouldBeNil) - test.That(t, time.Since(startTime), test.ShouldBeLessThan, waitDur) - test.That(t, mustGetGPIOPinByName(b, "1").Get(context.Background()), test.ShouldEqual, true) - test.That(t, mustGetGPIOPinByName(b, "3").PWM(context.Background()), test.ShouldEqual, 0) -} - -func TestGoForMath(t *testing.T) { - powerPct, waitDur := goForMath(100, 100, 100) - test.That(t, powerPct, test.ShouldEqual, 1) - test.That(t, waitDur, test.ShouldEqual, time.Minute) - - powerPct, waitDur = goForMath(100, -100, 100) - test.That(t, powerPct, test.ShouldEqual, -1) - test.That(t, waitDur, test.ShouldEqual, time.Minute) - - powerPct, waitDur = goForMath(100, -1000, 100) - test.That(t, powerPct, test.ShouldEqual, -1) - test.That(t, waitDur, test.ShouldEqual, time.Minute) - - powerPct, waitDur = goForMath(100, 1000, 200) - test.That(t, powerPct, test.ShouldEqual, 1) - test.That(t, waitDur, test.ShouldEqual, 2*time.Minute) - - powerPct, waitDur = goForMath(100, 1000, 50) - test.That(t, powerPct, test.ShouldEqual, 1) - test.That(t, waitDur, test.ShouldEqual, 30*time.Second) - - powerPct, waitDur = goForMath(200, 100, 50) - test.That(t, powerPct, test.ShouldAlmostEqual, 0.5) - test.That(t, waitDur, test.ShouldEqual, 30*time.Second) - - powerPct, waitDur = goForMath(200, 100, -50) - test.That(t, powerPct, test.ShouldAlmostEqual, -0.5) - test.That(t, waitDur, test.ShouldEqual, 30*time.Second) - - powerPct, waitDur = goForMath(200, 50, 0) - test.That(t, powerPct, test.ShouldEqual, 0.25) - test.That(t, waitDur, test.ShouldEqual, 0) - - powerPct, waitDur = goForMath(200, -50, 0) - test.That(t, powerPct, test.ShouldEqual, -0.25) - test.That(t, waitDur, test.ShouldEqual, 0) -} - -func TestOtherInitializationErrors(t *testing.T) { - b := &fakeboard.Board{GPIOPins: map[string]*fakeboard.GPIOPin{}} - logger := logging.NewTestLogger(t) - - mc := resource.Config{ - Name: "abc", - } - m, err := NewMotor(b, Config{ - Pins: PinConfig{EnablePinLow: "2", PWM: "3"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, m, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationError("", getPinConfigErrorMessage(onlyPWM))) - - m, err = NewMotor(b, Config{ - Pins: PinConfig{EnablePinLow: "2"}, - MaxRPM: maxRPM, PWMFreq: 4000, - }, mc.ResourceName(), logger) - test.That(t, m, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationError("", getPinConfigErrorMessage(noPins))) -} - -func mustGetGPIOPinByName(b board.Board, name string) mustGPIOPin { - pin, err := b.GPIOPinByName(name) - if err != nil { - panic(err) - } - return mustGPIOPin{pin} -} - -type mustGPIOPin struct { - pin board.GPIOPin -} - -func (m mustGPIOPin) Set(ctx context.Context, high bool) { - if err := m.pin.Set(ctx, high, nil); err != nil { - panic(err) - } -} - -func (m mustGPIOPin) Get(ctx context.Context) bool { - ret, err := m.pin.Get(ctx, nil) - if err != nil { - panic(err) - } - return ret -} - -func (m mustGPIOPin) PWM(ctx context.Context) float64 { - ret, err := m.pin.PWM(ctx, nil) - if err != nil { - panic(err) - } - return ret -} - -func (m mustGPIOPin) SetPWM(ctx context.Context, dutyCyclePct float64) { - if err := m.pin.SetPWM(ctx, dutyCyclePct, nil); err != nil { - panic(err) - } -} - -func (m mustGPIOPin) PWMFreq(ctx context.Context) uint { - ret, err := m.pin.PWMFreq(ctx, nil) - if err != nil { - panic(err) - } - return ret -} - -func (m mustGPIOPin) SetPWMFreq(ctx context.Context, freqHz uint) { - err := m.pin.SetPWMFreq(ctx, freqHz, nil) - if err != nil { - panic(err) - } -} diff --git a/components/motor/gpio/loop.go b/components/motor/gpio/loop.go deleted file mode 100644 index 24159453950..00000000000 --- a/components/motor/gpio/loop.go +++ /dev/null @@ -1,328 +0,0 @@ -package gpio - -import ( - "context" - "math" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/control" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - rdkutils "go.viam.com/rdk/utils" -) - -// SetState sets the state of the motor for the built-in control loop. -func (cm *controlledMotor) SetState(ctx context.Context, state []*control.Signal) error { - if cm.loop != nil && !cm.loop.Running() { - return nil - } - power := state[0].GetSignalValueAt(0) - return cm.real.SetPower(ctx, power, nil) -} - -// State gets the state of the motor for the built-in control loop. -func (cm *controlledMotor) State(ctx context.Context) ([]float64, error) { - ticks, _, err := cm.enc.Position(ctx, encoder.PositionTypeTicks, nil) - cm.mu.RLock() - defer cm.mu.RUnlock() - pos := ticks + cm.offsetInTicks - return []float64{pos}, err -} - -// updateControlBlockPosVel updates the trap profile and the constant set point for position and velocity control. -func (cm *controlledMotor) updateControlBlock(ctx context.Context, setPoint, maxVel float64) error { - // Update the Trapezoidal Velocity Profile block with the given maxVel for velocity control - dependsOn := []string{cm.blockNames[control.BlockNameConstant][0], cm.blockNames[control.BlockNameEndpoint][0]} - if err := control.UpdateTrapzBlock(ctx, cm.blockNames[control.BlockNameTrapezoidal][0], maxVel, dependsOn, cm.loop); err != nil { - return err - } - - // Update the Constant block with the given setPoint for position control - if err := control.UpdateConstantBlock(ctx, cm.blockNames[control.BlockNameConstant][0], setPoint, cm.loop); err != nil { - return err - } - - return nil -} - -func (cm *controlledMotor) setupControlLoop(conf *Config) error { - // set the necessary options for an encoded motor - options := control.Options{ - PositionControlUsingTrapz: true, - LoopFrequency: 100.0, - } - - // convert the motor config ControlParameters to the control.PIDConfig structure for use in setup_control.go - convertedControlParams := []control.PIDConfig{{ - Type: "", - P: conf.ControlParameters.P, - I: conf.ControlParameters.I, - D: conf.ControlParameters.D, - }} - - // auto tune motor if all ControlParameters are 0 - // since there's only one set of PID values for a motor, they will always be at convertedControlParams[0] - if convertedControlParams[0].NeedsAutoTuning() { - options.NeedsAutoTuning = true - } - - pl, err := control.SetupPIDControlConfig(convertedControlParams, cm.Name().ShortName(), options, cm, cm.logger) - if err != nil { - return err - } - - cm.controlLoopConfig = pl.ControlConf - cm.loop = pl.ControlLoop - cm.blockNames = pl.BlockNames - - return nil -} - -func (cm *controlledMotor) startControlLoop() error { - loop, err := control.NewLoop(cm.logger, cm.controlLoopConfig, cm) - if err != nil { - return err - } - if err := loop.Start(); err != nil { - return err - } - cm.loop = loop - - return nil -} - -func setupMotorWithControls( - _ context.Context, - m *Motor, - enc encoder.Encoder, - cfg resource.Config, - logger logging.Logger, -) (motor.Motor, error) { - conf, err := resource.NativeConfig[*Config](cfg) - if err != nil { - return nil, err - } - - tpr := float64(conf.TicksPerRotation) - if tpr == 0 { - tpr = 1.0 - } - - cm := &controlledMotor{ - Named: cfg.ResourceName().AsNamed(), - logger: logger, - opMgr: operation.NewSingleOperationManager(), - ticksPerRotation: tpr, - real: m, - enc: enc, - } - - // setup control loop - if conf.ControlParameters == nil { - return nil, motor.NewControlParametersUnimplementedError() - } - if err := cm.setupControlLoop(conf); err != nil { - return nil, err - } - - return cm, nil -} - -type controlledMotor struct { - resource.Named - resource.AlwaysRebuild - logger logging.Logger - opMgr *operation.SingleOperationManager - activeBackgroundWorkers sync.WaitGroup - - offsetInTicks float64 - ticksPerRotation float64 - - mu sync.RWMutex - real *Motor - enc encoder.Encoder - - controlLoopConfig control.Config - blockNames map[string][]string - loop *control.Loop -} - -// SetPower sets the percentage of power the motor should employ between -1 and 1. -// Negative power implies a backward directional rotational. -func (cm *controlledMotor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - cm.opMgr.CancelRunning(ctx) - if cm.loop != nil { - cm.loop.Pause() - } - return cm.real.SetPower(ctx, powerPct, nil) -} - -// IsPowered returns whether or not the motor is currently on, and the percent power (between 0 -// and 1, if the motor is off then the percent power will be 0). -func (cm *controlledMotor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return cm.real.IsPowered(ctx, extra) -} - -// IsMoving returns if the motor is moving or not. -func (cm *controlledMotor) IsMoving(ctx context.Context) (bool, error) { - return cm.real.IsMoving(ctx) -} - -// Stop stops rpmMonitor and stops the real motor. -func (cm *controlledMotor) Stop(ctx context.Context, extra map[string]interface{}) error { - // after the motor is created, Stop is called, but if the PID controller - // is auto-tuning, the loop needs to keep running - if cm.loop != nil && !cm.loop.GetTuning(ctx) { - cm.loop.Pause() - } - return cm.real.Stop(ctx, nil) -} - -// Close cleanly shuts down the motor. -func (cm *controlledMotor) Close(ctx context.Context) error { - if err := cm.Stop(ctx, nil); err != nil { - return err - } - if cm.loop != nil { - cm.loop.Stop() - cm.loop = nil - } - cm.activeBackgroundWorkers.Wait() - return nil -} - -// Properties returns whether or not the motor supports certain optional properties. -func (cm *controlledMotor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil -} - -// Position reports the position of the motor based on its encoder. If it's not supported, the returned -// data is undefined. The unit returned is the number of revolutions which is intended to be fed -// back into calls of GoFor. -func (cm *controlledMotor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - ticks, _, err := cm.enc.Position(ctx, encoder.PositionTypeTicks, extra) - if err != nil { - return 0, err - } - - // offsetTicks in Rotation can be changed by ResetPosition - cm.mu.RLock() - defer cm.mu.RUnlock() - return (ticks + cm.offsetInTicks) / cm.ticksPerRotation, nil -} - -// ResetZeroPosition sets the current position (+/- offset) to be the new zero (home) position. -func (cm *controlledMotor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - // Stop the motor if resetting position - if err := cm.Stop(ctx, extra); err != nil { - return err - } - if err := cm.enc.ResetPosition(ctx, extra); err != nil { - return err - } - - cm.mu.Lock() - defer cm.mu.Unlock() - cm.offsetInTicks = -1 * offset * cm.ticksPerRotation - return nil -} - -// GoTo instructs the motor to go to a specific position (provided in revolutions from home/zero), -// at a specific speed. Regardless of the directionality of the RPM this function will move the motor -// towards the specified target/position -// This will block until the position has been reached. -func (cm *controlledMotor) GoTo(ctx context.Context, rpm, targetPosition float64, extra map[string]interface{}) error { - // no op manager added, we're relying on GoFor's oepration manager in this driver - pos, err := cm.Position(ctx, extra) - if err != nil { - return err - } - // currRotations := pos / cm.ticksPerRotation - rotations := targetPosition - pos - // if you call GoFor with 0 revolutions, the motor will spin forever. If we are at the target, - // we must avoid this by not calling GoFor. - if rdkutils.Float64AlmostEqual(rotations, 0, 0.1) { - cm.logger.CDebug(ctx, "GoTo distance nearly zero, not moving") - return nil - } - return cm.GoFor(ctx, rpm, rotations, extra) -} - -// GoFor instructs the motor to go in a specific direction for a specific amount of -// revolutions at a given speed in revolutions per minute. Both the RPM and the revolutions -// can be assigned negative values to move in a backwards direction. Note: if both are -// negative the motor will spin in the forward direction. -// If revolutions is 0, this will run the motor at rpm indefinitely -// If revolutions != 0, this will block until the number of revolutions has been completed or another operation comes in. -func (cm *controlledMotor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - cm.opMgr.CancelRunning(ctx) - ctx, done := cm.opMgr.New(ctx) - defer done() - - switch speed := math.Abs(rpm); { - case speed < 0.1: - cm.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - case cm.real.maxRPM > 0 && speed > cm.real.maxRPM-0.1: - cm.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", cm.real.maxRPM) - default: - } - - currentTicks, _, err := cm.enc.Position(ctx, encoder.PositionTypeTicks, extra) - if err != nil { - return err - } - - goalPos, _, _ := encodedGoForMath(rpm, revolutions, currentTicks, cm.ticksPerRotation) - - if cm.loop == nil { - // create new control loop - if err := cm.startControlLoop(); err != nil { - return err - } - } - - cm.loop.Resume() - // set control loop values - velVal := math.Abs(rpm * cm.ticksPerRotation / 60) - // when rev = 0, only velocity is controlled - // setPoint is +/- infinity, maxVel is calculated velVal - if err := cm.updateControlBlock(ctx, goalPos, velVal); err != nil { - return err - } - - // we can probably use something in controls to make GoFor blockign without this - // helper function - positionReached := func(ctx context.Context) (bool, error) { - var errs error - pos, _, posErr := cm.enc.Position(ctx, encoder.PositionTypeTicks, extra) - errs = multierr.Combine(errs, posErr) - if rdkutils.Float64AlmostEqual(pos, goalPos, 2.0) { - stopErr := cm.Stop(ctx, extra) - errs = multierr.Combine(errs, stopErr) - return true, errs - } - return false, errs - } - err = cm.opMgr.WaitForSuccess( - ctx, - 10*time.Millisecond, - positionReached, - ) - // Ignore the context canceled error - this occurs when the motor is stopped - // at the beginning of goForInternal - if !errors.Is(err, context.Canceled) { - return err - } - - return nil -} diff --git a/components/motor/gpio/mathutils.go b/components/motor/gpio/mathutils.go deleted file mode 100644 index bd3eaaccf87..00000000000 --- a/components/motor/gpio/mathutils.go +++ /dev/null @@ -1,54 +0,0 @@ -package gpio - -import ( - "math" - "time" -) - -func fixPowerPct(powerPct, max float64) float64 { - powerPct = math.Min(powerPct, max) - powerPct = math.Max(powerPct, -1*max) - return powerPct -} - -func sign(x float64) float64 { // A quick helper function - if math.Signbit(x) { - return -1.0 - } - return 1.0 -} - -// If revolutions is 0, the returned wait duration will be 0 representing that -// the motor should run indefinitely. -func goForMath(maxRPM, rpm, revolutions float64) (float64, time.Duration) { - // need to do this so time is reasonable - if rpm > maxRPM { - rpm = maxRPM - } else if rpm < -1*maxRPM { - rpm = -1 * maxRPM - } - - if revolutions == 0 { - powerPct := rpm / maxRPM - return powerPct, 0 - } - - dir := rpm * revolutions / math.Abs(revolutions*rpm) - powerPct := math.Abs(rpm) / maxRPM * dir - waitDur := time.Duration(math.Abs(revolutions/rpm)*60*1000) * time.Millisecond - return powerPct, waitDur -} - -// goForMath calculates goalPos, goalRPM, and direction based on the given GoFor rpm and revolutions, and the current position. -func encodedGoForMath(rpm, revolutions, currentPos, ticksPerRotation float64) (float64, float64, float64) { - direction := sign(rpm * revolutions) - - goalPos := (math.Abs(revolutions) * ticksPerRotation * direction) + currentPos - goalRPM := math.Abs(rpm) * direction - - if revolutions == 0 { - goalPos = math.Inf(int(direction)) - } - - return goalPos, goalRPM, direction -} diff --git a/components/motor/gpio/motor_encoder.go b/components/motor/gpio/motor_encoder.go deleted file mode 100644 index 6f94c5c798d..00000000000 --- a/components/motor/gpio/motor_encoder.go +++ /dev/null @@ -1,417 +0,0 @@ -// Package gpio implements a GPIO based motor. -package gpio - -import ( - "context" - "fmt" - "math" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - rdkutils "go.viam.com/rdk/utils" -) - -// WrapMotorWithEncoder takes a motor and adds an encoder onto it in order to understand its odometry. -func WrapMotorWithEncoder( - ctx context.Context, - e encoder.Encoder, - c resource.Config, - mc Config, - m motor.Motor, - logger logging.Logger, -) (motor.Motor, error) { - if e == nil { - return m, nil - } - - if mc.TicksPerRotation < 0 { - return nil, resource.NewConfigValidationError("", errors.New("ticks_per_rotation should be positive or zero")) - } - - if mc.TicksPerRotation == 0 { - mc.TicksPerRotation = 1 - } - - mm, err := newEncodedMotor(c.ResourceName(), mc, m, e, logger) - if err != nil { - return nil, err - } - - return mm, nil -} - -// newEncodedMotor creates a new motor that supports an arbitrary source of encoder information. -func newEncodedMotor( - name resource.Name, - motorConfig Config, - realMotor motor.Motor, - realEncoder encoder.Encoder, - logger logging.Logger, -) (*EncodedMotor, error) { - localReal, err := resource.AsType[motor.Motor](realMotor) - if err != nil { - return nil, err - } - - if motorConfig.TicksPerRotation == 0 { - motorConfig.TicksPerRotation = 1 - } - - em := &EncodedMotor{ - Named: name.AsNamed(), - cfg: motorConfig, - ticksPerRotation: float64(motorConfig.TicksPerRotation), - real: localReal, - rampRate: motorConfig.RampRate, - maxPowerPct: motorConfig.MaxPowerPct, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - - em.encoder = realEncoder - - // TODO DOCS-1524: link to docs that explain control parameters - em.logger.Warn( - "recommended: for more accurate motor control, configure 'control_parameters' in the motor config") - - if em.rampRate < 0 || em.rampRate > 1 { - return nil, fmt.Errorf("ramp rate needs to be (0, 1] but is %v", em.rampRate) - } - if em.rampRate == 0 { - em.rampRate = 0.05 // Use a conservative value by default. - } - - if em.maxPowerPct < 0 || em.maxPowerPct > 1 { - return nil, fmt.Errorf("max power pct needs to be (0, 1] but is %v", em.maxPowerPct) - } - if em.maxPowerPct == 0 { - em.maxPowerPct = 1.0 - } - - return em, nil -} - -// EncodedMotor is a motor that utilizes an encoder to track its position. -type EncodedMotor struct { - resource.Named - resource.AlwaysRebuild - - activeBackgroundWorkers sync.WaitGroup - cfg Config - real motor.Motor - encoder encoder.Encoder - offsetInTicks float64 - - mu sync.RWMutex - rpmMonitorDone func() - - // how fast as we increase power do we do so - // valid numbers are (0, 1] - // .01 would ramp very slowly, 1 would ramp instantaneously - rampRate float64 - maxPowerPct float64 - ticksPerRotation float64 - - logger logging.Logger - opMgr *operation.SingleOperationManager -} - -// rpmMonitor keeps track of the desired RPM and position. -func (m *EncodedMotor) rpmMonitor(ctx context.Context, goalRPM, goalPos, direction float64) error { - lastPos, err := m.position(ctx, nil) - if err != nil { - return err - } - lastTime := time.Now().UnixNano() - _, lastPowerPct, err := m.real.IsPowered(ctx, nil) - if err != nil { - m.logger.Error(err) - return err - } - lastPowerPct = math.Abs(lastPowerPct) * direction - - for { - timer := time.NewTimer(50 * time.Millisecond) - select { - case <-ctx.Done(): - timer.Stop() - // do not return context canceled - return nil - case <-timer.C: - } - - pos, err := m.position(ctx, nil) - if err != nil { - m.logger.CInfo(ctx, "error getting encoder position, sleeping then continuing: %w", err) - if !utils.SelectContextOrWait(ctx, 100*time.Millisecond) { - m.logger.CInfo(ctx, "error sleeping, giving up %w", ctx.Err()) - return err - } - continue - } - now := time.Now().UnixNano() - - if (direction == 1 && pos >= goalPos) || (direction == -1 && pos <= goalPos) { - // stop motor when at or past goal position - return m.Stop(ctx, nil) - } - - newPower, err := m.makeAdjustments(ctx, pos, lastPos, goalRPM, goalPos, lastPowerPct, direction, now, lastTime) - if err != nil { - return err - } - - lastPos = pos - lastTime = now - lastPowerPct = newPower - } -} - -// makeAdjustments does the math required to see if the RPM is too high or too low, -// and if the goal position has been reached. -func (m *EncodedMotor) makeAdjustments( - ctx context.Context, pos, lastPos, goalRPM, goalPos, lastPowerPct, direction float64, - now, lastTime int64, -) (float64, error) { - m.mu.Lock() - defer m.mu.Unlock() - - newPowerPct := lastPowerPct - - // calculate RPM based on change in position and change in time - deltaPos := (pos - lastPos) / m.ticksPerRotation - // time is polled in nanoseconds, convert to minutes for rpm - deltaTime := (float64(now) - float64(lastTime)) / float64(6e10) - var currentRPM float64 - if deltaTime == 0.0 { - currentRPM = 0 - } else { - currentRPM = deltaPos / deltaTime - } - - m.logger.CDebug(ctx, "making adjustments") - m.logger.CDebugf(ctx, "lastPos: %v, pos: %v, goalPos: %v", lastPos, pos, goalPos) - m.logger.CDebugf(ctx, "lastTime: %v, now: %v", lastTime, now) - m.logger.CDebugf(ctx, "currentRPM: %v, goalRPM: %v", currentRPM, goalRPM) - - rpmErr := goalRPM - currentRPM - // adjust our power based on the error in rpm - // this does not depend on the motor position - newPowerPct += (m.rampRate * sign(rpmErr)) - - // prevents the motor from reversing - if sign(newPowerPct) != direction { - newPowerPct = lastPowerPct - } - - if err := m.real.SetPower(ctx, newPowerPct, nil); err != nil { - return 0, err - } - return newPowerPct, nil -} - -// SetPower sets the percentage of power the motor should employ between -1 and 1. -// Negative power implies a backward directional rotational. -func (m *EncodedMotor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - if m.rpmMonitorDone != nil { - m.rpmMonitorDone() - } - m.mu.Lock() - defer m.mu.Unlock() - powerPct = fixPowerPct(powerPct, m.maxPowerPct) - return m.real.SetPower(ctx, powerPct, nil) -} - -// goForMath calculates goalPos, goalRPM, and direction based on the given GoFor rpm and revolutions, and the current position. -func (m *EncodedMotor) goForMath(ctx context.Context, rpm, revolutions float64) (float64, float64, float64) { - direction := sign(rpm * revolutions) - - currentPos, err := m.position(ctx, nil) - if err != nil { - m.logger.CError(ctx, err) - } - goalPos := (math.Abs(revolutions) * m.ticksPerRotation * direction) + currentPos - goalRPM := math.Abs(rpm) * direction - - if revolutions == 0 { - goalPos = math.Inf(int(direction)) - } - - return goalPos, goalRPM, direction -} - -// GoFor instructs the motor to go in a specific direction for a specific amount of -// revolutions at a given speed in revolutions per minute. Both the RPM and the revolutions -// can be assigned negative values to move in a backwards direction. Note: if both are -// negative the motor will spin in the forward direction. -// If revolutions is 0, this will run the motor at rpm indefinitely -// If revolutions != 0, this will block until the number of revolutions has been completed or another operation comes in. -func (m *EncodedMotor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - ctx, done := m.opMgr.New(ctx) - defer done() - - goalPos, goalRPM, direction := m.goForMath(ctx, rpm, revolutions) - - if err := m.goForInternal(goalRPM, goalPos, direction); err != nil { - return err - } - - // return and run the motor at rpm indefinitely - if revolutions == 0 { - return nil - } - - positionReached := func(ctx context.Context) (bool, error) { - var errs error - pos, posErr := m.position(ctx, extra) - errs = multierr.Combine(errs, posErr) - if (direction == 1 && pos >= goalPos) || (direction == -1 && pos <= goalPos) { - stopErr := m.Stop(ctx, extra) - errs = multierr.Combine(errs, stopErr) - return true, errs - } - return false, errs - } - err := m.opMgr.WaitForSuccess( - ctx, - 10*time.Millisecond, - positionReached, - ) - // Ignore the context canceled error - this occurs when the rpmCtx is canceled - // with m.rpmMonitorDone in goForInternal and in Stop - if !errors.Is(err, context.Canceled) { - return err - } - return nil -} - -func (m *EncodedMotor) goForInternal(rpm, goalPos, direction float64) error { - switch speed := math.Abs(rpm); { - case speed < 0.1: - return motor.NewZeroRPMError() - case m.cfg.MaxRPM > 0 && speed > m.cfg.MaxRPM-0.1: - m.logger.Warn("motor speed is nearly the max rev_per_min (%f)", m.cfg.MaxRPM) - default: - } - - // cancel rpmMonitor if it already exists - if m.rpmMonitorDone != nil { - m.rpmMonitorDone() - } - // start a new rpmMonitor - var rpmCtx context.Context - rpmCtx, m.rpmMonitorDone = context.WithCancel(context.Background()) - m.activeBackgroundWorkers.Add(1) - go func() { - defer m.activeBackgroundWorkers.Done() - if err := m.rpmMonitor(rpmCtx, rpm, goalPos, direction); err != nil { - m.logger.Error(err) - } - }() - - return nil -} - -// GoTo instructs the motor to go to a specific position (provided in revolutions from home/zero), -// at a specific speed. Regardless of the directionality of the RPM this function will move the motor -// towards the specified target/position -// This will block until the position has been reached. -func (m *EncodedMotor) GoTo(ctx context.Context, rpm, targetPosition float64, extra map[string]interface{}) error { - pos, err := m.position(ctx, extra) - if err != nil { - return err - } - currRotations := pos / m.ticksPerRotation - rotations := targetPosition - currRotations - // if you call GoFor with 0 revolutions, the motor will spin forever. If we are at the target, - // we must avoid this by not calling GoFor. - if rdkutils.Float64AlmostEqual(rotations, 0, 0.1) { - m.logger.CDebug(ctx, "GoTo distance nearly zero, not moving") - return nil - } - return m.GoFor(ctx, rpm, rotations, extra) -} - -// ResetZeroPosition sets the current position (+/- offset) to be the new zero (home) position. -func (m *EncodedMotor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - if err := m.Stop(ctx, extra); err != nil { - return err - } - if err := m.encoder.ResetPosition(ctx, extra); err != nil { - return err - } - - m.mu.Lock() - defer m.mu.Unlock() - m.offsetInTicks = -1 * offset * m.ticksPerRotation - return nil -} - -// report position in ticks. -func (m *EncodedMotor) position(ctx context.Context, extra map[string]interface{}) (float64, error) { - ticks, _, err := m.encoder.Position(ctx, encoder.PositionTypeTicks, extra) - if err != nil { - return 0, err - } - m.mu.RLock() - defer m.mu.RUnlock() - pos := ticks + m.offsetInTicks - return pos, nil -} - -// Position reports the position of the motor based on its encoder. If it's not supported, the returned -// data is undefined. The unit returned is the number of revolutions which is intended to be fed -// back into calls of GoFor. -func (m *EncodedMotor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - ticks, err := m.position(ctx, extra) - if err != nil { - return 0, err - } - - return ticks / m.ticksPerRotation, nil -} - -// Properties returns whether or not the motor supports certain optional properties. -func (m *EncodedMotor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil -} - -// IsPowered returns whether or not the motor is currently on, and the percent power (between 0 -// and 1, if the motor is off then the percent power will be 0). -func (m *EncodedMotor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return m.real.IsPowered(ctx, extra) -} - -// IsMoving returns if the motor is moving or not. -func (m *EncodedMotor) IsMoving(ctx context.Context) (bool, error) { - return m.real.IsMoving(ctx) -} - -// Stop stops rpmMonitor and stops the real motor. -func (m *EncodedMotor) Stop(ctx context.Context, extra map[string]interface{}) error { - if m.rpmMonitorDone != nil { - m.rpmMonitorDone() - } - return m.real.Stop(ctx, nil) -} - -// Close cleanly shuts down the motor. -func (m *EncodedMotor) Close(ctx context.Context) error { - if err := m.Stop(ctx, nil); err != nil { - return err - } - m.activeBackgroundWorkers.Wait() - return nil -} diff --git a/components/motor/gpio/motor_encoder_test.go b/components/motor/gpio/motor_encoder_test.go deleted file mode 100644 index ab1d7212382..00000000000 --- a/components/motor/gpio/motor_encoder_test.go +++ /dev/null @@ -1,347 +0,0 @@ -package gpio - -import ( - "context" - "sync" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - encoderName = "encoder" - motorName = "motor" - boardName = "board" -) - -type injectedState struct { - mu sync.Mutex - position float64 - powerPct float64 -} - -var vals = injectedState{ - position: 0.0, - powerPct: 0.0, -} - -func injectEncoder() encoder.Encoder { - enc := inject.NewEncoder(encoderName) - enc.ResetPositionFunc = func(ctx context.Context, extra map[string]interface{}) error { - vals.mu.Lock() - defer vals.mu.Unlock() - vals.position = 0.0 - return nil - } - enc.PositionFunc = func(ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, - ) (float64, encoder.PositionType, error) { - vals.mu.Lock() - defer vals.mu.Unlock() - return vals.position, encoder.PositionTypeTicks, nil - } - enc.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - return encoder.Properties{TicksCountSupported: true}, nil - } - return enc -} - -func injectBoard() board.Board { - gpioPin := inject.GPIOPin{ - SetFunc: func(ctx context.Context, high bool, extra map[string]interface{}) error { return nil }, - GetFunc: func(ctx context.Context, extra map[string]interface{}) (bool, error) { return true, nil }, - SetPWMFunc: func(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { return nil }, - } - - b := inject.NewBoard(boardName) - b.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return &gpioPin, nil - } - - return b -} - -func injectMotor() motor.Motor { - m := inject.NewMotor(motorName) - m.SetPowerFunc = func(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - vals.mu.Lock() - defer vals.mu.Unlock() - vals.powerPct = powerPct - vals.position++ - return nil - } - m.GoForFunc = func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return nil - } - m.GoToFunc = func(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { - return nil - } - m.ResetZeroPositionFunc = func(ctx context.Context, offset float64, extra map[string]interface{}) error { - return nil - } - m.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0.0, nil - } - m.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, nil - } - m.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - vals.mu.Lock() - defer vals.mu.Unlock() - vals.powerPct = 0 - return nil - } - m.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - vals.mu.Lock() - defer vals.mu.Unlock() - if vals.powerPct != 0 { - return true, vals.powerPct, nil - } - return false, 0.0, nil - } - m.IsMovingFunc = func(context.Context) (bool, error) { - return false, nil - } - return m -} - -func TestEncodedMotor(t *testing.T) { - logger := logging.NewTestLogger(t) - - // create inject motor - fakeMotor := injectMotor() - - // create an inject encoder - enc := injectEncoder() - - // create an encoded motor - conf := resource.Config{ - Name: motorName, - ConvertedAttributes: &Config{}, - } - motorConf := Config{ - TicksPerRotation: 1, - } - wrappedMotor, err := WrapMotorWithEncoder(context.Background(), enc, conf, motorConf, fakeMotor, logger) - test.That(t, err, test.ShouldBeNil) - m, ok := wrappedMotor.(*EncodedMotor) - test.That(t, ok, test.ShouldBeTrue) - - defer func() { - test.That(t, m.Close(context.Background()), test.ShouldBeNil) - }() - - t.Run("encoded motor test Properties and IsMoving", func(t *testing.T) { - props, err := m.Properties(context.Background(), nil) - test.That(t, props.PositionReporting, test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) - - move, err := m.IsMoving(context.Background()) - test.That(t, move, test.ShouldBeFalse) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("encoded motor test SetPower, IsPowered, Stop, Position, and ResetZeroPosition", func(t *testing.T) { - // set power - test.That(t, m.SetPower(context.Background(), 0.5, nil), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(tb, on, test.ShouldBeTrue) - test.That(tb, powerPct, test.ShouldBeGreaterThan, 0) - test.That(tb, err, test.ShouldBeNil) - }) - - // stop motor - test.That(t, m.Stop(context.Background(), nil), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(tb, on, test.ShouldBeFalse) - test.That(tb, powerPct, test.ShouldEqual, 0) - test.That(tb, err, test.ShouldBeNil) - }) - - // check that position is positive - pos, err := m.Position(context.Background(), nil) - test.That(t, pos, test.ShouldBeGreaterThan, 0) - test.That(t, err, test.ShouldBeNil) - - // reset position - test.That(t, m.ResetZeroPosition(context.Background(), 0, nil), test.ShouldBeNil) - - // check that position is now 0 - pos, err = m.Position(context.Background(), nil) - test.That(t, pos, test.ShouldEqual, 0) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("encoded motor test GoFor forward", func(t *testing.T) { - t.Skip("temporary skip for flake") - test.That(t, m.goForInternal(10, 1, 1), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(tb, on, test.ShouldBeTrue) - test.That(tb, powerPct, test.ShouldBeGreaterThan, 0) - test.That(tb, err, test.ShouldBeNil) - }) - }) - - t.Run("encoded motor test GoFor backwards", func(t *testing.T) { - t.Skip("temporary skip for flake") - test.That(t, m.goForInternal(-10, -1, -1), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(tb, on, test.ShouldBeTrue) - test.That(tb, powerPct, test.ShouldBeLessThan, 0) - test.That(tb, err, test.ShouldBeNil) - }) - }) - - t.Run("encoded motor test goForMath", func(t *testing.T) { - t.Skip("temporary skip for flake") - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, m.ResetZeroPosition(context.Background(), 0, nil), test.ShouldBeNil) - }) - - expectedGoalPos, expectedGoalRPM, expectedDirection := 4.0, 10.0, 1.0 - goalPos, goalRPM, direction := m.goForMath(context.Background(), 10, 4) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, goalPos, test.ShouldEqual, expectedGoalPos) - test.That(tb, goalRPM, test.ShouldEqual, expectedGoalRPM) - test.That(tb, direction, test.ShouldEqual, expectedDirection) - }) - - expectedGoalPos, expectedGoalRPM, expectedDirection = -4.0, -10.0, -1.0 - goalPos, goalRPM, direction = m.goForMath(context.Background(), 10, -4) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, goalPos, test.ShouldEqual, expectedGoalPos) - test.That(tb, goalRPM, test.ShouldEqual, expectedGoalRPM) - test.That(tb, direction, test.ShouldEqual, expectedDirection) - }) - }) - - t.Run("encoded motor test SetPower interrupts GoFor", func(t *testing.T) { - t.Skip("temporary skip for flake") - go func() { - test.That(t, m.goForInternal(10, 1, 1), test.ShouldBeNil) - }() - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(tb, on, test.ShouldBeTrue) - test.That(tb, powerPct, test.ShouldBeGreaterThan, 0) - test.That(tb, err, test.ShouldBeNil) - }) - - test.That(t, m.SetPower(context.Background(), -0.5, nil), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(context.Background(), nil) - test.That(tb, on, test.ShouldBeTrue) - test.That(tb, powerPct, test.ShouldBeLessThan, 0) - test.That(tb, err, test.ShouldBeNil) - }) - }) -} - -func TestEncodedMotorControls(t *testing.T) { - logger := logging.NewTestLogger(t) - - // create inject motor - // note, all test files should have an inject motor and an inject - // board in the future - fakeMotor := &Motor{ - maxRPM: 100, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - motorType: DirectionPwm, - } - - // create an inject encoder - enc := injectEncoder() - - // create an encoded motor - conf := resource.Config{ - Name: motorName, - ConvertedAttributes: &Config{ - Encoder: encoderName, - TicksPerRotation: 1, - ControlParameters: &motorPIDConfig{ - P: 1, - I: 2, - D: 0, - }, - }, - } - - m, err := setupMotorWithControls(context.Background(), fakeMotor, enc, conf, logger) - test.That(t, err, test.ShouldBeNil) - cm, ok := m.(*controlledMotor) - test.That(t, ok, test.ShouldBeTrue) - - defer func() { - test.That(t, cm.Close(context.Background()), test.ShouldBeNil) - }() - - t.Run("encoded motor controls test loop exists", func(t *testing.T) { - test.That(t, cm.GoFor(context.Background(), 10, 1, nil), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, cm.loop, test.ShouldNotBeNil) - }) - }) -} - -func TestControlledMotorCreation(t *testing.T) { - logger := logging.NewTestLogger(t) - // create an encoded motor - conf := resource.Config{ - Name: motorName, - ConvertedAttributes: &Config{ - BoardName: boardName, - Pins: PinConfig{ - Direction: "1", - PWM: "2", - }, - Encoder: encoderName, - TicksPerRotation: 1, - ControlParameters: &motorPIDConfig{ - P: 1, - I: 2, - D: 0, - }, - }, - } - - deps := make(resource.Dependencies) - - deps[encoder.Named(encoderName)] = injectEncoder() - deps[board.Named(boardName)] = injectBoard() - - m, err := createNewMotor(context.Background(), deps, conf, logger) - test.That(t, err, test.ShouldBeNil) - cm, ok := m.(*controlledMotor) - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, cm.enc.Name().ShortName(), test.ShouldEqual, encoderName) - test.That(t, cm.real.Name().ShortName(), test.ShouldEqual, motorName) -} diff --git a/components/motor/gpio/setup.go b/components/motor/gpio/setup.go deleted file mode 100644 index 80a4847a348..00000000000 --- a/components/motor/gpio/setup.go +++ /dev/null @@ -1,229 +0,0 @@ -package gpio - -import ( - "context" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/components/encoder/single" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("gpio") - -// MotorType represents the three accepted pin configuration settings -// supported by a gpio motor. -type MotorType int - -type pinConfigError int - -// ABPwm, DirectionPwm, and AB represent the three pin setups supported by a gpio motor. -const ( - // ABPwm uses A and B direction pins and a pin for pwm signal. - ABPwm MotorType = iota - // DirectionPwm uses a single direction pin and a pin for pwm signal. - DirectionPwm - // AB uses a pwm signal on pin A for moving forwards and pin B for moving backwards. - AB - - aNotB pinConfigError = iota - bNotA - dirNotPWM - onlyPWM - noPins -) - -func getPinConfigErrorMessage(errorEnum pinConfigError) error { - var err error - switch errorEnum { - case aNotB: - err = errors.New("motor pin config has specified pin A but not pin B") - case bNotA: - err = errors.New("motor pin config has specified pin B but not pin A") - case dirNotPWM: - err = errors.New("motor pin config has direction pin but needs PWM pin") - case onlyPWM: - err = errors.New("motor pin config has PWM pin but needs either a direction pin, or A and B pins") - case noPins: - err = errors.New("motor pin config devoid of pin definitions (A, B, Direction, PWM are all missing)") - } - return err -} - -// PinConfig defines the mapping of where motor are wired. -type PinConfig struct { - A string `json:"a"` - B string `json:"b"` - Direction string `json:"dir"` - PWM string `json:"pwm"` - EnablePinHigh string `json:"en_high,omitempty"` - EnablePinLow string `json:"en_low,omitempty"` -} - -// MotorType deduces the type of motor from the pin configuration. -func (conf *PinConfig) MotorType(path string) (MotorType, error) { - hasA := conf.A != "" - hasB := conf.B != "" - hasDir := conf.Direction != "" - hasPwm := conf.PWM != "" - - var motorType MotorType - var errEnum pinConfigError - - switch { - case hasA && hasB: - if hasPwm { - motorType = ABPwm - } else { - motorType = AB - } - case hasDir && hasPwm: - motorType = DirectionPwm - case hasA && !hasB: - errEnum = aNotB - case hasB && !hasA: - errEnum = bNotA - case hasDir && !hasPwm: - errEnum = dirNotPWM - case hasPwm && !hasDir && !hasA && !hasB: - errEnum = onlyPWM - default: - errEnum = noPins - } - - if err := getPinConfigErrorMessage(errEnum); err != nil { - return motorType, resource.NewConfigValidationError(path, err) - } - return motorType, nil -} - -type motorPIDConfig struct { - P float64 `json:"p"` - I float64 `json:"i"` - D float64 `json:"d"` -} - -// Config describes the configuration of a motor. -type Config struct { - Pins PinConfig `json:"pins"` - BoardName string `json:"board"` - MinPowerPct float64 `json:"min_power_pct,omitempty"` // min power percentage to allow for this motor default is 0.0 - MaxPowerPct float64 `json:"max_power_pct,omitempty"` // max power percentage to allow for this motor (0.06 - 1.0) - PWMFreq uint `json:"pwm_freq,omitempty"` - DirectionFlip bool `json:"dir_flip,omitempty"` // Flip the direction of the signal sent if there is a Dir pin - Encoder string `json:"encoder,omitempty"` // name of encoder - RampRate float64 `json:"ramp_rate,omitempty"` // how fast to ramp power to motor when using rpm control - MaxRPM float64 `json:"max_rpm,omitempty"` - TicksPerRotation int `json:"ticks_per_rotation,omitempty"` - ControlParameters *motorPIDConfig `json:"control_parameters,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - - if conf.BoardName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - deps = append(deps, conf.BoardName) - - // ensure motor config represents one of three supported motor configuration types - // (see MotorType above) - if _, err := conf.Pins.MotorType(path); err != nil { - return deps, err - } - - // If an encoder is present the max_rpm field is optional, in the absence of an encoder the field is required - if conf.Encoder != "" { - if conf.TicksPerRotation <= 0 { - return nil, resource.NewConfigValidationError(path, errors.New("ticks_per_rotation should be positive or zero")) - } - deps = append(deps, conf.Encoder) - } else if conf.MaxRPM <= 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "max_rpm") - } - return deps, nil -} - -// init registers a pi motor based on pigpio. -func init() { - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *Config]{ - Constructor: createNewMotor, - }) -} - -func getBoardFromRobotConfig(deps resource.Dependencies, conf resource.Config) (board.Board, *Config, error) { - motorConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, nil, err - } - if motorConfig.BoardName == "" { - return nil, nil, errors.New("expected board name in config for motor") - } - b, err := board.FromDependencies(deps, motorConfig.BoardName) - if err != nil { - return nil, nil, err - } - return b, motorConfig, nil -} - -func createNewMotor( - ctx context.Context, deps resource.Dependencies, cfg resource.Config, logger logging.Logger, -) (motor.Motor, error) { - actualBoard, motorConfig, err := getBoardFromRobotConfig(deps, cfg) - if err != nil { - return nil, err - } - - m, err := NewMotor(actualBoard, *motorConfig, cfg.ResourceName(), logger) - if err != nil { - return nil, err - } - - if motorConfig.Encoder != "" { - basic := m.(*Motor) - e, err := encoder.FromDependencies(deps, motorConfig.Encoder) - if err != nil { - return nil, err - } - - props, err := e.Properties(context.Background(), nil) - if err != nil { - return nil, errors.New("cannot get encoder properties") - } - if !props.TicksCountSupported { - return nil, - encoder.NewEncodedMotorPositionTypeUnsupportedError(props) - } - - single, isSingle := e.(*single.Encoder) - if isSingle { - single.AttachDirectionalAwareness(basic) - logger.CInfo(ctx, "direction attached to single encoder from encoded motor") - } - - switch { - case motorConfig.ControlParameters == nil: - m, err = WrapMotorWithEncoder(ctx, e, cfg, *motorConfig, m, logger) - if err != nil { - return nil, err - } - default: - m, err = setupMotorWithControls(ctx, basic, e, cfg, logger) - if err != nil { - return nil, err - } - } - } - - err = m.Stop(ctx, nil) - if err != nil { - return nil, err - } - - return m, nil -} diff --git a/components/motor/gpiostepper/gpiostepper.go b/components/motor/gpiostepper/gpiostepper.go deleted file mode 100644 index f1f2aae3cb5..00000000000 --- a/components/motor/gpiostepper/gpiostepper.go +++ /dev/null @@ -1,491 +0,0 @@ -// Package gpiostepper implements a GPIO based stepper motor -package gpiostepper - -// This package is meant to be used with bipolar stepper motors connected to drivers that drive motors -// using high/low direction pins and pulses to step the motor armatures, the package can also set enable -// pins high or low if the driver needs them to power the stepper motor armatures -/* - Compatibility tested: - Stepper Motors: NEMA - Motor Driver: DRV8825, A4998, L298N igus-drylin D8(X) - Resources: - DRV8825: https://lastminuteengineers.com/drv8825-stepper-motor-driver-arduino-tutorial/ - A4998: https://lastminuteengineers.com/a4988-stepper-motor-driver-arduino-tutorial/ - L298N: https://lastminuteengineers.com/stepper-motor-l298n-arduino-tutorial/ - - This driver will drive the motor using a step pulse with a delay that matches the speed calculated by: - stepperDelay (ns) := 1min / (rpm (revs_per_minute) * spr (steps_per_revolution)) - The motor will then step and increment its position until it has reached a target or has been stopped. - - Configuration: - Required pins: a step pin to send pulses and a direction pin to set the direction. - Enabling current to flow through the armature and holding a position can be done by setting enable pins on - hardware that supports that functionality. - - An optional configurable stepper_delay parameter configures the minimum delay to set a pulse to high - for a particular stepper motor. This is usually motor specific and can be calculated using phase - resistance and induction data from the datasheet of your stepper motor. -*/ - -import ( - "context" - "fmt" - "math" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - rdkutils "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("gpiostepper") - -// PinConfig defines the mapping of where motor are wired. -type PinConfig struct { - Step string `json:"step"` - Direction string `json:"dir"` - EnablePinHigh string `json:"en_high,omitempty"` - EnablePinLow string `json:"en_low,omitempty"` -} - -// Config describes the configuration of a motor. -type Config struct { - Pins PinConfig `json:"pins"` - BoardName string `json:"board"` - StepperDelay int `json:"stepper_delay_usec,omitempty"` // When using stepper motors, the time to remain high - TicksPerRotation int `json:"ticks_per_rotation"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - if cfg.BoardName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - if cfg.TicksPerRotation == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "ticks_per_rotation") - } - if cfg.Pins.Direction == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "dir") - } - if cfg.Pins.Step == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "step") - } - deps = append(deps, cfg.BoardName) - return deps, nil -} - -func init() { - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - actualBoard, motorConfig, err := getBoardFromRobotConfig(deps, conf) - if err != nil { - return nil, err - } - - return newGPIOStepper(ctx, actualBoard, *motorConfig, conf.ResourceName(), logger) - }, - }) -} - -func getBoardFromRobotConfig(deps resource.Dependencies, conf resource.Config) (board.Board, *Config, error) { - motorConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, nil, err - } - if motorConfig.BoardName == "" { - return nil, nil, errors.New("expected board name in config for motor") - } - b, err := board.FromDependencies(deps, motorConfig.BoardName) - if err != nil { - return nil, nil, err - } - return b, motorConfig, nil -} - -func newGPIOStepper( - ctx context.Context, - b board.Board, - mc Config, - name resource.Name, - logger logging.Logger, -) (motor.Motor, error) { - if b == nil { - return nil, errors.New("board is required") - } - - if mc.TicksPerRotation == 0 { - return nil, errors.New("expected ticks_per_rotation in config for motor") - } - - m := &gpioStepper{ - Named: name.AsNamed(), - theBoard: b, - stepsPerRotation: mc.TicksPerRotation, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - } - - var err error - - // only set enable pins if they exist - if mc.Pins.EnablePinHigh != "" { - m.enablePinHigh, err = b.GPIOPinByName(mc.Pins.EnablePinHigh) - if err != nil { - return nil, err - } - } - if mc.Pins.EnablePinLow != "" { - m.enablePinLow, err = b.GPIOPinByName(mc.Pins.EnablePinLow) - if err != nil { - return nil, err - } - } - - // set the required step and direction pins - m.stepPin, err = b.GPIOPinByName(mc.Pins.Step) - if err != nil { - return nil, err - } - - m.dirPin, err = b.GPIOPinByName(mc.Pins.Direction) - if err != nil { - return nil, err - } - - if mc.StepperDelay > 0 { - m.minDelay = time.Duration(mc.StepperDelay * int(time.Microsecond)) - } - - err = m.enable(ctx, false) - if err != nil { - return nil, err - } - - m.startThread() - return m, nil -} - -type gpioStepper struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - - // config - theBoard board.Board - stepsPerRotation int - stepperDelay time.Duration - minDelay time.Duration - enablePinHigh, enablePinLow board.GPIOPin - stepPin, dirPin board.GPIOPin - logger logging.Logger - - // state - lock sync.Mutex - opMgr *operation.SingleOperationManager - - stepPosition int64 - threadStarted bool - targetStepPosition int64 - - cancel context.CancelFunc - waitGroup sync.WaitGroup -} - -// SetPower sets the percentage of power the motor should employ between 0-1. -func (m *gpioStepper) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - if math.Abs(powerPct) <= .0001 { - m.stop() - return nil - } - - if m.minDelay == 0 { - return errors.Errorf( - "if you want to set the power, set 'stepper_delay_usec' in the motor config at "+ - "the minimum time delay between pulses for your stepper motor (%s)", - m.Name().Name) - } - - m.stepperDelay = time.Duration(float64(m.minDelay) / math.Abs(powerPct)) - - if powerPct < 0 { - m.targetStepPosition = math.MinInt64 - } else { - m.targetStepPosition = math.MaxInt64 - } - - return nil -} - -func (m *gpioStepper) startThread() { - m.lock.Lock() - defer m.lock.Unlock() - - if m.threadStarted { - return - } - - m.logger.Debugf("starting control thread for motor (%s)", m.Name().Name) - - var ctxWG context.Context - ctxWG, m.cancel = context.WithCancel(context.Background()) - m.threadStarted = true - m.waitGroup.Add(1) - go func() { - defer m.waitGroup.Done() - for { - sleep, err := m.doCycle(ctxWG) - if err != nil { - m.logger.Warnf("error cycling gpioStepper (%s) %s", m.Name().Name, err.Error()) - } - - if !utils.SelectContextOrWait(ctxWG, sleep) { - // context done - return - } - } - }() -} - -func (m *gpioStepper) doCycle(ctx context.Context) (time.Duration, error) { - m.lock.Lock() - defer m.lock.Unlock() - - // thread waits until something changes the target position in the - // gpiostepper struct - if m.stepPosition == m.targetStepPosition { - return 5 * time.Millisecond, nil - } - - // TODO: Setting PWM here works much better than steps to set speed - // Redo this part with PWM logic, but also be aware that parallel - // logic to the PWM call will need to be implemented to account for position - // reporting - err := m.doStep(ctx, m.stepPosition < m.targetStepPosition) - if err != nil { - return time.Second, fmt.Errorf("error stepping motor (%s) %w", m.Name().Name, err) - } - - // wait the stepper delay to return from the doRun for loop or select - // context if the duration has not elapsed. - return 0, nil -} - -// have to be locked to call. -func (m *gpioStepper) doStep(ctx context.Context, forward bool) error { - err := multierr.Combine( - m.dirPin.Set(ctx, forward, nil), - m.stepPin.Set(ctx, true, nil)) - if err != nil { - return err - } - // stay high for half the delay - time.Sleep(m.stepperDelay / 2.0) - - if err := m.stepPin.Set(ctx, false, nil); err != nil { - return err - } - - // stay low for the other half - time.Sleep(m.stepperDelay / 2.0) - - if forward { - m.stepPosition++ - } else { - m.stepPosition-- - } - - return nil -} - -// GoFor instructs the motor to go in a specific direction for a specific amount of -// revolutions at a given speed in revolutions per minute. Both the RPM and the revolutions -// can be assigned negative values to move in a backwards direction. Note: if both are negative -// the motor will spin in the forward direction. -func (m *gpioStepper) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - ctx, done := m.opMgr.New(ctx) - defer done() - - err := m.enable(ctx, true) - if err != nil { - return errors.Wrapf(err, "error enabling motor in GoFor from motor (%s)", m.Name().Name) - } - - err = m.goForInternal(ctx, rpm, revolutions) - if err != nil { - return multierr.Combine( - m.enable(ctx, false), - errors.Wrapf(err, "error in GoFor from motor (%s)", m.Name().Name)) - } - - // this is a long-running operation, do not wait for Stop, do not disable enable pins - if revolutions == 0 { - return nil - } - - return multierr.Combine( - m.opMgr.WaitTillNotPowered(ctx, time.Millisecond, m, m.Stop), - m.enable(ctx, false)) -} - -func (m *gpioStepper) goForInternal(ctx context.Context, rpm, revolutions float64) error { - if revolutions == 0 { - // go a large number of revolutions if 0 is passed in, at the desired speed - revolutions = math.MaxInt64 - } - - speed := math.Abs(rpm) - if speed < 0.1 { - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return m.Stop(ctx, nil) - } - - var d int64 = 1 - if math.Signbit(revolutions) != math.Signbit(rpm) { - d = -1 - } - - m.lock.Lock() - defer m.lock.Unlock() - - // calculate delay between steps for the thread in the gorootuine that we started in component creation - m.stepperDelay = time.Duration(int64(float64(time.Minute) / (math.Abs(rpm) * float64(m.stepsPerRotation)))) - if m.stepperDelay < m.minDelay { - m.stepperDelay = m.minDelay - m.logger.CDebugf(ctx, - "calculated delay less than the minimum delay for stepper motor setting to %+v", m.stepperDelay, - ) - } - - if !m.threadStarted { - return errors.New("thread not started") - } - - m.targetStepPosition += d * int64(math.Abs(revolutions)*float64(m.stepsPerRotation)) - - return nil -} - -// GoTo instructs the motor to go to a specific position (provided in revolutions from home/zero), -// at a specific RPM. Regardless of the directionality of the RPM this function will move the motor -// towards the specified target. -func (m *gpioStepper) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - curPos, err := m.Position(ctx, extra) - if err != nil { - return errors.Wrapf(err, "error in GoTo from motor (%s)", m.Name().Name) - } - moveDistance := positionRevolutions - curPos - - // if you call GoFor with 0 revolutions, the motor will spin forever. If we are at the target, - // we must avoid this by not calling GoFor. - if rdkutils.Float64AlmostEqual(moveDistance, 0, 0.1) { - m.logger.CDebugf(ctx, "GoTo distance nearly zero for motor (%s), not moving", m.Name().Name) - return nil - } - - m.logger.CDebugf(ctx, "motor (%s) going to %.2f at rpm %.2f", m.Name().Name, moveDistance, math.Abs(rpm)) - return m.GoFor(ctx, math.Abs(rpm), moveDistance, extra) -} - -// Set the current position (+/- offset) to be the new zero (home) position. -func (m *gpioStepper) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - m.lock.Lock() - defer m.lock.Unlock() - m.stepPosition = int64(-1 * offset * float64(m.stepsPerRotation)) - m.targetStepPosition = m.stepPosition - return nil -} - -// Position reports the position of the motor based on its encoder. If it's not supported, the returned -// data is undefined. The unit returned is the number of revolutions which is intended to be fed -// back into calls of GoFor. -func (m *gpioStepper) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - m.lock.Lock() - defer m.lock.Unlock() - return float64(m.stepPosition) / float64(m.stepsPerRotation), nil -} - -// Properties returns the status of whether the motor supports certain optional properties. -func (m *gpioStepper) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil -} - -// IsMoving returns if the motor is currently moving. -func (m *gpioStepper) IsMoving(ctx context.Context) (bool, error) { - m.lock.Lock() - defer m.lock.Unlock() - return m.stepPosition != m.targetStepPosition, nil -} - -// Stop turns the power to the motor off immediately, without any gradual step down. -func (m *gpioStepper) Stop(ctx context.Context, extra map[string]interface{}) error { - m.stop() - m.lock.Lock() - defer m.lock.Unlock() - return m.enable(ctx, false) -} - -func (m *gpioStepper) stop() { - m.lock.Lock() - defer m.lock.Unlock() - m.targetStepPosition = m.stepPosition -} - -// IsPowered returns whether or not the motor is currently on. It also returns the percent power -// that the motor has, but stepper motors only have this set to 0% or 100%, so it's a little -// redundant. -func (m *gpioStepper) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - on, err := m.IsMoving(ctx) - if err != nil { - return on, 0.0, errors.Wrapf(err, "error in IsPowered from motor (%s)", m.Name().Name) - } - percent := 0.0 - if on { - percent = 1.0 - } - return on, percent, err -} - -func (m *gpioStepper) enable(ctx context.Context, on bool) error { - var err error - if m.enablePinHigh != nil { - err = multierr.Combine(err, m.enablePinHigh.Set(ctx, on, nil)) - } - - if m.enablePinLow != nil { - err = multierr.Combine(err, m.enablePinLow.Set(ctx, !on, nil)) - } - - return err -} - -func (m *gpioStepper) Close(ctx context.Context) error { - err := m.Stop(ctx, nil) - - m.lock.Lock() - if m.cancel != nil { - m.logger.CDebugf(ctx, "stopping control thread for motor (%s)", m.Name().Name) - m.cancel() - m.cancel = nil - m.threadStarted = false - } - m.lock.Unlock() - m.waitGroup.Wait() - - return err -} diff --git a/components/motor/gpiostepper/gpiostepper_test.go b/components/motor/gpiostepper/gpiostepper_test.go deleted file mode 100644 index 5d00fe6a951..00000000000 --- a/components/motor/gpiostepper/gpiostepper_test.go +++ /dev/null @@ -1,514 +0,0 @@ -package gpiostepper - -import ( - "context" - "fmt" - "sync" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - fakeboard "go.viam.com/rdk/components/board/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestConfigs(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - logger := logging.NewTestLogger(t) - c := resource.Config{ - Name: "fake_gpiostepper", - } - - goodConfig := Config{ - Pins: PinConfig{Direction: "b", Step: "c", EnablePinHigh: "d", EnablePinLow: "e"}, - TicksPerRotation: 200, - BoardName: "brd", - StepperDelay: 30, - } - - pinB := &fakeboard.GPIOPin{} - pinC := &fakeboard.GPIOPin{} - pinD := &fakeboard.GPIOPin{} - pinE := &fakeboard.GPIOPin{} - pinMap := map[string]*fakeboard.GPIOPin{ - "b": pinB, - "c": pinC, - "d": pinD, - "e": pinE, - } - b := fakeboard.Board{GPIOPins: pinMap} - - t.Run("config validation good", func(t *testing.T) { - mc := goodConfig - - deps, err := mc.Validate("") - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldResemble, []string{"brd"}) - - // remove optional fields - mc.StepperDelay = 0 - deps, err = mc.Validate("") - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldResemble, []string{"brd"}) - - mc.Pins.EnablePinHigh = "" - mc.Pins.EnablePinLow = "" - deps, err = mc.Validate("") - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldResemble, []string{"brd"}) - }) - - t.Run("config missing required pins", func(t *testing.T) { - mc := goodConfig - mc.Pins.Direction = "" - - _, err := mc.Validate("") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationFieldRequiredError("", "dir")) - - mc = goodConfig - mc.Pins.Step = "" - _, err = mc.Validate("") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationFieldRequiredError("", "step")) - }) - - t.Run("config missing ticks", func(t *testing.T) { - mc := goodConfig - mc.TicksPerRotation = 0 - - _, err := mc.Validate("") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationFieldRequiredError("", "ticks_per_rotation")) - }) - - t.Run("config missing board", func(t *testing.T) { - mc := goodConfig - mc.BoardName = "" - - _, err := mc.Validate("") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationFieldRequiredError("", "board")) - }) - - t.Run("initializing good with enable pins", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - test.That(t, s.minDelay, test.ShouldEqual, 30*time.Microsecond) - test.That(t, s.stepsPerRotation, test.ShouldEqual, 200) - test.That(t, s.dirPin, test.ShouldEqual, pinB) - test.That(t, s.stepPin, test.ShouldEqual, pinC) - test.That(t, s.enablePinHigh, test.ShouldEqual, pinD) - test.That(t, s.enablePinLow, test.ShouldEqual, pinE) - }) - - t.Run("initializing good without enable pins", func(t *testing.T) { - mc := goodConfig - mc.Pins.EnablePinHigh = "" - mc.Pins.EnablePinLow = "" - - m, err := newGPIOStepper(ctx, &b, mc, c.ResourceName(), logger) - s := m.(*gpioStepper) - - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - test.That(t, s.dirPin, test.ShouldEqual, pinB) - test.That(t, s.stepPin, test.ShouldEqual, pinC) - - // fake board auto-creates new pins by default. just make sure they're not what they would normally be. - test.That(t, s.enablePinHigh, test.ShouldNotEqual, pinD) - test.That(t, s.enablePinLow, test.ShouldNotEqual, pinE) - }) - - t.Run("initializing with no board", func(t *testing.T) { - _, err := newGPIOStepper(ctx, nil, goodConfig, c.ResourceName(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "board is required") - }) - - t.Run("initializing without ticks per rotation", func(t *testing.T) { - mc := goodConfig - mc.TicksPerRotation = 0 - - _, err := newGPIOStepper(ctx, &b, mc, c.ResourceName(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "expected ticks_per_rotation") - }) - - t.Run("initializing with negative stepper delay", func(t *testing.T) { - mc := goodConfig - mc.StepperDelay = -100 - - m, err := newGPIOStepper(ctx, &b, mc, c.ResourceName(), logger) - s := m.(*gpioStepper) - - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - test.That(t, s.minDelay, test.ShouldEqual, 0*time.Microsecond) - }) - - t.Run("motor supports position reporting", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - properties, err := m.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeTrue) - }) -} - -// Warning: Tests that run goForInternal may be racy. -func TestRunning(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - logger, obs := logging.NewObservedTestLogger(t) - c := resource.Config{ - Name: "fake_gpiostepper", - } - - goodConfig := Config{ - Pins: PinConfig{Direction: "b", Step: "c", EnablePinHigh: "d", EnablePinLow: "e"}, - TicksPerRotation: 200, - BoardName: "brd", - StepperDelay: 30, - } - - pinB := &fakeboard.GPIOPin{} - pinC := &fakeboard.GPIOPin{} - pinD := &fakeboard.GPIOPin{} - pinE := &fakeboard.GPIOPin{} - pinMap := map[string]*fakeboard.GPIOPin{ - "b": pinB, - "c": pinC, - "d": pinD, - "e": pinE, - } - b := fakeboard.Board{GPIOPins: pinMap} - - t.Run("isPowered false after init", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - h, err := pinD.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, h, test.ShouldBeFalse) - - l, err := pinE.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, l, test.ShouldBeTrue) - }) - - t.Run("IsPowered true", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - // long running goFor - err = s.goForInternal(ctx, 100, 3) - defer m.Stop(ctx, nil) - - test.That(t, err, test.ShouldBeNil) - - // the motor is running - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, 1.0) - }) - - // the motor finished running - testutils.WaitForAssertionWithSleep(t, 100*time.Millisecond, 100, func(tb testing.TB) { - tb.Helper() - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, on, test.ShouldEqual, false) - test.That(tb, powerPct, test.ShouldEqual, 0.0) - }) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 3) - test.That(t, s.targetStepPosition, test.ShouldEqual, 600) - }) - - t.Run("motor enable", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - err = s.enable(ctx, true) - test.That(t, err, test.ShouldBeNil) - - h, err := pinD.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, h, test.ShouldBeTrue) - - l, err := pinE.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, l, test.ShouldBeFalse) - - err = s.enable(ctx, false) - test.That(t, err, test.ShouldBeNil) - - h, err = pinD.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, h, test.ShouldBeFalse) - - l, err = pinE.Get(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, l, test.ShouldBeTrue) - }) - - t.Run("motor testing with positive rpm and positive revolutions", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - err = s.GoFor(ctx, 10000, 1, nil) - test.That(t, err, test.ShouldBeNil) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 1) - test.That(t, s.targetStepPosition, test.ShouldEqual, 200) - }) - - t.Run("motor testing with negative rpm and positive revolutions", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - err = m.GoFor(ctx, -10000, 1, nil) - test.That(t, err, test.ShouldBeNil) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, -1) - test.That(t, s.targetStepPosition, test.ShouldEqual, -200) - }) - - t.Run("motor testing with positive rpm and negative revolutions", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - err = m.GoFor(ctx, 10000, -1, nil) - test.That(t, err, test.ShouldBeNil) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, -1) - test.That(t, s.targetStepPosition, test.ShouldEqual, -200) - }) - - t.Run("motor testing with negative rpm and negative revolutions", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - err = m.GoFor(ctx, -10000, -1, nil) - test.That(t, err, test.ShouldBeNil) - - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 1) - test.That(t, s.targetStepPosition, test.ShouldEqual, 200) - }) - - t.Run("Ensure stop called when gofor is interrupted", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - ctx := context.Background() - var wg sync.WaitGroup - ctx, cancel := context.WithCancel(ctx) - wg.Add(1) - go func() { - m.GoFor(ctx, 100, 100, map[string]interface{}{}) - wg.Done() - }() - - // Make sure it starts moving - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, _, err := m.IsPowered(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, on, test.ShouldEqual, true) - - p, err := m.Position(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, p, test.ShouldBeGreaterThan, 0) - }) - - cancel() - wg.Wait() - - // Make sure it stops moving - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, _, err := m.IsPowered(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, on, test.ShouldEqual, false) - }) - test.That(t, ctx.Err(), test.ShouldNotBeNil) - - p, err := m.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - // stop() sets targetStepPosition to the stepPostion value - test.That(t, s.targetStepPosition, test.ShouldEqual, s.stepPosition) - test.That(t, s.targetStepPosition, test.ShouldBeBetweenOrEqual, 1, 100*200) - test.That(t, p, test.ShouldBeBetween, 0, 100) - }) - - t.Run("enable pins handled properly during GoFor", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - ctx := context.Background() - var wg sync.WaitGroup - ctx, cancel := context.WithCancel(ctx) - wg.Add(1) - go func() { - m.GoFor(ctx, 100, 100, map[string]interface{}{}) - wg.Done() - }() - - // Make sure it starts moving - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, _, err := m.IsPowered(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, on, test.ShouldEqual, true) - - h, err := pinD.Get(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, h, test.ShouldBeTrue) - - l, err := pinE.Get(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, l, test.ShouldBeFalse) - }) - - cancel() - wg.Wait() - - // Make sure it stops moving - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - on, _, err := m.IsPowered(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, on, test.ShouldEqual, false) - - h, err := pinD.Get(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, h, test.ShouldBeFalse) - - l, err := pinE.Get(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, l, test.ShouldBeTrue) - }) - test.That(t, ctx.Err(), test.ShouldNotBeNil) - }) - - t.Run("motor testing with large # of revolutions", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - s := m.(*gpioStepper) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - err = s.goForInternal(ctx, 1000, 200) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - - on, _, err := m.IsPowered(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, on, test.ShouldEqual, true) - - pos, err := m.Position(ctx, nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, pos, test.ShouldBeGreaterThan, 2) - }) - - err = m.Stop(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - on, _, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldBeGreaterThan, 2) - test.That(t, pos, test.ShouldBeLessThan, 202) - }) - - t.Run("motor testing with 0 rpm", func(t *testing.T) { - m, err := newGPIOStepper(ctx, &b, goodConfig, c.ResourceName(), logger) - test.That(t, err, test.ShouldBeNil) - defer m.Close(ctx) - - err = m.GoFor(ctx, 0, 1, nil) - test.That(t, err, test.ShouldBeNil) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - on, _, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - }) - - cancel() -} diff --git a/components/motor/i2cmotors/ezopmp.go b/components/motor/i2cmotors/ezopmp.go deleted file mode 100644 index 97da56aa2d8..00000000000 --- a/components/motor/i2cmotors/ezopmp.go +++ /dev/null @@ -1,359 +0,0 @@ -//go:build linux - -// Package ezopmp is a motor driver for the hydrogarden pump -package ezopmp - -import ( - "bytes" - "context" - "math" - "strconv" - "strings" - "time" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -// Config is user config inputs for ezopmp. -type Config struct { - BusName string `json:"i2c_bus"` - I2CAddress *byte `json:"i2c_addr"` - MaxReadBits *int `json:"max_read_bits"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - - if conf.BusName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "bus_name") - } - - if conf.I2CAddress == nil { - return nil, resource.NewConfigValidationFieldRequiredError(path, "i2c_address") - } - - if conf.MaxReadBits == nil { - return nil, resource.NewConfigValidationFieldRequiredError(path, "max_read_bits") - } - - return deps, nil -} - -var model = resource.DefaultModelFamily.WithModel("ezopmp") - -func init() { - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - return NewMotor(ctx, deps, newConf, conf.ResourceName(), logger) - }, - }) -} - -// Ezopmp represents a motor connected via the I2C protocol. -type Ezopmp struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - bus buses.I2C - I2CAddress byte - maxReadBits int - logger logging.Logger - maxPowerPct float64 - powerPct float64 - maxFlowRate float64 - opMgr *operation.SingleOperationManager -} - -// available commands. -const ( - dispenseStatus = "D,?" - stop = "X" - totVolDispensed = "TV,?" - clear = "clear" - maxFlowRate = "DC,?" -) - -// NewMotor returns a motor(Ezopmp) with I2C protocol. -func NewMotor(ctx context.Context, deps resource.Dependencies, c *Config, name resource.Name, - logger logging.Logger, -) (motor.Motor, error) { - bus, err := buses.NewI2cBus(c.BusName) - if err != nil { - return nil, err - } - - m := &Ezopmp{ - Named: name.AsNamed(), - bus: bus, - I2CAddress: *c.I2CAddress, - maxReadBits: *c.MaxReadBits, - logger: logger, - maxPowerPct: 1.0, - powerPct: 0.0, - opMgr: operation.NewSingleOperationManager(), - } - - flowRate, err := m.findMaxFlowRate(ctx) - if err != nil { - return nil, errors.Errorf("can't find max flow rate: %v", err) - } - m.maxFlowRate = flowRate - - if err := m.Validate(); err != nil { - return nil, err - } - - return m, nil -} - -// for this pump, it will return the total volume dispensed. -func (m *Ezopmp) findMaxFlowRate(ctx context.Context) (float64, error) { - command := []byte(maxFlowRate) - writeErr := m.writeReg(ctx, command) - if writeErr != nil { - return 0, writeErr - } - val, err := m.readReg(ctx) - if err != nil { - return 0, err - } - splitMsg := strings.Split(string(val), ",") - flowRate, err := strconv.ParseFloat(splitMsg[1], 64) - return flowRate, err -} - -// Validate if this config is valid. -func (m *Ezopmp) Validate() error { - if m.bus == nil { - return errors.New("need a bus for ezopmp") - } - - if m.I2CAddress == 0 { - m.logger.Warn("i2c address set at 103") - m.I2CAddress = 103 - } - - if m.maxReadBits == 0 { - m.logger.Warn("max_read_bits set to 39") - m.maxReadBits = 39 - } - - if m.maxPowerPct > 1 { - m.maxPowerPct = 1 - } - - if m.maxFlowRate == 0 { - m.maxFlowRate = 50.5 - } - return nil -} - -func (m *Ezopmp) writeReg(ctx context.Context, command []byte) error { - handle, err := m.bus.OpenHandle(m.I2CAddress) - if err != nil { - return err - } - defer func() { - if err := handle.Close(); err != nil { - m.logger.CError(ctx, err) - } - }() - - return handle.Write(ctx, command) -} - -func (m *Ezopmp) readReg(ctx context.Context) ([]byte, error) { - handle, err := m.bus.OpenHandle(m.I2CAddress) - if err != nil { - return nil, err - } - defer func() { - if err := handle.Close(); err != nil { - m.logger.CError(ctx, err) - } - }() - - readVal := []byte{254, 0} - for readVal[0] == 254 { - readVal, err = handle.Read(ctx, m.maxReadBits) - if err != nil { - return nil, err - } - } - - switch readVal[0] { - case 1: - noF := bytes.Trim(readVal[1:], "\xff") - return bytes.Trim(noF, "\x00"), nil - case 2: - return nil, errors.New("syntax error, code: 2") - case 255: - return nil, errors.New("no data to send, code: 255") - case 254: - return nil, errors.New("data not ready, code: 254") - default: - return nil, errors.Errorf("error code not understood %b", readVal[0]) - } -} - -// helper function to write the command and then read to check if success. -func (m *Ezopmp) writeRegWithCheck(ctx context.Context, command []byte) error { - writeErr := m.writeReg(ctx, command) - if writeErr != nil { - return writeErr - } - _, readErr := m.readReg(ctx) - return readErr -} - -// SetPower sets the percentage of power the motor should employ between -1 and 1. -// Negative power implies a backward directional rotational -// for this pump, it goes between 0.5ml to 105ml/min. -func (m *Ezopmp) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - - powerPct = math.Min(powerPct, m.maxPowerPct) - powerPct = math.Max(powerPct, -1*m.maxPowerPct) - m.powerPct = powerPct - - var command []byte - if powerPct == 0 { - command = []byte(stop) - } else { - var powerVal float64 - if powerPct < 0 { - powerVal = (powerPct * 104.5) - 0.5 - } else { - powerVal = (powerPct * 104.5) + 0.5 - } - stringVal := "DC," + strconv.FormatFloat(powerVal, 'f', -1, 64) + ",*" - command = []byte(stringVal) - } - - return m.writeRegWithCheck(ctx, command) -} - -// GoFor sets a constant flow rate -// mLPerMin = rpm, mins = revolutions. -func (m *Ezopmp) GoFor(ctx context.Context, mLPerMin, mins float64, extra map[string]interface{}) error { - switch speed := math.Abs(mLPerMin); { - case speed < 0.1: - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - case m.maxFlowRate > 0 && speed > m.maxFlowRate-0.1: - m.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.maxFlowRate) - default: - } - - ctx, done := m.opMgr.New(ctx) - defer done() - - commandString := "DC," + strconv.FormatFloat(mLPerMin, 'f', -1, 64) + "," + strconv.FormatFloat(mins, 'f', -1, 64) - command := []byte(commandString) - if err := m.writeRegWithCheck(ctx, command); err != nil { - return errors.Wrap(err, "error in GoFor") - } - - return m.opMgr.WaitTillNotPowered(ctx, time.Millisecond, m, m.Stop) -} - -// GoTo uses the Dose Over Time Command in the EZO-PMP datasheet -// mLPerMin = rpm, mins = revolutions. -func (m *Ezopmp) GoTo(ctx context.Context, mLPerMin, mins float64, extra map[string]interface{}) error { - switch speed := math.Abs(mLPerMin); { - case speed < 0.5: - return errors.New("cannot move this slowly") - case speed > 105: - return errors.New("cannot move this fast") - } - - commandString := "D," + strconv.FormatFloat(mLPerMin, 'f', -1, 64) + "," + strconv.FormatFloat(mins, 'f', -1, 64) - command := []byte(commandString) - if err := m.writeRegWithCheck(ctx, command); err != nil { - return errors.Wrap(err, "error in GoTo") - } - return m.opMgr.WaitTillNotPowered(ctx, time.Millisecond, m, m.Stop) -} - -// ResetZeroPosition clears the amount of volume that has been dispensed. -func (m *Ezopmp) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - m.logger.CWarnf(ctx, "cannot reset position of motor (%v) because position refers to the total volume dispensed", m.Name().ShortName()) - return motor.NewResetZeroPositionUnsupportedError(m.Name().ShortName()) -} - -// Position will return the total volume dispensed. -func (m *Ezopmp) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - command := []byte(totVolDispensed) - writeErr := m.writeReg(ctx, command) - if writeErr != nil { - return 0, errors.Wrap(writeErr, "error in Position") - } - val, err := m.readReg(ctx) - if err != nil { - return 0, errors.Wrap(err, "error in Position") - } - splitMsg := strings.Split(string(val), ",") - floatVal, err := strconv.ParseFloat(splitMsg[1], 64) - return floatVal, err -} - -// Properties returns the status of optional properties on the motor. -func (m *Ezopmp) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil -} - -// Stop turns the power to the motor off immediately, without any gradual step down. -func (m *Ezopmp) Stop(ctx context.Context, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - command := []byte(stop) - return m.writeRegWithCheck(ctx, command) -} - -// IsMoving returns whether or not the motor is currently moving. -func (m *Ezopmp) IsMoving(ctx context.Context) (bool, error) { - on, _, err := m.IsPowered(ctx, nil) - return on, err -} - -// IsPowered returns whether or not the motor is currently on, and how much power it's getting. -func (m *Ezopmp) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - command := []byte(dispenseStatus) - writeErr := m.writeReg(ctx, command) - if writeErr != nil { - return false, 0, errors.Wrap(writeErr, "error in IsPowered") - } - val, err := m.readReg(ctx) - if err != nil { - return false, 0, errors.Wrap(err, "error in IsPowered") - } - - splitMsg := strings.Split(string(val), ",") - - pumpStatus, err := strconv.ParseFloat(splitMsg[2], 64) - if err != nil { - return false, 0, errors.Wrap(err, "error in IsPowered") - } - - if pumpStatus == 1 || pumpStatus == -1 { - return true, m.powerPct, nil - } - return false, 0.0, nil -} diff --git a/components/motor/i2cmotors/ezopmp_nonlinux.go b/components/motor/i2cmotors/ezopmp_nonlinux.go deleted file mode 100644 index 6d820232ac2..00000000000 --- a/components/motor/i2cmotors/ezopmp_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package ezopmp is a motor driver for the hydrogarden pump, but our implementation is Linux-only. -package ezopmp diff --git a/components/motor/motor.go b/components/motor/motor.go deleted file mode 100644 index 8f0642b284e..00000000000 --- a/components/motor/motor.go +++ /dev/null @@ -1,173 +0,0 @@ -package motor - -import ( - "context" - - pb "go.viam.com/api/component/motor/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Motor]{ - Status: resource.StatusFunc(CreateStatus), - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterMotorServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.MotorService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: position.String(), - }, newPositionCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: isPowered.String(), - }, newIsPoweredCollector) -} - -// SubtypeName is a constant that identifies the component resource API string "motor". -const SubtypeName = "motor" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// A Motor represents a physical motor connected to a board. -// -// SetPower example: -// -// myMotorComponent, err := motor.FromRobot(machine, "my_motor") -// // Set the motor power to 40% forwards. -// myMotorComponent.SetPower(context.Background(), 0.4, nil) -// -// GoFor example: -// -// myMotorComponent, err := motor.FromRobot(machine, "my_motor") -// // Turn the motor 7.2 revolutions at 60 RPM. -// myMotorComponent.GoFor(context.Background(), 60, 7.2, nil) -// -// GoTo example: -// -// // Turn the motor to 8.3 revolutions from home at 75 RPM. -// myMotorComponent.GoTo(context.Background(), 75, 8.3, nil) -// -// ResetZeroPostion example: -// -// // Set the current position as the new home position with no offset. -// myMotorComponent.ResetZeroPosition(context.Background(), 0.0, nil) -// -// Position example: -// -// // Get the current position of an encoded motor. -// position, err := myMotorComponent.Position(context.Background(), nil) -// -// // Log the position -// logger.Info("Position:") -// logger.Info(position) -// -// Properties example: -// -// // Return whether or not the motor supports certain optional features. -// properties, err := myMotorComponent.Properties(context.Background(), nil) -// -// // Log the properties. -// logger.Info("Properties:") -// logger.Info(properties) -// -// IsPowered example: -// -// // Check whether the motor is currently running. -// powered, pct, err := myMotorComponent.IsPowered(context.Background(), nil) -// -// logger.Info("Is powered?") -// logger.Info(powered) -// logger.Info("Power percent:") -// logger.Info(pct) -type Motor interface { - resource.Resource - resource.Actuator - - // SetPower sets the percentage of power the motor should employ between -1 and 1. - // Negative power corresponds to a backward direction of rotation - SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error - - // GoFor instructs the motor to go in a specific direction for a specific amount of - // revolutions at a given speed in revolutions per minute. Both the RPM and the revolutions - // can be assigned negative values to move in a backwards direction. Note: if both are - // negative the motor will spin in the forward direction. - // If revolutions is 0, this will run the motor at rpm indefinitely - // If revolutions != 0, this will block until the number of revolutions has been completed or another operation comes in. - GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error - - // GoTo instructs the motor to go to a specific position (provided in revolutions from home/zero), - // at a specific speed. Regardless of the directionality of the RPM this function will move the motor - // towards the specified target/position - // This will block until the position has been reached - GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error - - // Set an encoded motor's current position (+/- offset) to be the new zero (home) position. - ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error - - // Position reports the position of an encoded motor based on its encoder. If it's not supported, - // the returned data is undefined. The unit returned is the number of revolutions which is - // intended to be fed back into calls of GoFor. - Position(ctx context.Context, extra map[string]interface{}) (float64, error) - - // Properties returns whether or not the motor supports certain optional properties. - Properties(ctx context.Context, extra map[string]interface{}) (Properties, error) - - // IsPowered returns whether or not the motor is currently on, and the percent power (between 0 - // and 1, if the motor is off then the percent power will be 0). - IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) -} - -// Named is a helper for getting the named Motor's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromDependencies is a helper for getting the named motor from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Motor, error) { - return resource.FromDependencies[Motor](deps, Named(name)) -} - -// FromRobot is a helper for getting the named motor from the given Robot. -func FromRobot(r robot.Robot, name string) (Motor, error) { - return robot.ResourceFromRobot[Motor](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all motor names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// CreateStatus creates a status from the motor. -func CreateStatus(ctx context.Context, m Motor) (*pb.Status, error) { - isPowered, _, err := m.IsPowered(ctx, nil) - if err != nil { - return nil, err - } - properties, err := m.Properties(ctx, nil) - if err != nil { - return nil, err - } - var position float64 - if properties.PositionReporting { - position, err = m.Position(ctx, nil) - if err != nil { - return nil, err - } - } - isMoving, err := m.IsMoving(ctx) - if err != nil { - return nil, err - } - return &pb.Status{ - IsPowered: isPowered, - Position: position, - IsMoving: isMoving, - }, nil -} diff --git a/components/motor/motor_test.go b/components/motor/motor_test.go deleted file mode 100644 index 3f9908a0e6e..00000000000 --- a/components/motor/motor_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package motor_test - -import ( - "context" - "testing" - - "github.com/go-viper/mapstructure/v2" - "github.com/pkg/errors" - pb "go.viam.com/api/component/motor/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testMotorName = "motor1" - failMotorName = "motor2" - fakeMotorName = "motor3" -) - -func TestStatusValid(t *testing.T) { - status := &pb.Status{IsPowered: true, Position: 7.7, IsMoving: true} - newStruct, err := protoutils.StructToStructPb(status) - test.That(t, err, test.ShouldBeNil) - test.That( - t, - newStruct.AsMap(), - test.ShouldResemble, - map[string]interface{}{"is_powered": true, "position": 7.7, "is_moving": true}, - ) - - convMap := &pb.Status{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(newStruct.AsMap()) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, status) - - status = &pb.Status{Position: 7.7} - newStruct, err = protoutils.StructToStructPb(status) - test.That(t, err, test.ShouldBeNil) - test.That(t, newStruct.AsMap(), test.ShouldResemble, map[string]interface{}{"position": 7.7}) - - convMap = &pb.Status{} - decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(newStruct.AsMap()) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, status) -} - -func TestCreateStatus(t *testing.T) { - status := &pb.Status{IsPowered: true, Position: 7.7, IsMoving: true} - - injectMotor := &inject.Motor{} - injectMotor.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return status.IsPowered, 1.0, nil - } - injectMotor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{PositionReporting: true}, nil - } - injectMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return status.Position, nil - } - injectMotor.IsMovingFunc = func(context.Context) (bool, error) { - return true, nil - } - - t.Run("working", func(t *testing.T) { - status1, err := motor.CreateStatus(context.Background(), injectMotor) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - - resourceAPI, ok, err := resource.LookupAPIRegistration[motor.Motor](motor.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - status2, err := resourceAPI.Status(context.Background(), injectMotor) - test.That(t, err, test.ShouldBeNil) - test.That(t, status2, test.ShouldResemble, status) - }) - - t.Run("not moving", func(t *testing.T) { - injectMotor.IsMovingFunc = func(context.Context) (bool, error) { - return false, nil - } - - status2 := &pb.Status{IsPowered: true, Position: 7.7, IsMoving: false} - status1, err := motor.CreateStatus(context.Background(), injectMotor) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status2) - }) - - t.Run("fail on Position", func(t *testing.T) { - errFail := errors.New("can't get position") - injectMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, errFail - } - _, err := motor.CreateStatus(context.Background(), injectMotor) - test.That(t, err, test.ShouldBeError, errFail) - }) - - t.Run("position not supported", func(t *testing.T) { - injectMotor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{PositionReporting: false}, nil - } - - status1, err := motor.CreateStatus(context.Background(), injectMotor) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, &pb.Status{IsPowered: true}) - }) - - t.Run("fail on Properties", func(t *testing.T) { - errFail := errors.New("can't get properties") - injectMotor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, errFail - } - _, err := motor.CreateStatus(context.Background(), injectMotor) - test.That(t, err, test.ShouldBeError, errFail) - }) - - t.Run("fail on IsPowered", func(t *testing.T) { - errFail := errors.New("can't get is powered") - injectMotor.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return false, 0.0, errFail - } - _, err := motor.CreateStatus(context.Background(), injectMotor) - test.That(t, err, test.ShouldBeError, errFail) - }) -} diff --git a/components/motor/properties.go b/components/motor/properties.go deleted file mode 100644 index 7e7dae58af1..00000000000 --- a/components/motor/properties.go +++ /dev/null @@ -1,29 +0,0 @@ -// Package motor contains a struct representing optional motor properties -package motor - -import ( - pb "go.viam.com/api/component/motor/v1" -) - -// Properties is struct contaning the motor properties. -type Properties struct { - PositionReporting bool -} - -// ProtoFeaturesToProperties takes a GetPropertiesResponse and returns -// an equivalent Properties struct. -func ProtoFeaturesToProperties(resp *pb.GetPropertiesResponse) Properties { - return Properties{ - PositionReporting: resp.PositionReporting, - } -} - -// PropertiesToProtoResponse takes a Properties struct (indicating -// whether the property is supported) and converts it to a GetPropertiesResponse. -func PropertiesToProtoResponse( - props Properties, -) (*pb.GetPropertiesResponse, error) { - return &pb.GetPropertiesResponse{ - PositionReporting: props.PositionReporting, - }, nil -} diff --git a/components/motor/register/register.go b/components/motor/register/register.go deleted file mode 100644 index 90cf47566f4..00000000000 --- a/components/motor/register/register.go +++ /dev/null @@ -1,15 +0,0 @@ -// Package register registers all relevant motors -package register - -import ( - // for motors. - _ "go.viam.com/rdk/components/motor/dimensionengineering" - _ "go.viam.com/rdk/components/motor/dmc4000" - _ "go.viam.com/rdk/components/motor/fake" - _ "go.viam.com/rdk/components/motor/gpio" - _ "go.viam.com/rdk/components/motor/gpiostepper" - _ "go.viam.com/rdk/components/motor/i2cmotors" - _ "go.viam.com/rdk/components/motor/roboclaw" - _ "go.viam.com/rdk/components/motor/tmcstepper" - _ "go.viam.com/rdk/components/motor/ulnstepper" -) diff --git a/components/motor/roboclaw/roboclaw.go b/components/motor/roboclaw/roboclaw.go deleted file mode 100644 index 0cef4d98743..00000000000 --- a/components/motor/roboclaw/roboclaw.go +++ /dev/null @@ -1,374 +0,0 @@ -// Package roboclaw is the driver for the roboclaw motor drivers -// NOTE: This implementation is experimental and incomplete. Expect backward-breaking changes. -package roboclaw - -/* Manufacturer: Basicmicro -Supported Models: Roboclaw 2x7A, Roboclaw 2x15A (other models have not been tested) -Resources: - 2x7A DataSheet: https://downloads.basicmicro.com/docs/roboclaw_datasheet_2x7A.pdf - 2x15A DataSheet: https://downloads.basicmicro.com/docs/roboclaw_datasheet_2x15A.pdf - User Manual: https://downloads.basicmicro.com/docs/roboclaw_user_manual.pdf - -This driver can connect to the roboclaw DC motor controller using a usb connection given as a serial path. -Note that the roboclaw must be initialized using the BasicMicro Motion Studio application prior to use. -The roboclaw must be in packet serial mode. The default address is 128. -Encoders can be attached to the roboclaw controller using the EN1 and EN2 pins. If encoders are connected, -update the ticks_per_rotation field in the config. - -Configuration: -Motor Channel: specfies the channel the motor is connected to on the controller (1 or 2) -Serial baud rate: default of the roboclaw is 38400 -Serial path: path to serial file -*/ - -import ( - "context" - "fmt" - "math" - "time" - - "github.com/CPRT/roboclaw" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" - rutils "go.viam.com/rdk/utils" -) - -var ( - model = resource.DefaultModelFamily.WithModel("roboclaw") - connections map[string]*roboclaw.Roboclaw - baudRates map[*roboclaw.Roboclaw]int - validBaudRates = []uint{460800, 230400, 115200, 57600, 38400, 19200, 9600, 2400} - newConnectionNeeded bool -) - -// Note that this maxRPM value was determined through very limited testing. -const ( - maxRPM = 250 - minutesToMS = 60000 -) - -// Config is used for converting motor config attributes. -type Config struct { - SerialPath string `json:"serial_path"` - SerialBaud int `json:"serial_baud_rate"` - Channel int `json:"motor_channel"` // this is 1 or 2 - Address int `json:"address,omitempty"` - TicksPerRotation int `json:"ticks_per_rotation,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - if conf.Channel < 1 || conf.Channel > 2 { - return nil, conf.wrongChannelError() - } - if conf.SerialPath == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "serial_path") - } - if conf.Address != 0 && (conf.Address < 128 || conf.Address > 135) { - return nil, errors.New("serial address must be between 128 and 135") - } - - if conf.TicksPerRotation < 0 { - return nil, resource.NewConfigValidationError(path, errors.New("Ticks Per Rotation must be a positive number")) - } - - if !rutils.ValidateBaudRate(validBaudRates, conf.SerialBaud) { - return nil, resource.NewConfigValidationError(path, errors.Errorf("Baud rate invalid, must be one of these values: %v", validBaudRates)) - } - return nil, nil -} - -// Reconfigure automatically reconfigures the roboclaw when the config changes. -func (m *roboclawMotor) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - newConnectionNeeded = false - newConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - if m.conf.TicksPerRotation != newConfig.TicksPerRotation { - m.conf.TicksPerRotation = newConfig.TicksPerRotation - } - - if m.conf.SerialBaud != newConfig.SerialBaud { - m.conf.SerialBaud = newConfig.SerialBaud - newConnectionNeeded = true - } - - if m.conf.SerialPath != newConfig.SerialPath { - m.conf.SerialBaud = newConfig.SerialBaud - newConnectionNeeded = true - } - - if m.conf.Channel != newConfig.Channel { - m.conf.Channel = newConfig.Channel - } - - if newConfig.Address != 0 && m.conf.Address != newConfig.Address { - m.conf.Address = newConfig.Address - m.addr = uint8(newConfig.Address) - newConnectionNeeded = true - } - - if newConnectionNeeded { - conn, err := getOrCreateConnection(newConfig) - if err != nil { - return err - } - m.conn = conn - } - - return nil -} - -func (conf *Config) wrongChannelError() error { - return fmt.Errorf("roboclaw motor channel has to be 1 or 2, but is %d", conf.Channel) -} - -func init() { - connections = make(map[string]*roboclaw.Roboclaw) - baudRates = make(map[*roboclaw.Roboclaw]int) - resource.RegisterComponent( - motor.API, - model, - resource.Registration[motor.Motor, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - return newRoboClaw(conf, logger) - }, - }, - ) -} - -func getOrCreateConnection(config *Config) (*roboclaw.Roboclaw, error) { - // Check if there is already a roboclaw motor connection with the same serial config. This allows - // multiple motors to share the same controller without stepping on each other. - connection, ok := connections[config.SerialPath] - if !ok { - c := &roboclaw.Config{Name: config.SerialPath, Retries: 3} - if config.SerialBaud > 0 { - c.Baud = config.SerialBaud - } - newConn, err := roboclaw.Init(c) - if err != nil { - return nil, err - } - connections[config.SerialPath] = newConn - baudRates[newConn] = config.SerialBaud - return newConn, nil - } - - if baudRates[connection] != config.SerialBaud { - return nil, errors.New("cannot have multiple roboclaw motors with different baud rates") - } - return connection, nil -} - -func newRoboClaw(conf resource.Config, logger logging.Logger) (motor.Motor, error) { - motorConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - if motorConfig.Channel < 1 || motorConfig.Channel > 2 { - return nil, motorConfig.wrongChannelError() - } - - if motorConfig.Address == 0 { - motorConfig.Address = 128 - } - - c, err := getOrCreateConnection(motorConfig) - if err != nil { - return nil, err - } - - return &roboclawMotor{ - Named: conf.ResourceName().AsNamed(), - conn: c, - conf: motorConfig, - addr: uint8(motorConfig.Address), - logger: logger, - opMgr: operation.NewSingleOperationManager(), - maxRPM: maxRPM, - }, nil -} - -type roboclawMotor struct { - resource.Named - resource.TriviallyCloseable - conn *roboclaw.Roboclaw - conf *Config - - addr uint8 - maxRPM float64 - - logger logging.Logger - opMgr *operation.SingleOperationManager - - powerPct float64 -} - -func (m *roboclawMotor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - - if powerPct > 1 { - powerPct = 1 - } else if powerPct < -1 { - powerPct = -1 - } - - switch m.conf.Channel { - case 1: - m.powerPct = powerPct - return m.conn.DutyM1(m.addr, int16(powerPct*32767)) - case 2: - m.powerPct = powerPct - return m.conn.DutyM2(m.addr, int16(powerPct*32767)) - default: - return m.conf.wrongChannelError() - } -} - -func goForMath(rpm, revolutions float64) (float64, time.Duration) { - // If revolutions is 0, the returned wait duration will be 0 representing that - // the motor should run indefinitely. - if revolutions == 0 { - powerPct := 1.0 - return powerPct, 0 - } - - dir := rpm * revolutions / math.Abs(revolutions*rpm) - powerPct := math.Abs(rpm) / maxRPM * dir - waitDur := time.Duration(math.Abs(revolutions/rpm)*minutesToMS) * time.Millisecond - return powerPct, waitDur -} - -func (m *roboclawMotor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - speed := math.Abs(rpm) - if speed < 0.1 { - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - } - - // If no encoders present, distance traveled is estimated based on max RPM. - if m.conf.TicksPerRotation == 0 { - if rpm > maxRPM { - rpm = maxRPM - } else if rpm < -1*maxRPM { - rpm = -1 * maxRPM - } - powerPct, waitDur := goForMath(rpm, revolutions) - m.logger.CInfo(ctx, "distance traveled is a time based estimation with max RPM 250. For increased accuracy, connect encoders") - err := m.SetPower(ctx, powerPct, extra) - if err != nil { - return errors.Wrap(err, "error in GoFor") - } - if revolutions == 0 { - return nil - } - if m.opMgr.NewTimedWaitOp(ctx, waitDur) { - return m.Stop(ctx, extra) - } - } - - ctx, done := m.opMgr.New(ctx) - defer done() - - ticks := uint32(revolutions * float64(m.conf.TicksPerRotation)) - ticksPerSecond := int32((rpm * float64(m.conf.TicksPerRotation)) / 60) - - var err error - - switch m.conf.Channel { - case 1: - err = m.conn.SpeedDistanceM1(m.addr, ticksPerSecond, ticks, true) - case 2: - err = m.conn.SpeedDistanceM2(m.addr, ticksPerSecond, ticks, true) - default: - return m.conf.wrongChannelError() - } - if err != nil { - return err - } - return m.opMgr.WaitTillNotPowered(ctx, time.Millisecond, m, m.Stop) -} - -func (m *roboclawMotor) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - if m.conf.TicksPerRotation == 0 { - return errors.New("roboclaw needs an encoder connected to use GoTo") - } - pos, err := m.Position(ctx, extra) - if err != nil { - return err - } - return m.GoFor(ctx, rpm, positionRevolutions-pos, extra) -} - -func (m *roboclawMotor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - newTicks := int32(-1 * offset * float64(m.conf.TicksPerRotation)) - switch m.conf.Channel { - case 1: - return m.conn.SetEncM1(m.addr, newTicks) - case 2: - return m.conn.SetEncM2(m.addr, newTicks) - default: - return m.conf.wrongChannelError() - } -} - -func (m *roboclawMotor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - var ticks uint32 - var err error - - switch m.conf.Channel { - case 1: - ticks, _, err = m.conn.ReadEncM1(m.addr) - case 2: - ticks, _, err = m.conn.ReadEncM2(m.addr) - default: - return 0, m.conf.wrongChannelError() - } - if err != nil { - return 0, err - } - return float64(ticks) / float64(m.conf.TicksPerRotation), nil -} - -func (m *roboclawMotor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil -} - -func (m *roboclawMotor) Stop(ctx context.Context, extra map[string]interface{}) error { - return m.SetPower(ctx, 0, extra) -} - -func (m *roboclawMotor) IsMoving(ctx context.Context) (bool, error) { - on, _, err := m.IsPowered(ctx, nil) - return on, err -} - -func (m *roboclawMotor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - pow1, pow2, err := m.conn.ReadPWMs(m.addr) - if err != nil { - return false, 0.0, err - } - switch m.conf.Channel { - case 1: - return pow1 != 0, m.powerPct, nil - case 2: - return pow2 != 0, m.powerPct, nil - default: - return false, 0.0, m.conf.wrongChannelError() - } -} diff --git a/components/motor/server.go b/components/motor/server.go deleted file mode 100644 index 5cb4aa61dbd..00000000000 --- a/components/motor/server.go +++ /dev/null @@ -1,177 +0,0 @@ -// Package motor contains a gRPC based motor service server -package motor - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/motor/v1" - - "go.viam.com/rdk/operation" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -type serviceServer struct { - pb.UnimplementedMotorServiceServer - coll resource.APIResourceCollection[Motor] -} - -// NewRPCServiceServer constructs a motor gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Motor]) interface{} { - return &serviceServer{coll: coll} -} - -// SetPower sets the percentage of power the motor of the underlying robot should employ between 0-1. -func (server *serviceServer) SetPower( - ctx context.Context, - req *pb.SetPowerRequest, -) (*pb.SetPowerResponse, error) { - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - return &pb.SetPowerResponse{}, motor.SetPower(ctx, req.GetPowerPct(), req.Extra.AsMap()) -} - -// GoFor requests the motor of the underlying robot to go for a certain amount based off -// the request. -func (server *serviceServer) GoFor( - ctx context.Context, - req *pb.GoForRequest, -) (*pb.GoForResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - - return &pb.GoForResponse{}, motor.GoFor(ctx, req.GetRpm(), req.GetRevolutions(), req.Extra.AsMap()) -} - -// GetPosition reports the position of the motor of the underlying robot -// based on its encoder. If it's not supported, the returned data is undefined. -// The unit returned is the number of revolutions which is intended to be fed -// back into calls of GoFor. -func (server *serviceServer) GetPosition( - ctx context.Context, - req *pb.GetPositionRequest, -) (*pb.GetPositionResponse, error) { - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - - pos, err := motor.Position(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetPositionResponse{Position: pos}, nil -} - -// GetProperties returns a message of booleans indicating which optional properties the robot's motor supports. -func (server *serviceServer) GetProperties( - ctx context.Context, - req *pb.GetPropertiesRequest, -) (*pb.GetPropertiesResponse, error) { - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - props, err := motor.Properties(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return PropertiesToProtoResponse(props) -} - -// Stop turns the motor of the underlying robot off. -func (server *serviceServer) Stop( - ctx context.Context, - req *pb.StopRequest, -) (*pb.StopResponse, error) { - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - - return &pb.StopResponse{}, motor.Stop(ctx, req.Extra.AsMap()) -} - -// IsPowered returns whether or not the motor of the underlying robot is currently on. -func (server *serviceServer) IsPowered( - ctx context.Context, - req *pb.IsPoweredRequest, -) (*pb.IsPoweredResponse, error) { - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - - isOn, powerPct, err := motor.IsPowered(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.IsPoweredResponse{IsOn: isOn, PowerPct: powerPct}, nil -} - -// GoTo requests the motor of the underlying robot to go a specific position. -func (server *serviceServer) GoTo( - ctx context.Context, - req *pb.GoToRequest, -) (*pb.GoToResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - - return &pb.GoToResponse{}, motor.GoTo(ctx, req.GetRpm(), req.GetPositionRevolutions(), req.Extra.AsMap()) -} - -// ResetZeroPosition sets the current position of the motor specified by the request -// (adjusted by a given offset) to be its new zero position. -func (server *serviceServer) ResetZeroPosition( - ctx context.Context, - req *pb.ResetZeroPositionRequest, -) (*pb.ResetZeroPositionResponse, error) { - motorName := req.GetName() - motor, err := server.coll.Resource(motorName) - if err != nil { - return nil, err - } - - return &pb.ResetZeroPositionResponse{}, motor.ResetZeroPosition(ctx, req.GetOffset(), req.Extra.AsMap()) -} - -// IsMoving queries of a component is in motion. -func (server *serviceServer) IsMoving(ctx context.Context, req *pb.IsMovingRequest) (*pb.IsMovingResponse, error) { - motor, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - moving, err := motor.IsMoving(ctx) - if err != nil { - return nil, err - } - return &pb.IsMovingResponse{IsMoving: moving}, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - motor, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, motor, req) -} diff --git a/components/motor/server_test.go b/components/motor/server_test.go deleted file mode 100644 index e3265f4e4fd..00000000000 --- a/components/motor/server_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package motor_test - -import ( - "context" - "errors" - "testing" - - pb "go.viam.com/api/component/motor/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errPositionUnavailable = errors.New("position unavailable") - errResetZeroFailed = errors.New("set to zero failed") - errPropertiesNotFound = errors.New("properties not found") - errGetPropertiesFailed = errors.New("get properties failed") - errSetPowerFailed = errors.New("set power failed") - errGoForFailed = errors.New("go for failed") - errStopFailed = errors.New("stop failed") - errIsPoweredFailed = errors.New("could not determine if motor is on") - errGoToFailed = errors.New("go to failed") -) - -func newServer() (pb.MotorServiceServer, *inject.Motor, *inject.Motor, error) { - injectMotor1 := &inject.Motor{} - injectMotor2 := &inject.Motor{} - - resourceMap := map[resource.Name]motor.Motor{ - motor.Named(testMotorName): injectMotor1, - motor.Named(failMotorName): injectMotor2, - } - - injectSvc, err := resource.NewAPIResourceCollection(motor.API, resourceMap) - if err != nil { - return nil, nil, nil, err - } - return motor.NewRPCServiceServer(injectSvc).(pb.MotorServiceServer), injectMotor1, injectMotor2, nil -} - -//nolint:dupl -func TestServerSetPower(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.SetPowerRequest{Name: fakeMotorName} - resp, err := motorServer.SetPower(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingMotor.SetPowerFunc = func(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - return errSetPowerFailed - } - req = pb.SetPowerRequest{Name: failMotorName, PowerPct: 0.5} - resp, err = motorServer.SetPower(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.SetPowerFunc = func(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - return nil - } - req = pb.SetPowerRequest{Name: testMotorName, PowerPct: 0.5} - resp, err = motorServer.SetPower(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -//nolint:dupl -func TestServerGoFor(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.GoForRequest{Name: fakeMotorName} - resp, err := motorServer.GoFor(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingMotor.GoForFunc = func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return errGoForFailed - } - req = pb.GoForRequest{Name: failMotorName, Rpm: 42.0, Revolutions: 42.1} - resp, err = motorServer.GoFor(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.GoForFunc = func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - return nil - } - req = pb.GoForRequest{Name: testMotorName, Rpm: 42.0, Revolutions: 42.1} - resp, err = motorServer.GoFor(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerPosition(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.GetPositionRequest{Name: fakeMotorName} - resp, err := motorServer.GetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - - failingMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, errPositionUnavailable - } - req = pb.GetPositionRequest{Name: failMotorName} - resp, err = motorServer.GetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 42.0, nil - } - req = pb.GetPositionRequest{Name: testMotorName} - resp, err = motorServer.GetPosition(context.Background(), &req) - test.That(t, resp.GetPosition(), test.ShouldEqual, 42.0) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerGetProperties(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.GetPropertiesRequest{Name: fakeMotorName} - resp, err := motorServer.GetProperties(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingMotor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, errGetPropertiesFailed - } - req = pb.GetPropertiesRequest{Name: failMotorName} - resp, err = motorServer.GetProperties(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil - } - req = pb.GetPropertiesRequest{Name: testMotorName} - resp, err = motorServer.GetProperties(context.Background(), &req) - test.That(t, resp.GetPositionReporting(), test.ShouldBeTrue) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerStop(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.StopRequest{Name: fakeMotorName} - resp, err := motorServer.Stop(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingMotor.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopFailed - } - req = pb.StopRequest{Name: failMotorName} - resp, err = motorServer.Stop(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return nil - } - req = pb.StopRequest{Name: testMotorName} - resp, err = motorServer.Stop(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerIsOn(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.IsPoweredRequest{Name: fakeMotorName} - resp, err := motorServer.IsPowered(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingMotor.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return false, 0.0, errIsPoweredFailed - } - req = pb.IsPoweredRequest{Name: failMotorName} - resp, err = motorServer.IsPowered(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.IsPoweredFunc = func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return true, 1.0, nil - } - req = pb.IsPoweredRequest{Name: testMotorName} - resp, err = motorServer.IsPowered(context.Background(), &req) - test.That(t, resp.GetIsOn(), test.ShouldBeTrue) - test.That(t, resp.GetPowerPct(), test.ShouldEqual, 1.0) - test.That(t, err, test.ShouldBeNil) -} - -//nolint:dupl -func TestServerGoTo(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.GoToRequest{Name: fakeMotorName} - resp, err := motorServer.GoTo(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingMotor.GoToFunc = func(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { - return errGoToFailed - } - req = pb.GoToRequest{Name: failMotorName, Rpm: 20.0, PositionRevolutions: 2.5} - resp, err = motorServer.GoTo(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.GoToFunc = func(ctx context.Context, rpm, position float64, extra map[string]interface{}) error { - return nil - } - req = pb.GoToRequest{Name: testMotorName, Rpm: 20.0, PositionRevolutions: 2.5} - resp, err = motorServer.GoTo(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -//nolint:dupl -func TestServerResetZeroPosition(t *testing.T) { - motorServer, workingMotor, failingMotor, _ := newServer() - - // fails on a bad motor - req := pb.ResetZeroPositionRequest{Name: fakeMotorName} - resp, err := motorServer.ResetZeroPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - failingMotor.ResetZeroPositionFunc = func(ctx context.Context, offset float64, extra map[string]interface{}) error { - return errResetZeroFailed - } - req = pb.ResetZeroPositionRequest{Name: failMotorName, Offset: 1.1} - resp, err = motorServer.ResetZeroPosition(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldNotBeNil) - - workingMotor.ResetZeroPositionFunc = func(ctx context.Context, offset float64, extra map[string]interface{}) error { - return nil - } - req = pb.ResetZeroPositionRequest{Name: testMotorName, Offset: 1.1} - resp, err = motorServer.ResetZeroPosition(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -func TestServerExtraParams(t *testing.T) { - motorServer, workingMotor, _, _ := newServer() - - var actualExtra map[string]interface{} - workingMotor.ResetZeroPositionFunc = func(ctx context.Context, offset float64, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - - expectedExtra := map[string]interface{}{"foo": "bar", "baz": []interface{}{1., 2., 3.}} - - ext, err := protoutils.StructToStructPb(expectedExtra) - test.That(t, err, test.ShouldBeNil) - - req := pb.ResetZeroPositionRequest{Name: testMotorName, Offset: 1.1, Extra: ext} - resp, err := motorServer.ResetZeroPosition(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, actualExtra["foo"], test.ShouldEqual, expectedExtra["foo"]) - test.That(t, actualExtra["baz"], test.ShouldResemble, expectedExtra["baz"]) -} diff --git a/components/motor/tmcstepper/stepper_motor_tmc.go b/components/motor/tmcstepper/stepper_motor_tmc.go deleted file mode 100644 index 189351abc63..00000000000 --- a/components/motor/tmcstepper/stepper_motor_tmc.go +++ /dev/null @@ -1,710 +0,0 @@ -//go:build linux - -// Package tmcstepper implements a TMC stepper motor. -package tmcstepper - -import ( - "context" - "math" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -// PinConfig defines the mapping of where motor are wired. -type PinConfig struct { - EnablePinLow string `json:"en_low,omitempty"` -} - -// TMC5072Config describes the configuration of a motor. -type TMC5072Config struct { - Pins PinConfig `json:"pins"` - BoardName string `json:"board,omitempty"` // used solely for the PinConfig - MaxRPM float64 `json:"max_rpm,omitempty"` - MaxAcceleration float64 `json:"max_acceleration_rpm_per_sec,omitempty"` - TicksPerRotation int `json:"ticks_per_rotation"` - SPIBus string `json:"spi_bus"` - ChipSelect string `json:"chip_select"` - Index int `json:"index"` - SGThresh int32 `json:"sg_thresh,omitempty"` - HomeRPM float64 `json:"home_rpm,omitempty"` - CalFactor float64 `json:"cal_factor,omitempty"` - RunCurrent int32 `json:"run_current,omitempty"` // 1-32 as a percentage of rsense voltage, 15 default - HoldCurrent int32 `json:"hold_current,omitempty"` // 1-32 as a percentage of rsense voltage, 8 default - HoldDelay int32 `json:"hold_delay,omitempty"` // 0=instant powerdown, 1-15=delay * 2^18 clocks, 6 default -} - -var model = resource.DefaultModelFamily.WithModel("TMC5072") - -// Validate ensures all parts of the config are valid. -func (config *TMC5072Config) Validate(path string) ([]string, error) { - var deps []string - if config.Pins.EnablePinLow != "" { - if config.BoardName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - deps = append(deps, config.BoardName) - } - if config.SPIBus == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "spi_bus") - } - if config.ChipSelect == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "chip_select") - } - if config.Index <= 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "index") - } - if config.TicksPerRotation <= 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "ticks_per_rotation") - } - return deps, nil -} - -func init() { - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *TMC5072Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - newConf, err := resource.NativeConfig[*TMC5072Config](conf) - if err != nil { - return nil, err - } - return NewMotor(ctx, deps, *newConf, conf.ResourceName(), logger) - }, - }) -} - -// A Motor represents a brushless motor connected via a TMC controller chip (ex: TMC5072). -type Motor struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - bus buses.SPI - csPin string - index int - enLowPin board.GPIOPin - stepsPerRev int - homeRPM float64 - maxRPM float64 - maxAcc float64 - fClk float64 - logger logging.Logger - opMgr *operation.SingleOperationManager - powerPct float64 - motorName string -} - -// TMC5072 Values. -const ( - baseClk = 13200000 // Nominal 13.2mhz internal clock speed - uSteps = 256 // Microsteps per fullstep -) - -// TMC5072 Register Addressses (for motor index 0) -// TODO full register set. -const ( - // add 0x10 for motor 2. - chopConf = 0x6C - coolConf = 0x6D - drvStatus = 0x6F - - // add 0x20 for motor 2. - rampMode = 0x20 - xActual = 0x21 - // vActual = 0x22. - vStart = 0x23 - a1 = 0x24 - v1 = 0x25 - aMax = 0x26 - vMax = 0x27 - dMax = 0x28 - d1 = 0x2A - vStop = 0x2B - xTarget = 0x2D - iHoldIRun = 0x30 - vCoolThres = 0x31 - swMode = 0x34 - rampStat = 0x35 -) - -// TMC5072 ramp modes. -const ( - modePosition = int32(0) - modeVelPos = int32(1) - modeVelNeg = int32(2) - modeHold = int32(3) -) - -// NewMotor returns a TMC5072 driven motor. -func NewMotor(ctx context.Context, deps resource.Dependencies, c TMC5072Config, name resource.Name, - logger logging.Logger, -) (motor.Motor, error) { - bus := buses.NewSpiBus(c.SPIBus) - return makeMotor(ctx, deps, c, name, logger, bus) -} - -// makeMotor returns a TMC5072 driven motor. It is separate from NewMotor, above, so you can inject -// a mock SPI bus in here during testing. -func makeMotor(ctx context.Context, deps resource.Dependencies, c TMC5072Config, name resource.Name, - logger logging.Logger, bus buses.SPI, -) (motor.Motor, error) { - if c.CalFactor == 0 { - c.CalFactor = 1.0 - } - - if c.TicksPerRotation == 0 { - return nil, errors.New("ticks_per_rotation isn't set") - } - - if c.HomeRPM == 0 { - logger.CWarn(ctx, "home_rpm not set: defaulting to 1/4 of max_rpm") - c.HomeRPM = c.MaxRPM / 4 - } - c.HomeRPM *= -1 - - m := &Motor{ - Named: name.AsNamed(), - bus: bus, - csPin: c.ChipSelect, - index: c.Index, - stepsPerRev: c.TicksPerRotation * uSteps, - homeRPM: c.HomeRPM, - maxRPM: c.MaxRPM, - maxAcc: c.MaxAcceleration, - fClk: baseClk / c.CalFactor, - logger: logger, - opMgr: operation.NewSingleOperationManager(), - motorName: name.ShortName(), - } - - rawMaxAcc := m.rpmsToA(m.maxAcc) - - if c.SGThresh > 63 { - c.SGThresh = 63 - } else if c.SGThresh < -64 { - c.SGThresh = -64 - } - // The register is a 6 bit signed int - if c.SGThresh < 0 { - c.SGThresh = int32(64 + math.Abs(float64(c.SGThresh))) - } - - // Hold/Run currents are 0-31 (linear scale), - // but we'll take 1-32 so zero can remain default - if c.RunCurrent == 0 { - c.RunCurrent = 15 // Default - } else { - c.RunCurrent-- - } - - if c.RunCurrent > 31 { - c.RunCurrent = 31 - } else if c.RunCurrent < 0 { - c.RunCurrent = 0 - } - - if c.HoldCurrent == 0 { - c.HoldCurrent = 8 // Default - } else { - c.HoldCurrent-- - } - - if c.HoldCurrent > 31 { - c.HoldCurrent = 31 - } else if c.HoldCurrent < 0 { - c.HoldCurrent = 0 - } - - // HoldDelay is 2^18 clocks per step between current stepdown phases - // Approximately 1/16th of a second for default 16mhz clock - // Repurposing zero for default, and -1 for "instant" - if c.HoldDelay == 0 { - c.HoldDelay = 6 // default - } else if c.HoldDelay < 0 { - c.HoldDelay = 0 - } - - if c.HoldDelay > 15 { - c.HoldDelay = 15 - } - - coolConfig := c.SGThresh << 16 - - iCfg := c.HoldDelay<<16 | c.RunCurrent<<8 | c.HoldCurrent - - err := multierr.Combine( - m.writeReg(ctx, chopConf, 0x000100C3), // TOFF=3, HSTRT=4, HEND=1, TBL=2, CHM=0 (spreadCycle) - m.writeReg(ctx, iHoldIRun, iCfg), - m.writeReg(ctx, coolConf, coolConfig), // Sets just the SGThreshold (for now) - - // Set max acceleration and decceleration - m.writeReg(ctx, a1, rawMaxAcc), - m.writeReg(ctx, aMax, rawMaxAcc), - m.writeReg(ctx, d1, rawMaxAcc), - m.writeReg(ctx, dMax, rawMaxAcc), - - m.writeReg(ctx, vStart, 1), // Always start at min speed - m.writeReg(ctx, vStop, 10), // Always count a stop as LOW speed, but where vStop > vStart - m.writeReg(ctx, v1, m.rpmToV(m.maxRPM/4)), // Transition ramp at 25% speed (if d1 and a1 are set different) - m.writeReg(ctx, vCoolThres, m.rpmToV(m.maxRPM/20)), // Set minimum speed for stall detection and coolstep - m.writeReg(ctx, vMax, m.rpmToV(0)), // Max velocity to zero, we don't want to move - - m.writeReg(ctx, rampMode, modeVelPos), // Lastly, set velocity mode to force a stop in case chip was left in moving state - m.writeReg(ctx, xActual, 0), // Zero the position - ) - if err != nil { - return nil, err - } - - if c.Pins.EnablePinLow != "" { - b, err := board.FromDependencies(deps, c.BoardName) - if err != nil { - return nil, errors.Errorf("%q is not a board", c.BoardName) - } - - m.enLowPin, err = b.GPIOPinByName(c.Pins.EnablePinLow) - if err != nil { - return nil, err - } - err = m.Enable(ctx, true) - if err != nil { - return nil, err - } - } - - return m, nil -} - -func (m *Motor) shiftAddr(addr uint8) uint8 { - // Shift register address for motor 1 instead of motor zero - if m.index == 1 { - switch { - case addr >= 0x10 && addr <= 0x11: - addr += 0x08 - case addr >= 0x20 && addr <= 0x3C: - addr += 0x20 - case addr >= 0x6A && addr <= 0x6F: - addr += 0x10 - } - } - return addr -} - -func (m *Motor) writeReg(ctx context.Context, addr uint8, value int32) error { - addr = m.shiftAddr(addr) - - var buf [5]byte - buf[0] = addr | 0x80 - buf[1] = 0xFF & byte(value>>24) - buf[2] = 0xFF & byte(value>>16) - buf[3] = 0xFF & byte(value>>8) - buf[4] = 0xFF & byte(value) - - handle, err := m.bus.OpenHandle() - if err != nil { - return err - } - defer func() { - if err := handle.Close(); err != nil { - m.logger.CError(ctx, err) - } - }() - - // m.logger.Debug("Write: ", buf) - - _, err = handle.Xfer(ctx, 1000000, m.csPin, 3, buf[:]) // SPI Mode 3, 1mhz - if err != nil { - return err - } - - return nil -} - -func (m *Motor) readReg(ctx context.Context, addr uint8) (int32, error) { - addr = m.shiftAddr(addr) - - var tbuf [5]byte - tbuf[0] = addr - - handle, err := m.bus.OpenHandle() - if err != nil { - return 0, err - } - defer func() { - if err := handle.Close(); err != nil { - m.logger.CError(ctx, err) - } - }() - - // m.logger.Debug("ReadT: ", tbuf) - - // Read access returns data from the address sent in the PREVIOUS "packet," so we transmit, then read - _, err = handle.Xfer(ctx, 1000000, m.csPin, 3, tbuf[:]) // SPI Mode 3, 1mhz - if err != nil { - return 0, err - } - - rbuf, err := handle.Xfer(ctx, 1000000, m.csPin, 3, tbuf[:]) - if err != nil { - return 0, err - } - - var value int32 - value = int32(rbuf[1]) - value <<= 8 - value |= int32(rbuf[2]) - value <<= 8 - value |= int32(rbuf[3]) - value <<= 8 - value |= int32(rbuf[4]) - - // m.logger.Debug("ReadR: ", rbuf) - // m.logger.Debug("Read: ", value) - - return value, nil -} - -// GetSG returns the current StallGuard reading (effectively an indication of motor load.) -func (m *Motor) GetSG(ctx context.Context) (int32, error) { - rawRead, err := m.readReg(ctx, drvStatus) - if err != nil { - return 0, err - } - - rawRead &= 1023 - return rawRead, nil -} - -// Position gives the current motor position. -func (m *Motor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - rawPos, err := m.readReg(ctx, xActual) - if err != nil { - return 0, errors.Wrapf(err, "error in Position from motor (%s)", m.motorName) - } - return float64(rawPos) / float64(m.stepsPerRev), nil -} - -// Properties returns the status of optional properties on the motor. -func (m *Motor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil -} - -// SetPower sets the motor at a particular rpm based on the percent of -// maxRPM supplied by powerPct (between -1 and 1). -func (m *Motor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - m.powerPct = powerPct - return m.doJog(ctx, powerPct*m.maxRPM) -} - -// Jog sets a fixed RPM. -func (m *Motor) Jog(ctx context.Context, rpm float64) error { - m.opMgr.CancelRunning(ctx) - return m.doJog(ctx, rpm) -} - -func (m *Motor) doJog(ctx context.Context, rpm float64) error { - mode := modeVelPos - if rpm < 0 { - mode = modeVelNeg - } - - switch speed0 := math.Abs(rpm); { - case speed0 < 0.1: - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - case m.maxRPM > 0 && speed0 > m.maxRPM: - m.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.maxRPM) - default: - } - - speed := m.rpmToV(math.Abs(rpm)) - return multierr.Combine( - m.writeReg(ctx, rampMode, mode), - m.writeReg(ctx, vMax, speed), - ) -} - -// GoFor turns in the given direction the given number of times at the given speed. -// Both the RPM and the revolutions can be assigned negative values to move in a backwards direction. -// Note: if both are negative the motor will spin in the forward direction. -func (m *Motor) GoFor(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error { - if math.Abs(rpm) < 0.1 { - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - } - - curPos, err := m.Position(ctx, extra) - if err != nil { - return errors.Wrapf(err, "error in GoFor from motor (%s)", m.motorName) - } - - var d int64 = 1 - if math.Signbit(rotations) != math.Signbit(rpm) { - d *= -1 - } - - rotations = math.Abs(rotations) * float64(d) - rpm = math.Abs(rpm) - - target := curPos + rotations - return m.GoTo(ctx, rpm, target, extra) -} - -// Convert rpm to TMC5072 steps/s. -func (m *Motor) rpmToV(rpm float64) int32 { - if rpm > m.maxRPM { - rpm = m.maxRPM - } - // Time constant for velocities in TMC5072 - tConst := m.fClk / math.Pow(2, 24) - speed := rpm / 60 * float64(m.stepsPerRev) / tConst - return int32(speed) -} - -// Convert rpm/s to TMC5072 steps/taConst^2. -func (m *Motor) rpmsToA(acc float64) int32 { - // Time constant for accelerations in TMC5072 - taConst := math.Pow(2, 41) / math.Pow(m.fClk, 2) - rawMaxAcc := acc / 60 * float64(m.stepsPerRev) * taConst - return int32(rawMaxAcc) -} - -// GoTo moves to the specified position in terms of (provided in revolutions from home/zero), -// at a specific speed. Regardless of the directionality of the RPM this function will move the -// motor towards the specified target. -func (m *Motor) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - ctx, done := m.opMgr.New(ctx) - defer done() - - positionRevolutions *= float64(m.stepsPerRev) - - switch speed := math.Abs(rpm); { - case speed < 0.1: - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - case m.maxRPM > 0 && speed > m.maxRPM-0.1: - m.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", m.maxRPM) - default: - } - - err := multierr.Combine( - m.writeReg(ctx, rampMode, modePosition), - m.writeReg(ctx, vMax, m.rpmToV(math.Abs(rpm))), - m.writeReg(ctx, xTarget, int32(positionRevolutions)), - ) - if err != nil { - return errors.Wrapf(err, "error in GoTo from motor (%s)", m.motorName) - } - - return m.opMgr.WaitForSuccess( - ctx, - time.Millisecond*10, - m.IsStopped, - ) -} - -// IsPowered returns true if the motor is currently moving. -func (m *Motor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - on, err := m.IsMoving(ctx) - if err != nil { - return on, m.powerPct, errors.Wrapf(err, "error in IsPowered from motor (%s)", m.motorName) - } - return on, m.powerPct, err -} - -// IsStopped returns true if the motor is NOT moving. -func (m *Motor) IsStopped(ctx context.Context) (bool, error) { - stat, err := m.readReg(ctx, rampStat) - if err != nil { - return false, errors.Wrapf(err, "error in IsStopped from motor (%s)", m.motorName) - } - // Look for vzero flag - return stat&0x400 == 0x400, nil -} - -// AtVelocity returns true if the motor has reached the requested velocity. -func (m *Motor) AtVelocity(ctx context.Context) (bool, error) { - stat, err := m.readReg(ctx, rampStat) - if err != nil { - return false, err - } - // Look for velocity reached flag - return stat&0x100 == 0x100, nil -} - -// Enable pulls down the hardware enable pin, activating the power stage of the chip. -func (m *Motor) Enable(ctx context.Context, turnOn bool) error { - if m.enLowPin == nil { - return errors.New("no enable pin configured") - } - return m.enLowPin.Set(ctx, !turnOn, nil) -} - -// Stop stops the motor. -func (m *Motor) Stop(ctx context.Context, extra map[string]interface{}) error { - m.opMgr.CancelRunning(ctx) - return m.doJog(ctx, 0) -} - -// IsMoving returns true if the motor is currently moving. -func (m *Motor) IsMoving(ctx context.Context) (bool, error) { - stop, err := m.IsStopped(ctx) - return !stop, err -} - -// home homes the motor using stallguard. -func (m *Motor) home(ctx context.Context) error { - err := m.goTillStop(ctx, m.homeRPM, nil) - if err != nil { - return err - } - for { - stopped, err := m.IsStopped(ctx) - if err != nil { - return err - } - if stopped { - break - } - } - - return m.ResetZeroPosition(ctx, 0, nil) -} - -// goTillStop enables StallGuard detection, then moves in the direction/speed given until resistance (endstop) is detected. -func (m *Motor) goTillStop(ctx context.Context, rpm float64, stopFunc func(ctx context.Context) bool) error { - if err := m.Jog(ctx, rpm); err != nil { - return err - } - ctx, done := m.opMgr.New(ctx) - defer done() - - // Disable stallguard and turn off if we fail homing - defer func() { - if err := multierr.Combine( - m.writeReg(ctx, swMode, 0x000), - m.doJog(ctx, 0), - ); err != nil { - m.logger.CError(ctx, err) - } - }() - - // Get up to speed - var fails int - for { - if !utils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return errors.New("context cancelled: duration timeout trying to get up to speed while homing") - } - - if stopFunc != nil && stopFunc(ctx) { - return nil - } - - ready, err := m.AtVelocity(ctx) - if err != nil { - return err - } - - if ready { - break - } - - if fails >= 500 { - return errors.New("over 500 failures trying to get up to speed while homing") - } - fails++ - } - - // Now enable stallguard - if err := m.writeReg(ctx, swMode, 0x400); err != nil { - return err - } - - // Wait for motion to stop at endstop - fails = 0 - for { - if !utils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return errors.New("context cancelled: duration timeout trying to stop at the endstop while homing") - } - - if stopFunc != nil && stopFunc(ctx) { - return nil - } - - stopped, err := m.IsStopped(ctx) - if err != nil { - return err - } - if stopped { - break - } - - if fails >= 10000 { - return errors.New("over 1000 failures trying to stop at endstop while homing") - } - fails++ - } - - return nil -} - -// ResetZeroPosition sets the current position of the motor specified by the request -// (adjusted by a given offset) to be its new zero position. -func (m *Motor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - on, _, err := m.IsPowered(ctx, extra) - if err != nil { - return errors.Wrapf(err, "error in ResetZeroPosition from motor (%s)", m.motorName) - } else if on { - return errors.Errorf("can't zero motor (%s) while moving", m.motorName) - } - return multierr.Combine( - m.writeReg(ctx, rampMode, modeHold), - m.writeReg(ctx, xTarget, int32(-1*offset*float64(m.stepsPerRev))), - m.writeReg(ctx, xActual, int32(-1*offset*float64(m.stepsPerRev))), - ) -} - -// DoCommand() related constants. -const ( - Command = "command" - Home = "home" - Jog = "jog" - RPMVal = "rpm" -) - -// DoCommand executes additional commands beyond the Motor{} interface. -func (m *Motor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - name, ok := cmd["command"] - if !ok { - return nil, errors.Errorf("missing %s value", Command) - } - switch name { - case Home: - return nil, m.home(ctx) - case Jog: - rpmRaw, ok := cmd[RPMVal] - if !ok { - return nil, errors.Errorf("need %s value for jog", RPMVal) - } - rpm, ok := rpmRaw.(float64) - if !ok { - return nil, errors.New("rpm value must be floating point") - } - return nil, m.Jog(ctx, rpm) - default: - return nil, errors.Errorf("no such command: %s", name) - } -} diff --git a/components/motor/tmcstepper/stepper_motor_tmc_nonlinux.go b/components/motor/tmcstepper/stepper_motor_tmc_nonlinux.go deleted file mode 100644 index 4773a90f92b..00000000000 --- a/components/motor/tmcstepper/stepper_motor_tmc_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package tmcstepper is only implemented on Linux. -package tmcstepper diff --git a/components/motor/tmcstepper/stepper_motor_tmc_test.go b/components/motor/tmcstepper/stepper_motor_tmc_test.go deleted file mode 100644 index 5a7c2f5acbb..00000000000 --- a/components/motor/tmcstepper/stepper_motor_tmc_test.go +++ /dev/null @@ -1,718 +0,0 @@ -//go:build linux - -// Package tmcstepper contains the TMC stepper motor driver. This file contains unit tests for it. -package tmcstepper - -import ( - "context" - "fmt" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -type fakeSpiHandle struct { - tx, rx [][]byte // tx and rx must have the same length - i int // Index of the next tx/rx pair to use - tb testing.TB -} - -func newFakeSpiHandle(tb testing.TB) fakeSpiHandle { - h := fakeSpiHandle{} - h.rx = [][]byte{} - h.tx = [][]byte{} - h.i = 0 - h.tb = tb - return h -} - -func (h *fakeSpiHandle) Xfer( - ctx context.Context, - baud uint, - chipSelect string, - mode uint, - tx []byte, -) ([]byte, error) { - test.That(h.tb, tx, test.ShouldResemble, h.tx[h.i]) - result := h.rx[h.i] - h.i++ - return result, nil -} - -func (h *fakeSpiHandle) Close() error { - return nil -} - -func (h *fakeSpiHandle) AddExpectedTx(expects [][]byte) { - for _, line := range expects { - h.tx = append(h.tx, line) - h.rx = append(h.rx, make([]byte, len(line))) - } -} - -func (h *fakeSpiHandle) AddExpectedRx(expects, sends [][]byte) { - h.tx = append(h.tx, expects...) - h.rx = append(h.rx, sends...) -} - -func (h *fakeSpiHandle) ExpectDone() { - // Assert that all expected data was transmitted - test.That(h.tb, h.i, test.ShouldEqual, len(h.tx)) -} - -func newFakeSpi(tb testing.TB) (*fakeSpiHandle, buses.SPI) { - handle := newFakeSpiHandle(tb) - fakeSpi := inject.SPI{} - fakeSpi.OpenHandleFunc = func() (buses.SPIHandle, error) { - return &handle, nil - } - - return &handle, &fakeSpi -} - -const maxRpm = 500 - -func TestRPMBounds(t *testing.T) { - ctx := context.Background() - logger, obs := logging.NewObservedTestLogger(t) - - fakeSpiHandle, fakeSpi := newFakeSpi(t) - var deps resource.Dependencies - - mc := TMC5072Config{ - SPIBus: "3", - ChipSelect: "40", - Index: 0, - SGThresh: 0, - CalFactor: 1.0, - MaxAcceleration: 500, - MaxRPM: maxRpm, - TicksPerRotation: 200, - } - - // These are the setup register writes - fakeSpiHandle.AddExpectedTx([][]byte{ - {236, 0, 1, 0, 195}, - {176, 0, 6, 15, 8}, - {237, 0, 0, 0, 0}, - {164, 0, 0, 21, 8}, - {166, 0, 0, 21, 8}, - {170, 0, 0, 21, 8}, - {168, 0, 0, 21, 8}, - {163, 0, 0, 0, 1}, - {171, 0, 0, 0, 10}, - {165, 0, 2, 17, 149}, - {177, 0, 0, 105, 234}, - {167, 0, 0, 0, 0}, - {160, 0, 0, 0, 1}, - {161, 0, 0, 0, 0}, - }) - - name := resource.NewName(motor.API, "motor1") - motorDep, err := makeMotor(ctx, deps, mc, name, logger, fakeSpi) - test.That(t, err, test.ShouldBeNil) - defer func() { - fakeSpiHandle.ExpectDone() - test.That(t, motorDep.Close(context.Background()), test.ShouldBeNil) - }() - - test.That(t, motorDep.GoFor(ctx, 0.05, 6.6, nil), test.ShouldNotBeNil) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - // Check with position at 0.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 8, 70, 85}, - {173, 0, 5, 40, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, 500, 6.6, nil), test.ShouldBeNil) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") -} - -func TestTMCStepperMotor(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - fakeSpiHandle, fakeSpi := newFakeSpi(t) - var deps resource.Dependencies - - mc := TMC5072Config{ - SPIBus: "main", - ChipSelect: "40", - Index: 0, - SGThresh: 0, - CalFactor: 1.0, - MaxAcceleration: 500, - MaxRPM: maxRpm, - TicksPerRotation: 200, - } - - // These are the setup register writes - fakeSpiHandle.AddExpectedTx([][]byte{ - {236, 0, 1, 0, 195}, - {176, 0, 6, 15, 8}, - {237, 0, 0, 0, 0}, - {164, 0, 0, 21, 8}, - {166, 0, 0, 21, 8}, - {170, 0, 0, 21, 8}, - {168, 0, 0, 21, 8}, - {163, 0, 0, 0, 1}, - {171, 0, 0, 0, 10}, - {165, 0, 2, 17, 149}, - {177, 0, 0, 105, 234}, - {167, 0, 0, 0, 0}, - {160, 0, 0, 0, 1}, - {161, 0, 0, 0, 0}, - }) - - name := resource.NewName(motor.API, "motor1") - motorDep, err := makeMotor(ctx, deps, mc, name, logger, fakeSpi) - test.That(t, err, test.ShouldBeNil) - defer func() { - fakeSpiHandle.ExpectDone() - test.That(t, motorDep.Close(context.Background()), test.ShouldBeNil) - }() - - t.Run("motor supports position reporting", func(t *testing.T) { - properties, err := motorDep.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeTrue) - }) - - t.Run("motor SetPower testing", func(t *testing.T) { - // Test Go forward at half speed - fakeSpiHandle.AddExpectedTx([][]byte{ - {160, 0, 0, 0, 1}, - {167, 0, 4, 35, 42}, - }) - test.That(t, motorDep.SetPower(ctx, 0.5, nil), test.ShouldBeNil) - - // Test Go backward at quarter speed - fakeSpiHandle.AddExpectedTx([][]byte{ - {160, 0, 0, 0, 2}, - {167, 0, 2, 17, 149}, - }) - test.That(t, motorDep.SetPower(ctx, -0.25, nil), test.ShouldBeNil) - }) - - t.Run("motor Off testing", func(t *testing.T) { - fakeSpiHandle.AddExpectedTx([][]byte{ - {160, 0, 0, 0, 1}, - {167, 0, 0, 0, 0}, - }) - test.That(t, motorDep.Stop(ctx, nil), test.ShouldBeNil) - }) - - t.Run("motor position testing", func(t *testing.T) { - // Check at 4.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 1, 6, 18, 3}, // Can be gibberish, only second register is valid - {0, 0, 3, 32, 0}, - }, - ) - pos, err := motorDep.Position(ctx, nil) - test.That(t, pos, test.ShouldEqual, 4.0) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("motor GoFor with positive rpm and positive revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 2, 128, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 5, 160, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 8, 98, 98, 7}, // Can be gibberish - {0, 0, 3, 32, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 6, 24, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 240, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, 6.6, nil), test.ShouldBeNil) - }) - - t.Run("motor GoFor with negative rpm and positive revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 255, 253, 128, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 0, 159, 255}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 8, 98, 98, 7}, // Can be gibberish - {0, 0, 3, 32, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, 3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 255, 251, 200, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 240, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, 6.6, nil), test.ShouldBeNil) - }) - - t.Run("motor GoFor with positive rpm and negative revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 255, 253, 128, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 0, 159, 255}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 8, 98, 98, 7}, // Can be gibberish - {0, 0, 3, 32, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 255, 251, 200, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 240, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, 50.0, -6.6, nil), test.ShouldBeNil) - }) - - t.Run("motor GoFor with negative rpm and negative revolutions", func(t *testing.T) { - // Check with position at 0.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 2, 128, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 4.0 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 5, 160, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 8, 98, 98, 7}, // Can be gibberish - {0, 0, 3, 32, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, -3.2, nil), test.ShouldBeNil) - - // Check with position at 1.2 revolutions - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {33, 0, 0, 0, 0}, - {33, 0, 0, 0, 0}, - {160, 0, 0, 0, 0}, - {167, 0, 0, 211, 213}, - {173, 0, 6, 24, 0}, - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 0, 0}, - {0, 0, 0, 240, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - test.That(t, motorDep.GoFor(ctx, -50.0, -6.6, nil), test.ShouldBeNil) - }) - - t.Run("motor GoFor with zero rpm", func(t *testing.T) { - test.That(t, motorDep.GoFor(ctx, 0, 1, nil), test.ShouldBeError, motor.NewZeroRPMError()) - }) - - t.Run("motor is on testing", func(t *testing.T) { - // Off - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - }, - ) - on, powerPct, err := motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, -0.25) - test.That(t, err, test.ShouldBeNil) - - // On - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 5, 0}, - {0, 0, 0, 0, 0}, - }, - ) - on, powerPct, err = motorDep.IsPowered(ctx, nil) - test.That(t, on, test.ShouldEqual, true) - test.That(t, powerPct, test.ShouldEqual, -0.25) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("motor zero testing", func(t *testing.T) { - // No offset (and when actually off) - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - {160, 0, 0, 0, 3}, - {173, 0, 0, 0, 0}, - {161, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - }, - ) - test.That(t, motorDep.ResetZeroPosition(ctx, 0, nil), test.ShouldBeNil) - - // No offset (and when actually on) - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - }, - [][]byte{ - {0, 0, 128, 0, 4}, - {0, 0, 0, 0, 0}, - }, - ) - test.That(t, motorDep.ResetZeroPosition(ctx, 0, nil), test.ShouldNotBeNil) - - // 3.1 offset (and when actually off) - fakeSpiHandle.AddExpectedRx( - [][]byte{ - {53, 0, 0, 0, 0}, - {53, 0, 0, 0, 0}, - {160, 0, 0, 0, 3}, - {173, 255, 253, 148, 0}, - {161, 255, 253, 148, 0}, - }, - [][]byte{ - {0, 0, 0, 4, 0}, - {0, 0, 0, 4, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0}, - }, - ) - test.That(t, motorDep.ResetZeroPosition(ctx, 3.1, nil), test.ShouldBeNil) - }) - - //nolint:dupl - t.Run("test over-limit current settings", func(*testing.T) { - mc.HoldDelay = 9999 - mc.RunCurrent = 9999 - mc.HoldCurrent = 9999 - - fakeSpiHandle, fakeSpi := newFakeSpi(t) - - // These are the setup register writes - fakeSpiHandle.AddExpectedTx([][]byte{ - {236, 0, 1, 0, 195}, - {176, 0, 15, 31, 31}, // Last three are delay, run, and hold - {237, 0, 0, 0, 0}, - {164, 0, 0, 21, 8}, - {166, 0, 0, 21, 8}, - {170, 0, 0, 21, 8}, - {168, 0, 0, 21, 8}, - {163, 0, 0, 0, 1}, - {171, 0, 0, 0, 10}, - {165, 0, 2, 17, 149}, - {177, 0, 0, 105, 234}, - {167, 0, 0, 0, 0}, - {160, 0, 0, 0, 1}, - {161, 0, 0, 0, 0}, - }) - - m, err := makeMotor(ctx, deps, mc, name, logger, fakeSpi) - test.That(t, err, test.ShouldBeNil) - fakeSpiHandle.ExpectDone() - test.That(t, m.Close(context.Background()), test.ShouldBeNil) - }) - - t.Run("test under-limit current settings", func(*testing.T) { - mc.HoldDelay = -9999 - mc.RunCurrent = -9999 - mc.HoldCurrent = -9999 - - fakeSpiHandle, fakeSpi := newFakeSpi(t) - - // These are the setup register writes - fakeSpiHandle.AddExpectedTx([][]byte{ - {236, 0, 1, 0, 195}, - {176, 0, 0, 0, 0}, // Last three are delay, run, and hold - {237, 0, 0, 0, 0}, - {164, 0, 0, 21, 8}, - {166, 0, 0, 21, 8}, - {170, 0, 0, 21, 8}, - {168, 0, 0, 21, 8}, - {163, 0, 0, 0, 1}, - {171, 0, 0, 0, 10}, - {165, 0, 2, 17, 149}, - {177, 0, 0, 105, 234}, - {167, 0, 0, 0, 0}, - {160, 0, 0, 0, 1}, - {161, 0, 0, 0, 0}, - }) - - m, err := makeMotor(ctx, deps, mc, name, logger, fakeSpi) - test.That(t, err, test.ShouldBeNil) - fakeSpiHandle.ExpectDone() - test.That(t, m.Close(context.Background()), test.ShouldBeNil) - }) - - //nolint:dupl - t.Run("test explicit current settings", func(*testing.T) { - mc.HoldDelay = 12 - // Currents will be passed as one less, as default is repurposed - mc.RunCurrent = 27 - mc.HoldCurrent = 14 - - fakeSpiHandle, fakeSpi := newFakeSpi(t) - - // These are the setup register writes - fakeSpiHandle.AddExpectedTx([][]byte{ - {236, 0, 1, 0, 195}, - {176, 0, 12, 26, 13}, // Last three are delay, run, and hold - {237, 0, 0, 0, 0}, - {164, 0, 0, 21, 8}, - {166, 0, 0, 21, 8}, - {170, 0, 0, 21, 8}, - {168, 0, 0, 21, 8}, - {163, 0, 0, 0, 1}, - {171, 0, 0, 0, 10}, - {165, 0, 2, 17, 149}, - {177, 0, 0, 105, 234}, - {167, 0, 0, 0, 0}, - {160, 0, 0, 0, 1}, - {161, 0, 0, 0, 0}, - }) - - m, err := makeMotor(ctx, deps, mc, name, logger, fakeSpi) - test.That(t, err, test.ShouldBeNil) - fakeSpiHandle.ExpectDone() - test.That(t, m.Close(context.Background()), test.ShouldBeNil) - }) -} diff --git a/components/motor/ulnstepper/28byj-48.go b/components/motor/ulnstepper/28byj-48.go deleted file mode 100644 index 3ef89f7b0d7..00000000000 --- a/components/motor/ulnstepper/28byj-48.go +++ /dev/null @@ -1,371 +0,0 @@ -// Package uln28byj implements a GPIO based -// stepper motor (model: 28byj-48) with uln2003 controler. -package uln28byj - -/* - Motor Name: 28byj-48 - Motor Controler: ULN2003 - Datasheet: - ULN2003: https://www.makerguides.com/wp-content/uploads/2019/04/ULN2003-Datasheet.pdf - 28byj-48: https://components101.com/sites/default/files/component_datasheet/28byj48-step-motor-datasheet.pdf - - This driver will drive the motor with half-step driving method (instead of full-step drive) for higher resolutions. - In half-step the current vector divides a circle into eight parts. The eight step switching sequence is shown in - stepSequence below. The motor takes 5.625*(1/64)° per step. For 360° the motor will take 4096 steps. - - The motor can run at a max speed of ~146rpm. Though it is recommended to not run the motor at max speed as it can - damage the gears. -*/ - -import ( - "context" - "math" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -var ( - model = resource.DefaultModelFamily.WithModel("28byj48") - minDelayBetweenTicks = 100 * time.Microsecond // minimum sleep time between each ticks -) - -// stepSequence contains switching signal for uln2003 pins. -// Treversing through stepSequence once is one step. -var stepSequence = [8][4]bool{ - {false, false, false, true}, - {true, false, false, true}, - {true, false, false, false}, - {true, true, false, false}, - {false, true, false, false}, - {false, true, true, false}, - {false, false, true, false}, - {false, false, true, true}, -} - -// PinConfig defines the mapping of where motor are wired. -type PinConfig struct { - In1 string `json:"in1"` - In2 string `json:"in2"` - In3 string `json:"in3"` - In4 string `json:"in4"` -} - -// Config describes the configuration of a motor. -type Config struct { - Pins PinConfig `json:"pins"` - BoardName string `json:"board"` - TicksPerRotation int `json:"ticks_per_rotation"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - if conf.BoardName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - - if conf.Pins.In1 == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "in1") - } - - if conf.Pins.In2 == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "in2") - } - - if conf.Pins.In3 == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "in3") - } - - if conf.Pins.In4 == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "in4") - } - - deps = append(deps, conf.BoardName) - return deps, nil -} - -func init() { - resource.RegisterComponent(motor.API, model, resource.Registration[motor.Motor, *Config]{ - Constructor: new28byj, - }) -} - -func new28byj( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (motor.Motor, error) { - mc, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - b, err := board.FromDependencies(deps, mc.BoardName) - if err != nil { - return nil, errors.Wrap(err, "expected board name in config for motor") - } - - if mc.TicksPerRotation <= 0 { - return nil, errors.New("expected ticks_per_rotation to be greater than zero in config for motor") - } - - m := &uln28byj{ - Named: conf.ResourceName().AsNamed(), - theBoard: b, - ticksPerRotation: mc.TicksPerRotation, - logger: logger, - motorName: conf.Name, - opMgr: operation.NewSingleOperationManager(), - } - - in1, err := b.GPIOPinByName(mc.Pins.In1) - if err != nil { - return nil, errors.Wrapf(err, "in in1 in motor (%s)", m.motorName) - } - m.in1 = in1 - - in2, err := b.GPIOPinByName(mc.Pins.In2) - if err != nil { - return nil, errors.Wrapf(err, "in in2 in motor (%s)", m.motorName) - } - m.in2 = in2 - - in3, err := b.GPIOPinByName(mc.Pins.In3) - if err != nil { - return nil, errors.Wrapf(err, "in in3 in motor (%s)", m.motorName) - } - m.in3 = in3 - - in4, err := b.GPIOPinByName(mc.Pins.In4) - if err != nil { - return nil, errors.Wrapf(err, "in in4 in motor (%s)", m.motorName) - } - m.in4 = in4 - - return m, nil -} - -// struct is named after the controler uln28byj. -type uln28byj struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - theBoard board.Board - ticksPerRotation int - in1, in2, in3, in4 board.GPIOPin - logger logging.Logger - motorName string - - // state - lock sync.Mutex - opMgr *operation.SingleOperationManager - - stepPosition int64 - stepperDelay time.Duration - targetStepPosition int64 -} - -// doRun runs the motor till it reaches target step position. -func (m *uln28byj) doRun(ctx context.Context) error { - for { - select { - case <-ctx.Done(): - return nil - default: - } - - m.lock.Lock() - - // This condition cannot be locked for the duration of the loop as - // Stop() modifies m.targetStepPosition to interrupt the run - if m.stepPosition == m.targetStepPosition { - err := m.setPins(ctx, [4]bool{false, false, false, false}) - if err != nil { - return errors.Wrapf(err, "error while disabling motor (%s)", m.motorName) - } - m.lock.Unlock() - break - } - - err := m.doStep(ctx, m.stepPosition < m.targetStepPosition) - m.lock.Unlock() - if err != nil { - return errors.Errorf("error stepping %v", err) - } - } - return nil -} - -// doStep has to be locked to call. -// Depending on the direction, doStep will either treverse the stepSequence array in ascending -// or descending order. -func (m *uln28byj) doStep(ctx context.Context, forward bool) error { - if forward { - m.stepPosition++ - } else { - m.stepPosition-- - } - - var nextStepSequence int - if m.stepPosition < 0 { - nextStepSequence = 7 + int(m.stepPosition%8) - } else { - nextStepSequence = int(m.stepPosition % 8) - } - - err := m.setPins(ctx, stepSequence[nextStepSequence]) - if err != nil { - return err - } - - time.Sleep(m.stepperDelay) - return nil -} - -// doTicks sets all 4 pins. -// must be called in locked context. -func (m *uln28byj) setPins(ctx context.Context, pins [4]bool) error { - err := multierr.Combine( - m.in1.Set(ctx, pins[0], nil), - m.in2.Set(ctx, pins[1], nil), - m.in3.Set(ctx, pins[2], nil), - m.in4.Set(ctx, pins[3], nil), - ) - - return err -} - -// GoFor instructs the motor to go in a specific direction for a specific amount of -// revolutions at a given speed in revolutions per minute. Both the RPM and the revolutions -// can be assigned negative values to move in a backwards direction. Note: if both are negative -// the motor will spin in the forward direction. -func (m *uln28byj) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - ctx, done := m.opMgr.New(ctx) - defer done() - - switch speed := math.Abs(rpm); { - case speed < 0.1: - m.logger.CWarn(ctx, "motor speed is nearly 0 rev_per_min") - return motor.NewZeroRPMError() - case speed > 146-0.1: - m.logger.CWarnf(ctx, "motor speed is nearly the max rev_per_min (%f)", 146) - return m.Stop(ctx, nil) - default: - } - - m.lock.Lock() - m.targetStepPosition, m.stepperDelay = m.goMath(ctx, rpm, revolutions) - m.lock.Unlock() - - err := m.doRun(ctx) - if err != nil { - return errors.Errorf(" error while running motor %v", err) - } - return nil -} - -func (m *uln28byj) goMath(ctx context.Context, rpm, revolutions float64) (int64, time.Duration) { - var d int64 = 1 - - if math.Signbit(revolutions) != math.Signbit(rpm) { - d = -1 - } - - revolutions = math.Abs(revolutions) - rpm = math.Abs(rpm) * float64(d) - - targetPosition := m.stepPosition + int64(float64(d)*revolutions*float64(m.ticksPerRotation)) - - stepperDelay := time.Duration(int64((1/(math.Abs(rpm)*float64(m.ticksPerRotation)/60.0))*1000000)) * time.Microsecond - if stepperDelay < minDelayBetweenTicks { - m.logger.CDebugf(ctx, "Computed sleep time between ticks (%v) too short. Defaulting to %v", stepperDelay, minDelayBetweenTicks) - stepperDelay = minDelayBetweenTicks - } - - return targetPosition, stepperDelay -} - -// GoTo instructs the motor to go to a specific position (provided in revolutions from home/zero), -// at a specific RPM. Regardless of the directionality of the RPM this function will move the motor -// towards the specified target. -func (m *uln28byj) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - curPos, err := m.Position(ctx, extra) - if err != nil { - return errors.Wrapf(err, "error in GoTo from motor (%s)", m.motorName) - } - moveDistance := positionRevolutions - curPos - - m.logger.CDebugf(ctx, "Moving %v ticks at %v rpm", moveDistance, rpm) - - if moveDistance == 0 { - return nil - } - - return m.GoFor(ctx, math.Abs(rpm), moveDistance, extra) -} - -// Set the current position (+/- offset) to be the new zero (home) position. -func (m *uln28byj) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - m.lock.Lock() - defer m.lock.Unlock() - m.stepPosition = int64(-1 * offset * float64(m.ticksPerRotation)) - m.targetStepPosition = m.stepPosition - return nil -} - -// SetPower is invalid for this motor. -func (m *uln28byj) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - return errors.Errorf("raw power not supported in stepper motor (%s)", m.motorName) -} - -// Position reports the current step position of the motor. If it's not supported, the returned -// data is undefined. -func (m *uln28byj) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - m.lock.Lock() - defer m.lock.Unlock() - return float64(m.stepPosition) / float64(m.ticksPerRotation), nil -} - -// Properties returns the status of whether the motor supports certain optional properties. -func (m *uln28byj) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{ - PositionReporting: true, - }, nil -} - -// IsMoving returns if the motor is currently moving. -func (m *uln28byj) IsMoving(ctx context.Context) (bool, error) { - m.lock.Lock() - defer m.lock.Unlock() - return m.stepPosition != m.targetStepPosition, nil -} - -// Stop turns the power to the motor off immediately, without any gradual step down. -func (m *uln28byj) Stop(ctx context.Context, extra map[string]interface{}) error { - m.lock.Lock() - defer m.lock.Unlock() - m.targetStepPosition = m.stepPosition - return nil -} - -// IsPowered returns whether or not the motor is currently on. It also returns the percent power -// that the motor has, but stepper motors only have this set to 0% or 100%, so it's a little -// redundant. -func (m *uln28byj) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - on, err := m.IsMoving(ctx) - if err != nil { - return on, 0.0, errors.Wrapf(err, "error in IsPowered from motor (%s)", m.motorName) - } - percent := 0.0 - if on { - percent = 1.0 - } - return on, percent, err -} diff --git a/components/motor/ulnstepper/28byj-48_test.go b/components/motor/ulnstepper/28byj-48_test.go deleted file mode 100644 index 02161c74e08..00000000000 --- a/components/motor/ulnstepper/28byj-48_test.go +++ /dev/null @@ -1,343 +0,0 @@ -package uln28byj - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testBoardName = "fake_board" -) - -func setupDependencies(t *testing.T) resource.Dependencies { - t.Helper() - - testBoard := &inject.Board{} - in1 := &mockGPIOPin{} - in2 := &mockGPIOPin{} - in3 := &mockGPIOPin{} - in4 := &mockGPIOPin{} - - testBoard.GPIOPinByNameFunc = func(pin string) (board.GPIOPin, error) { - switch pin { - case "1": - return in1, nil - case "2": - return in2, nil - case "3": - return in3, nil - case "4": - return in4, nil - } - return nil, errors.New("pin name not found") - } - deps := make(resource.Dependencies) - deps[board.Named(testBoardName)] = testBoard - return deps -} - -func TestValid(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - logger := logging.NewTestLogger(t) - deps := setupDependencies(t) - - mc := Config{ - Pins: PinConfig{ - In1: "1", - In2: "2", - In3: "3", - In4: "4", - }, - BoardName: testBoardName, - } - - c := resource.Config{ - Name: "fake_28byj", - ConvertedAttributes: &mc, - } - - // Create motor with no board and default config - t.Run("motor initializing test with no board and default config", func(t *testing.T) { - _, err := new28byj(ctx, deps, c, logger) - test.That(t, err, test.ShouldNotBeNil) - }) - - // Create motor with board and default config - t.Run("gpiostepper initializing test with board and default config", func(t *testing.T) { - _, err := new28byj(ctx, deps, c, logger) - test.That(t, err, test.ShouldNotBeNil) - }) - _, err := new28byj(ctx, deps, c, logger) - test.That(t, err, test.ShouldNotBeNil) - - mc.TicksPerRotation = 200 - - mm, err := new28byj(ctx, deps, c, logger) - test.That(t, err, test.ShouldBeNil) - - m := mm.(*uln28byj) - - t.Run("motor test supports position reporting", func(t *testing.T) { - properties, err := m.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.PositionReporting, test.ShouldBeTrue) - }) - - t.Run("motor test isOn functionality", func(t *testing.T) { - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - }) - - t.Run("motor testing with positive rpm and positive revolutions", func(t *testing.T) { - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - }) - - t.Run("motor testing with negative rpm and positive revolutions", func(t *testing.T) { - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - }) - - t.Run("motor testing with positive rpm and negative revolutions", func(t *testing.T) { - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - }) - - t.Run("motor testing with negative rpm and negative revolutions", func(t *testing.T) { - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - }) - - t.Run("motor testing with large # of revolutions", func(t *testing.T) { - on, powerPct, err := m.IsPowered(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, on, test.ShouldEqual, false) - test.That(t, powerPct, test.ShouldEqual, 0.0) - - err = m.Stop(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldBeGreaterThanOrEqualTo, 0) - test.That(t, pos, test.ShouldBeLessThan, 202) - }) - - cancel() -} - -func TestFunctions(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - logger, obs := logging.NewObservedTestLogger(t) - deps := setupDependencies(t) - - mc := Config{ - Pins: PinConfig{ - In1: "1", - In2: "2", - In3: "3", - In4: "4", - }, - BoardName: testBoardName, - TicksPerRotation: 100, - } - - c := resource.Config{ - Name: "fake_28byj", - ConvertedAttributes: &mc, - } - mm, _ := new28byj(ctx, deps, c, logger) - m := mm.(*uln28byj) - - t.Run("test goMath", func(t *testing.T) { - targetPos, stepperdelay := m.goMath(ctx, 100, 100) - test.That(t, targetPos, test.ShouldEqual, 10000) - test.That(t, stepperdelay, test.ShouldEqual, (6 * time.Millisecond)) - - targetPos, stepperdelay = m.goMath(ctx, -100, 100) - test.That(t, targetPos, test.ShouldEqual, -10000) - test.That(t, stepperdelay, test.ShouldEqual, (6 * time.Millisecond)) - - targetPos, stepperdelay = m.goMath(ctx, -100, -100) - test.That(t, targetPos, test.ShouldEqual, 10000) - test.That(t, stepperdelay, test.ShouldEqual, (6 * time.Millisecond)) - - targetPos, stepperdelay = m.goMath(ctx, -2, 50) - test.That(t, targetPos, test.ShouldEqual, -5000) - test.That(t, stepperdelay, test.ShouldEqual, (300 * time.Millisecond)) - - targetPos, stepperdelay = m.goMath(ctx, 1, 400) - test.That(t, targetPos, test.ShouldEqual, 40000) - test.That(t, stepperdelay, test.ShouldEqual, (600 * time.Millisecond)) - - targetPos, stepperdelay = m.goMath(ctx, 400, 2) - test.That(t, targetPos, test.ShouldEqual, 200) - test.That(t, stepperdelay, test.ShouldEqual, (1500 * time.Microsecond)) - - targetPos, stepperdelay = m.goMath(ctx, 0, 2) - test.That(t, targetPos, test.ShouldEqual, 200) - test.That(t, stepperdelay, test.ShouldEqual, (100 * time.Microsecond)) - }) - - t.Run("test position", func(t *testing.T) { - m.stepPosition = 3 - pos, err := m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0.03) - - m.stepPosition = -3 - pos, err = m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, -0.03) - - m.stepPosition = 0 - pos, err = m.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - }) - - t.Run("test GoFor", func(t *testing.T) { - err := m.GoFor(ctx, 0, 1, nil) - test.That(t, err, test.ShouldBeError) - allObs := obs.All() - latestLoggedEntry := allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly 0") - - err = m.GoFor(ctx, -.009, 1, nil) - test.That(t, err, test.ShouldNotBeNil) - - err = m.GoFor(ctx, 146, 1, nil) - test.That(t, err, test.ShouldBeNil) - allObs = obs.All() - latestLoggedEntry = allObs[len(allObs)-1] - test.That(t, fmt.Sprint(latestLoggedEntry), test.ShouldContainSubstring, "nearly the max") - }) - - cancel() -} - -func TestState(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - logger := logging.NewTestLogger(t) - deps := setupDependencies(t) - - mc := Config{ - Pins: PinConfig{ - In1: "1", - In2: "2", - In3: "3", - In4: "4", - }, - BoardName: testBoardName, - TicksPerRotation: 100, - } - - c := resource.Config{ - Name: "fake_28byj", - ConvertedAttributes: &mc, - } - mm, _ := new28byj(ctx, deps, c, logger) - m := mm.(*uln28byj) - - t.Run("test state", func(t *testing.T) { - m.stepPosition = 9 - b := m.theBoard - var pin1Arr []bool - var pin2Arr []bool - var pin3Arr []bool - var pin4Arr []bool - - arrIn1 := []bool{true, true, false, false, false, false, false, true} - arrIn2 := []bool{false, true, true, true, false, false, false, false} - arrIn3 := []bool{false, false, false, true, true, true, false, false} - arrIn4 := []bool{false, false, false, false, false, true, true, true} - - for i := 0; i < 8; i++ { - // moving forward. - err := m.doStep(ctx, true) - test.That(t, err, test.ShouldBeNil) - - PinOut1, err := b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - pinStruct, ok := PinOut1.(*mockGPIOPin) - test.That(t, ok, test.ShouldBeTrue) - pin1Arr = pinStruct.pinStates - - PinOut2, err := b.GPIOPinByName("2") - test.That(t, err, test.ShouldBeNil) - pinStruct2, ok := PinOut2.(*mockGPIOPin) - test.That(t, ok, test.ShouldBeTrue) - pin2Arr = pinStruct2.pinStates - - PinOut3, err := b.GPIOPinByName("3") - test.That(t, err, test.ShouldBeNil) - pinStruct3, ok := PinOut3.(*mockGPIOPin) - test.That(t, ok, test.ShouldBeTrue) - pin3Arr = pinStruct3.pinStates - - PinOut4, err := b.GPIOPinByName("4") - test.That(t, err, test.ShouldBeNil) - pinStruct4, ok := PinOut4.(*mockGPIOPin) - test.That(t, ok, test.ShouldBeTrue) - pin4Arr = pinStruct4.pinStates - } - - m.logger.Info("pin1Arr ", pin1Arr) - m.logger.Info("pin2Arr ", pin2Arr) - m.logger.Info("pin3Arr ", pin3Arr) - m.logger.Info("pin4Arr ", pin4Arr) - - test.That(t, pin1Arr, test.ShouldResemble, arrIn1) - test.That(t, pin2Arr, test.ShouldResemble, arrIn2) - test.That(t, pin3Arr, test.ShouldResemble, arrIn3) - test.That(t, pin4Arr, test.ShouldResemble, arrIn4) - }) - - cancel() -} - -type mockGPIOPin struct { - board.GPIOPin - pinStates []bool -} - -func (m *mockGPIOPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - m.pinStates = append(m.pinStates, high) - return nil -} diff --git a/components/movementsensor/accuracy.go b/components/movementsensor/accuracy.go deleted file mode 100644 index 3c6571078c4..00000000000 --- a/components/movementsensor/accuracy.go +++ /dev/null @@ -1,116 +0,0 @@ -package movementsensor - -import ( - pb "go.viam.com/api/component/movementsensor/v1" -) - -// Accuracy defines the precision and reliability metrics of a movement sensor. -// It includes various parameters to assess the sensor's accuracy in different dimensions. -// -// Fields: -// -// AccuracyMap: A mapping of specific measurement parameters to their accuracy values. -// The keys are string identifiers for each measurement (e.g., "Hdop", "Vdop"), -// and the values are their corresponding accuracy levels as float32. -// -// Hdop: Horizontal Dilution of Precision (HDOP) value. It indicates the level of accuracy -// of horizontal measurements. Lower values represent higher precision. -// -// Vdop: Vertical Dilution of Precision (VDOP) value. Similar to HDOP, it denotes the -// accuracy level of vertical measurements. Lower VDOP values signify better precision. -// -// NmeaFix: An integer value representing the quality of the NMEA fix. -// Higher values generally indicate a better quality fix, with specific meanings depending -// on the sensor and context. Generally a fix of 1 or 2 lends to large position errors, -// ideally we want a fix of 4-5. Other fixes are unsuitable for outdoor navigation. -// The meaning of each fix value is documented here -// https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm#GPSQualityIndicators -// -// CompassDegreeError: The estimated error in compass readings, measured in degrees. -// It signifies the deviation or uncertainty in the sensor's compass measurements. -// A lower value implies a more accurate compass direction. -type Accuracy struct { - AccuracyMap map[string]float32 - Hdop float32 - Vdop float32 - NmeaFix int32 - CompassDegreeError float32 -} - -// ProtoFeaturesToAccuracy converts a GetAccuracyResponse from a protocol buffer (protobuf) -// into an Accuracy struct. -// used by the client. -func protoFeaturesToAccuracy(resp *pb.GetAccuracyResponse) *Accuracy { - uacc := UnimplementedOptionalAccuracies() - if resp == nil { - return UnimplementedOptionalAccuracies() - } - - hdop := resp.PositionHdop - if hdop == nil { - hdop = &uacc.Hdop - } - - vdop := resp.PositionVdop - if vdop == nil { - vdop = &uacc.Vdop - } - - compass := resp.CompassDegreesError - if compass == nil { - compass = &uacc.CompassDegreeError - } - - nmeaFix := resp.PositionNmeaGgaFix - if nmeaFix == nil { - nmeaFix = &uacc.NmeaFix - } - - return &Accuracy{ - AccuracyMap: resp.Accuracy, - Hdop: *hdop, - Vdop: *vdop, - NmeaFix: *nmeaFix, - CompassDegreeError: *compass, - } -} - -// AccuracyToProtoResponse converts an Accuracy struct into a protobuf GetAccuracyResponse. -// used by the server. -func accuracyToProtoResponse(acc *Accuracy) (*pb.GetAccuracyResponse, error) { - uacc := UnimplementedOptionalAccuracies() - if acc == nil { - return &pb.GetAccuracyResponse{ - Accuracy: map[string]float32{}, - PositionHdop: &uacc.Hdop, - PositionVdop: &uacc.Vdop, - CompassDegreesError: &uacc.CompassDegreeError, - // default value of the GGA NMEA Fix when Accuracy struct is nil is -1 - a meaningless value in terms of GGA Fixes. - PositionNmeaGgaFix: &uacc.NmeaFix, - }, nil - } - - hdop := uacc.Hdop - if acc.Hdop > 0 { - hdop = acc.Hdop - } - - vdop := uacc.Vdop - if acc.Vdop > 0 { - vdop = acc.Vdop - } - - compass := uacc.CompassDegreeError - if acc.CompassDegreeError > 0 { - compass = acc.CompassDegreeError - } - - return &pb.GetAccuracyResponse{ - Accuracy: acc.AccuracyMap, - PositionHdop: &hdop, - PositionVdop: &vdop, - CompassDegreesError: &compass, - // default value of the GGA NMEA Fix when Accuracy struct is non-nil is 0 - invalid GGA Fix. - PositionNmeaGgaFix: &acc.NmeaFix, - }, nil -} diff --git a/components/movementsensor/adxl345/adxl345.go b/components/movementsensor/adxl345/adxl345.go deleted file mode 100644 index 4add5b3f3ff..00000000000 --- a/components/movementsensor/adxl345/adxl345.go +++ /dev/null @@ -1,593 +0,0 @@ -//go:build linux - -// Package adxl345 implements the MovementSensor interface for the ADXL345 accelerometer. -package adxl345 - -/* - This package supports ADXL345 accelerometer attached to an I2C bus on the robot (the chip supports - communicating over SPI as well, but this package does not support that interface). - The datasheet for this chip is available at: - https://www.analog.com/media/en/technical-documentation/data-sheets/adxl345.pdf - - Because we only support I2C interaction, the CS pin must be wired to hot (which tells the chip - which communication interface to use). The chip has two possible I2C addresses, which can be - selected by wiring the SDO pin to either hot or ground: - - if SDO is wired to ground, it uses the default I2C address of 0x53 - - if SDO is wired to hot, it uses the alternate I2C address of 0x1D - - If you use the alternate address, your config file for this component must set its - "use_alternate_i2c_address" boolean to true. -*/ - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - rutils "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("accel-adxl345") - -const ( - defaultI2CAddress = 0x53 - alternateI2CAddress = 0x1D - deviceIDRegister = 0 - expectedDeviceID = 0xE5 - powerControlRegister = 0x2D -) - -// Config is a description of how to find an ADXL345 accelerometer on the robot. -type Config struct { - I2cBus string `json:"i2c_bus"` - UseAlternateI2CAddress bool `json:"use_alternate_i2c_address,omitempty"` - BoardName string `json:"board,omitempty"` - SingleTap *TapConfig `json:"tap,omitempty"` - FreeFall *FreeFallConfig `json:"free_fall,omitempty"` -} - -// TapConfig is a description of the configs for tap registers. -type TapConfig struct { - AccelerometerPin int `json:"accelerometer_pin"` - InterruptPin string `json:"interrupt_pin"` - ExcludeX bool `json:"exclude_x,omitempty"` - ExcludeY bool `json:"exclude_y,omitempty"` - ExcludeZ bool `json:"exclude_z,omitempty"` - Threshold float32 `json:"threshold,omitempty"` - Dur float32 `json:"dur_us,omitempty"` -} - -// FreeFallConfig is a description of the configs for free fall registers. -type FreeFallConfig struct { - AccelerometerPin int `json:"accelerometer_pin"` - InterruptPin string `json:"interrupt_pin"` - Threshold float32 `json:"threshold,omitempty"` - Time float32 `json:"time_ms,omitempty"` -} - -// validateTapConfigs validates the tap piece of the config. -func (tapCfg *TapConfig) validateTapConfigs() error { - if tapCfg.AccelerometerPin != 1 && tapCfg.AccelerometerPin != 2 { - return errors.New("Accelerometer pin on the ADXL345 must be 1 or 2") - } - if tapCfg.Threshold != 0 { - if tapCfg.Threshold < 0 || tapCfg.Threshold > (255*threshTapScaleFactor) { - return errors.New("Tap threshold on the ADXL345 must be 0 between and 15,937mg") - } - } - if tapCfg.Dur != 0 { - if tapCfg.Dur < 0 || tapCfg.Dur > (255*durScaleFactor) { - return errors.New("Tap dur on the ADXL345 must be between 0 and 160,000µs") - } - } - return nil -} - -// validateFreeFallConfigs validates the freefall piece of the config. -func (freefallCfg *FreeFallConfig) validateFreeFallConfigs() error { - if freefallCfg.AccelerometerPin != 1 && freefallCfg.AccelerometerPin != 2 { - return errors.New("Accelerometer pin on the ADXL345 must be 1 or 2") - } - if freefallCfg.Threshold != 0 { - if freefallCfg.Threshold < 0 || freefallCfg.Threshold > (255*threshFfScaleFactor) { - return errors.New("Accelerometer tap threshold on the ADXL345 must be 0 between and 15,937mg") - } - } - if freefallCfg.Time != 0 { - if freefallCfg.Time < 0 || freefallCfg.Time > (255*timeFfScaleFactor) { - return errors.New("Accelerometer tap time on the ADXL345 must be between 0 and 1,275ms") - } - } - return nil -} - -// Validate ensures all parts of the config are valid, and then returns the list of things we -// depend on. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - if cfg.BoardName == "" { - // The board name is only required for interrupt-related functionality. - if cfg.SingleTap != nil || cfg.FreeFall != nil { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - } else { - if cfg.SingleTap != nil || cfg.FreeFall != nil { - // The board is actually used! Add it to the dependencies. - deps = append(deps, cfg.BoardName) - } - } - if cfg.I2cBus == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "i2c_bus") - } - if cfg.SingleTap != nil { - if err := cfg.SingleTap.validateTapConfigs(); err != nil { - return nil, err - } - } - if cfg.FreeFall != nil { - if err := cfg.FreeFall.validateFreeFallConfigs(); err != nil { - return nil, err - } - } - return deps, nil -} - -func init() { - resource.RegisterComponent( - movementsensor.API, - model, - resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newAdxl345, - }) -} - -type adxl345 struct { - resource.Named - resource.AlwaysRebuild - - bus buses.I2C - i2cAddress byte - logger logging.Logger - interruptsEnabled byte - interruptsFound map[InterruptID]int - configuredRegisterValues map[byte]byte - - // Used only to remove the callbacks from the interrupts upon closing component. - interruptChannels map[board.DigitalInterrupt]chan board.Tick - - // Lock the mutex when you want to read or write either the acceleration or the last error. - mu sync.Mutex - linearAcceleration r3.Vector - err movementsensor.LastError - - // Used to shut down the background goroutine which polls the sensor. - cancelContext context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup -} - -// newAdxl345 is a constructor to create a new object representing an ADXL345 accelerometer. -func newAdxl345( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - bus, err := buses.NewI2cBus(newConf.I2cBus) - if err != nil { - msg := fmt.Sprintf("can't find I2C bus '%q' for ADXL345 sensor", newConf.I2cBus) - return nil, errors.Wrap(err, msg) - } - - // The rest of the constructor is separated out so that you can pass in a mock I2C bus during - // tests. - return makeAdxl345(ctx, deps, conf, logger, bus) -} - -// makeAdxl345 is split out solely to be used during unit tests: it constructs a new object -// representing an AXDL345 accelerometer, but with the I2C bus already created and passed in as an -// argument. This lets you inject a mock I2C bus during the tests. It should not be used directly -// in production code (instead, use NewAdxl345, above). -func makeAdxl345( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - bus buses.I2C, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - var address byte - if newConf.UseAlternateI2CAddress { - address = alternateI2CAddress - } else { - address = defaultI2CAddress - } - - interruptConfigurations := getInterruptConfigurations(newConf) - configuredRegisterValues := getFreeFallRegisterValues(newConf.FreeFall) - for k, v := range getSingleTapRegisterValues(newConf.SingleTap) { - configuredRegisterValues[k] = v - } - cancelContext, cancelFunc := context.WithCancel(context.Background()) - - sensor := &adxl345{ - Named: conf.ResourceName().AsNamed(), - bus: bus, - i2cAddress: address, - interruptsEnabled: interruptConfigurations[intEnableAddr], - logger: logger, - cancelContext: cancelContext, - cancelFunc: cancelFunc, - configuredRegisterValues: configuredRegisterValues, - interruptsFound: make(map[InterruptID]int), - interruptChannels: make(map[board.DigitalInterrupt]chan board.Tick), - - // On overloaded boards, sometimes the I2C bus can be flaky. Only report errors if at least - // 5 of the last 10 times we've tried interacting with the device have had problems. - err: movementsensor.NewLastError(10, 5), - } - - // To check that we're able to talk to the chip, we should be able to read register 0 and get - // back the device ID (0xE5). - deviceID, err := sensor.readByte(ctx, deviceIDRegister) - if err != nil { - return nil, movementsensor.AddressReadError(err, address, newConf.I2cBus) - } - if deviceID != expectedDeviceID { - return nil, movementsensor.UnexpectedDeviceError(address, deviceID, sensor.Name().Name) - } - - // The chip starts out in standby mode. Set it to measurement mode so we can get data from it. - // To do this, we set the Power Control register (0x2D) to turn on the 8's bit. - if err = sensor.writeByte(ctx, powerControlRegister, 0x08); err != nil { - return nil, errors.Wrap(err, "unable to put ADXL345 into measurement mode") - } - - // Now, turn on the background goroutine that constantly reads from the chip and stores data in - // the object we created. - sensor.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer sensor.activeBackgroundWorkers.Done() - // Reading data a thousand times per second is probably fast enough. - timer := time.NewTicker(time.Millisecond) - defer timer.Stop() - - for { - select { - case <-sensor.cancelContext.Done(): - return - default: - } - select { - case <-timer.C: - // The registers with data are 0x32 through 0x37: two bytes each for X, Y, and Z. - rawData, err := sensor.readBlock(sensor.cancelContext, 0x32, 6) - // Record the errors no matter what: if the error is nil, that's useful information - // that will prevent errors from being returned later. - sensor.err.Set(err) - if err != nil { - continue - } - - linearAcceleration := toLinearAcceleration(rawData) - // Only lock the mutex to write to the shared data, so other threads can read the - // data as often as they want. - sensor.mu.Lock() - sensor.linearAcceleration = linearAcceleration - sensor.mu.Unlock() - case <-sensor.cancelContext.Done(): - return - } - } - }) - - // Clear out the source register upon starting the component - if _, err := sensor.readByte(ctx, intSourceAddr); err != nil { - // shut down goroutine reading sensor in the background - sensor.cancelFunc() - return nil, err - } - - if err := sensor.configureInterruptRegisters(ctx, interruptConfigurations[intMapAddr]); err != nil { - // shut down goroutine reading sensor in the background - sensor.cancelFunc() - return nil, err - } - - interruptList := []string{} - if (newConf.SingleTap != nil) && (newConf.SingleTap.InterruptPin != "") { - interruptList = append(interruptList, newConf.SingleTap.InterruptPin) - } - - if (newConf.FreeFall != nil) && (newConf.FreeFall.InterruptPin != "") { - interruptList = append(interruptList, newConf.FreeFall.InterruptPin) - } - - if len(interruptList) > 0 { - b, err := board.FromDependencies(deps, newConf.BoardName) - if err != nil { - return nil, err - } - interrupts := []board.DigitalInterrupt{} - for _, name := range interruptList { - interrupt, err := b.DigitalInterruptByName(name) - if err != nil { - return nil, err - } - interrupts = append(interrupts, interrupt) - } - ticksChan := make(chan board.Tick) - err = b.StreamTicks(sensor.cancelContext, interrupts, ticksChan, nil) - if err != nil { - return nil, err - } - sensor.startInterruptMonitoring(ticksChan) - } - - return sensor, nil -} - -func (adxl *adxl345) startInterruptMonitoring(ticksChan chan board.Tick) { - utils.PanicCapturingGo(func() { - for { - select { - case <-adxl.cancelContext.Done(): - return - case tick := <-ticksChan: - if tick.High { - utils.UncheckedError(adxl.readInterrupts(adxl.cancelContext)) - } - } - } - }) -} - -// This returns a map from register addresses to data which should be written to that register to configure the interrupt pin. -func getInterruptConfigurations(cfg *Config) map[byte]byte { - var intEnabled byte - var intMap byte - - if cfg.FreeFall != nil { - intEnabled += interruptBitPosition[freeFall] - if cfg.FreeFall.AccelerometerPin == 2 { - intMap |= interruptBitPosition[freeFall] - } else { - // Clear the freefall bit in the map to send the signal to pin INT1. - intMap &^= interruptBitPosition[freeFall] - } - } - if cfg.SingleTap != nil { - intEnabled += interruptBitPosition[singleTap] - if cfg.SingleTap.AccelerometerPin == 2 { - intMap |= interruptBitPosition[singleTap] - } else { - // Clear the single tap bit in the map to send the signal to pin INT1. - intMap &^= interruptBitPosition[singleTap] - } - } - - return map[byte]byte{intEnableAddr: intEnabled, intMapAddr: intMap} -} - -// This returns a map from register addresses to data which should be written to that register to configure single tap. -func getSingleTapRegisterValues(singleTapConfigs *TapConfig) map[byte]byte { - registerValues := map[byte]byte{} - if singleTapConfigs == nil { - return registerValues - } - - registerValues[tapAxesAddr] = getAxes(singleTapConfigs.ExcludeX, singleTapConfigs.ExcludeY, singleTapConfigs.ExcludeZ) - - if singleTapConfigs.Threshold != 0 { - registerValues[threshTapAddr] = byte((singleTapConfigs.Threshold / threshTapScaleFactor)) - } - if singleTapConfigs.Dur != 0 { - registerValues[durAddr] = byte((singleTapConfigs.Dur / durScaleFactor)) - } - return registerValues -} - -// This returns a map from register addresses to data which should be written to that register to configure freefall. -func getFreeFallRegisterValues(freeFallConfigs *FreeFallConfig) map[byte]byte { - registerValues := map[byte]byte{} - if freeFallConfigs == nil { - return registerValues - } - if freeFallConfigs.Threshold != 0 { - registerValues[threshFfAddr] = byte((freeFallConfigs.Threshold / threshFfScaleFactor)) - } - if freeFallConfigs.Time != 0 { - registerValues[timeFfAddr] = byte((freeFallConfigs.Time / timeFfScaleFactor)) - } - return registerValues -} - -func (adxl *adxl345) readByte(ctx context.Context, register byte) (byte, error) { - result, err := adxl.readBlock(ctx, register, 1) - if err != nil { - return 0, err - } - return result[0], err -} - -func (adxl *adxl345) readBlock(ctx context.Context, register byte, length uint8) ([]byte, error) { - handle, err := adxl.bus.OpenHandle(adxl.i2cAddress) - if err != nil { - return nil, err - } - defer func() { - err := handle.Close() - if err != nil { - adxl.logger.CError(ctx, err) - } - }() - - results, err := handle.ReadBlockData(ctx, register, length) - return results, err -} - -func (adxl *adxl345) writeByte(ctx context.Context, register, value byte) error { - handle, err := adxl.bus.OpenHandle(adxl.i2cAddress) - if err != nil { - return err - } - defer func() { - err := handle.Close() - if err != nil { - adxl.logger.CError(ctx, err) - } - }() - - return handle.WriteByteData(ctx, register, value) -} - -func (adxl *adxl345) configureInterruptRegisters(ctx context.Context, interruptBitMap byte) error { - if adxl.interruptsEnabled == 0 { - return nil - } - adxl.configuredRegisterValues[intEnableAddr] = adxl.interruptsEnabled - adxl.configuredRegisterValues[intMapAddr] = interruptBitMap - for key, value := range defaultRegisterValues { - if configuredVal, ok := adxl.configuredRegisterValues[key]; ok { - value = configuredVal - } - if err := adxl.writeByte(ctx, key, value); err != nil { - return err - } - } - return nil -} - -func (adxl *adxl345) readInterrupts(ctx context.Context) error { - adxl.mu.Lock() - defer adxl.mu.Unlock() - intSourceRegister, err := adxl.readByte(ctx, intSourceAddr) - if err != nil { - return err - } - - for key, val := range interruptBitPosition { - if intSourceRegister&val&adxl.interruptsEnabled != 0 { - adxl.interruptsFound[key]++ - } - } - return nil -} - -// Given a value, scales it so that the range of values read in becomes the range of +/- maxValue. -// The trick here is that although the values are stored in 16 bits, the sensor only has 10 bits of -// resolution. So, there are only (1 << 9) possible positive values, and a similar number of -// negative ones. -func setScale(value int, maxValue float64) float64 { - return float64(value) * maxValue / (1 << 9) -} - -func toLinearAcceleration(data []byte) r3.Vector { - // Vectors take ints, but we've got int16's, so we need to convert. - x := int(rutils.Int16FromBytesLE(data[0:2])) - y := int(rutils.Int16FromBytesLE(data[2:4])) - z := int(rutils.Int16FromBytesLE(data[4:6])) - - // The default scale is +/- 2G's, but our units should be m/sec/sec. - maxAcceleration := 2.0 * 9.81 /* m/sec/sec */ - return r3.Vector{ - X: setScale(x, maxAcceleration), - Y: setScale(y, maxAcceleration), - Z: setScale(z, maxAcceleration), - } -} - -func (adxl *adxl345) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{}, movementsensor.ErrMethodUnimplementedAngularVelocity -} - -func (adxl *adxl345) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearVelocity -} - -func (adxl *adxl345) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - adxl.mu.Lock() - defer adxl.mu.Unlock() - lastError := adxl.err.Get() - - if lastError != nil { - return r3.Vector{}, lastError - } - - return adxl.linearAcceleration, nil -} - -func (adxl *adxl345) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return spatialmath.NewOrientationVector(), movementsensor.ErrMethodUnimplementedOrientation -} - -func (adxl *adxl345) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, movementsensor.ErrMethodUnimplementedCompassHeading -} - -func (adxl *adxl345) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(0, 0), 0, movementsensor.ErrMethodUnimplementedPosition -} - -func (adxl *adxl345) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - // this driver is unable to provide positional or compass heading data - return movementsensor.UnimplementedOptionalAccuracies(), nil -} - -func (adxl *adxl345) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - readings, err := movementsensor.DefaultAPIReadings(ctx, adxl, extra) - if err != nil { - return nil, err - } - - adxl.mu.Lock() - defer adxl.mu.Unlock() - - readings["single_tap_count"] = adxl.interruptsFound[singleTap] - readings["freefall_count"] = adxl.interruptsFound[freeFall] - - return readings, adxl.err.Get() -} - -func (adxl *adxl345) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - LinearAccelerationSupported: true, - }, nil -} - -// Puts the chip into standby mode. -func (adxl *adxl345) Close(ctx context.Context) error { - adxl.cancelFunc() - adxl.activeBackgroundWorkers.Wait() - - adxl.mu.Lock() - defer adxl.mu.Unlock() - - // Put the chip into standby mode by setting the Power Control register (0x2D) to 0. - err := adxl.writeByte(ctx, powerControlRegister, 0x00) - if err != nil { - adxl.logger.CErrorf(ctx, "unable to turn off ADXL345 accelerometer: '%s'", err) - } - return nil -} diff --git a/components/movementsensor/adxl345/adxl345_nonlinux.go b/components/movementsensor/adxl345/adxl345_nonlinux.go deleted file mode 100644 index cd6ae67ae7b..00000000000 --- a/components/movementsensor/adxl345/adxl345_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package adxl345 is Linux-only. -package adxl345 diff --git a/components/movementsensor/adxl345/adxl345_test.go b/components/movementsensor/adxl345/adxl345_test.go deleted file mode 100644 index 93ecf6c5891..00000000000 --- a/components/movementsensor/adxl345/adxl345_test.go +++ /dev/null @@ -1,400 +0,0 @@ -//go:build linux - -package adxl345 - -import ( - "context" - "testing" - "time" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func nowNanosTest() uint64 { - return uint64(time.Now().UnixNano()) -} - -func setupDependencies(mockData []byte) (resource.Config, resource.Dependencies, buses.I2C) { - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - I2cBus: "2", - UseAlternateI2CAddress: true, - }, - } - - i2cHandle := &inject.I2CHandle{} - i2cHandle.ReadBlockDataFunc = func(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - if register == 0 { - return []byte{0xE5}, nil - } - return mockData, nil - } - i2cHandle.WriteByteDataFunc = func(ctx context.Context, b1, b2 byte) error { - return nil - } - i2cHandle.CloseFunc = func() error { return nil } - - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - - return cfg, resource.Dependencies{}, i2c -} - -func sendInterrupt(ctx context.Context, adxl movementsensor.MovementSensor, t *testing.T, interrupt board.DigitalInterrupt, key string) { - interrupt.(*inject.DigitalInterrupt).Tick(ctx, true, nowNanosTest()) - testutils.WaitForAssertion(t, func(tb testing.TB) { - readings, err := adxl.Readings(ctx, map[string]interface{}{}) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, readings[key], test.ShouldNotBeZeroValue) - }) -} - -func TestValidateConfig(t *testing.T) { - boardName := "local" - t.Run("fails when interrupts are used without a supplied board", func(t *testing.T) { - tapCfg := TapConfig{ - AccelerometerPin: 1, - InterruptPin: "on_missing_board", - } - cfg := Config{ - I2cBus: "3", - SingleTap: &tapCfg, - } - deps, err := cfg.Validate("path") - expectedErr := resource.NewConfigValidationFieldRequiredError("path", "board") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) - }) - - t.Run("fails with no I2C bus", func(t *testing.T) { - cfg := Config{} - deps, err := cfg.Validate("path") - expectedErr := resource.NewConfigValidationFieldRequiredError("path", "i2c_bus") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) - }) - - t.Run("passes with no board supplied, no dependencies", func(t *testing.T) { - cfg := Config{ - I2cBus: "3", - } - deps, err := cfg.Validate("path") - test.That(t, err, test.ShouldBeNil) - test.That(t, len(deps), test.ShouldEqual, 0) - }) - - t.Run("adds board name to dependencies on success with interrupts", func(t *testing.T) { - tapCfg := TapConfig{ - AccelerometerPin: 1, - InterruptPin: "on_present_board", - } - cfg := Config{ - BoardName: boardName, - I2cBus: "2", - SingleTap: &tapCfg, - } - deps, err := cfg.Validate("path") - - test.That(t, err, test.ShouldBeNil) - test.That(t, len(deps), test.ShouldEqual, 1) - test.That(t, deps[0], test.ShouldResemble, boardName) - }) -} - -func TestInitializationFailureOnChipCommunication(t *testing.T) { - logger := logging.NewTestLogger(t) - - t.Run("fails on read error", func(t *testing.T) { - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - I2cBus: "2", - }, - } - - i2cHandle := &inject.I2CHandle{} - readErr := errors.New("read error") - i2cHandle.ReadBlockDataFunc = func(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - if register == deviceIDRegister { - return nil, readErr - } - return []byte{}, nil - } - i2cHandle.CloseFunc = func() error { return nil } - - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - - sensor, err := makeAdxl345(context.Background(), resource.Dependencies{}, cfg, logger, i2c) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, sensor, test.ShouldBeNil) - }) -} - -func TestInterrupts(t *testing.T) { - ctx := context.Background() - callbacks := []chan board.Tick{} - - interrupt := &inject.DigitalInterrupt{} - - interrupt.TickFunc = func(ctx context.Context, high bool, nanoseconds uint64) error { - tick := board.Tick{High: high, TimestampNanosec: nanoseconds} - for _, cb := range callbacks { - cb <- tick - } - return nil - } - - mockBoard := &inject.Board{} - mockBoard.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { return interrupt, nil } - mockBoard.StreamTicksFunc = func(ctx context.Context, interrupts []board.DigitalInterrupt, ch chan board.Tick, - extra map[string]interface{}, - ) error { - callbacks = append(callbacks, ch) - return nil - } - - i2cHandle := &inject.I2CHandle{} - i2cHandle.CloseFunc = func() error { return nil } - i2cHandle.WriteByteDataFunc = func(context.Context, byte, byte) error { return nil } - // The data returned from the readByteFunction is intended to signify which interrupts have gone off - i2cHandle.ReadByteDataFunc = func(context.Context, byte) (byte, error) { return byte(1<<6 + 1<<2), nil } - // i2cHandle.ReadBlockDataFunc gets called multiple times. The first time we need the first byte to be 0xE5 and the next - // time we need 6 bytes. This return provides more data than necessary for the first call to the function but allows - // both calls to it to work properly. - i2cHandle.ReadBlockDataFunc = func(context.Context, byte, uint8) ([]byte, error) { - return []byte{byte(0xE5), byte(0x1), byte(0x2), byte(0x3), byte(0x4), byte(0x5), byte(0x6)}, nil - } - - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { return i2cHandle, nil } - - logger := logging.NewTestLogger(t) - - deps := resource.Dependencies{ - resource.NewName(board.API, "board"): mockBoard, - } - - tap := &TapConfig{ - AccelerometerPin: 1, - InterruptPin: "int1", - } - - ff := &FreeFallConfig{ - AccelerometerPin: 1, - InterruptPin: "int1", - } - - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - BoardName: "board", - I2cBus: "3", - SingleTap: tap, - FreeFall: ff, - }, - } - - t.Run("new adxl has interrupt counts set to 0", func(t *testing.T) { - adxl, err := makeAdxl345(ctx, deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - - readings, err := adxl.Readings(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings["freefall_count"], test.ShouldEqual, 0) - test.That(t, readings["single_tap_count"], test.ShouldEqual, 0) - }) - - t.Run("interrupts have been found correctly when both are configured to the same pin", func(t *testing.T) { - adxl, err := makeAdxl345(ctx, deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - - sendInterrupt(ctx, adxl, t, interrupt, "freefall_count") - - readings, err := adxl.Readings(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings["freefall_count"], test.ShouldEqual, 1) - test.That(t, readings["single_tap_count"], test.ShouldEqual, 1) - }) - - t.Run("interrupts have been found correctly only tap has been configured", func(t *testing.T) { - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - BoardName: "board", - I2cBus: "3", - SingleTap: tap, - }, - } - - adxl, err := makeAdxl345(ctx, deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - - sendInterrupt(ctx, adxl, t, interrupt, "single_tap_count") - - readings, err := adxl.Readings(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings["freefall_count"], test.ShouldEqual, 0) - test.That(t, readings["single_tap_count"], test.ShouldEqual, 1) - }) - - t.Run("interrupts have been found correctly only freefall has been configured", func(t *testing.T) { - cfg = resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - BoardName: "board", - I2cBus: "3", - FreeFall: ff, - }, - } - - adxl, err := makeAdxl345(ctx, deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - - sendInterrupt(ctx, adxl, t, interrupt, "freefall_count") - - readings, err := adxl.Readings(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings["freefall_count"], test.ShouldEqual, 1) - test.That(t, readings["single_tap_count"], test.ShouldEqual, 0) - }) -} - -func TestReadInterrupts(t *testing.T) { - ctx := context.Background() - cancelContext, cancelFunc := context.WithCancel(ctx) - i2cHandle := &inject.I2CHandle{} - i2cHandle.CloseFunc = func() error { return nil } - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - - t.Run("increments tap and freefall counts when both interrupts have gone off", func(t *testing.T) { - i2cHandle.ReadBlockDataFunc = func(context.Context, byte, uint8) ([]byte, error) { - intSourceRegister := byte(1<<6) + byte(1<<2) - return []byte{intSourceRegister}, nil - } - - sensor := &adxl345{ - bus: i2c, - interruptsFound: map[InterruptID]int{}, - cancelContext: cancelContext, - cancelFunc: cancelFunc, - interruptsEnabled: byte(1<<6 + 1<<2), - } - sensor.readInterrupts(sensor.cancelContext) - test.That(t, sensor.interruptsFound[singleTap], test.ShouldEqual, 1) - test.That(t, sensor.interruptsFound[freeFall], test.ShouldEqual, 1) - }) - - t.Run("increments freefall count only when freefall has gone off", func(t *testing.T) { - i2cHandle.ReadBlockDataFunc = func(context.Context, byte, uint8) ([]byte, error) { - intSourceRegister := byte(1 << 2) - return []byte{intSourceRegister}, nil - } - - sensor := &adxl345{ - bus: i2c, - interruptsFound: map[InterruptID]int{}, - cancelContext: cancelContext, - cancelFunc: cancelFunc, - interruptsEnabled: byte(1<<6 + 1<<2), - } - sensor.readInterrupts(sensor.cancelContext) - test.That(t, sensor.interruptsFound[singleTap], test.ShouldEqual, 0) - test.That(t, sensor.interruptsFound[freeFall], test.ShouldEqual, 1) - }) - - t.Run("increments tap count only when only tap has gone off", func(t *testing.T) { - i2cHandle.ReadBlockDataFunc = func(context.Context, byte, uint8) ([]byte, error) { - intSourceRegister := byte(1 << 6) - return []byte{intSourceRegister}, nil - } - - sensor := &adxl345{ - bus: i2c, - interruptsFound: map[InterruptID]int{}, - cancelContext: cancelContext, - cancelFunc: cancelFunc, - interruptsEnabled: byte(1<<6 + 1<<2), - } - sensor.readInterrupts(sensor.cancelContext) - test.That(t, sensor.interruptsFound[singleTap], test.ShouldEqual, 1) - test.That(t, sensor.interruptsFound[freeFall], test.ShouldEqual, 0) - }) - - t.Run("does not increment counts when neither interrupt has gone off", func(t *testing.T) { - i2cHandle.ReadBlockDataFunc = func(context.Context, byte, uint8) ([]byte, error) { - intSourceRegister := byte(0) - return []byte{intSourceRegister}, nil - } - - sensor := &adxl345{ - bus: i2c, - interruptsFound: map[InterruptID]int{}, - cancelContext: cancelContext, - cancelFunc: cancelFunc, - interruptsEnabled: byte(1<<6 + 1<<2), - } - sensor.readInterrupts(sensor.cancelContext) - test.That(t, sensor.interruptsFound[singleTap], test.ShouldEqual, 0) - test.That(t, sensor.interruptsFound[freeFall], test.ShouldEqual, 0) - }) -} - -func TestLinearAcceleration(t *testing.T) { - linearAccelMockData := make([]byte, 16) - // x-accel - linearAccelMockData[0] = 40 - linearAccelMockData[1] = 0 - expectedAccelX := 1.5328125000000001 - // y-accel - linearAccelMockData[2] = 50 - linearAccelMockData[3] = 0 - expectedAccelY := 1.916015625 - // z-accel - linearAccelMockData[4] = 80 - linearAccelMockData[5] = 0 - expectedAccelZ := 3.0656250000000003 - - logger := logging.NewTestLogger(t) - cfg, deps, i2c := setupDependencies(linearAccelMockData) - sensor, err := makeAdxl345(context.Background(), deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - defer sensor.Close(context.Background()) - testutils.WaitForAssertion(t, func(tb testing.TB) { - linAcc, err := sensor.LinearAcceleration(context.Background(), nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, linAcc, test.ShouldNotBeZeroValue) - }) - accel, err := sensor.LinearAcceleration(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, accel.X, test.ShouldEqual, expectedAccelX) - test.That(t, accel.Y, test.ShouldEqual, expectedAccelY) - test.That(t, accel.Z, test.ShouldEqual, expectedAccelZ) -} diff --git a/components/movementsensor/adxl345/interrupts.go b/components/movementsensor/adxl345/interrupts.go deleted file mode 100644 index 768b313c82c..00000000000 --- a/components/movementsensor/adxl345/interrupts.go +++ /dev/null @@ -1,99 +0,0 @@ -//go:build linux - -// Package adxl345 is for an ADXL345 accelerometer. This file is for the interrupt-based -// functionality on the chip. -package adxl345 - -// addresses relevant to interrupts. -const ( - // an unsigned time value representing the maximum time that an event must be above the - // THRESH_TAP threshold to qualify as a tap event [625 µs/LSB]. - durAddr byte = 0x21 - // info on which interrupts have been enabled. - intEnableAddr byte = 0x2E - // info on which interrupt pin to send each interrupt. - intMapAddr byte = 0x2F - // info on which interrupt has gone off since the last time this address has been read from. - intSourceAddr byte = 0x30 - // an unsigned time value representing the wait time from the detection of a tap event to the - // start of the time window [1.25 ms/LSB]. - latentAddr byte = 0x22 - // info on which axes have been turned on for taps (X, Y, Z are bits 2, 1, 0 respectively). - tapAxesAddr byte = 0x2A - // an unsigned threshold value for tap interrupts [62.5 mg/LSB ]. - threshTapAddr byte = 0x1D - // che threshold value, in unsigned format, for free-fall detection. - threshFfAddr byte = 0x28 - // the minimum time that the value of all axes must be less than THRESH_FF to generate a free-fall interrupt. - timeFfAddr byte = 0x29 -) - -// InterruptID is a type of interrupts available on ADXL345. -type InterruptID = uint8 - -const ( - // singleTap is a key value used to find various needs associated with this interrupt. - singleTap InterruptID = iota - // freeFall is a key value used to find various needs associated with this interrupt. - freeFall InterruptID = iota -) - -var interruptBitPosition = map[InterruptID]byte{ - singleTap: 1 << 6, - freeFall: 1 << 2, -} - -/* -From the data sheet: - - In general, a good starting point is to set the Dur register to a value greater - than 0x10 (10 ms), the Latent register to a value greater than 0x10 (20 ms), the - Window register to a value greater than 0x40 (80 ms), and the ThreshTap register - to a value greater than 0x30 (3 g). -*/ -var defaultRegisterValues = map[byte]byte{ - // Interrupt Enabled - intEnableAddr: 0x00, - intMapAddr: 0x00, - - // Single Tap & Double Tap - tapAxesAddr: 0x07, - threshTapAddr: 0x30, - durAddr: 0x10, - latentAddr: 0x10, - - // Free Fall - timeFfAddr: 0x20, // 0x14 - 0x46 are recommended - threshFfAddr: 0x07, // 0x05 - 0x09 are recommended -} - -const ( - // threshTapScaleFactor is the scale factor for THRESH_TAP register. - threshTapScaleFactor float32 = 62.5 - // durScaleFactor is the scale factor for DUR register. - durScaleFactor float32 = 625 - // timeFfScaleFactor is the scale factor for TIME_FF register. - timeFfScaleFactor float32 = .5 - // threshFfScaleFactor is the scale factor for THRESH_FF register. - threshFfScaleFactor float32 = 62.5 -) - -const ( - xBit byte = 1 << 0 - yBit = 1 << 1 - zBit = 1 << 2 -) - -func getAxes(excludeX, excludeY, excludeZ bool) byte { - var tapAxes byte - if !excludeX { - tapAxes |= xBit - } - if !excludeY { - tapAxes |= yBit - } - if !excludeZ { - tapAxes |= zBit - } - return tapAxes -} diff --git a/components/movementsensor/client.go b/components/movementsensor/client.go deleted file mode 100644 index db0f0806d32..00000000000 --- a/components/movementsensor/client.go +++ /dev/null @@ -1,187 +0,0 @@ -package movementsensor - -import ( - "context" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/movementsensor/v1" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// client implements MovementSensorServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.MovementSensorServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (MovementSensor, error) { - c := pb.NewMovementSensorServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - }, nil -} - -func (c *client) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return nil, 0, err - } - resp, err := c.client.GetPosition(ctx, &pb.GetPositionRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, 0, err - } - return geo.NewPoint(resp.Coordinate.Latitude, resp.Coordinate.Longitude), - float64(resp.AltitudeM), - nil -} - -func (c *client) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return r3.Vector{}, err - } - resp, err := c.client.GetLinearVelocity(ctx, &pb.GetLinearVelocityRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return r3.Vector{}, err - } - return protoutils.ConvertVectorProtoToR3(resp.LinearVelocity), nil -} - -func (c *client) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return spatialmath.AngularVelocity{}, err - } - resp, err := c.client.GetAngularVelocity(ctx, &pb.GetAngularVelocityRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return spatialmath.AngularVelocity{}, err - } - return spatialmath.AngularVelocity(protoutils.ConvertVectorProtoToR3(resp.AngularVelocity)), nil -} - -func (c *client) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return r3.Vector{}, err - } - resp, err := c.client.GetLinearAcceleration(ctx, &pb.GetLinearAccelerationRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return r3.Vector{}, err - } - return protoutils.ConvertVectorProtoToR3(resp.LinearAcceleration), nil -} - -func (c *client) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return 0, err - } - resp, err := c.client.GetCompassHeading(ctx, &pb.GetCompassHeadingRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return 0, err - } - return resp.Value, nil -} - -func (c *client) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return spatialmath.NewZeroOrientation(), err - } - resp, err := c.client.GetOrientation(ctx, &pb.GetOrientationRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return spatialmath.NewZeroOrientation(), err - } - return protoutils.ConvertProtoToOrientation(resp.Orientation), nil -} - -func (c *client) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetReadings(ctx, &commonpb.GetReadingsRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - - return protoutils.ReadingProtoToGo(resp.Readings) -} - -func (c *client) Accuracy(ctx context.Context, extra map[string]interface{}) (*Accuracy, error, -) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetAccuracy(ctx, &pb.GetAccuracyRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - return protoFeaturesToAccuracy(resp), nil -} - -func (c *client) Properties(ctx context.Context, extra map[string]interface{}) (*Properties, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetProperties(ctx, &pb.GetPropertiesRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - return ProtoFeaturesToProperties(resp), nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return protoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/components/movementsensor/client_test.go b/components/movementsensor/client_test.go deleted file mode 100644 index 6165e1ff63a..00000000000 --- a/components/movementsensor/client_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package movementsensor_test - -import ( - "context" - "errors" - "math" - "net" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/movementsensor" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testMovementSensorName = "ms1" - failMovementSensorName = "ms2" - missingMovementSensorName = "ms4" - errLocation = errors.New("can't get location") - errLinearVelocity = errors.New("can't get linear velocity") - errLinearAcceleration = errors.New("can't get linear acceleration") - errAngularVelocity = errors.New("can't get angular velocity") - errOrientation = errors.New("can't get orientation") - errCompassHeading = errors.New("can't get compass heading") - errProperties = errors.New("can't get properties") - errAccuracy = errors.New("can't get accuracy") -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - loc := geo.NewPoint(90, 1) - alt := 50.5 - speed := 5.4 - ang := 1.1 - ori := spatialmath.NewEulerAngles() - ori.Roll = 1.1 - heading := 202. - props := &movementsensor.Properties{LinearVelocitySupported: true} - aclZ := 1.0 - acy := &movementsensor.Accuracy{AccuracyMap: map[string]float32{"x": 1.1}} - rs := map[string]interface{}{ - "position": loc, - "altitude": alt, - "linear_velocity": r3.Vector{X: 0, Y: speed, Z: 0}, - "linear_acceleration": r3.Vector{X: 0, Y: 0, Z: aclZ}, - "angular_velocity": spatialmath.AngularVelocity{X: 0, Y: 0, Z: ang}, - "compass": heading, - "orientation": ori.OrientationVectorDegrees(), - } - - injectMovementSensor := &inject.MovementSensor{} - injectMovementSensor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return loc, alt, nil - } - injectMovementSensor.LinearVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{X: 0, Y: speed, Z: 0}, nil - } - injectMovementSensor.LinearAccelerationFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{X: 0, Y: 0, Z: aclZ}, nil - } - injectMovementSensor.AngularVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{X: 0, Y: 0, Z: ang}, nil - } - injectMovementSensor.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return ori, nil - } - injectMovementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { return heading, nil } - injectMovementSensor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return props, nil - } - injectMovementSensor.AccuracyFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - return acy, nil - } - injectMovementSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return rs, nil - } - - injectMovementSensor2 := &inject.MovementSensor{} - injectMovementSensor2.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return nil, 0, errLocation - } - injectMovementSensor2.LinearVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, errLinearVelocity - } - injectMovementSensor2.LinearAccelerationFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, errLinearAcceleration - } - injectMovementSensor2.AngularVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{}, errAngularVelocity - } - injectMovementSensor2.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return nil, errOrientation - } - injectMovementSensor2.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, errCompassHeading - } - injectMovementSensor2.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return nil, errReadingsFailed - } - injectMovementSensor2.AccuracyFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - return nil, errAccuracy - } - injectMovementSensor2.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return nil, errProperties - } - - gpsSvc, err := resource.NewAPIResourceCollection(movementsensor.API, map[resource.Name]movementsensor.MovementSensor{ - movementsensor.Named(testMovementSensorName): injectMovementSensor, - movementsensor.Named(failMovementSensorName): injectMovementSensor2, - }) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[movementsensor.MovementSensor](movementsensor.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, gpsSvc), test.ShouldBeNil) - - injectMovementSensor.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("MovementSensor client 1", func(t *testing.T) { - // working - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - gps1Client, err := movementsensor.NewClientFromConn(context.Background(), conn, "", movementsensor.Named(testMovementSensorName), logger) - test.That(t, err, test.ShouldBeNil) - - // DoCommand - resp, err := gps1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - loc1, alt1, err := gps1Client.Position(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, loc1, test.ShouldResemble, loc) - test.That(t, alt1, test.ShouldAlmostEqual, alt) - test.That(t, injectMovementSensor.PositionFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - vel1, err := gps1Client.LinearVelocity(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, vel1.Y, test.ShouldAlmostEqual, speed) - test.That(t, injectMovementSensor.LinearVelocityFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - av1, err := gps1Client.AngularVelocity(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, av1.Z, test.ShouldAlmostEqual, ang) - test.That(t, injectMovementSensor.AngularVelocityFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - o1, err := gps1Client.Orientation(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, o1.OrientationVectorDegrees(), test.ShouldResemble, ori.OrientationVectorDegrees()) - test.That(t, injectMovementSensor.OrientationFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - ch, err := gps1Client.CompassHeading(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ch, test.ShouldResemble, heading) - test.That(t, injectMovementSensor.CompassHeadingFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - props1, err := gps1Client.Properties(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, props1.LinearVelocitySupported, test.ShouldResemble, props.LinearVelocitySupported) - test.That(t, injectMovementSensor.PropertiesFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - acc1, err := gps1Client.Accuracy(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, acc1.AccuracyMap, test.ShouldResemble, acy.AccuracyMap) - test.That(t, math.IsNaN(float64(acc1.Hdop)), test.ShouldBeTrue) - test.That(t, injectMovementSensor.AccuracyFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - la1, err := gps1Client.LinearAcceleration(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, la1.Z, test.ShouldResemble, aclZ) - test.That(t, injectMovementSensor.LinearAccelerationExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - rs1, err := gps1Client.Readings(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs1["position"], test.ShouldResemble, rs["position"]) - test.That(t, rs1["altitude"], test.ShouldResemble, rs["altitude"]) - test.That(t, rs1["linear_velocity"], test.ShouldResemble, rs["linear_velocity"]) - test.That(t, rs1["linear_acceleration"], test.ShouldResemble, rs["linear_acceleration"]) - test.That(t, rs1["angular_velocity"], test.ShouldResemble, rs["angular_velocity"]) - test.That(t, rs1["compass"], test.ShouldResemble, rs["compass"]) - test.That(t, rs1["orientation"], test.ShouldResemble, rs["orientation"]) - - test.That(t, gps1Client.Close(context.Background()), test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("MovementSensor client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", movementsensor.Named(failMovementSensorName), logger) - test.That(t, err, test.ShouldBeNil) - - _, _, err = client2.Position(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errLocation.Error()) - - _, err = client2.LinearVelocity(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errLinearVelocity.Error()) - - _, err = client2.LinearAcceleration(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errLinearAcceleration.Error()) - - _, err = client2.AngularVelocity(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errAngularVelocity.Error()) - - _, err = client2.Readings(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errReadingsFailed.Error()) - - _, err = client2.Accuracy(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errAccuracy.Error()) - - _, err = client2.Properties(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errProperties.Error()) - - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/movementsensor/collectors.go b/components/movementsensor/collectors.go deleted file mode 100644 index 2d985d26a3f..00000000000 --- a/components/movementsensor/collectors.go +++ /dev/null @@ -1,263 +0,0 @@ -package movementsensor - -import ( - "context" - "errors" - - v1 "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/movementsensor/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/spatialmath" -) - -type method int64 - -const ( - position method = iota - linearVelocity - angularVelocity - compassHeading - linearAcceleration - orientation - readings -) - -func (m method) String() string { - switch m { - case position: - return "Position" - case linearVelocity: - return "LinearVelocity" - case angularVelocity: - return "AngularVelocity" - case compassHeading: - return "CompassHeading" - case linearAcceleration: - return "LinearAcceleration" - case orientation: - return "Orientation" - case readings: - return "Readings" - } - return "Unknown" -} - -func assertMovementSensor(resource interface{}) (MovementSensor, error) { - ms, ok := resource.(MovementSensor) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - return ms, nil -} - -// newLinearVelocityCollector returns a collector to register a linear velocity method. If one is already registered -// with the same MethodMetadata it will panic. -func newLinearVelocityCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - ms, err := assertMovementSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, extra map[string]*anypb.Any) (interface{}, error) { - vec, err := ms.LinearVelocity(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, position.String(), err) - } - return pb.GetLinearVelocityResponse{ - LinearVelocity: &v1.Vector3{ - X: vec.X, - Y: vec.Y, - Z: vec.Z, - }, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newPositionCollector returns a collector to register a position method. If one is already registered -// with the same MethodMetadata it will panic. -func newPositionCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - ms, err := assertMovementSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, extra map[string]*anypb.Any) (interface{}, error) { - pos, altitude, err := ms.Position(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, linearVelocity.String(), err) - } - var lat, lng float64 - if pos != nil { - lat = pos.Lat() - lng = pos.Lng() - } - return pb.GetPositionResponse{ - Coordinate: &v1.GeoPoint{ - Latitude: lat, - Longitude: lng, - }, - AltitudeM: float32(altitude), - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newAngularVelocityCollector returns a collector to register an angular velocity method. If one is already registered -// with the same MethodMetadata it will panic. -func newAngularVelocityCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - ms, err := assertMovementSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, extra map[string]*anypb.Any) (interface{}, error) { - vel, err := ms.AngularVelocity(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, angularVelocity.String(), err) - } - return pb.GetAngularVelocityResponse{ - AngularVelocity: &v1.Vector3{ - X: vel.X, - Y: vel.Y, - Z: vel.Z, - }, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newCompassHeadingCollector returns a collector to register a compass heading method. If one is already registered -// with the same MethodMetadata it will panic. -func newCompassHeadingCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - ms, err := assertMovementSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, extra map[string]*anypb.Any) (interface{}, error) { - heading, err := ms.CompassHeading(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, compassHeading.String(), err) - } - return pb.GetCompassHeadingResponse{ - Value: heading, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newLinearAccelerationCollector returns a collector to register a linear acceleration method. If one is already registered -// with the same MethodMetadata it will panic. -func newLinearAccelerationCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - ms, err := assertMovementSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, extra map[string]*anypb.Any) (interface{}, error) { - accel, err := ms.LinearAcceleration(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, linearAcceleration.String(), err) - } - return pb.GetLinearAccelerationResponse{ - LinearAcceleration: &v1.Vector3{ - X: accel.X, - Y: accel.Y, - Z: accel.Z, - }, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newOrientationCollector returns a collector to register an orientation method. If one is already registered -// with the same MethodMetadata it will panic. -func newOrientationCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - ms, err := assertMovementSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, extra map[string]*anypb.Any) (interface{}, error) { - orient, err := ms.Orientation(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, orientation.String(), err) - } - var orientVector *spatialmath.OrientationVectorDegrees - if orient != nil { - orientVector = orient.OrientationVectorDegrees() - } - return pb.GetOrientationResponse{ - Orientation: &v1.Orientation{ - OX: orientVector.OX, - OY: orientVector.OY, - OZ: orientVector.OZ, - Theta: orientVector.Theta, - }, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -// newReadingsCollector returns a collector to register a readings method. If one is already registered -// with the same MethodMetadata it will panic. -func newReadingsCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - ms, err := assertMovementSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, arg map[string]*anypb.Any) (interface{}, error) { - values, err := ms.Readings(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, readings.String(), err) - } - readings, err := protoutils.ReadingGoToProto(values) - if err != nil { - return nil, err - } - return v1.GetReadingsResponse{ - Readings: readings, - }, nil - }) - return data.NewCollector(cFunc, params) -} diff --git a/components/movementsensor/collectors_test.go b/components/movementsensor/collectors_test.go deleted file mode 100644 index 38b77e21526..00000000000 --- a/components/movementsensor/collectors_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package movementsensor_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - v1 "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/movementsensor/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/data" - du "go.viam.com/rdk/data/testutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/spatialmath" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - componentName = "movementsensor" - captureInterval = time.Second - numRetries = 5 -) - -var vec = r3.Vector{ - X: 1.0, - Y: 2.0, - Z: 3.0, -} - -var readingMap = map[string]any{"reading1": false, "reading2": "test"} - -func TestMovementSensorCollectors(t *testing.T) { - tests := []struct { - name string - collector data.CollectorConstructor - expected map[string]any - }{ - { - name: "Movement sensor linear velocity collector should write a velocity response", - collector: movementsensor.NewLinearVelocityCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetLinearVelocityResponse{ - LinearVelocity: r3VectorToV1Vector(vec), - }), - }, - { - name: "Movement sensor position collector should write a position response", - collector: movementsensor.NewPositionCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetPositionResponse{ - Coordinate: &v1.GeoPoint{ - Latitude: 1.0, - Longitude: 2.0, - }, - AltitudeM: 3.0, - }), - }, - { - name: "Movement sensor angular velocity collector should write a velocity response", - collector: movementsensor.NewAngularVelocityCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetAngularVelocityResponse{ - AngularVelocity: r3VectorToV1Vector(vec), - }), - }, - { - name: "Movement sensor compass heading collector should write a heading response", - collector: movementsensor.NewCompassHeadingCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetCompassHeadingResponse{ - Value: 1.0, - }), - }, - { - name: "Movement sensor linear acceleration collector should write an acceleration response", - collector: movementsensor.NewLinearAccelerationCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetLinearAccelerationResponse{ - LinearAcceleration: r3VectorToV1Vector(vec), - }), - }, - { - name: "Movement sensor orientation collector should write an orientation response", - collector: movementsensor.NewOrientationCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetOrientationResponse{ - Orientation: getExpectedOrientation(), - }), - }, - { - name: "Movement sensor readings collector should write a readings response", - collector: movementsensor.NewReadingsCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(du.GetExpectedReadingsStruct(readingMap).AsMap()), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: componentName, - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Clock: mockClock, - Target: &buf, - } - - movSens := newMovementSensor() - col, err := tc.collector(movSens, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, tc.expected) - }) - } -} - -func newMovementSensor() movementsensor.MovementSensor { - m := &inject.MovementSensor{} - m.LinearVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return vec, nil - } - m.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(1.0, 2.0), 3.0, nil - } - m.AngularVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{ - X: 1.0, - Y: 2.0, - Z: 3.0, - }, nil - } - m.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 1.0, nil - } - m.LinearAccelerationFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return vec, nil - } - m.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return spatialmath.NewZeroOrientation(), nil - } - m.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return readingMap, nil - } - return m -} - -func r3VectorToV1Vector(vec r3.Vector) *v1.Vector3 { - return &v1.Vector3{ - X: vec.X, - Y: vec.Y, - Z: vec.Z, - } -} - -func getExpectedOrientation() *v1.Orientation { - convertedAngles := spatialmath.NewZeroOrientation().AxisAngles() - return &v1.Orientation{ - OX: convertedAngles.RX, - OY: convertedAngles.RY, - OZ: convertedAngles.RZ, - Theta: convertedAngles.Theta, - } -} diff --git a/components/movementsensor/dualgps/dualgps.go b/components/movementsensor/dualgps/dualgps.go deleted file mode 100644 index 19f99a409d3..00000000000 --- a/components/movementsensor/dualgps/dualgps.go +++ /dev/null @@ -1,281 +0,0 @@ -// Package dualgps implements a movement sensor that calculates compass heading -// from two gps movement sensors -package dualgps - -import ( - "context" - "errors" - "math" - "sync" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -// the default offset between the two gps devices describes a setup where -// one gps is mounted on the right side of the base and -// the other gps is mounted on the left side of the base. -// This driver is not guaranteed to be performant with -// non-rtk corrected gps modules with larger error in their position. -// ___________ -// | base | -// | | -// GPS2 GPS1 -// | | -// |___________| - -const defaultOffsetDegrees = 90.0 - -var ( - errFirstGPSInvalid = errors.New("only using second gps position, error getting position from first gps") - errSecondGPSInvalid = errors.New("only using first gps position, error getting position from second gps") - errBothGPSInvalid = errors.New("unable to get a position from either GPS device, not reporting position") -) - -var model = resource.DefaultModelFamily.WithModel("dual-gps-rtk") - -// Config is used for converting the movementsensor attributes. -type Config struct { - Gps1 string `json:"first_gps"` - Gps2 string `json:"second_gps"` - Offset *float64 `json:"offset_degrees,omitempty"` -} - -// Validate validates the dual gps model's config to -// make sure that it has two gps movement sensors. -func (c *Config) Validate(path string) ([]string, error) { - var deps []string - - if c.Gps1 == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "first_gps") - } - deps = append(deps, c.Gps1) - - if c.Gps2 == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "second_gps") - } - deps = append(deps, c.Gps2) - - if c.Offset != nil && (*c.Offset < 0 || *c.Offset > 360) { - return nil, resource.NewConfigValidationError( - path, - errors.New("this driver only allows offset values from 0 to 360")) - } - return deps, nil -} - -func init() { - resource.RegisterComponent( - movementsensor.API, - model, - resource.Registration[movementsensor.MovementSensor, *Config]{Constructor: newDualGPS}) -} - -type dualGPS struct { - resource.Named - logger logging.Logger - mu sync.Mutex - offset float64 - - gps1 movementsensor.MovementSensor - gps2 movementsensor.MovementSensor -} - -// newDualGPS makes a new movement sensor. -func newDualGPS(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (movementsensor.MovementSensor, error) { - dg := dualGPS{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - - if err := dg.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return &dg, nil -} - -func (dg *dualGPS) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - dg.mu.Lock() - defer dg.mu.Unlock() - - first, err := movementsensor.FromDependencies(deps, newConf.Gps1) - if err != nil { - return err - } - dg.gps1 = first - - second, err := movementsensor.FromDependencies(deps, newConf.Gps2) - if err != nil { - return err - } - dg.gps2 = second - - dg.offset = defaultOffsetDegrees - if newConf.Offset != nil { - dg.offset = *newConf.Offset - } - - dg.logger.Debug( - "using gps named %v as first gps and gps named %v as second gps, with an offset of %v", - first.Name().ShortName(), - second.Name().ShortName(), - dg.offset, - ) - - return nil -} - -// getHeading calculates bearing, absolute heading and standardBearing angles given 2 geoPoint coordinates -// heading: 0 degrees is North, 90 degrees is East, 180 degrees is South, 270 is West. -// bearing: 0 degrees is North, 90 degrees is East, 180 degrees is South, 270 is West. -// standarBearing: 0 degrees is North, 90 degrees is East, 180 degrees is South, -90 is West. -// reference: https://www.igismap.com/formula-to-find-bearing-or-heading-angle-between-two-points-latitude-longitude/ -func getHeading(firstPoint, secondPoint *geo.Point, yawOffset float64, -) (float64, float64, float64) { - // convert latitude and longitude readings from degrees to radians - // so we can use go's periodic math functions. - firstLat := utils.DegToRad(firstPoint.Lat()) - firstLong := utils.DegToRad(firstPoint.Lng()) - secondLat := utils.DegToRad(secondPoint.Lat()) - secondLong := utils.DegToRad(secondPoint.Lng()) - - // calculate the bearing between gps1 and gps2. - deltaLong := secondLong - firstLong - y := math.Sin(deltaLong) * math.Cos(secondLat) - x := math.Cos(firstLat)*math.Sin(secondLat) - math.Sin(firstLat)*math.Cos(secondLat)*math.Cos(deltaLong) - // return the standard bearing in the -180 to 180 range, with North being 0, East being 90 - // South being 180/-180 and West being -90 - standardBearing := utils.RadToDeg(math.Atan2(y, x)) - - // maps the bearing to the range of 0-360 degrees. - bearing := standardBearing - if bearing < 0 { - bearing += 360 - } - - // calculate heading from bearing, accounting for yaw offset between the two gps - // e.g if the MovementSensor antennas are mounted on the left and right sides of the robot, - // the yaw offset would be roughly 90 degrees - heading := bearing - yawOffset - // make heading positive again - if heading < 0 { - heading += 360 - } - - return bearing, heading, standardBearing -} - -func (dg *dualGPS) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - dg.mu.Lock() - defer dg.mu.Unlock() - - geoPoint1, _, err := dg.gps1.Position(context.Background(), extra) - if err != nil { - return math.NaN(), err - } - - geoPoint2, _, err := dg.gps2.Position(context.Background(), extra) - if err != nil { - return math.NaN(), err - } - - _, heading, _ := getHeading(geoPoint1, geoPoint2, dg.offset) - return heading, nil -} - -func (dg *dualGPS) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - CompassHeadingSupported: true, - PositionSupported: true, - LinearVelocitySupported: false, - AngularVelocitySupported: false, - OrientationSupported: false, - LinearAccelerationSupported: false, - }, nil -} - -func (dg *dualGPS) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return movementsensor.DefaultAPIReadings(ctx, dg, extra) -} - -func (dg *dualGPS) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - dg.mu.Lock() - defer dg.mu.Unlock() - - geoPoint1, alt1, err1 := dg.gps1.Position(ctx, nil) - geoPoint2, alt2, err2 := dg.gps2.Position(ctx, nil) - - var mid *geo.Point - var alt float64 - var err error - - switch { - case (err1 != nil) && (err2 == nil): - mid = geoPoint2 - alt = alt2 - dg.logger.CError(ctx, err1) - err = errFirstGPSInvalid - case (err2 != nil) && (err1 == nil): - mid = geoPoint1 - alt = alt1 - dg.logger.CError(ctx, err2) - err = errSecondGPSInvalid - case (err1 != nil) && (err2 != nil): - mid = geo.NewPoint(math.NaN(), math.NaN()) - alt = math.NaN() - dg.logger.CError(ctx, err1, err2) - err = errBothGPSInvalid - default: - mid = geoPoint1.MidpointTo(geoPoint2) - alt = (alt2 + alt1) * 0.5 - err = nil - } - - return mid, alt, err -} - -// Unimplemented functions. -func (dg *dualGPS) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearAcceleration -} - -func (dg *dualGPS) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearVelocity -} - -func (dg *dualGPS) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{}, movementsensor.ErrMethodUnimplementedAngularVelocity -} - -func (dg *dualGPS) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return spatialmath.NewZeroOrientation(), movementsensor.ErrMethodUnimplementedOrientation -} - -func (dg *dualGPS) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - // TODO: RSDK-6389: for this driver, find the highest value VDOP and HDOP to show the worst accuracy in position - // check the fix and return the compass error using a calculation based on the fix and variation - // of the two positions. - // return the NMEA Fix that appears from either gps in this order: (worst) 0, 1, 2, 5, 4 (best), or -1 if - // it is not any of these (other nmea GGA fixes indicate simulated or deadreckoning gps devices) - return movementsensor.UnimplementedOptionalAccuracies(), nil -} - -func (dg *dualGPS) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return map[string]interface{}{}, resource.ErrDoUnimplemented -} - -func (dg *dualGPS) Close(ctx context.Context) error { - return nil -} diff --git a/components/movementsensor/dualgps/dualgps_test.go b/components/movementsensor/dualgps/dualgps_test.go deleted file mode 100644 index fe596a4397d..00000000000 --- a/components/movementsensor/dualgps/dualgps_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package dualgps - -import ( - "context" - "errors" - "math" - "testing" - - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testName = "test" - testGPS1 = "gps1" - testGPS2 = "gps2" - testGPS3 = "gps3" - testGPS4 = "gps4" - testPath = "somepath" -) - -func makeNewFakeGPS(name string, point *geo.Point, alt float64, err error) *inject.MovementSensor { - ms := inject.NewMovementSensor(name) - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return point, alt, err - } - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{PositionSupported: true}, nil - } - return ms -} - -func TestCreateValidateAndReconfigure(t *testing.T) { - cfg := resource.Config{ - Name: testName, - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - Gps1: testGPS1, - }, - } - implicits, err := cfg.Validate(testPath, movementsensor.API.SubtypeName) - test.That(t, implicits, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationFieldRequiredError(testPath, "second_gps")) - - cfg = resource.Config{ - Name: testName, - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - Gps1: testGPS1, - Gps2: testGPS2, - }, - } - implicits, err = cfg.Validate(testPath, movementsensor.API.SubtypeName) - test.That(t, implicits, test.ShouldResemble, []string{testGPS1, testGPS2}) - test.That(t, err, test.ShouldBeNil) - - deps := make(resource.Dependencies) - deps[movementsensor.Named(testGPS1)] = makeNewFakeGPS(testGPS1, geo.NewPoint(2.0, 2.0), 2.0, nil) - deps[movementsensor.Named(testGPS2)] = makeNewFakeGPS(testGPS2, geo.NewPoint(4.0, 4.0), 2.0, nil) - deps[movementsensor.Named(testGPS3)] = makeNewFakeGPS(testGPS3, geo.NewPoint(6.0, 6.0), 1.0, errors.New("fail")) - deps[movementsensor.Named(testGPS4)] = makeNewFakeGPS(testGPS4, geo.NewPoint(8.0, 8.0), 1.0, errors.New("bad")) - - ms, err := newDualGPS( - context.Background(), - deps, - cfg, - logging.NewDebugLogger("testLogger")) - test.That(t, err, test.ShouldBeNil) - test.That(t, ms, test.ShouldNotBeNil) - dgps, ok := ms.(*dualGPS) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, dgps.gps2.Name().ShortName(), test.ShouldResemble, testGPS2) - - pos, alt, err := ms.Position(context.Background(), nil) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 3.0, 1e-1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 3.0, 1e-1) - test.That(t, alt, test.ShouldAlmostEqual, 2.0) - test.That(t, err, test.ShouldBeNil) - compass, err := ms.CompassHeading(context.Background(), nil) - test.That(t, compass, test.ShouldAlmostEqual, 315, 1e-1) - test.That(t, err, test.ShouldBeNil) - - cfg = resource.Config{ - Name: testName, - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - Gps1: testGPS1, - Gps2: testGPS3, - }, - } - err = ms.Reconfigure(context.Background(), deps, cfg) - test.That(t, err, test.ShouldBeNil) - dgps, ok = ms.(*dualGPS) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, dgps.gps2.Name().ShortName(), test.ShouldResemble, testGPS3) - - pos, alt, err = ms.Position(context.Background(), nil) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 2.0) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 2.0) - test.That(t, alt, test.ShouldAlmostEqual, 2.0) - test.That(t, err, test.ShouldBeError, errSecondGPSInvalid) - compass, err = ms.CompassHeading(context.Background(), nil) - test.That(t, math.IsNaN(compass), test.ShouldBeTrue) - test.That(t, err, test.ShouldNotBeNil) - - cfg = resource.Config{ - Name: testName, - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - Gps1: testGPS4, - Gps2: testGPS3, - }, - } - err = ms.Reconfigure(context.Background(), deps, cfg) - test.That(t, err, test.ShouldBeNil) - dgps, ok = ms.(*dualGPS) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, dgps.gps2.Name().ShortName(), test.ShouldResemble, testGPS3) - - pos, alt, err = ms.Position(context.Background(), nil) - test.That(t, math.IsNaN(pos.Lat()), test.ShouldBeTrue) - test.That(t, math.IsNaN(pos.Lng()), test.ShouldBeTrue) - test.That(t, math.IsNaN(alt), test.ShouldBeTrue) - test.That(t, err, test.ShouldBeError, errBothGPSInvalid) - compass, err = ms.CompassHeading(context.Background(), nil) - test.That(t, math.IsNaN(compass), test.ShouldBeTrue) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestGetHeading(t *testing.T) { - testPos1 := geo.NewPoint(8.46696, -17.03663) - testPos2 := geo.NewPoint(65.35996, -17.03663) - // test case 1, standard bearing = 0, heading = 270 - bearing, heading, standardBearing := getHeading(testPos1, testPos2, 90) - test.That(t, bearing, test.ShouldAlmostEqual, 0) - test.That(t, heading, test.ShouldAlmostEqual, 270) - test.That(t, standardBearing, test.ShouldAlmostEqual, 0) - - // test case 2, reversed test case 1. - testPos1 = geo.NewPoint(65.35996, -17.03663) - testPos2 = geo.NewPoint(8.46696, -17.03663) - - bearing, heading, standardBearing = getHeading(testPos1, testPos2, 90) - test.That(t, bearing, test.ShouldAlmostEqual, 180) - test.That(t, heading, test.ShouldAlmostEqual, 90) - test.That(t, standardBearing, test.ShouldAlmostEqual, 180) - - // test case 2.5, changed yaw offsets - testPos1 = geo.NewPoint(65.35996, -17.03663) - testPos2 = geo.NewPoint(8.46696, -17.03663) - - bearing, heading, standardBearing = getHeading(testPos1, testPos2, 270) - test.That(t, bearing, test.ShouldAlmostEqual, 180) - test.That(t, heading, test.ShouldAlmostEqual, 270) - test.That(t, standardBearing, test.ShouldAlmostEqual, 180) - - // test case 3 - testPos1 = geo.NewPoint(8.46696, -17.03663) - testPos2 = geo.NewPoint(56.74367734077241, 29.369620000000015) - - bearing, heading, standardBearing = getHeading(testPos1, testPos2, 90) - test.That(t, bearing, test.ShouldAlmostEqual, 27.2412, 1e-3) - test.That(t, heading, test.ShouldAlmostEqual, 297.24126, 1e-3) - test.That(t, standardBearing, test.ShouldAlmostEqual, 27.24126, 1e-3) - - // test case 4, reversed coordinates - testPos1 = geo.NewPoint(56.74367734077241, 29.369620000000015) - testPos2 = geo.NewPoint(8.46696, -17.03663) - - bearing, heading, standardBearing = getHeading(testPos1, testPos2, 90) - test.That(t, bearing, test.ShouldAlmostEqual, 235.6498, 1e-3) - test.That(t, heading, test.ShouldAlmostEqual, 145.6498, 1e-3) - test.That(t, standardBearing, test.ShouldAlmostEqual, -124.3501, 1e-3) - - // test case 4.5, changed yaw Offset - testPos1 = geo.NewPoint(56.74367734077241, 29.369620000000015) - testPos2 = geo.NewPoint(8.46696, -17.03663) - - bearing, heading, standardBearing = getHeading(testPos1, testPos2, 270) - test.That(t, bearing, test.ShouldAlmostEqual, 235.6498, 1e-3) - test.That(t, heading, test.ShouldAlmostEqual, 325.6498, 1e-3) - test.That(t, standardBearing, test.ShouldAlmostEqual, -124.3501, 1e-3) -} diff --git a/components/movementsensor/errors.go b/components/movementsensor/errors.go deleted file mode 100644 index efe29ed7db9..00000000000 --- a/components/movementsensor/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package movementsensor - -import ( - "fmt" - - "github.com/pkg/errors" -) - -// AddressReadError returns a standard error for when we cannot read from an I2C bus. -func AddressReadError(err error, address byte, bus string) error { - msg := fmt.Sprintf("can't read from I2C address %d on bus %s", address, bus) - return errors.Wrap(err, msg) -} - -// UnexpectedDeviceError returns a standard error for we cannot find the expected device -// at the given address. -func UnexpectedDeviceError(address, response byte, deviceName string) error { - return errors.Errorf("unexpected non-%s device at address %d: response '%d'", - deviceName, address, response) -} diff --git a/components/movementsensor/export_collectors_test.go b/components/movementsensor/export_collectors_test.go deleted file mode 100644 index 946edfebb0f..00000000000 --- a/components/movementsensor/export_collectors_test.go +++ /dev/null @@ -1,13 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package movementsensor - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var ( - NewPositionCollector = newPositionCollector - NewLinearVelocityCollector = newLinearVelocityCollector - NewAngularVelocityCollector = newAngularVelocityCollector - NewCompassHeadingCollector = newCompassHeadingCollector - NewLinearAccelerationCollector = newLinearAccelerationCollector - NewOrientationCollector = newOrientationCollector - NewReadingsCollector = newReadingsCollector -) diff --git a/components/movementsensor/fake/movementsensor.go b/components/movementsensor/fake/movementsensor.go deleted file mode 100644 index bc5855dcd50..00000000000 --- a/components/movementsensor/fake/movementsensor.go +++ /dev/null @@ -1,123 +0,0 @@ -// Package fake is a fake MovementSensor for testing -package fake - -import ( - "context" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -var model = resource.DefaultModelFamily.WithModel("fake") - -// Config is used for converting fake movementsensor attributes. -type Config struct { - resource.TriviallyValidateConfig -} - -func init() { - resource.RegisterComponent( - movementsensor.API, - model, - resource.Registration[movementsensor.MovementSensor, *Config]{Constructor: NewMovementSensor}) -} - -// NewMovementSensor makes a new fake movement sensor. -func NewMovementSensor(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (movementsensor.MovementSensor, error) { - return &MovementSensor{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - }, nil -} - -// MovementSensor implements is a fake movement sensor interface. -type MovementSensor struct { - resource.Named - resource.AlwaysRebuild - logger logging.Logger -} - -// Position gets the position of a fake movementsensor. -func (f *MovementSensor) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - p := geo.NewPoint(40.7, -73.98) - return p, 50.5, nil -} - -// LinearVelocity gets the linear velocity of a fake movementsensor. -func (f *MovementSensor) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{Y: 5.4}, nil -} - -// LinearAcceleration gets the linear acceleration of a fake movementsensor. -func (f *MovementSensor) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{X: 2.2, Y: 4.5, Z: 2}, nil -} - -// AngularVelocity gets the angular velocity of a fake movementsensor. -func (f *MovementSensor) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{Z: 1}, nil -} - -// CompassHeading gets the compass headings of a fake movementsensor. -func (f *MovementSensor) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 25, nil -} - -// Orientation gets the orientation of a fake movementsensor. -func (f *MovementSensor) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return spatialmath.NewZeroOrientation(), nil -} - -// DoCommand uses a map string to run custom functionality of a fake movementsensor. -func (f *MovementSensor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return map[string]interface{}{}, nil -} - -// Accuracy gets the accuracy of a fake movementsensor. -func (f *MovementSensor) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - acc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{}, - Hdop: 0, - Vdop: 0, - NmeaFix: 4, - CompassDegreeError: 0, - } - return acc, nil -} - -// Readings gets the readings of a fake movementsensor. -func (f *MovementSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return movementsensor.DefaultAPIReadings(ctx, f, extra) -} - -// Properties returns the properties of a fake movementsensor. -func (f *MovementSensor) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - LinearVelocitySupported: true, - AngularVelocitySupported: true, - OrientationSupported: true, - PositionSupported: true, - CompassHeadingSupported: true, - LinearAccelerationSupported: true, - }, nil -} - -// Start returns the fix of a fake gps movementsensor. -func (f *MovementSensor) Start(ctx context.Context) error { return nil } - -// Close returns the fix of a fake gps movementsensor. -func (f *MovementSensor) Close(ctx context.Context) error { - return nil -} - -// ReadFix returns the fix of a fake gps movementsensor. -func (f *MovementSensor) ReadFix(ctx context.Context) (int, error) { return 1, nil } - -// ReadSatsInView returns the number of satellites in view. -func (f *MovementSensor) ReadSatsInView(ctx context.Context) (int, error) { return 2, nil } diff --git a/components/movementsensor/gpsnmea/component.go b/components/movementsensor/gpsnmea/component.go deleted file mode 100644 index acab5011a6e..00000000000 --- a/components/movementsensor/gpsnmea/component.go +++ /dev/null @@ -1,137 +0,0 @@ -// Package gpsnmea implements an NMEA gps. -package gpsnmea - -import ( - "context" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// NMEAMovementSensor allows the use of any MovementSensor chip via a DataReader. -type NMEAMovementSensor struct { - resource.Named - resource.AlwaysRebuild - logger logging.Logger - cachedData *gpsutils.CachedData -} - -// newNMEAMovementSensor creates a new movement sensor. -func newNMEAMovementSensor( - _ context.Context, name resource.Name, dev gpsutils.DataReader, logger logging.Logger, -) (NmeaMovementSensor, error) { - g := &NMEAMovementSensor{ - Named: name.AsNamed(), - logger: logger, - cachedData: gpsutils.NewCachedData(dev, logger), - } - - return g, nil -} - -// Position returns the position and altitide of the sensor, or an error. -func (g *NMEAMovementSensor) Position( - ctx context.Context, extra map[string]interface{}, -) (*geo.Point, float64, error) { - return g.cachedData.Position(ctx, extra) -} - -// Accuracy returns the accuracy map, hDOP, vDOP, Fixquality and compass heading error. -func (g *NMEAMovementSensor) Accuracy( - ctx context.Context, extra map[string]interface{}, -) (*movementsensor.Accuracy, error) { - return g.cachedData.Accuracy(ctx, extra) -} - -// LinearVelocity returns the sensor's linear velocity. It requires having a compass heading, so we -// know which direction our speed is in. We assume all of this speed is horizontal, and not in -// gaining/losing altitude. -func (g *NMEAMovementSensor) LinearVelocity( - ctx context.Context, extra map[string]interface{}, -) (r3.Vector, error) { - return g.cachedData.LinearVelocity(ctx, extra) -} - -// LinearAcceleration returns the sensor's linear acceleration. -func (g *NMEAMovementSensor) LinearAcceleration( - ctx context.Context, extra map[string]interface{}, -) (r3.Vector, error) { - return g.cachedData.LinearAcceleration(ctx, extra) -} - -// AngularVelocity returns the sensor's angular velocity. -func (g *NMEAMovementSensor) AngularVelocity( - ctx context.Context, extra map[string]interface{}, -) (spatialmath.AngularVelocity, error) { - return g.cachedData.AngularVelocity(ctx, extra) -} - -// Orientation returns the sensor's orientation. -func (g *NMEAMovementSensor) Orientation( - ctx context.Context, extra map[string]interface{}, -) (spatialmath.Orientation, error) { - return g.cachedData.Orientation(ctx, extra) -} - -// CompassHeading returns the heading, from the range 0->360. -func (g *NMEAMovementSensor) CompassHeading( - ctx context.Context, extra map[string]interface{}, -) (float64, error) { - return g.cachedData.CompassHeading(ctx, extra) -} - -// ReadFix returns Fix quality of MovementSensor measurements. -func (g *NMEAMovementSensor) ReadFix(ctx context.Context) (int, error) { - return g.cachedData.ReadFix(ctx) -} - -// ReadSatsInView returns the number of satellites in view. -func (g *NMEAMovementSensor) ReadSatsInView(ctx context.Context) (int, error) { - return g.cachedData.ReadSatsInView(ctx) -} - -// Readings will use return all of the MovementSensor Readings. -func (g *NMEAMovementSensor) Readings( - ctx context.Context, extra map[string]interface{}, -) (map[string]interface{}, error) { - readings, err := movementsensor.DefaultAPIReadings(ctx, g, extra) - if err != nil { - return nil, err - } - - fix, err := g.ReadFix(ctx) - if err != nil { - return nil, err - } - satsInView, err := g.ReadSatsInView(ctx) - if err != nil { - return nil, err - } - readings["fix"] = fix - readings["satellites_in_view"] = satsInView - - return readings, nil -} - -// Properties returns what movement sensor capabilities we have. -func (g *NMEAMovementSensor) Properties( - ctx context.Context, extra map[string]interface{}, -) (*movementsensor.Properties, error) { - return g.cachedData.Properties(ctx, extra) -} - -// Close shuts down the NMEAMovementSensor. -func (g *NMEAMovementSensor) Close(ctx context.Context) error { - g.logger.CDebug(ctx, "Closing NMEAMovementSensor") - // In some of the unit tests, the cachedData is nil. Only close it if it's not. - if g.cachedData != nil { - return g.cachedData.Close(ctx) - } - return nil -} diff --git a/components/movementsensor/gpsnmea/data/gps_test.json b/components/movementsensor/gpsnmea/data/gps_test.json deleted file mode 100644 index 190f7c83d0d..00000000000 --- a/components/movementsensor/gpsnmea/data/gps_test.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "components": [ - { - "name": "gps", - "model": "rtk", - "type": "gps", - "attributes": { - "ntrip_addr": "http://rtn.dot.ny.gov:8082", - "ntrip_username": "username", - "ntrip_password": "password", - "ntrip_mountpoint": "NJI2", - "ntrip_path": "", - "ntrip_baud": 115200, - "ntrip_send_nmea": true, - "ntrip_connect_attempts": 10, - "ntrip_input_protocol": "serial", - "path": "/dev/serial/by-id/usb-u-blox_AG_-_www.u-blox.com_u-blox_GNSS_receiver-if00" - } - } - ] -} diff --git a/components/movementsensor/gpsnmea/gpsnmea.go b/components/movementsensor/gpsnmea/gpsnmea.go deleted file mode 100644 index 4a0f69284a9..00000000000 --- a/components/movementsensor/gpsnmea/gpsnmea.go +++ /dev/null @@ -1,106 +0,0 @@ -// Package gpsnmea implements an NMEA serial gps. -package gpsnmea - -/* - This package supports GPS NMEA over Serial or I2C. - - NMEA reference manual: - https://www.sparkfun.com/datasheets/GPS/NMEA%20Reference%20Manual-Rev2.1-Dec07.pdf - - Example GPS NMEA chip datasheet: - https://content.u-blox.com/sites/default/files/NEO-M9N-00B_DataSheet_UBX-19014285.pdf - -*/ - -import ( - "context" - "strings" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func connectionTypeError(connType, serialConn, i2cConn string) error { - return errors.Errorf("%s is not a valid connection_type of %s, %s", - connType, - serialConn, - i2cConn) -} - -// Config is used for converting NMEA Movement Sensor attibutes. -type Config struct { - ConnectionType string `json:"connection_type"` - - *gpsutils.SerialConfig `json:"serial_attributes,omitempty"` - *gpsutils.I2CConfig `json:"i2c_attributes,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.ConnectionType == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "connection_type") - } - - switch strings.ToLower(cfg.ConnectionType) { - case i2cStr: - return nil, cfg.I2CConfig.Validate(path) - case serialStr: - return nil, cfg.SerialConfig.Validate(path) - default: - return nil, connectionTypeError(cfg.ConnectionType, serialStr, i2cStr) - } -} - -var model = resource.DefaultModelFamily.WithModel("gps-nmea") - -// NmeaMovementSensor implements a gps that sends nmea messages for movement data. -type NmeaMovementSensor interface { - movementsensor.MovementSensor - Close(ctx context.Context) error // Close MovementSensor - ReadFix(ctx context.Context) (int, error) // Returns the fix quality of the current MovementSensor measurements - ReadSatsInView(ctx context.Context) (int, error) // Returns the number of satellites in view -} - -func init() { - resource.RegisterComponent( - movementsensor.API, - model, - resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newNMEAGPS, - }) -} - -const ( - connectionType = "connection_type" - i2cStr = "i2c" - serialStr = "serial" - rtkStr = "rtk" -) - -func newNMEAGPS( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - switch strings.ToLower(newConf.ConnectionType) { - case serialStr: - return NewSerialGPSNMEA(ctx, conf.ResourceName(), newConf, logger) - case i2cStr: - return NewPmtkI2CGPSNMEA(ctx, deps, conf.ResourceName(), newConf, logger) - default: - return nil, connectionTypeError( - newConf.ConnectionType, - i2cStr, - serialStr) - } -} diff --git a/components/movementsensor/gpsnmea/pmtkI2C.go b/components/movementsensor/gpsnmea/pmtkI2C.go deleted file mode 100644 index 37fb00558b6..00000000000 --- a/components/movementsensor/gpsnmea/pmtkI2C.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build linux - -// Package gpsnmea implements a GPS NMEA component. -package gpsnmea - -import ( - "context" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// NewPmtkI2CGPSNMEA implements a gps that communicates over i2c. -func NewPmtkI2CGPSNMEA( - ctx context.Context, - deps resource.Dependencies, - name resource.Name, - conf *Config, - logger logging.Logger, -) (NmeaMovementSensor, error) { - // The nil on this next line means "use a real I2C bus, because we're not going to pass in a - // mock one." - return MakePmtkI2cGpsNmea(ctx, deps, name, conf, logger, nil) -} - -// MakePmtkI2cGpsNmea is only split out for ease of testing: you can pass in your own mock I2C bus, -// or pass in nil to have it create a real one. It is public so it can also be called from within -// the gpsrtkpmtk package. -func MakePmtkI2cGpsNmea( - ctx context.Context, - deps resource.Dependencies, - name resource.Name, - conf *Config, - logger logging.Logger, - i2cBus buses.I2C, -) (NmeaMovementSensor, error) { - dev, err := gpsutils.NewI2cDataReader(*conf.I2CConfig, i2cBus, logger) - if err != nil { - return nil, err - } - - return newNMEAMovementSensor(ctx, name, dev, logger) -} diff --git a/components/movementsensor/gpsnmea/pmtkI2C_nonlinux.go b/components/movementsensor/gpsnmea/pmtkI2C_nonlinux.go deleted file mode 100644 index d51bdc09455..00000000000 --- a/components/movementsensor/gpsnmea/pmtkI2C_nonlinux.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !linux - -// Package gpsnmea implements a GPS NMEA component. This file contains just a stub of a constructor -// for a Linux-only version of the component (using the I2C bus). -package gpsnmea - -import ( - "context" - "errors" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// NewPmtkI2CGPSNMEA implements a gps that communicates over i2c. -func NewPmtkI2CGPSNMEA( - ctx context.Context, - deps resource.Dependencies, - name resource.Name, - conf *Config, - logger logging.Logger, -) (NmeaMovementSensor, error) { - // The nil on this next line means "use a real I2C bus, because we're not going to pass in a - // mock one." - return nil, errors.New("all I2C components are only available on Linux") -} diff --git a/components/movementsensor/gpsnmea/pmtkI2C_test.go b/components/movementsensor/gpsnmea/pmtkI2C_test.go deleted file mode 100644 index 09a3aba9276..00000000000 --- a/components/movementsensor/gpsnmea/pmtkI2C_test.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build linux - -package gpsnmea - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -const ( - testBoardName = "board1" - testBusName = "1" -) - -func createMockI2c() buses.I2C { - i2c := &inject.I2C{} - handle := &inject.I2CHandle{} - handle.WriteFunc = func(ctx context.Context, b []byte) error { - return nil - } - handle.ReadFunc = func(ctx context.Context, count int) ([]byte, error) { - return nil, nil - } - handle.CloseFunc = func() error { - return nil - } - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return handle, nil - } - return i2c -} - -func TestNewI2CMovementSensor(t *testing.T) { - conf := resource.Config{ - Name: "movementsensor1", - Model: resource.DefaultModelFamily.WithModel("gps-nmea"), - API: movementsensor.API, - } - - var deps resource.Dependencies - logger := logging.NewTestLogger(t) - ctx := context.Background() - - // We try constructing a "real" component here, expecting that we never get past the config - // validation step. - g1, err := newNMEAGPS(ctx, deps, conf, logger) - test.That(t, g1, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, - utils.NewUnexpectedTypeError[*Config](conf.ConvertedAttributes)) - - conf = resource.Config{ - Name: "movementsensor2", - Model: resource.DefaultModelFamily.WithModel("gps-nmea"), - API: movementsensor.API, - ConvertedAttributes: &Config{ - ConnectionType: "I2C", - I2CConfig: &gpsutils.I2CConfig{I2CBus: testBusName}, - }, - } - config, err := resource.NativeConfig[*Config](conf) - test.That(t, err, test.ShouldBeNil) - mockI2c := createMockI2c() - - // This time, we *do* expect to construct a real object, so we need to pass in a mock I2C bus. - g2, err := MakePmtkI2cGpsNmea(ctx, deps, conf.ResourceName(), config, logger, mockI2c) - test.That(t, err, test.ShouldBeNil) - test.That(t, g2.Close(context.Background()), test.ShouldBeNil) - test.That(t, g2, test.ShouldNotBeNil) -} diff --git a/components/movementsensor/gpsnmea/serial.go b/components/movementsensor/gpsnmea/serial.go deleted file mode 100644 index dd39fd26791..00000000000 --- a/components/movementsensor/gpsnmea/serial.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package gpsnmea implements an NMEA gps. -package gpsnmea - -import ( - "context" - - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// NewSerialGPSNMEA creates a component that communicates over a serial port. -func NewSerialGPSNMEA(ctx context.Context, name resource.Name, conf *Config, logger logging.Logger) (NmeaMovementSensor, error) { - dev, err := gpsutils.NewSerialDataReader(conf.SerialConfig, logger) - if err != nil { - return nil, err - } - - return newNMEAMovementSensor(ctx, name, dev, logger) -} diff --git a/components/movementsensor/gpsnmea/serial_test.go b/components/movementsensor/gpsnmea/serial_test.go deleted file mode 100644 index f3c13163ede..00000000000 --- a/components/movementsensor/gpsnmea/serial_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package gpsnmea - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - rutils "go.viam.com/rdk/utils" -) - -func TestNewSerialMovementSensor(t *testing.T) { - var deps resource.Dependencies - path := "somepath" - - cfig := resource.Config{ - Name: "movementsensor1", - Model: resource.DefaultModelFamily.WithModel("gps-nmea"), - API: movementsensor.API, - Attributes: rutils.AttributeMap{ - "path": "", - "correction_path": "", - }, - } - - logger := logging.NewTestLogger(t) - ctx := context.Background() - - g, err := newNMEAGPS(ctx, deps, cfig, logger) - test.That(t, g, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - cfig = resource.Config{ - Name: "movementsensor1", - Model: resource.DefaultModelFamily.WithModel("gps-nmea"), - API: movementsensor.API, - ConvertedAttributes: &Config{ - ConnectionType: "serial", - SerialConfig: &gpsutils.SerialConfig{ - SerialPath: path, - SerialBaudRate: 0, - }, - }, - } - g, err = newNMEAGPS(ctx, deps, cfig, logger) - passErr := "open " + path + ": no such file or directory" - if err == nil || err.Error() != passErr { - test.That(t, err, test.ShouldBeNil) - test.That(t, g, test.ShouldNotBeNil) - } -} - -func TestCloseSerial(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - g := &NMEAMovementSensor{ - logger: logger, - } - - err := g.Close(ctx) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk.go b/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk.go deleted file mode 100644 index 06f89548adc..00000000000 --- a/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk.go +++ /dev/null @@ -1,660 +0,0 @@ -//go:build linux - -// Package gpsrtkpmtk implements a gps using serial connection -package gpsrtkpmtk - -/* - This package supports GPS RTK (Real Time Kinematics), which takes in the normal signals - from the GNSS (Global Navigation Satellite Systems) along with a correction stream to achieve - positional accuracy (accuracy tbd), over I2C bus. - - Example GPS RTK chip datasheet: - https://content.u-blox.com/sites/default/files/ZED-F9P-04B_DataSheet_UBX-21044850.pdf - - Example configuration: - - { - "name": "my-gps-rtk", - "type": "movement_sensor", - "model": "gps-nmea-rtk-pmtk", - "attributes": { - "i2c_bus": "1", - "i2c_addr": 66, - "i2c_baud_rate": 115200, - "ntrip_connect_attempts": 12, - "ntrip_mountpoint": "MNTPT", - "ntrip_password": "pass", - "ntrip_url": "http://ntrip/url", - "ntrip_username": "usr" - }, - "depends_on": [], - } - -*/ - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "math" - "strings" - "sync" - - "github.com/go-gnss/rtcm/rtcm3" - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/utils" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -var rtkmodel = resource.DefaultModelFamily.WithModel("gps-nmea-rtk-pmtk") - -// Config is used for converting NMEA MovementSensor with RTK capabilities config attributes. -type Config struct { - I2CBus string `json:"i2c_bus"` - I2CAddr int `json:"i2c_addr"` - I2CBaudRate int `json:"i2c_baud_rate,omitempty"` - - NtripURL string `json:"ntrip_url"` - NtripConnectAttempts int `json:"ntrip_connect_attempts,omitempty"` - NtripMountpoint string `json:"ntrip_mountpoint,omitempty"` - NtripPass string `json:"ntrip_password,omitempty"` - NtripUser string `json:"ntrip_username,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - err := cfg.validateI2C(path) - if err != nil { - return nil, err - } - - err = cfg.validateNtrip(path) - if err != nil { - return nil, err - } - - return []string{}, nil -} - -// validateI2C ensures all parts of the config are valid. -func (cfg *Config) validateI2C(path string) error { - if cfg.I2CBus == "" { - return resource.NewConfigValidationFieldRequiredError(path, "i2c_bus") - } - if cfg.I2CAddr == 0 { - return resource.NewConfigValidationFieldRequiredError(path, "i2c_addr") - } - return nil -} - -// validateNtrip ensures all parts of the config are valid. -func (cfg *Config) validateNtrip(path string) error { - if cfg.NtripURL == "" { - return resource.NewConfigValidationFieldRequiredError(path, "ntrip_url") - } - return nil -} - -func init() { - resource.RegisterComponent( - movementsensor.API, - rtkmodel, - resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newRTKI2C, - }) -} - -// rtkI2C is an nmea movementsensor model that can intake RTK correction data via I2C. -type rtkI2C struct { - resource.Named - resource.AlwaysRebuild - logger logging.Logger - cancelCtx context.Context - cancelFunc func() - - activeBackgroundWorkers sync.WaitGroup - - mu sync.Mutex - ntripClient *gpsutils.NtripInfo - ntripStatus bool - - err movementsensor.LastError - lastposition movementsensor.LastPosition - - cachedData *gpsutils.CachedData - correctionWriter io.ReadWriteCloser - - bus buses.I2C - mockI2c buses.I2C // Will be nil unless we're in a unit test - wbaud int - addr byte -} - -// Reconfigure reconfigures attributes. -func (g *rtkI2C) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - g.mu.Lock() - defer g.mu.Unlock() - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - if newConf.I2CBaudRate == 0 { - g.wbaud = 115200 - } else { - g.wbaud = newConf.I2CBaudRate - } - - g.addr = byte(newConf.I2CAddr) - - if g.mockI2c == nil { - i2cbus, err := buses.NewI2cBus(newConf.I2CBus) - if err != nil { - return fmt.Errorf("gps init: failed to find i2c bus %s: %w", newConf.I2CBus, err) - } - g.bus = i2cbus - } else { - g.bus = g.mockI2c - } - - ntripConfig := &gpsutils.NtripConfig{ - NtripURL: newConf.NtripURL, - NtripUser: newConf.NtripUser, - NtripPass: newConf.NtripPass, - NtripMountpoint: newConf.NtripMountpoint, - NtripConnectAttempts: newConf.NtripConnectAttempts, - } - - // Init ntripInfo from attributes - tempNtripClient, err := gpsutils.NewNtripInfo(ntripConfig, g.logger) - if err != nil { - return err - } - - if g.ntripClient == nil { - g.ntripClient = tempNtripClient - } else { - tempNtripClient.Client = g.ntripClient.Client - tempNtripClient.Stream = g.ntripClient.Stream - - g.ntripClient = tempNtripClient - } - - g.logger.CDebug(ctx, "done reconfiguring") - - return nil -} - -func newRTKI2C( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - return makeRTKI2C(ctx, deps, conf, logger, nil) -} - -// makeRTKI2C is separate from newRTKI2C, above, so we can pass in a non-nil mock I2C bus during -// unit tests. -func makeRTKI2C( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - mockI2c buses.I2C, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - g := &rtkI2C{ - Named: conf.ResourceName().AsNamed(), - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - logger: logger, - err: movementsensor.NewLastError(1, 1), - lastposition: movementsensor.NewLastPosition(), - mockI2c: mockI2c, - } - - if err = g.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - config := gpsutils.I2CConfig{ - I2CBus: newConf.I2CBus, - I2CBaudRate: newConf.I2CBaudRate, - I2CAddr: newConf.I2CAddr, - } - if config.I2CBaudRate == 0 { - config.I2CBaudRate = 115200 - } - - // If we have a mock I2C bus, pass that in, too. If we don't, it'll be nil and constructing the - // reader will create a real I2C bus instead. - dev, err := gpsutils.NewI2cDataReader(config, mockI2c, logger) - if err != nil { - return nil, err - } - g.cachedData = gpsutils.NewCachedData(dev, logger) - - if err := g.start(); err != nil { - return nil, err - } - return g, g.err.Get() -} - -// Start begins NTRIP receiver with i2c protocol and begins reading/updating MovementSensor measurements. -func (g *rtkI2C) start() error { - g.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { g.receiveAndWriteI2C(g.cancelCtx) }) - - return g.err.Get() -} - -// getStream attempts to connect to ntrip stream until successful connection or timeout. -func (g *rtkI2C) getStream(mountPoint string, maxAttempts int) error { - success := false - attempts := 0 - - var rc io.ReadCloser - var err error - - g.logger.Debug("Getting NTRIP stream") - - for !success && attempts < maxAttempts { - select { - case <-g.cancelCtx.Done(): - return errors.New("Canceled") - default: - } - - rc, err = func() (io.ReadCloser, error) { - g.mu.Lock() - defer g.mu.Unlock() - return g.ntripClient.Client.GetStream(mountPoint) - }() - if err == nil { - success = true - } - attempts++ - } - - if err != nil { - // if the error is related to ICY, we log it as warning. - if strings.Contains(err.Error(), "ICY") { - g.logger.Warnf("Detected old HTTP protocol: %s", err) - } else { - g.logger.Errorf("Can't connect to NTRIP stream: %s", err) - return err - } - } - - g.logger.Debug("Connected to stream") - g.mu.Lock() - defer g.mu.Unlock() - - g.ntripClient.Stream = rc - return g.err.Get() -} - -// receiveAndWriteI2C connects to NTRIP receiver and sends correction stream to the MovementSensor through I2C protocol. -func (g *rtkI2C) receiveAndWriteI2C(ctx context.Context) { - defer g.activeBackgroundWorkers.Done() - if err := g.cancelCtx.Err(); err != nil { - return - } - err := g.ntripClient.Connect(g.cancelCtx, g.logger) - if err != nil { - g.err.Set(err) - return - } - - if !g.ntripClient.Client.IsCasterAlive() { - g.logger.CInfof(ctx, "caster %s seems to be down", g.ntripClient.URL) - } - - // establish I2C connection - handle, err := g.bus.OpenHandle(g.addr) - if err != nil { - g.logger.CErrorf(ctx, "can't open gps i2c %s", err) - g.err.Set(err) - return - } - defer utils.UncheckedErrorFunc(handle.Close) - - // Send GLL, RMC, VTG, GGA, GSA, and GSV sentences each 1000ms - baudcmd := fmt.Sprintf("PMTK251,%d", g.wbaud) - cmd251 := movementsensor.PMTKAddChk([]byte(baudcmd)) - cmd314 := movementsensor.PMTKAddChk([]byte("PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0")) - cmd220 := movementsensor.PMTKAddChk([]byte("PMTK220,1000")) - - err = handle.Write(ctx, cmd251) - if err != nil { - g.logger.CDebug(ctx, "Failed to set baud rate") - } - - err = handle.Write(ctx, cmd314) - if err != nil { - g.logger.CDebug(ctx, "failed to set NMEA output") - g.err.Set(err) - return - } - - err = handle.Write(ctx, cmd220) - if err != nil { - g.logger.CDebug(ctx, "failed to set NMEA update rate") - g.err.Set(err) - return - } - - err = g.getStream(g.ntripClient.MountPoint, g.ntripClient.MaxConnectAttempts) - if err != nil { - g.err.Set(err) - return - } - - // create a buffer - w := &bytes.Buffer{} - r := io.TeeReader(g.ntripClient.Stream, w) - - buf := make([]byte, 1100) - n, err := g.ntripClient.Stream.Read(buf) - if err != nil { - g.err.Set(err) - return - } - - wI2C := movementsensor.PMTKAddChk(buf[:n]) - - // port still open - err = handle.Write(ctx, wI2C) - if err != nil { - g.logger.CErrorf(ctx, "i2c handle write failed %s", err) - g.err.Set(err) - return - } - - scanner := rtcm3.NewScanner(r) - - g.mu.Lock() - g.ntripStatus = true - g.mu.Unlock() - - // It's okay to skip the mutex on this next line: g.ntripStatus can only be mutated by this - // goroutine itself. - for g.ntripStatus { - select { - case <-g.cancelCtx.Done(): - g.err.Set(err) - return - default: - } - - msg, err := scanner.NextMessage() - if err != nil { - g.mu.Lock() - g.ntripStatus = false - g.mu.Unlock() - - if msg == nil { - g.logger.CDebug(ctx, "No message... reconnecting to stream...") - err = g.getStream(g.ntripClient.MountPoint, g.ntripClient.MaxConnectAttempts) - if err != nil { - g.err.Set(err) - return - } - - w = &bytes.Buffer{} - r = io.TeeReader(g.ntripClient.Stream, w) - - buf = make([]byte, 1100) - n, err := g.ntripClient.Stream.Read(buf) - if err != nil { - g.err.Set(err) - return - } - wI2C := movementsensor.PMTKAddChk(buf[:n]) - - err = handle.Write(ctx, wI2C) - - if err != nil { - g.logger.CErrorf(ctx, "i2c handle write failed %s", err) - g.err.Set(err) - return - } - - scanner = rtcm3.NewScanner(r) - g.mu.Lock() - g.ntripStatus = true - g.mu.Unlock() - continue - } - } - } -} - -// getNtripConnectionStatus returns true if connection to NTRIP stream is OK, false if not -// -//nolint:all -func (g *rtkI2C) getNtripConnectionStatus() (bool, error) { - g.mu.Lock() - defer g.mu.Unlock() - return g.ntripStatus, g.err.Get() -} - -// Position returns the current geographic location of the MOVEMENTSENSOR. -func (g *rtkI2C) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - g.mu.Lock() - lastError := g.err.Get() - if lastError != nil { - lastPosition := g.lastposition.GetLastPosition() - g.mu.Unlock() - if lastPosition != nil { - return lastPosition, 0, nil - } - return geo.NewPoint(math.NaN(), math.NaN()), math.NaN(), lastError - } - g.mu.Unlock() - - position, alt, err := g.cachedData.Position(ctx, extra) - if err != nil { - // Use the last known valid position if current position is (0,0)/ NaN. - if position != nil && (movementsensor.IsZeroPosition(position) || movementsensor.IsPositionNaN(position)) { - lastPosition := g.lastposition.GetLastPosition() - if lastPosition != nil { - return lastPosition, alt, nil - } - } - return geo.NewPoint(math.NaN(), math.NaN()), math.NaN(), err - } - - if movementsensor.IsPositionNaN(position) { - position = g.lastposition.GetLastPosition() - } - - return position, alt, nil -} - -// LinearVelocity passthrough. -func (g *rtkI2C) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - g.mu.Lock() - lastError := g.err.Get() - if lastError != nil { - defer g.mu.Unlock() - return r3.Vector{}, lastError - } - g.mu.Unlock() - - return g.cachedData.LinearVelocity(ctx, extra) -} - -// LinearAcceleration passthrough. -func (g *rtkI2C) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - lastError := g.err.Get() - if lastError != nil { - return r3.Vector{}, lastError - } - return g.cachedData.LinearAcceleration(ctx, extra) -} - -// AngularVelocity passthrough. -func (g *rtkI2C) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - g.mu.Lock() - lastError := g.err.Get() - if lastError != nil { - defer g.mu.Unlock() - return spatialmath.AngularVelocity{}, lastError - } - g.mu.Unlock() - - return g.cachedData.AngularVelocity(ctx, extra) -} - -// CompassHeading passthrough. -func (g *rtkI2C) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - g.mu.Lock() - lastError := g.err.Get() - if lastError != nil { - defer g.mu.Unlock() - return 0, lastError - } - g.mu.Unlock() - - return g.cachedData.CompassHeading(ctx, extra) -} - -// Orientation passthrough. -func (g *rtkI2C) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - g.mu.Lock() - lastError := g.err.Get() - if lastError != nil { - defer g.mu.Unlock() - return spatialmath.NewZeroOrientation(), lastError - } - g.mu.Unlock() - - return g.cachedData.Orientation(ctx, extra) -} - -// readFix passthrough. -func (g *rtkI2C) readFix(ctx context.Context) (int, error) { - g.mu.Lock() - lastError := g.err.Get() - if lastError != nil { - defer g.mu.Unlock() - return 0, lastError - } - g.mu.Unlock() - - return g.cachedData.ReadFix(ctx) -} - -func (g *rtkI2C) readSatsInView(ctx context.Context) (int, error) { - g.mu.Lock() - lastError := g.err.Get() - if lastError != nil { - defer g.mu.Unlock() - return 0, lastError - } - g.mu.Unlock() - - return g.cachedData.ReadSatsInView(ctx) -} - -// Properties passthrough. -func (g *rtkI2C) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - lastError := g.err.Get() - if lastError != nil { - return &movementsensor.Properties{}, lastError - } - - return g.cachedData.Properties(ctx, extra) -} - -// Accuracy passthrough. -func (g *rtkI2C) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - lastError := g.err.Get() - if lastError != nil { - return nil, lastError - } - - return g.cachedData.Accuracy(ctx, extra) -} - -// Readings will use the default MovementSensor Readings if not provided. -func (g *rtkI2C) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - readings, err := movementsensor.DefaultAPIReadings(ctx, g, extra) - if err != nil { - return nil, err - } - - fix, err := g.readFix(ctx) - if err != nil { - return nil, err - } - - satsInView, err := g.readSatsInView(ctx) - if err != nil { - return nil, err - } - - readings["fix"] = fix - readings["satellites_in_view"] = satsInView - - return readings, nil -} - -// Close shuts down the rtkI2C. -func (g *rtkI2C) Close(ctx context.Context) error { - g.mu.Lock() - g.cancelFunc() - - if err := g.cachedData.Close(ctx); err != nil { - g.mu.Unlock() - return err - } - - // close ntrip writer - if g.correctionWriter != nil { - if err := g.correctionWriter.Close(); err != nil { - g.mu.Unlock() - return err - } - g.correctionWriter = nil - } - - // close ntrip client and stream - if g.ntripClient.Client != nil { - g.ntripClient.Client.CloseIdleConnections() - g.ntripClient.Client = nil - } - - if g.ntripClient.Stream != nil { - if err := g.ntripClient.Stream.Close(); err != nil { - g.mu.Unlock() - return err - } - g.ntripClient.Stream = nil - } - - g.mu.Unlock() - g.activeBackgroundWorkers.Wait() - - if err := g.err.Get(); err != nil && !errors.Is(err, context.Canceled) { - return err - } - - return nil -} diff --git a/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk_nonlinux.go b/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk_nonlinux.go deleted file mode 100644 index 47734c9d77c..00000000000 --- a/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package gpsrtkpmtk is unimplemented for non-Linux OSes. -package gpsrtkpmtk diff --git a/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk_test.go b/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk_test.go deleted file mode 100644 index 75ae13218a2..00000000000 --- a/components/movementsensor/gpsrtkpmtk/gpsrtkpmtk_test.go +++ /dev/null @@ -1,125 +0,0 @@ -//go:build linux - -package gpsrtkpmtk - -import ( - "context" - "testing" - - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - "go.viam.com/rdk/components/movementsensor/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testRoverName = "testRover" - testStationName = "testStation" - testI2cBus = "1" - testI2cAddr = 44 -) - -func TestValidateRTK(t *testing.T) { - path := "path" - cfg := Config{ - NtripURL: "http://fakeurl", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - I2CBus: testI2cBus, - I2CAddr: testI2cAddr, - } - t.Run("valid config", func(t *testing.T) { - _, err := cfg.Validate(path) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("invalid ntrip url", func(t *testing.T) { - cfg := Config{ - NtripURL: "", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - I2CBus: testI2cBus, - I2CAddr: testI2cAddr, - } - _, err := cfg.Validate(path) - test.That(t, err, test.ShouldBeError, - resource.NewConfigValidationFieldRequiredError(path, "ntrip_url")) - }) - - t.Run("invalid i2c bus", func(t *testing.T) { - cfg := Config{ - I2CBus: "", - NtripURL: "http://fakeurl", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - I2CAddr: testI2cAddr, - } - _, err := cfg.Validate(path) - test.That(t, err, test.ShouldBeError, - resource.NewConfigValidationFieldRequiredError(path, "i2c_bus")) - }) - - t.Run("invalid i2c addr", func(t *testing.T) { - cfg := Config{ - I2CAddr: 0, - NtripURL: "http://fakeurl", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - I2CBus: testI2cBus, - } - _, err := cfg.Validate(path) - test.That(t, err, test.ShouldBeError, - resource.NewConfigValidationFieldRequiredError(path, "i2c_addr")) - }) -} - -func TestReconfigure(t *testing.T) { - mockI2c := inject.I2C{} - g := &rtkI2C{ - wbaud: 9600, - addr: byte(66), - mockI2c: &mockI2c, - logger: logging.NewTestLogger(t), - } - conf := resource.Config{ - Name: "reconfig1", - ConvertedAttributes: &Config{ - NtripURL: "http://fakeurl", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - I2CBus: testI2cBus, - I2CAddr: testI2cAddr, - I2CBaudRate: 115200, - }, - } - err := g.Reconfigure(context.Background(), nil, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, g.wbaud, test.ShouldEqual, 115200) - test.That(t, g.addr, test.ShouldEqual, byte(44)) -} - -type CustomMovementSensor struct { - *fake.MovementSensor - PositionFunc func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) -} - -func (c *CustomMovementSensor) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - if c.PositionFunc != nil { - return c.PositionFunc(ctx, extra) - } - // Fallback to the default implementation if PositionFunc is not set. - return c.MovementSensor.Position(ctx, extra) -} diff --git a/components/movementsensor/gpsrtkserial/gpsrtkserial.go b/components/movementsensor/gpsrtkserial/gpsrtkserial.go deleted file mode 100644 index 7d4a20f2c80..00000000000 --- a/components/movementsensor/gpsrtkserial/gpsrtkserial.go +++ /dev/null @@ -1,703 +0,0 @@ -// Package gpsrtkserial implements a gps using serial connection -package gpsrtkserial - -/* - This package supports GPS RTK (Real Time Kinematics), which takes in the normal signals - from the GNSS (Global Navigation Satellite Systems) along with a correction stream to achieve - positional accuracy (accuracy tbd), over Serial. - - Example GPS RTK chip datasheet: - https://content.u-blox.com/sites/default/files/ZED-F9P-04B_DataSheet_UBX-21044850.pdf - - Ntrip Documentation: - https://gssc.esa.int/wp-content/uploads/2018/07/NtripDocumentation.pdf - - Example configuration: - { - "type": "movement_sensor", - "model": "gps-nmea-rtk-serial", - "name": "my-gps-rtk" - "attributes": { - "ntrip_url": "url", - "ntrip_username": "usr", - "ntrip_connect_attempts": 10, - "ntrip_mountpoint": "MTPT", - "ntrip_password": "pwd", - "serial_baud_rate": 115200, - "serial_path": "serial-path" - }, - "depends_on": [], - } - -*/ - -import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "math" - "strings" - "sync" - - "github.com/go-gnss/rtcm/rtcm3" - "github.com/golang/geo/r3" - slib "github.com/jacobsa/go-serial/serial" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/utils" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/movementsensor/gpsutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -var rtkmodel = resource.DefaultModelFamily.WithModel("gps-nmea-rtk-serial") - -const ( - serialStr = "serial" - ntripStr = "ntrip" -) - -// Config is used for converting NMEA MovementSensor with RTK capabilities config attributes. -type Config struct { - SerialPath string `json:"serial_path"` - SerialBaudRate int `json:"serial_baud_rate,omitempty"` - - NtripURL string `json:"ntrip_url"` - NtripConnectAttempts int `json:"ntrip_connect_attempts,omitempty"` - NtripMountpoint string `json:"ntrip_mountpoint,omitempty"` - NtripPass string `json:"ntrip_password,omitempty"` - NtripUser string `json:"ntrip_username,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.SerialPath == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "serial_path") - } - - if cfg.NtripURL == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "ntrip_url") - } - - return nil, nil -} - -func init() { - resource.RegisterComponent( - movementsensor.API, - rtkmodel, - resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newRTKSerial, - }) -} - -// rtkSerial is an nmea movementsensor model that can intake RTK correction data. -type rtkSerial struct { - resource.Named - resource.AlwaysRebuild - logger logging.Logger - cancelCtx context.Context - cancelFunc func() - - activeBackgroundWorkers sync.WaitGroup - - err movementsensor.LastError - lastposition movementsensor.LastPosition - lastcompassheading movementsensor.LastCompassHeading - InputProtocol string - isClosed bool - - mu sync.Mutex - - // everything below this comment is protected by mu - isConnectedToNtrip bool - ntripClient *gpsutils.NtripInfo - cachedData *gpsutils.CachedData - correctionWriter io.ReadWriteCloser - writePath string - wbaud int - isVirtualBase bool - readerWriter *bufio.ReadWriter - writer io.Writer - reader io.Reader -} - -// Reconfigure reconfigures attributes. -func (g *rtkSerial) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - g.mu.Lock() - defer g.mu.Unlock() - - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - if newConf.SerialPath != "" { - g.writePath = newConf.SerialPath - g.logger.CInfof(ctx, "updated serial_path to #%v", newConf.SerialPath) - } - - if newConf.SerialBaudRate != 0 { - g.wbaud = newConf.SerialBaudRate - g.logger.CInfof(ctx, "updated serial_baud_rate to %v", newConf.SerialBaudRate) - } else { - g.wbaud = 38400 - g.logger.CInfo(ctx, "serial_baud_rate using default baud rate 38400") - } - - ntripConfig := &gpsutils.NtripConfig{ - NtripURL: newConf.NtripURL, - NtripUser: newConf.NtripUser, - NtripPass: newConf.NtripPass, - NtripMountpoint: newConf.NtripMountpoint, - NtripConnectAttempts: newConf.NtripConnectAttempts, - } - - // Init ntripInfo from attributes - tempNtripClient, err := gpsutils.NewNtripInfo(ntripConfig, g.logger) - if err != nil { - return err - } - - if g.ntripClient != nil { // Copy over the old state - tempNtripClient.Client = g.ntripClient.Client - tempNtripClient.Stream = g.ntripClient.Stream - } - - g.ntripClient = tempNtripClient - - g.logger.Debug("done reconfiguring") - return nil -} - -func newRTKSerial( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - g := &rtkSerial{ - Named: conf.ResourceName().AsNamed(), - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - logger: logger, - err: movementsensor.NewLastError(1, 1), - lastposition: movementsensor.NewLastPosition(), - lastcompassheading: movementsensor.NewLastCompassHeading(), - } - - if err := g.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - g.InputProtocol = serialStr - - serialConfig := &gpsutils.SerialConfig{ - SerialPath: newConf.SerialPath, - SerialBaudRate: newConf.SerialBaudRate, - } - dev, err := gpsutils.NewSerialDataReader(serialConfig, logger) - if err != nil { - return nil, err - } - g.cachedData = gpsutils.NewCachedData(dev, logger) - - if err := g.start(); err != nil { - return nil, err - } - return g, g.err.Get() -} - -func (g *rtkSerial) start() error { - err := g.connectToNTRIP() - if err != nil { - return err - } - g.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(g.receiveAndWriteSerial) - return g.err.Get() -} - -// getStream attempts to connect to ntrip stream. We give up after maxAttempts unsuccessful tries. -func (g *rtkSerial) getStream(mountPoint string, maxAttempts int) error { - success := false - attempts := 0 - - var rc io.ReadCloser - var err error - - g.logger.Debug("Getting NTRIP stream") - - for !success && attempts < maxAttempts { - select { - case <-g.cancelCtx.Done(): - return errors.New("Canceled") - default: - } - - rc, err = func() (io.ReadCloser, error) { - return g.ntripClient.Client.GetStream(mountPoint) - }() - if err == nil { - success = true - } - attempts++ - } - - if err != nil { - g.logger.Errorf("Can't connect to NTRIP stream: %s", err) - return err - } - g.logger.Debug("Connected to stream") - - g.mu.Lock() - defer g.mu.Unlock() - - g.ntripClient.Stream = rc - return g.err.Get() -} - -// openPort opens the serial port for writing. -func (g *rtkSerial) openPort() error { - options := slib.OpenOptions{ - PortName: g.writePath, - BaudRate: uint(g.wbaud), - DataBits: 8, - StopBits: 1, - MinimumReadSize: 1, - } - - if err := g.cancelCtx.Err(); err != nil { - return err - } - - var err error - g.correctionWriter, err = slib.Open(options) - if err != nil { - g.logger.Errorf("serial.Open: %v", err) - return err - } - - return nil -} - -// closePort closes the serial port. -func (g *rtkSerial) closePort() { - g.mu.Lock() - defer g.mu.Unlock() - - if g.correctionWriter != nil { - err := g.correctionWriter.Close() - if err != nil { - g.logger.Errorf("Error closing port: %v", err) - } - } -} - -// connectAndParseSourceTable connects to the NTRIP caster, gets and parses source table -// from the caster. -func (g *rtkSerial) connectAndParseSourceTable() error { - if err := g.cancelCtx.Err(); err != nil { - return g.err.Get() - } - - err := g.ntripClient.Connect(g.cancelCtx, g.logger) - if err != nil { - g.err.Set(err) - return g.err.Get() - } - - if !g.ntripClient.Client.IsCasterAlive() { - g.logger.Infof("caster %s seems to be down, retrying", g.ntripClient.URL) - attempts := 0 - // we will try to connect to the caster five times if it's down. - for attempts < 5 { - if !g.ntripClient.Client.IsCasterAlive() { - attempts++ - g.logger.Debugf("attempt(s) to connect to caster: %v ", attempts) - } else { - break - } - } - if attempts == 5 { - return fmt.Errorf("caster %s is down", g.ntripClient.URL) - } - } - - g.logger.Debug("getting source table") - - srcTable, err := g.ntripClient.ParseSourcetable(g.logger) - if err != nil { - g.logger.Errorf("failed to get source table: %v", err) - return err - } - g.logger.Debugf("sourceTable is: %v\n", srcTable) - - g.logger.Debug("got sourcetable, parsing it...") - g.isVirtualBase, err = gpsutils.HasVRSStream(srcTable, g.ntripClient.MountPoint) - if err != nil { - g.logger.Errorf("can't find mountpoint in source table, found err %v\n", err) - return err - } - - return nil -} - -// connectToNTRIP connects to NTRIP stream. -func (g *rtkSerial) connectToNTRIP() error { - select { - case <-g.cancelCtx.Done(): - return errors.New("context canceled") - default: - } - err := g.connectAndParseSourceTable() - if err != nil { - return err - } - - err = g.openPort() - if err != nil { - return err - } - - if g.isVirtualBase { - g.logger.Debug("connecting to a Virtual Reference Station") - err = g.getNtripFromVRS() - if err != nil { - return err - } - } else { - g.logger.Debug("connecting to NTRIP stream........") - g.writer = bufio.NewWriter(g.correctionWriter) - err = g.getStream(g.ntripClient.MountPoint, g.ntripClient.MaxConnectAttempts) - if err != nil { - return err - } - - g.reader = io.TeeReader(g.ntripClient.Stream, g.writer) - } - - return nil -} - -// receiveAndWriteSerial connects to NTRIP receiver and sends correction stream to the MovementSensor through serial. -func (g *rtkSerial) receiveAndWriteSerial() { - defer g.activeBackgroundWorkers.Done() - defer g.closePort() - - var scanner rtcm3.Scanner - - if g.isVirtualBase { - scanner = rtcm3.NewScanner(g.readerWriter) - } else { - scanner = rtcm3.NewScanner(g.reader) - } - - g.mu.Lock() - g.isConnectedToNtrip = true - g.mu.Unlock() - - // It's okay to skip the mutex on this next line: g.isConnectedToNtrip can only be mutated by this - // goroutine itself - for g.isConnectedToNtrip && !g.isClosed { - select { - case <-g.cancelCtx.Done(): - return - default: - } - - msg, err := scanner.NextMessage() - if err != nil { - g.mu.Lock() - g.isConnectedToNtrip = false - g.mu.Unlock() - - if msg == nil { - if g.isClosed { - return - } - - if g.isVirtualBase { - g.logger.Debug("reconnecting to the Virtual Reference Station") - - err = g.getNtripFromVRS() - if err != nil && !errors.Is(err, io.EOF) { - g.err.Set(err) - return - } - - scanner = rtcm3.NewScanner(g.readerWriter) - } else { - g.logger.Debug("No message... reconnecting to stream...") - - err = g.getStream(g.ntripClient.MountPoint, g.ntripClient.MaxConnectAttempts) - if err != nil { - g.err.Set(err) - return - } - g.reader = io.TeeReader(g.ntripClient.Stream, g.writer) - scanner = rtcm3.NewScanner(g.reader) - } - - g.mu.Lock() - g.isConnectedToNtrip = true - g.mu.Unlock() - - continue - } - } - } -} - -// Most of the movementsensor functions here don't have mutex locks since g.cachedData is protected by -// it's own mutex and not having mutex around g.err is alright. - -// Position returns the current geographic location of the MOVEMENTSENSOR. -func (g *rtkSerial) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - lastError := g.err.Get() - if lastError != nil { - lastPosition := g.lastposition.GetLastPosition() - if lastPosition != nil { - return lastPosition, 0, nil - } - return geo.NewPoint(math.NaN(), math.NaN()), math.NaN(), lastError - } - - position, alt, err := g.cachedData.Position(ctx, extra) - if err != nil { - // Use the last known valid position if current position is (0,0)/ NaN. - if position != nil && (movementsensor.IsZeroPosition(position) || movementsensor.IsPositionNaN(position)) { - lastPosition := g.lastposition.GetLastPosition() - if lastPosition != nil { - return lastPosition, alt, nil - } - } - return geo.NewPoint(math.NaN(), math.NaN()), math.NaN(), err - } - - if movementsensor.IsPositionNaN(position) { - position = g.lastposition.GetLastPosition() - } - return position, alt, nil -} - -// LinearVelocity passthrough. -func (g *rtkSerial) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - lastError := g.err.Get() - if lastError != nil { - return r3.Vector{}, lastError - } - - return g.cachedData.LinearVelocity(ctx, extra) -} - -// LinearAcceleration passthrough. -func (g *rtkSerial) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - lastError := g.err.Get() - if lastError != nil { - return r3.Vector{}, lastError - } - return g.cachedData.LinearAcceleration(ctx, extra) -} - -// AngularVelocity passthrough. -func (g *rtkSerial) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - lastError := g.err.Get() - if lastError != nil { - return spatialmath.AngularVelocity{}, lastError - } - - return g.cachedData.AngularVelocity(ctx, extra) -} - -// CompassHeading passthrough. -func (g *rtkSerial) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - lastError := g.err.Get() - if lastError != nil { - return 0, lastError - } - return g.cachedData.CompassHeading(ctx, extra) -} - -// Orientation passthrough. -func (g *rtkSerial) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - lastError := g.err.Get() - if lastError != nil { - return spatialmath.NewZeroOrientation(), lastError - } - return g.cachedData.Orientation(ctx, extra) -} - -// readFix passthrough. -func (g *rtkSerial) readFix(ctx context.Context) (int, error) { - lastError := g.err.Get() - if lastError != nil { - return 0, lastError - } - return g.cachedData.ReadFix(ctx) -} - -// readSatsInView returns the number of satellites in view. -func (g *rtkSerial) readSatsInView(ctx context.Context) (int, error) { - lastError := g.err.Get() - if lastError != nil { - return 0, lastError - } - - return g.cachedData.ReadSatsInView(ctx) -} - -// Properties passthrough. -func (g *rtkSerial) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - lastError := g.err.Get() - if lastError != nil { - return &movementsensor.Properties{}, lastError - } - - return g.cachedData.Properties(ctx, extra) -} - -// Accuracy passthrough. -func (g *rtkSerial) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error, -) { - lastError := g.err.Get() - if lastError != nil { - return nil, lastError - } - - return g.cachedData.Accuracy(ctx, extra) -} - -// Readings will use the default MovementSensor Readings if not provided. -func (g *rtkSerial) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - readings, err := movementsensor.DefaultAPIReadings(ctx, g, extra) - if err != nil { - return nil, err - } - - fix, err := g.readFix(ctx) - if err != nil { - return nil, err - } - - satsInView, err := g.readSatsInView(ctx) - if err != nil { - return nil, err - } - - readings["fix"] = fix - readings["satellites_in_view"] = satsInView - - return readings, nil -} - -// Close shuts down the rtkSerial. -func (g *rtkSerial) Close(ctx context.Context) error { - g.mu.Lock() - g.cancelFunc() - - g.logger.Debug("Closing GPS RTK Serial") - if err := g.cachedData.Close(ctx); err != nil { - g.mu.Unlock() - return err - } - - // close ntrip writer - if g.correctionWriter != nil { - if err := g.correctionWriter.Close(); err != nil { - g.isClosed = true - g.mu.Unlock() - return err - } - g.correctionWriter = nil - } - - // close ntrip client and stream - if g.ntripClient.Client != nil { - g.ntripClient.Client.CloseIdleConnections() - g.ntripClient.Client = nil - } - - if g.ntripClient.Stream != nil { - if err := g.ntripClient.Stream.Close(); err != nil { - g.mu.Unlock() - return err - } - g.ntripClient.Stream = nil - } - - g.mu.Unlock() - g.activeBackgroundWorkers.Wait() - - if err := g.err.Get(); err != nil && !errors.Is(err, context.Canceled) { - return err - } - - g.logger.Debug("GPS RTK Serial is closed") - return nil -} - -// getNtripFromVRS sends GGA messages to the NTRIP Caster over a TCP connection -// to get the NTRIP steam when the mount point is a Virtual Reference Station. -func (g *rtkSerial) getNtripFromVRS() error { - g.mu.Lock() - defer g.mu.Unlock() - - g.readerWriter = gpsutils.ConnectToVirtualBase(g.ntripClient, g.logger) - - // read from the socket until we know if a successful connection has been - // established. - for { - line, _, err := g.readerWriter.ReadLine() - if err != nil { - if errors.Is(err, io.EOF) { - g.readerWriter = nil - return err - } - g.logger.Error("Failed to read server response:", err) - return err - } - - if strings.HasPrefix(string(line), "HTTP/1.1 ") { - if strings.Contains(string(line), "200 OK") { - break - } - g.logger.Errorf("Bad HTTP response: %v", string(line)) - return err - } - } - - ggaMessage, err := gpsutils.GetGGAMessage(g.correctionWriter, g.logger) - if err != nil { - g.logger.Error("Failed to get GGA message") - return err - } - - g.logger.Debugf("Writing GGA message: %v\n", string(ggaMessage)) - - _, err = g.readerWriter.WriteString(string(ggaMessage)) - if err != nil { - g.logger.Error("Failed to send NMEA data:", err) - return err - } - - err = g.readerWriter.Flush() - if err != nil { - g.logger.Error("failed to write to buffer: ", err) - return err - } - - g.logger.Debug("GGA message sent successfully.") - - return nil -} diff --git a/components/movementsensor/gpsrtkserial/gpsrtkserial_test.go b/components/movementsensor/gpsrtkserial/gpsrtkserial_test.go deleted file mode 100644 index 775d28551cf..00000000000 --- a/components/movementsensor/gpsrtkserial/gpsrtkserial_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package gpsrtkserial - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestValidateRTK(t *testing.T) { - path := "path" - t.Run("valid config", func(t *testing.T) { - cfg := Config{ - NtripURL: "http//fakeurl", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - SerialPath: path, - SerialBaudRate: 115200, - } - _, err := cfg.Validate(path) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("invalid config", func(t *testing.T) { - cfg := Config{ - NtripURL: "", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - SerialPath: path, - SerialBaudRate: 115200, - } - - _, err := cfg.Validate(path) - test.That(t, err, test.ShouldBeError, - resource.NewConfigValidationFieldRequiredError(path, "ntrip_url")) - }) - - t.Run("invalid config", func(t *testing.T) { - cfg := Config{ - NtripURL: "http//fakeurl", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - SerialPath: "", - SerialBaudRate: 115200, - } - - _, err := cfg.Validate(path) - test.That(t, err, test.ShouldBeError, - resource.NewConfigValidationFieldRequiredError(path, "serial_path")) - }) -} - -func TestReconfigure(t *testing.T) { - g := &rtkSerial{ - writePath: "/dev/ttyUSB0", - wbaud: 9600, - logger: logging.NewTestLogger(t), - } - - conf := resource.Config{ - Name: "reconfig1", - ConvertedAttributes: &Config{ - SerialPath: "/dev/ttyUSB1", - SerialBaudRate: 115200, - NtripURL: "http//fakeurl", - NtripConnectAttempts: 10, - NtripPass: "somepass", - NtripUser: "someuser", - NtripMountpoint: "NYC", - }, - } - - err := g.Reconfigure(context.Background(), nil, conf) - - test.That(t, err, test.ShouldBeNil) - test.That(t, g.writePath, test.ShouldResemble, "/dev/ttyUSB1") - test.That(t, g.wbaud, test.ShouldEqual, 115200) -} diff --git a/components/movementsensor/gpsutils/cachedData.go b/components/movementsensor/gpsutils/cachedData.go deleted file mode 100644 index 56c9f58fe14..00000000000 --- a/components/movementsensor/gpsutils/cachedData.go +++ /dev/null @@ -1,271 +0,0 @@ -package gpsutils - -import ( - "context" - "math" - "sync" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var errNilLocation = errors.New("nil gps location, check nmea message parsing") - -// DataReader represents a way to get data from a GPS NMEA device. We can read data from it using -// the channel in Messages, and we can close the device when we're done. -type DataReader interface { - Messages() chan string - Close() error -} - -// CachedData allows the use of any MovementSensor chip via a DataReader. -type CachedData struct { - mu sync.RWMutex - nmeaData NmeaParser - - err movementsensor.LastError - lastPosition movementsensor.LastPosition - lastCompassHeading movementsensor.LastCompassHeading - - dev DataReader - logger logging.Logger - - cancelCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup -} - -// NewCachedData creates a new CachedData object. -func NewCachedData(dev DataReader, logger logging.Logger) *CachedData { - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - g := CachedData{ - err: movementsensor.NewLastError(1, 1), - lastPosition: movementsensor.NewLastPosition(), - lastCompassHeading: movementsensor.NewLastCompassHeading(), - dev: dev, - logger: logger, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - } - g.start() - return &g -} - -// start begins reading nmea messages from dev and updates gps data. -func (g *CachedData) start() { - g.activeBackgroundWorkers.Add(1) - goutils.PanicCapturingGo(func() { - defer g.activeBackgroundWorkers.Done() - - messages := g.dev.Messages() - done := g.cancelCtx.Done() - for { - // First, check if we're supposed to shut down. - select { - case <-done: - return - default: - } - - // Next, wait until either we're supposed to shut down or we have new data to process. - select { - case <-done: - return - case message := <-messages: - // Update our struct's gps data in-place - err := g.ParseAndUpdate(message) - if err != nil { - g.logger.CWarnf(g.cancelCtx, "can't parse nmea sentence: %#v", err) - g.logger.Debug("Check: GPS requires clear sky view." + - "Ensure the antenna is outdoors if signal is weak or unavailable indoors.") - } - } - } - }) -} - -// ParseAndUpdate passes the provided message into the inner NmeaParser object, which parses the -// NMEA message and updates its state to match. -func (g *CachedData) ParseAndUpdate(line string) error { - g.mu.Lock() - defer g.mu.Unlock() - return g.nmeaData.ParseAndUpdate(line) -} - -// Position returns the position and altitide of the sensor, or an error. -func (g *CachedData) Position( - ctx context.Context, extra map[string]interface{}, -) (*geo.Point, float64, error) { - g.mu.RLock() - defer g.mu.RUnlock() - - lastPosition := g.lastPosition.GetLastPosition() - currentPosition := g.nmeaData.Location - - if currentPosition == nil { - return lastPosition, 0, errNilLocation - } - - // if current position is (0,0) we will return the last non-zero position - if movementsensor.IsZeroPosition(currentPosition) && !movementsensor.IsZeroPosition(lastPosition) { - return lastPosition, g.nmeaData.Alt, g.err.Get() - } - - // updating the last known valid position if the current position is non-zero - if !movementsensor.IsZeroPosition(currentPosition) && !movementsensor.IsPositionNaN(currentPosition) { - g.lastPosition.SetLastPosition(currentPosition) - } - - return currentPosition, g.nmeaData.Alt, g.err.Get() -} - -// Accuracy returns the accuracy map, hDOP, vDOP, Fixquality and compass heading error. -func (g *CachedData) Accuracy( - ctx context.Context, extra map[string]interface{}, -) (*movementsensor.Accuracy, error) { - g.mu.RLock() - defer g.mu.RUnlock() - - compassDegreeError := g.calculateCompassDegreeError(g.lastPosition.GetLastPosition(), g.nmeaData.Location) - - acc := movementsensor.Accuracy{ - AccuracyMap: map[string]float32{ - "hDOP": float32(g.nmeaData.HDOP), - "vDOP": float32(g.nmeaData.VDOP), - }, - Hdop: float32(g.nmeaData.HDOP), - Vdop: float32(g.nmeaData.VDOP), - NmeaFix: int32(g.nmeaData.FixQuality), - CompassDegreeError: float32(compassDegreeError), - } - return &acc, g.err.Get() -} - -// LinearVelocity returns the sensor's linear velocity. It requires having a compass heading, so we -// know which direction our speed is in. We assume all of this speed is horizontal, and not in -// gaining/losing altitude. -func (g *CachedData) LinearVelocity( - ctx context.Context, extra map[string]interface{}, -) (r3.Vector, error) { - g.mu.RLock() - defer g.mu.RUnlock() - - if math.IsNaN(g.nmeaData.CompassHeading) { - return r3.Vector{}, g.err.Get() - } - - headingInRadians := g.nmeaData.CompassHeading * (math.Pi / 180) - xVelocity := g.nmeaData.Speed * math.Sin(headingInRadians) - yVelocity := g.nmeaData.Speed * math.Cos(headingInRadians) - - return r3.Vector{X: xVelocity, Y: yVelocity, Z: 0}, g.err.Get() -} - -// LinearAcceleration returns the sensor's linear acceleration. -func (g *CachedData) LinearAcceleration( - ctx context.Context, extra map[string]interface{}, -) (r3.Vector, error) { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearAcceleration -} - -// AngularVelocity returns the sensor's angular velocity. -func (g *CachedData) AngularVelocity( - ctx context.Context, extra map[string]interface{}, -) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{}, movementsensor.ErrMethodUnimplementedAngularVelocity -} - -// Orientation returns the sensor's orientation. -func (g *CachedData) Orientation( - ctx context.Context, extra map[string]interface{}, -) (spatialmath.Orientation, error) { - return nil, movementsensor.ErrMethodUnimplementedOrientation -} - -// CompassHeading returns the heading, from the range 0->360. -func (g *CachedData) CompassHeading( - ctx context.Context, extra map[string]interface{}, -) (float64, error) { - g.mu.RLock() - defer g.mu.RUnlock() - - lastHeading := g.lastCompassHeading.GetLastCompassHeading() - currentHeading := g.nmeaData.CompassHeading - - if !math.IsNaN(lastHeading) && math.IsNaN(currentHeading) { - return lastHeading, nil - } - - if !math.IsNaN(currentHeading) && currentHeading != lastHeading { - g.lastCompassHeading.SetLastCompassHeading(currentHeading) - } - - return currentHeading, nil -} - -// ReadFix returns Fix quality of MovementSensor measurements. -func (g *CachedData) ReadFix(ctx context.Context) (int, error) { - g.mu.RLock() - defer g.mu.RUnlock() - return g.nmeaData.FixQuality, nil -} - -// ReadSatsInView returns the number of satellites in view. -func (g *CachedData) ReadSatsInView(ctx context.Context) (int, error) { - g.mu.RLock() - defer g.mu.RUnlock() - return g.nmeaData.SatsInView, nil -} - -// Properties returns what movement sensor capabilities we have. -func (g *CachedData) Properties( - ctx context.Context, extra map[string]interface{}, -) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - LinearVelocitySupported: true, - PositionSupported: true, - CompassHeadingSupported: true, - }, nil -} - -// calculateCompassDegreeError calculates the compass degree error -// of two geo points. -// GPS provides heading data only when it has a course of direction. -// This function provides an estimated error for that data. -func (g *CachedData) calculateCompassDegreeError(p1, p2 *geo.Point) float64 { - // if either geo points are nil, we don't calculate compass degree error - if p1 == nil || p2 == nil { - return math.NaN() - } - - adjacent := p1.GreatCircleDistance(p2) - - // If adjacent is 0, atan2 will be 90 degrees which is not desired. - if adjacent == 0 { - return math.NaN() - } - // by default we assume fix is 1-2. In this case, we assume radius to be 5m. - radius := 5.0 - // when fix is 4 or higher, we set radius to be 10cm. - if g.nmeaData.FixQuality >= 4 { - radius = 0.1 - } - // math.Atan2 returns the angle in radians, so we convert it to degrees. - thetaRadians := math.Atan2(radius, adjacent) - thetaDegrees := utils.RadToDeg(thetaRadians) - return thetaDegrees -} - -// Close shuts down the DataReader feeding this struct. -func (g *CachedData) Close(ctx context.Context) error { - g.cancelFunc() - g.activeBackgroundWorkers.Wait() - return g.dev.Close() -} diff --git a/components/movementsensor/gpsutils/cachedData_test.go b/components/movementsensor/gpsutils/cachedData_test.go deleted file mode 100644 index 552525e21c7..00000000000 --- a/components/movementsensor/gpsutils/cachedData_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package gpsutils - -import ( - "context" - "testing" - - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - "go.viam.com/rdk/logging" -) - -var loc = geo.NewPoint(90, 1) - -const ( - alt = 50.5 - speed = 5.4 - activeSats = 1 - totalSats = 2 - hAcc = 0.7 - vAcc = 0.8 - fix = 1 -) - -type mockDataReader struct{} - -func (d *mockDataReader) Messages() chan string { - return nil -} - -func (d *mockDataReader) Close() error { - return nil -} - -func TestReadingsSerial(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - g := NewCachedData(&mockDataReader{}, logger) - g.nmeaData = NmeaParser{ - Location: loc, - Alt: alt, - Speed: speed, - VDOP: vAcc, - HDOP: hAcc, - SatsInView: totalSats, - SatsInUse: activeSats, - FixQuality: fix, - } - - loc1, alt1, err := g.Position(ctx, make(map[string]interface{})) - test.That(t, err, test.ShouldBeNil) - test.That(t, loc1, test.ShouldEqual, loc) - test.That(t, alt1, test.ShouldEqual, alt) - - speed1, err := g.LinearVelocity(ctx, make(map[string]interface{})) - test.That(t, err, test.ShouldBeNil) - test.That(t, speed1.Y, test.ShouldEqual, speed) - - fix1, err := g.ReadFix(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, fix1, test.ShouldEqual, fix) -} diff --git a/components/movementsensor/gpsutils/config.go b/components/movementsensor/gpsutils/config.go deleted file mode 100644 index c01188a8586..00000000000 --- a/components/movementsensor/gpsutils/config.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package gpsutils contains GPS-related code shared between multiple components. This file -// describes data structures used to configure reading from NMEA devices. -package gpsutils - -import ( - "go.viam.com/rdk/resource" -) - -// SerialConfig is used for converting Serial NMEA MovementSensor config attributes. -type SerialConfig struct { - SerialPath string `json:"serial_path"` - SerialBaudRate int `json:"serial_baud_rate,omitempty"` - - // TestChan is a fake "serial" path for test use only - TestChan chan []uint8 `json:"-"` -} - -// I2CConfig is used for converting Serial NMEA MovementSensor config attributes. -type I2CConfig struct { - I2CBus string `json:"i2c_bus"` - I2CAddr int `json:"i2c_addr"` - I2CBaudRate int `json:"i2c_baud_rate,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *I2CConfig) Validate(path string) error { - if cfg.I2CBus == "" { - return resource.NewConfigValidationFieldRequiredError(path, "i2c_bus") - } - if cfg.I2CAddr == 0 { - return resource.NewConfigValidationFieldRequiredError(path, "i2c_addr") - } - return nil -} - -// Validate ensures all parts of the config are valid. -func (cfg *SerialConfig) Validate(path string) error { - if cfg.SerialPath == "" { - return resource.NewConfigValidationFieldRequiredError(path, "serial_path") - } - return nil -} diff --git a/components/movementsensor/gpsutils/config_test.go b/components/movementsensor/gpsutils/config_test.go deleted file mode 100644 index bb992f51810..00000000000 --- a/components/movementsensor/gpsutils/config_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package gpsutils - -import ( - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/resource" -) - -func TestValidateSerial(t *testing.T) { - fakecfg := &SerialConfig{} - path := "path" - err := fakecfg.Validate(path) - test.That(t, err, test.ShouldBeError, resource.NewConfigValidationFieldRequiredError(path, "serial_path")) - - fakecfg.SerialPath = "some-path" - err = fakecfg.Validate(path) - test.That(t, err, test.ShouldBeNil) -} - -func TestValidateI2C(t *testing.T) { - fakecfg := &I2CConfig{I2CBus: "1"} - - path := "path" - err := fakecfg.Validate(path) - test.That(t, err, test.ShouldBeError, - resource.NewConfigValidationFieldRequiredError(path, "i2c_addr")) - - fakecfg.I2CAddr = 66 - err = fakecfg.Validate(path) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/movementsensor/gpsutils/i2c_data_reader.go b/components/movementsensor/gpsutils/i2c_data_reader.go deleted file mode 100644 index 9f6d859aaa5..00000000000 --- a/components/movementsensor/gpsutils/i2c_data_reader.go +++ /dev/null @@ -1,198 +0,0 @@ -//go:build linux - -// Package gpsutils implements a GPS NMEA component. This file contains ways to read data from a -// PMTK device connected over the I2C bus. -package gpsutils - -import ( - "context" - "errors" - "fmt" - "sync" - - "go.viam.com/utils" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" -) - -// PmtkI2cDataReader implements the DataReader interface for a PMTK device by communicating with it -// over an I2C bus. -type PmtkI2cDataReader struct { - data chan string - - cancelCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup - logger logging.Logger - - bus buses.I2C - addr byte - baud int -} - -// NewI2cDataReader constructs a new DataReader that gets its NMEA messages over an I2C bus. -func NewI2cDataReader(config I2CConfig, bus buses.I2C, logger logging.Logger) (DataReader, error) { - if bus == nil { - var err error - bus, err = buses.NewI2cBus(config.I2CBus) - if err != nil { - return nil, fmt.Errorf("gps init: failed to find i2c bus %s: %w", config.I2CBus, err) - } - } - addr := config.I2CAddr - if addr == -1 { - return nil, errors.New("must specify gps i2c address") - } - baud := config.I2CBaudRate - if baud == 0 { - baud = 38400 - logger.Warn("using default baudrate: 38400") - } - - data := make(chan string) - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - - reader := PmtkI2cDataReader{ - data: data, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - logger: logger, - bus: bus, - addr: byte(addr), - baud: baud, - } - - if err := reader.initialize(); err != nil { - return nil, err - } - - reader.start() - return &reader, nil -} - -// initialize sends commands to the device to put it into a state where we can read data from it. -func (dr *PmtkI2cDataReader) initialize() error { - handle, err := dr.bus.OpenHandle(dr.addr) - if err != nil { - dr.logger.CErrorf(dr.cancelCtx, "can't open gps i2c %s", err) - return err - } - defer utils.UncheckedErrorFunc(handle.Close) - - // Set the baud rate - // TODO: does this actually do anything in the current context? The baud rate should be - // governed by the clock line on the I2C bus, not on the device. - baudcmd := fmt.Sprintf("PMTK251,%d", dr.baud) - cmd251 := movementsensor.PMTKAddChk([]byte(baudcmd)) - // Output GLL, RMC, VTG, GGA, GSA, and GSV sentences, and nothing else, every position fix - cmd314 := movementsensor.PMTKAddChk([]byte("PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0")) - // Ask for updates every 1000 ms (every second) - cmd220 := movementsensor.PMTKAddChk([]byte("PMTK220,1000")) - - err = handle.Write(dr.cancelCtx, cmd251) - if err != nil { - dr.logger.CDebug(dr.cancelCtx, "Failed to set baud rate") - return err - } - err = handle.Write(dr.cancelCtx, cmd314) - if err != nil { - return err - } - err = handle.Write(dr.cancelCtx, cmd220) - if err != nil { - return err - } - return nil -} - -func (dr *PmtkI2cDataReader) readData() ([]byte, error) { - handle, err := dr.bus.OpenHandle(dr.addr) - if err != nil { - dr.logger.CErrorf(dr.cancelCtx, "can't open gps i2c %s", err) - return nil, err - } - defer utils.UncheckedErrorFunc(handle.Close) - - buffer, err := handle.Read(dr.cancelCtx, 1024) - if err != nil { - dr.logger.CErrorf(dr.cancelCtx, "failed to read handle %s", err) - return nil, err - } - - return buffer, nil -} - -// start spins up a background coroutine to read data from the I2C bus and put it into the channel -// of complete messages. -func (dr *PmtkI2cDataReader) start() { - dr.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer dr.activeBackgroundWorkers.Done() - defer close(dr.data) - - strBuf := "" - for { - select { - case <-dr.cancelCtx.Done(): - return - default: - } - - buffer, err := dr.readData() - if err != nil { - dr.logger.CErrorf(dr.cancelCtx, "failed to read data, retrying: %s", err) - continue - } - - for _, b := range buffer { - // PMTK uses CRLF line endings to terminate sentences, but just LF to blank data. - // Since CR should never appear except at the end of our sentence, we use that to - // determine sentence end. LF is merely ignored. - if b == 0x0D { // 0x0D is the ASCII value for a carriage return - if strBuf != "" { - // Sometimes we miss "$" on the first message of the buffer. If the first - // character we read is a "G", it's likely that this has occurred, and we - // should add a "$" at the beginning. - if strBuf[0] == 0x47 { // 0x47 is the ASCII value for "G" - strBuf = "$" + strBuf - } - - dr.data <- strBuf - strBuf = "" - - // Check if we're supposed to shut down again. Perhaps we waited a long - // time to put data into the channel. - select { - case <-dr.cancelCtx.Done(): - return - default: - } - } - } else if b != 0x0A && b < 0x7F { // only append valid (printable) bytes - strBuf += string(b) - } - } - } - }) -} - -// Messages returns the channel of complete NMEA sentences we have read off of the device. It's part -// of the DataReader interface. -func (dr *PmtkI2cDataReader) Messages() chan string { - return dr.data -} - -// Close is part of the DataReader interface. It shuts everything down. -func (dr *PmtkI2cDataReader) Close() error { - dr.cancelFunc() - // If the background coroutine is trying to put a new line of data into the channel, it won't - // notice that we've canceled it until something tries taking the line out of the channel. So, - // let's try to read that out so the coroutine isn't stuck. If the background coroutine shut - // itself down already, the channel will be closed and reading something out of it will just - // return the empty string. - <-dr.data - dr.activeBackgroundWorkers.Wait() - return nil -} diff --git a/components/movementsensor/gpsutils/nmeaParser.go b/components/movementsensor/gpsutils/nmeaParser.go deleted file mode 100644 index 09078aada82..00000000000 --- a/components/movementsensor/gpsutils/nmeaParser.go +++ /dev/null @@ -1,283 +0,0 @@ -package gpsutils - -import ( - "fmt" - "math" - "strconv" - "strings" - - "github.com/adrianmo/go-nmea" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.uber.org/multierr" -) - -const ( - knotsToMPerSec = 0.51444 - kphToMPerSec = 0.27778 -) - -// NmeaParser struct combines various attributes related to GPS. -type NmeaParser struct { - Location *geo.Point - Alt float64 - Speed float64 // ground speed in m per sec - VDOP float64 // vertical accuracy - HDOP float64 // horizontal accuracy - SatsInView int // quantity satellites in view - SatsInUse int // quantity satellites in view - valid bool - FixQuality int - CompassHeading float64 // true compass heading in degree - isEast bool // direction for magnetic variation which outputs East or West. - validCompassHeading bool // true if we get course of direction instead of empty strings. -} - -func errInvalidFix(sentenceType, badFix, goodFix string) error { - return errors.Errorf("type %q sentence fix is not valid have: %q want %q", sentenceType, badFix, goodFix) -} - -// ParseAndUpdate will attempt to parse a line to an NMEA sentence, and if valid, will try to update the given struct -// with the values for that line. Nothing will be updated if there is not a valid gps fix. -func (g *NmeaParser) ParseAndUpdate(line string) error { - // Each line should start with a dollar sign and a capital G. If we start reading in the middle - // of a sentence, though, we'll get unparseable readings. Strip out everything from before the - // dollar sign, to avoid this confusion. - ind := strings.Index(line, "$G") - if ind == -1 { - line = "" // This line does not contain the start of a message!? - } else { - line = line[ind:] - } - - s, err := nmea.Parse(line) - if err != nil { - return err - } - - // The nmea.RMC message does not support parsing compass heading in its messages. So, we check - // on that separately, before updating the data we parsed with the third-party package. - if s.DataType() == nmea.TypeRMC { - g.parseRMC(line) - } - err = g.updateData(s) - - if g.Location == nil { - g.Location = geo.NewPoint(math.NaN(), math.NaN()) - return multierr.Combine(err, errors.New("no Location parsed for nmea gps, using default value of lat: NaN, long: NaN")) - } - - return nil -} - -// Given an NMEA sentence, updateData updates it. An error is returned if any of -// the function calls fails. -func (g *NmeaParser) updateData(s nmea.Sentence) error { - switch sentence := s.(type) { - case nmea.GSV: - if gsv, ok := s.(nmea.GSV); ok { - return g.updateGSV(gsv) - } - case nmea.RMC: - if rmc, ok := s.(nmea.RMC); ok { - return g.updateRMC(rmc) - } - case nmea.GSA: - if gsa, ok := s.(nmea.GSA); ok { - return g.updateGSA(gsa) - } - case nmea.GGA: - if gga, ok := s.(nmea.GGA); ok { - return g.updateGGA(gga) - } - case nmea.GLL: - if gll, ok := s.(nmea.GLL); ok { - return g.updateGLL(gll) - } - case nmea.VTG: - if vtg, ok := s.(nmea.VTG); ok { - return g.updateVTG(vtg) - } - case nmea.GNS: - if gns, ok := s.(nmea.GNS); ok { - return g.updateGNS(gns) - } - case nmea.HDT: - if hdt, ok := s.(nmea.HDT); ok { - return g.updateHDT(hdt) - } - default: - return fmt.Errorf("unrecognized sentence type: %T", sentence) - } - - return fmt.Errorf("could not cast sentence to expected type: %v", s) -} - -// updateGSV updates g.SatsInView with the information from the provided -// GSV (GPS Satellites in View) data. -func (g *NmeaParser) updateGSV(gsv nmea.GSV) error { - // GSV provides the number of satellites in view - - g.SatsInView = int(gsv.NumberSVsInView) - return nil -} - -// updateRMC updates the NmeaParser object with the information from the provided -// RMC (Recommended Minimum Navigation Information) data. -func (g *NmeaParser) updateRMC(rmc nmea.RMC) error { - if rmc.Validity != "A" { - g.valid = false - return errInvalidFix(rmc.Type, rmc.Validity, "A") - } - - g.valid = true - g.Speed = rmc.Speed * knotsToMPerSec - g.Location = geo.NewPoint(rmc.Latitude, rmc.Longitude) - - if g.validCompassHeading { - g.CompassHeading = calculateTrueHeading(rmc.Course, rmc.Variation, g.isEast) - } else { - g.CompassHeading = math.NaN() - } - return nil -} - -// updateGSA updates the NmeaParser object with the information from the provided -// GSA (GPS DOP and Active Satellites) data. -func (g *NmeaParser) updateGSA(gsa nmea.GSA) error { - switch gsa.FixType { - case "2": - // 2d fix, valid lat/lon but invalid Alt - g.valid = true - case "3": - // 3d fix - g.valid = true - default: - // No fix - g.valid = false - return errInvalidFix(gsa.Type, gsa.FixType, "2 or 3") - } - - if g.valid { - g.VDOP = gsa.VDOP - g.HDOP = gsa.HDOP - } - g.SatsInUse = len(gsa.SV) - - return nil -} - -// updateGGA updates the NmeaParser object with the information from the provided -// GGA (Global Positioning System Fix Data) data. -func (g *NmeaParser) updateGGA(gga nmea.GGA) error { - var err error - - g.FixQuality, err = strconv.Atoi(gga.FixQuality) - if err != nil { - return err - } - - if gga.FixQuality == "0" { - g.valid = false - return errInvalidFix(gga.Type, gga.FixQuality, "1 to 6") - } - - g.valid = true - g.Location = geo.NewPoint(gga.Latitude, gga.Longitude) - g.SatsInUse = int(gga.NumSatellites) - g.HDOP = gga.HDOP - g.Alt = gga.Altitude - return nil -} - -// updateGLL updates g.Location with the location information from the provided -// GLL (Geographic Position - Latitude/Longitude) data. -func (g *NmeaParser) updateGLL(gll nmea.GLL) error { - now := geo.NewPoint(gll.Latitude, gll.Longitude) - g.Location = now - return nil -} - -// updateVTG updates g.Speed with the ground speed information from the provided -// VTG (Velocity Made Good) data. -func (g *NmeaParser) updateVTG(vtg nmea.VTG) error { - // VTG provides ground speed - g.Speed = vtg.GroundSpeedKPH * kphToMPerSec - - // Check if the true heading is provided before updating - if vtg.TrueTrack != 0 { - // Update the true heading in degrees - g.CompassHeading = vtg.TrueTrack - } - - return nil -} - -// updateGNS updates the NmeaParser object with the information from the provided -// GNS (Global Navigation Satellite System) data. -func (g *NmeaParser) updateGNS(gns nmea.GNS) error { - // For each satellite we've heard from, make sure the mode is valid. If any of them are not - // valid, this entire message should not be trusted. - for _, mode := range gns.Mode { - if mode == "N" { // No satellite fix - g.valid = false - return errInvalidFix(gns.Type, mode, "A, D, P, R, F, E, M or S") - } - } - - if !g.valid { // This value gets set elsewhere, such as in a GGA message. - return nil // Don't parse this message; we're not set up yet. - } - - g.Location = geo.NewPoint(gns.Latitude, gns.Longitude) - g.SatsInUse = int(gns.SVs) - g.HDOP = gns.HDOP - g.Alt = gns.Altitude - return nil -} - -// updateHDT updates g.CompassHeading with the ground speed information from the provided. -func (g *NmeaParser) updateHDT(hdt nmea.HDT) error { - // HDT provides compass heading - g.CompassHeading = hdt.Heading - return nil -} - -// calculateTrueHeading is used to get true compass heading from RCM messages. -func calculateTrueHeading(heading, magneticDeclination float64, isEast bool) float64 { - var adjustment float64 - if isEast { - adjustment = magneticDeclination - } else { - adjustment = -magneticDeclination - } - - trueHeading := heading + adjustment - if trueHeading < 0 { - trueHeading += 360.0 - } else if trueHeading >= 360 { - trueHeading -= 360.0 - } - - return trueHeading -} - -// parseRMC sets g.isEast bool value by parsing the RMC message for compass heading -// and sets g.validCompassHeading bool since RMC message sends empty strings if -// there is no movement. -// NOTE: The go-nmea library does not provide this feature, which is why we're doing it ourselves. -func (g *NmeaParser) parseRMC(message string) { - data := strings.Split(message, ",") - if len(data) < 10 { - return - } - - if data[8] == "" { - g.validCompassHeading = false - } else { - g.validCompassHeading = true - } - - // Check if the magnetic declination is East or West - g.isEast = strings.Contains(data[10], "E") -} diff --git a/components/movementsensor/gpsutils/nmeaParser_test.go b/components/movementsensor/gpsutils/nmeaParser_test.go deleted file mode 100644 index cca30284c9f..00000000000 --- a/components/movementsensor/gpsutils/nmeaParser_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package gpsutils - -import ( - "math" - "testing" - - "go.viam.com/test" -) - -func TestParse2(t *testing.T) { - var data NmeaParser - nmeaSentence := "$GBGSV,1,1,01,33,56,045,27,1*40" - err := data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, data.Speed, test.ShouldAlmostEqual, 0) - test.That(t, math.IsNaN(data.Location.Lng()), test.ShouldBeTrue) - test.That(t, math.IsNaN(data.Location.Lat()), test.ShouldBeTrue) - - nmeaSentence = "$GNGLL,4046.43133,N,07358.90383,W,203755.00,A,A*6B" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 40.773855499999996, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, -73.9817305, 0.001) - - nmeaSentence = "$GNRMC,203756.00,A,4046.43152,N,07358.90347,W,0.059,,120723,,,A,V*0D" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Speed, test.ShouldAlmostEqual, 0.030351959999999997) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 40.77385866666667, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, -73.9817245, 0.001) - test.That(t, math.IsNaN(data.CompassHeading), test.ShouldBeTrue) - - nmeaSentence = "$GPRMC,210230,A,3855.4487,N,09446.0071,W,0.0,076.2,130495,003.8,E*69" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Speed, test.ShouldAlmostEqual, 0) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 38.924144999999996, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, -94.76678500000001, 0.001) - test.That(t, data.CompassHeading, test.ShouldAlmostEqual, 72.4) - - nmeaSentence = "$GNVTG,,T,,M,0.059,N,0.108,K,A*38" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Speed, test.ShouldEqual, 0.03000024) - - nmeaSentence = "$GNGGA,203756.00,4046.43152,N,07358.90347,W,1,05,4.65,141.4,M,-34.4,M,,*7E" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.valid, test.ShouldBeTrue) - test.That(t, data.Alt, test.ShouldEqual, 141.4) - test.That(t, data.SatsInUse, test.ShouldEqual, 5) - test.That(t, data.HDOP, test.ShouldEqual, 4.65) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 40.77385866666667, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, -73.9817245, 0.001) - - nmeaSentence = "$GNGSA,A,3,05,23,15,18,,,,,,,,,5.37,4.65,2.69,1*03" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.HDOP, test.ShouldEqual, 4.65) - test.That(t, data.VDOP, test.ShouldEqual, 2.69) -} - -func TestParsing(t *testing.T) { - var data NmeaParser - // Test a GGA sentence - nmeaSentence := "$GNGGA,191351.000,4403.4655,N,12118.7950,W,1,6,1.72,1094.5,M,-19.6,M,,*47" - err := data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.valid, test.ShouldBeTrue) - test.That(t, data.Alt, test.ShouldEqual, 1094.5) - test.That(t, data.SatsInUse, test.ShouldEqual, 6) - test.That(t, data.HDOP, test.ShouldEqual, 1.72) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 44.05776, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, -121.31325, 0.001) - - // Test GSA, should update HDOP - nmeaSentence = "$GPGSA,A,3,21,10,27,08,,,,,,,,,1.98,2.99,0.98*0E" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.HDOP, test.ShouldEqual, 2.99) - test.That(t, data.VDOP, test.ShouldEqual, 0.98) - - // Test VTG, should update speed - nmeaSentence = "$GNVTG,176.25,T,,M,0.13,N,0.25,K,A*21" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Speed, test.ShouldEqual, 0.069445) - - // Test RMC, should update speed and position - nmeaSentence = "$GNRMC,191352.000,A,4503.4656,N,13118.7951,W,0.04,90.29,011021,,,A*59" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Speed, test.ShouldAlmostEqual, 0.0205776) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 45.05776, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, -131.31325, 0.001) - - // Test GSV, should update total sats in view - nmeaSentence = " $GLGSV,2,2,07,85,23,327,34,70,21,234,21,77,07,028,*50" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.SatsInView, test.ShouldEqual, 7) - - // Test GNS, should update same fields as GGA - nmeaSentence = "$GNGNS,014035.00,4332.69262,S,17235.48549,E,RR,13,0.9,25.63,11.24,,*70" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.valid, test.ShouldBeTrue) - test.That(t, data.Alt, test.ShouldEqual, 25.63) - test.That(t, data.SatsInUse, test.ShouldEqual, 13) - test.That(t, data.HDOP, test.ShouldEqual, 0.9) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, -43.544877, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, 172.59142, 0.001) - - // Test GLL, should update location - nmeaSentence = "$GPGLL,4112.26,N,11332.22,E,213276,A,*05" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 41.20433, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, 113.537, 0.001) - - nmeaSentence = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A" - err = data.ParseAndUpdate(nmeaSentence) - test.That(t, err, test.ShouldBeNil) - test.That(t, data.Speed, test.ShouldAlmostEqual, 11.523456) - test.That(t, data.Location.Lat(), test.ShouldAlmostEqual, 48.117299999, 0.001) - test.That(t, data.Location.Lng(), test.ShouldAlmostEqual, 11.516666666, 0.001) - test.That(t, data.CompassHeading, test.ShouldAlmostEqual, 87.5) -} diff --git a/components/movementsensor/gpsutils/ntrip.go b/components/movementsensor/gpsutils/ntrip.go deleted file mode 100644 index 316c1d993bf..00000000000 --- a/components/movementsensor/gpsutils/ntrip.go +++ /dev/null @@ -1,261 +0,0 @@ -// Package gpsutils implements necessary functions to set and return -// NTRIP information here. -package gpsutils - -import ( - "bufio" - "context" - "fmt" - "io" - "strconv" - "strings" - - "github.com/de-bkg/gognss/pkg/ntrip" - - "go.viam.com/rdk/logging" -) - -const ( - // id numbers of the different fields returned in the standard - // Stream response from the ntrip client, numbered 1-18. - // Information on each field is explained int the comments - // of the Stream struct. - mp = 1 - id = 2 - format = 3 - formatDetails = 4 - carrierField = 5 - navsystem = 6 - network = 7 - country = 8 - latitude = 9 - longitude = 10 - nmeaBit = 11 - solution = 12 - generator = 13 - compression = 14 - auth = 15 - feeBit = 16 - bitRateField = 17 - misc = 18 - floatbitsize = 32 - streamSize = 200 -) - -// NtripInfo contains the information necessary to connect to a mountpoint. -type NtripInfo struct { - URL string - username string - password string - MountPoint string - Client *ntrip.Client - Stream io.ReadCloser - MaxConnectAttempts int -} - -// NtripConfig is used for converting attributes for a correction source. -type NtripConfig struct { - NtripURL string `json:"ntrip_url"` - NtripConnectAttempts int `json:"ntrip_connect_attempts,omitempty"` - NtripMountpoint string `json:"ntrip_mountpoint,omitempty"` - NtripUser string `json:"ntrip_username,omitempty"` - NtripPass string `json:"ntrip_password,omitempty"` -} - -// Sourcetable struct contains the stream. -type Sourcetable struct { - Streams []Stream -} - -// Stream contrains a stream record in sourcetable. -// https://software.rtcm-ntrip.org/wiki/STR -type Stream struct { - MP string // Datastream mountpoint - Identifier string // Source identifier (most time nearest city) - Format string // Data format of generic type (https://software.rtcm-ntrip.org/wiki/STR#DataFormats) - FormatDetails string // Specifics of data format (https://software.rtcm-ntrip.org/wiki/STR#DataFormats) - Carrier int // Phase information about GNSS correction (https://software.rtcm-ntrip.org/wiki/STR#Carrier) - NavSystem []string // Multiple navigation system (https://software.rtcm-ntrip.org/wiki/STR#NavigationSystem) - Network string // Network record in sourcetable (https://software.rtcm-ntrip.org/wiki/NET) - Country string // ISO 3166 country code (https://en.wikipedia.org/wiki/ISO_3166-1) - Latitude float32 // Position, Latitude in degree - Longitude float32 // Position, Longitude in degree - Nmea bool // Caster requires NMEA input (1) or not (0) - Solution int // Generated by single base (0) or network (1) - Generator string // Generating soft- or hardware - Compression string // Compression algorithm - Authentication string // Access protection for data streams None (N), Basic (B) or Digest (D) - Fee bool // User fee for data access: yes (Y) or no (N) - BitRate int // Datarate in bits per second - Misc string // Miscellaneous information -} - -// NewNtripInfo function validates and sets NtripConfig arributes and returns NtripInfo. -func NewNtripInfo(cfg *NtripConfig, logger logging.Logger) (*NtripInfo, error) { - n := &NtripInfo{} - - // Init NtripInfo from attributes - n.URL = cfg.NtripURL - if n.URL == "" { - return nil, fmt.Errorf("NTRIP expected non-empty string for %q", cfg.NtripURL) - } - n.username = cfg.NtripUser - if n.username == "" { - logger.Info("ntrip_username set to empty") - } - n.password = cfg.NtripPass - if n.password == "" { - logger.Info("ntrip_password set to empty") - } - n.MountPoint = cfg.NtripMountpoint - if n.MountPoint == "" { - logger.Info("ntrip_mountpoint set to empty") - } - n.MaxConnectAttempts = cfg.NtripConnectAttempts - if n.MaxConnectAttempts == 0 { - logger.Info("ntrip_connect_attempts using default 10") - n.MaxConnectAttempts = 10 - } - - logger.Debug("Returning n") - return n, nil -} - -// ParseSourcetable gets the sourcetable and parses it. -func (n *NtripInfo) ParseSourcetable(logger logging.Logger) (*Sourcetable, error) { - reader, err := n.Client.GetSourcetable() - if err != nil { - return nil, err - } - defer func() { - if err := reader.Close(); err != nil { - logger.Errorf("Error closing reader:", err) - } - }() - - st := &Sourcetable{} - st.Streams = make([]Stream, 0, streamSize) - scanner := bufio.NewScanner(reader) - -Loop: - for scanner.Scan() { - ln := scanner.Text() - - // Check if the line is a comment. - if strings.HasPrefix(ln, "#") || strings.HasPrefix(ln, "*") { - continue - } - fields := strings.Split(ln, ";") - switch fields[0] { - case "CAS": - continue - case "NET": - continue - case "STR": - if fields[mp] == n.MountPoint { - str, err := parseStream(ln) - if err != nil { - return nil, fmt.Errorf("error while parsing stream: %w", err) - } - st.Streams = append(st.Streams, str) - } - case "ENDSOURCETABLE": - break Loop - default: - return nil, fmt.Errorf("%s: illegal sourcetable line: '%s'", n.URL, ln) - } - } - - return st, nil -} - -// ParseStream parses a line from the sourcetable. -func parseStream(line string) (Stream, error) { - fields := strings.Split(line, ";") - - // Standard stream contains 19 fields. - // They are enumerated by their constants at the top of the file - if len(fields) < 19 { - return Stream{}, fmt.Errorf("missing fields at stream line: %s", line) - } - - carrier, err := strconv.Atoi(fields[carrierField]) - if err != nil { - return Stream{}, fmt.Errorf("cannot parse the streams carrier in line: %s", line) - } - - satSystems := strings.Split(fields[navsystem], "+") - - lat, err := strconv.ParseFloat(fields[latitude], floatbitsize) - if err != nil { - return Stream{}, fmt.Errorf("cannot parse the streams latitude in line: %s", line) - } - lon, err := strconv.ParseFloat(fields[longitude], floatbitsize) - if err != nil { - return Stream{}, fmt.Errorf("cannot parse the streams longitude in line: %s", line) - } - - nmea, err := strconv.ParseBool(fields[nmeaBit]) - if err != nil { - return Stream{}, fmt.Errorf("cannot parse the streams nmea in line: %s", line) - } - - sol, err := strconv.Atoi(fields[solution]) - if err != nil { - return Stream{}, fmt.Errorf("cannot parse the streams solution in line: %s", line) - } - - fee := false - if fields[feeBit] == "Y" { - fee = true - } - - bitrate, err := strconv.Atoi(fields[bitRateField]) - if err != nil { - bitrate = 0 - } - - return Stream{ - MP: fields[mp], Identifier: fields[id], Format: fields[format], FormatDetails: fields[formatDetails], - Carrier: carrier, NavSystem: satSystems, Network: fields[network], Country: fields[country], - Latitude: float32(lat), Longitude: float32(lon), Nmea: nmea, Solution: sol, Generator: fields[generator], - Compression: fields[compression], Authentication: fields[auth], Fee: fee, BitRate: bitrate, Misc: fields[misc], - }, nil -} - -// Connect attempts to initialize a new ntrip client. If we're unable to connect after multiple -// attempts, we return the last error. -func (n *NtripInfo) Connect(ctx context.Context, logger logging.Logger) error { - var c *ntrip.Client - var err error - - logger.Debug("Connecting to NTRIP caster") - for attempts := 0; attempts < n.MaxConnectAttempts; attempts++ { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - - c, err = ntrip.NewClient(n.URL, ntrip.Options{Username: n.username, Password: n.password}) - if err == nil { // Success! - logger.Info("Connected to NTRIP caster") - n.Client = c - return nil - } - } - - logger.Errorf("Can't connect to NTRIP caster: %s", err) - return err -} - -// HasStream checks if the sourcetable contains the given mountpoint in it's stream. -func (st *Sourcetable) HasStream(mountpoint string) (Stream, bool) { - for _, str := range st.Streams { - if str.MP == mountpoint { - return str, true - } - } - - return Stream{}, false -} diff --git a/components/movementsensor/gpsutils/ntrip_test.go b/components/movementsensor/gpsutils/ntrip_test.go deleted file mode 100644 index 70cd5ee5581..00000000000 --- a/components/movementsensor/gpsutils/ntrip_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package gpsutils - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" -) - -func TestConnectInvalidURL(t *testing.T) { - logger := logging.NewTestLogger(t) - cancelCtx, cancelFn := context.WithCancel(context.Background()) - defer cancelFn() - - // Note: if MaxConnectAttempts is 0, we don't bother trying to connect. - ntripInfo := &NtripInfo{MaxConnectAttempts: 1} - err := ntripInfo.Connect(cancelCtx, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `address must start with http://`) -} - -func TestConnectSucceeds(t *testing.T) { - logger := logging.NewTestLogger(t) - cancelCtx, cancelFn := context.WithCancel(context.Background()) - defer cancelFn() - - config := NtripConfig{ - NtripURL: "http://fakeurl", - NtripConnectAttempts: 10, - NtripMountpoint: "", - NtripUser: "user", - NtripPass: "pwd", - } - - ntripInfo, err := NewNtripInfo(&config, logger) - test.That(t, err, test.ShouldBeNil) - - err = ntripInfo.Connect(cancelCtx, logger) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/movementsensor/gpsutils/serial_data_reader.go b/components/movementsensor/gpsutils/serial_data_reader.go deleted file mode 100644 index d1cbeadd48f..00000000000 --- a/components/movementsensor/gpsutils/serial_data_reader.go +++ /dev/null @@ -1,113 +0,0 @@ -// Package gpsutils contains code shared between multiple GPS implementations. This file is about -// how to interact with a PMTK device (a device which gets data from GPS satellites) connected by a -// serial port. -package gpsutils - -import ( - "bufio" - "context" - "fmt" - "io" - "sync" - - "github.com/jacobsa/go-serial/serial" - "go.viam.com/utils" - - "go.viam.com/rdk/logging" -) - -// SerialDataReader implements the DataReader interface (defined in component.go) by interacting -// with the device over a serial port. -type SerialDataReader struct { - dev io.ReadWriteCloser - data chan string - cancelCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup - logger logging.Logger -} - -// NewSerialDataReader constructs a new DataReader that gets its NMEA messages over a serial port. -func NewSerialDataReader(config *SerialConfig, logger logging.Logger) (DataReader, error) { - serialPath := config.SerialPath - if serialPath == "" { - return nil, fmt.Errorf("SerialNMEAMovementSensor expected non-empty string for %q", config.SerialPath) - } - - baudRate := config.SerialBaudRate - if baudRate == 0 { - baudRate = 38400 - logger.Info("SerialNMEAMovementSensor: serial_baud_rate using default 38400") - } - - options := serial.OpenOptions{ - PortName: serialPath, - BaudRate: uint(baudRate), - DataBits: 8, - StopBits: 1, - MinimumReadSize: 4, - } - - dev, err := serial.Open(options) - if err != nil { - return nil, err - } - - data := make(chan string) - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - - reader := SerialDataReader{ - dev: dev, - data: data, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - logger: logger, - } - reader.start() - - return &reader, nil -} - -func (dr *SerialDataReader) start() { - dr.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer dr.activeBackgroundWorkers.Done() - defer close(dr.data) - - r := bufio.NewReader(dr.dev) - for { - select { - case <-dr.cancelCtx.Done(): - return - default: - } - - line, err := r.ReadString('\n') - if err != nil { - dr.logger.CErrorf(dr.cancelCtx, "can't read gps serial %s", err) - continue // The line has bogus data; don't put it in the channel. - } - dr.data <- line - } - }) -} - -// Messages returns the channel of complete NMEA sentences we have read off of the device. It's part -// of the DataReader interface. -func (dr *SerialDataReader) Messages() chan string { - return dr.data -} - -// Close is part of the DataReader interface. It shuts everything down, including our connection to -// the serial port. -func (dr *SerialDataReader) Close() error { - dr.cancelFunc() - // If the background coroutine is trying to put a new line of data into the channel, it won't - // notice that we've canceled it until something tries taking the line out of the channel. So, - // let's try to read that out so the coroutine isn't stuck. If the background coroutine shut - // itself down already, the channel will be closed and reading something out of it will just - // return the empty string. - <-dr.data - dr.activeBackgroundWorkers.Wait() - return dr.dev.Close() -} diff --git a/components/movementsensor/gpsutils/vrs.go b/components/movementsensor/gpsutils/vrs.go deleted file mode 100644 index a09d9913d76..00000000000 --- a/components/movementsensor/gpsutils/vrs.go +++ /dev/null @@ -1,105 +0,0 @@ -// Package gpsutils implements functions that are used in the gpsrtkserial and gpsrtkpmtk. -package gpsutils - -import ( - "bufio" - "encoding/base64" - "errors" - "fmt" - "io" - "net" - "strings" - - "go.viam.com/rdk/logging" -) - -// ConnectToVirtualBase is responsible for establishing a connection to -// a virtual base station using the NTRIP protocol. -func ConnectToVirtualBase(ntripInfo *NtripInfo, - logger logging.Logger, -) *bufio.ReadWriter { - mp := "/" + ntripInfo.MountPoint - credentials := ntripInfo.username + ":" + ntripInfo.password - credentialsBase64 := base64.StdEncoding.EncodeToString([]byte(credentials)) - - // Process the server URL - serverAddr := ntripInfo.URL - serverAddr = strings.TrimPrefix(serverAddr, "http://") - serverAddr = strings.TrimPrefix(serverAddr, "https://") - - conn, err := net.Dial("tcp", serverAddr) - if err != nil { - return nil - } - - rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) - - // Construct HTTP headers with CRLF line endings - httpHeaders := "GET " + mp + " HTTP/1.1\r\n" + - "Host: " + serverAddr + "\r\n" + - "Authorization: Basic " + credentialsBase64 + "\r\n" + - "Accept: */*\r\n" + - "Ntrip-Version: Ntrip/2.0\r\n" + - "User-Agent: NTRIP viam\r\n\r\n" - - // Send HTTP headers over the TCP connection - _, err = rw.Write([]byte(httpHeaders)) - if err != nil { - logger.Error("Failed to send HTTP headers:", err) - return nil - } - err = rw.Flush() - if err != nil { - logger.Error("failed to write to buffer") - return nil - } - - logger.Debugf("request header: %v\n", httpHeaders) - logger.Debug("HTTP headers sent successfully.") - return rw -} - -// GetGGAMessage checks if a GGA message exists in the buffer and returns it. -func GetGGAMessage(correctionWriter io.ReadWriteCloser, logger logging.Logger) ([]byte, error) { - buffer := make([]byte, 1024) - var totalBytesRead int - - for { - n, err := correctionWriter.Read(buffer[totalBytesRead:]) - if err != nil { - logger.Errorf("Error reading from Ntrip stream: %v", err) - return nil, err - } - - totalBytesRead += n - - // Check if the received data contains "GGA" - if ContainsGGAMessage(buffer[:totalBytesRead]) { - return buffer[:totalBytesRead], nil - } - - // If we haven't found the "GGA" message, and we've reached the end of - // the buffer, return error. - if totalBytesRead >= len(buffer) { - return nil, errors.New("GGA message not found in the received data") - } - } -} - -// ContainsGGAMessage returns true if data contains GGA message. -func ContainsGGAMessage(data []byte) bool { - dataStr := string(data) - return strings.Contains(dataStr, "GGA") -} - -// HasVRSStream returns the NMEA field associated with the given mountpoint -// and whether it is a Virtual Reference Station. -func HasVRSStream(sourceTable *Sourcetable, mountPoint string) (bool, error) { - stream, isFound := sourceTable.HasStream(mountPoint) - - if !isFound { - return false, fmt.Errorf("can not find mountpoint %s in sourcetable", mountPoint) - } - - return stream.Nmea, nil -} diff --git a/components/movementsensor/imuvectornav/imu.go b/components/movementsensor/imuvectornav/imu.go deleted file mode 100644 index 430017867f3..00000000000 --- a/components/movementsensor/imuvectornav/imu.go +++ /dev/null @@ -1,547 +0,0 @@ -//go:build linux - -// Package imuvectornav implements a component for a vectornav IMU. -package imuvectornav - -import ( - "context" - "encoding/binary" - "sync" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("imu-vectornav") - -// Config is used for converting a vectornav IMU MovementSensor config attributes. -type Config struct { - SPI string `json:"spi_bus"` - Speed *int `json:"spi_baud_rate"` - Pfreq *int `json:"polling_freq_hz"` - CSPin string `json:"chip_select_pin"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - if cfg.SPI == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "spi") - } - - if cfg.Speed == nil { - return nil, resource.NewConfigValidationFieldRequiredError(path, "spi_baud_rate") - } - - if cfg.Pfreq == nil { - return nil, resource.NewConfigValidationFieldRequiredError(path, "polling_freq_hz") - } - - if cfg.CSPin == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "cs_pin (chip select pin)") - } - return deps, nil -} - -func init() { - resource.RegisterComponent(movementsensor.API, model, resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newVectorNav, - }) -} - -type vectornav struct { - resource.Named - resource.AlwaysRebuild - angularVelocity spatialmath.AngularVelocity - acceleration r3.Vector - magnetometer r3.Vector - dV r3.Vector - dTheta r3.Vector - dt float32 - orientation spatialmath.EulerAngles - - mu sync.Mutex - spiMu sync.Mutex - polling int - - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup - bus buses.SPI - cs string - speed int - logger logging.Logger - busClosed bool - - bdVX float64 - bdVY float64 - bdVZ float64 -} - -const ( - vectorNavSPIRead uint = 1 - vectorNavSPIWrite uint = 2 - vectorNavSPITare uint = 5 -) - -type vectornavRegister uint - -const ( - modelNumber vectornavRegister = 1 - serialNumber vectornavRegister = 3 - firmwareVersion vectornavRegister = 4 - deltaVDeltaTheta vectornavRegister = 80 - deltaVDeltaThetaConfig vectornavRegister = 82 - yawPitchRollMagAccGyro vectornavRegister = 27 - acceleration vectornavRegister = 18 - referenceVectorConfiguration vectornavRegister = 83 - magAccRefVectors vectornavRegister = 21 - accCompensationConfiguration vectornavRegister = 25 - vpeAccTunning vectornavRegister = 38 -) - -// newVectorNav connect and set up a vectornav IMU over SPI. -// Will also compensate for acceleration and delta velocity bias over one second so be -// sure the IMU is still when calling this function. -func newVectorNav( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - speed := *newConf.Speed - if speed == 0 { - speed = 8000000 - } - - pfreq := *newConf.Pfreq - v := &vectornav{ - Named: conf.ResourceName().AsNamed(), - bus: buses.NewSpiBus(newConf.SPI), - logger: logger, - cs: newConf.CSPin, - speed: speed, - busClosed: false, - polling: pfreq, - } - mdl, err := v.readRegisterSPI(ctx, modelNumber, 24) - if err != nil { - return nil, err - } - sn, err := v.readRegisterSPI(ctx, serialNumber, 4) - if err != nil { - return nil, err - } - fwver, err := v.readRegisterSPI(ctx, firmwareVersion, 4) - if err != nil { - return nil, err - } - logger.CDebugf(ctx, - "model detected %s sn %d %d.%d.%d.%d", - string(mdl), - binary.LittleEndian.Uint32(sn), - fwver[0], - fwver[1], - fwver[2], - fwver[3], - ) - - // set imu location to New York for the WGM model - refvec := []byte{1, 1, 0, 0} - refvec = append(refvec, utils.BytesFromUint32LE(1000)...) - refvec = append(refvec, utils.BytesFromFloat32LE(2010.0)...) - refvec = append(refvec, []byte{0, 0, 0, 0}...) - refvec = append(refvec, utils.BytesFromFloat64LE(40.730610)...) - refvec = append(refvec, utils.BytesFromFloat64LE(-73.935242)...) - refvec = append(refvec, utils.BytesFromFloat64LE(10.0)...) - err = v.writeRegisterSPI(ctx, referenceVectorConfiguration, refvec) - if err != nil { - return nil, errors.Wrap(err, "couldn't set reference vector") - } - // enforce acceleration tuinning and reduce "trust" in acceleration data - accVpeTunning := []byte{} - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(3)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(3)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(3)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(10)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(10)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(10)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(10)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(10)...) - accVpeTunning = append(accVpeTunning, utils.BytesFromFloat32LE(10)...) - - err = v.writeRegisterSPI(ctx, vpeAccTunning, accVpeTunning) - if err != nil { - return nil, errors.Wrap(err, "couldn't set vpe adaptive tunning") - } - err = v.writeRegisterSPI(ctx, deltaVDeltaThetaConfig, []byte{0, 0, 0, 0, 0, 0}) - if err != nil { - return nil, errors.Wrap(err, "couldn't configure deltaV register") - } - // tare the heading - err = v.vectornavTareSPI(ctx) - if err != nil { - return nil, err - } - smp := uint(100) - if v.polling > 0 { - smp = uint(v.polling) - } - - // compensate for acceleration bias due to misalignement - err = v.compensateAccelBias(ctx, smp) - if err != nil { - return nil, err - } - // compensate for constant DV bias in mesurament - err = v.compensateDVBias(ctx, smp) - if err != nil { - return nil, err - } - var cancelCtx context.Context - cancelCtx, v.cancelFunc = context.WithCancel(context.Background()) - // optionally start a polling goroutine - if pfreq > 0 { - logger.CDebugf(ctx, "vecnav: will pool at %d Hz", pfreq) - waitCh := make(chan struct{}) - s := 1.0 / float64(pfreq) - v.activeBackgroundWorkers.Add(1) - goutils.PanicCapturingGo(func() { - defer v.activeBackgroundWorkers.Done() - timer := time.NewTicker(time.Duration(s * float64(time.Second))) - defer timer.Stop() - close(waitCh) - for { - select { - case <-cancelCtx.Done(): - return - default: - } - select { - case <-cancelCtx.Done(): - return - case <-timer.C: - err := v.getReadings(ctx) - if err != nil { - return - } - } - } - }) - <-waitCh - } - return v, nil -} - -func (vn *vectornav) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - vn.mu.Lock() - defer vn.mu.Unlock() - return vn.angularVelocity, nil -} - -func (vn *vectornav) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - vn.mu.Lock() - defer vn.mu.Unlock() - return vn.acceleration, nil -} - -func (vn *vectornav) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - vn.mu.Lock() - defer vn.mu.Unlock() - return &vn.orientation, nil -} - -func (vn *vectornav) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - vn.mu.Lock() - defer vn.mu.Unlock() - return vn.orientation.Yaw, nil -} - -func (vn *vectornav) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearVelocity -} - -func (vn *vectornav) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return nil, 0, movementsensor.ErrMethodUnimplementedPosition -} - -func (vn *vectornav) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - // TODO: RSDK-6389 check the vectornav's datasheet to determine what is best to return from the vector nav. - // can be done in a seprate ticket from the one mentioned in this comment. - return movementsensor.UnimplementedOptionalAccuracies(), nil -} - -func (vn *vectornav) GetMagnetometer(ctx context.Context) (r3.Vector, error) { - vn.mu.Lock() - defer vn.mu.Unlock() - return vn.magnetometer, nil -} - -func (vn *vectornav) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - AngularVelocitySupported: true, - OrientationSupported: true, - LinearAccelerationSupported: true, - }, nil -} - -func (vn *vectornav) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return movementsensor.DefaultAPIReadings(ctx, vn, extra) -} - -func (vn *vectornav) getReadings(ctx context.Context) error { - out, err := vn.readRegisterSPI(ctx, yawPitchRollMagAccGyro, 48) - if err != nil { - return err - } - vn.mu.Lock() - defer vn.mu.Unlock() - vn.orientation.Yaw = utils.DegToRad(float64(utils.Float32FromBytesLE(out[0:4]))) - vn.orientation.Pitch = utils.DegToRad(float64(utils.Float32FromBytesLE(out[4:8]))) - vn.orientation.Roll = utils.DegToRad(float64(utils.Float32FromBytesLE(out[8:12]))) - // unit gauss - vn.magnetometer.X = float64(utils.Float32FromBytesLE(out[12:16])) - vn.magnetometer.Y = float64(utils.Float32FromBytesLE(out[16:20])) - vn.magnetometer.Z = float64(utils.Float32FromBytesLE(out[20:24])) - // unit mm/s^2 - vn.acceleration.X = float64(utils.Float32FromBytesLE(out[24:28])) * 1000 - vn.acceleration.Y = float64(utils.Float32FromBytesLE(out[28:32])) * 1000 - vn.acceleration.Z = float64(utils.Float32FromBytesLE(out[32:36])) * 1000 - // unit rad/s - vn.angularVelocity.X = utils.RadToDeg(float64(utils.Float32FromBytesLE(out[36:40]))) - vn.angularVelocity.Y = utils.RadToDeg(float64(utils.Float32FromBytesLE(out[40:44]))) - vn.angularVelocity.Z = utils.RadToDeg(float64(utils.Float32FromBytesLE(out[44:48]))) - dv, err := vn.readRegisterSPI(ctx, deltaVDeltaTheta, 28) - if err != nil { - return err - } - // unit deg/s - vn.dTheta.X = float64(utils.Float32FromBytesLE(dv[4:8])) - vn.dTheta.Y = float64(utils.Float32FromBytesLE(dv[8:12])) - vn.dTheta.Z = float64(utils.Float32FromBytesLE(dv[12:16])) - // unit m/s - vn.dV.X = float64(utils.Float32FromBytesLE(dv[16:20])) - vn.bdVX - vn.dV.Y = float64(utils.Float32FromBytesLE(dv[20:24])) - vn.bdVY - vn.dV.Z = float64(utils.Float32FromBytesLE(dv[24:28])) - vn.bdVZ - // unit s - vn.dt = utils.Float32FromBytesLE(dv[0:4]) - return nil -} - -func (vn *vectornav) readRegisterSPI(ctx context.Context, reg vectornavRegister, readLen uint) ([]byte, error) { - vn.spiMu.Lock() - defer vn.spiMu.Unlock() - if vn.busClosed { - return nil, errors.New("C=cannot read spi register the bus is closed") - } - hnd, err := vn.bus.OpenHandle() - if err != nil { - return nil, err - } - cmd := []byte{byte(vectorNavSPIRead), byte(reg), 0, 0} - _, err = hnd.Xfer(ctx, uint(vn.speed), vn.cs, 3, cmd) - if err != nil { - return nil, err - } - goutils.SelectContextOrWait(ctx, 110*time.Microsecond) - cmd = make([]byte, readLen+4) - out, err := hnd.Xfer(ctx, uint(vn.speed), vn.cs, 3, cmd) - if err != nil { - return nil, err - } - if out[3] != 0 { - return nil, errors.Errorf("vectornav read error returned %d speed was %d", out[3], vn.speed) - } - err = hnd.Close() - if err != nil { - return nil, err - } - goutils.SelectContextOrWait(ctx, 110*time.Microsecond) - return out[4:], nil -} - -func (vn *vectornav) writeRegisterSPI(ctx context.Context, reg vectornavRegister, data []byte) error { - vn.spiMu.Lock() - defer vn.spiMu.Unlock() - if vn.busClosed { - return errors.New("Cannot write spi register the bus is closed") - } - hnd, err := vn.bus.OpenHandle() - if err != nil { - return err - } - cmd := []byte{byte(vectorNavSPIWrite), byte(reg), 0, 0} - cmd = append(cmd, data...) - _, err = hnd.Xfer(ctx, uint(vn.speed), vn.cs, 3, cmd) - if err != nil { - return err - } - goutils.SelectContextOrWait(ctx, 110*time.Microsecond) - cmd = make([]byte, len(data)+4) - out, err := hnd.Xfer(ctx, uint(vn.speed), vn.cs, 3, cmd) - if err != nil { - return err - } - if out[3] != 0 { - return errors.Errorf("vectornav write error returned %d", out[3]) - } - err = hnd.Close() - if err != nil { - return err - } - goutils.SelectContextOrWait(ctx, 110*time.Microsecond) - return nil -} - -func (vn *vectornav) vectornavTareSPI(ctx context.Context) error { - vn.spiMu.Lock() - defer vn.spiMu.Unlock() - if vn.busClosed { - return errors.New("Cannot write spi register the bus is closed") - } - hnd, err := vn.bus.OpenHandle() - if err != nil { - return err - } - cmd := []byte{byte(vectorNavSPITare), 0, 0, 0} - _, err = hnd.Xfer(ctx, uint(vn.speed), vn.cs, 3, cmd) - if err != nil { - return err - } - goutils.SelectContextOrWait(ctx, 110*time.Microsecond) - cmd = []byte{0, 0, 0, 0} - out, err := hnd.Xfer(ctx, uint(vn.speed), vn.cs, 3, cmd) - if err != nil { - return err - } - if out[3] != 0 { - return errors.Errorf("vectornav write error returned %d", out[3]) - } - err = hnd.Close() - if err != nil { - return err - } - goutils.SelectContextOrWait(ctx, 110*time.Microsecond) - return nil -} - -func (vn *vectornav) compensateAccelBias(ctx context.Context, smpSize uint) error { - var msg []byte - msg = append(msg, utils.BytesFromFloat32LE(1.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(1.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(1.0)...) - - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - err := vn.writeRegisterSPI(ctx, accCompensationConfiguration, msg) - if err != nil { - return errors.Wrap(err, "couldn't write the acceleration compensation register") - } - mdlG, err := vn.readRegisterSPI(ctx, magAccRefVectors, 24) - if err != nil { - return errors.Wrap(err, "couldn't calculate acceleration bias") - } - accZ := utils.Float32FromBytesLE(mdlG[20:24]) - var accMX, accMY, accMZ float32 - for i := uint(0); i < smpSize; i++ { - acc, err := vn.readRegisterSPI(ctx, acceleration, 12) - if err != nil { - return errors.Wrap(err, "error reading acceleration register during bias compensation") - } - accMX += utils.Float32FromBytesLE(acc[0:4]) - accMY += utils.Float32FromBytesLE(acc[4:8]) - accMZ += utils.Float32FromBytesLE(acc[8:12]) - if !goutils.SelectContextOrWait(ctx, 10*time.Millisecond) { - return errors.New("error in context during acceleration compensation") - } - } - accMX /= float32(smpSize) - accMY /= float32(smpSize) - accMZ /= float32(smpSize) - msg = []byte{} - msg = append(msg, utils.BytesFromFloat32LE(1.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(1.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(0.0)...) - msg = append(msg, utils.BytesFromFloat32LE(1.0)...) - - msg = append(msg, utils.BytesFromFloat32LE(accMX)...) - msg = append(msg, utils.BytesFromFloat32LE(accMY)...) - msg = append(msg, utils.BytesFromFloat32LE(accZ+accMZ)...) - - err = vn.writeRegisterSPI(ctx, accCompensationConfiguration, msg) - if err != nil { - return errors.Wrap(err, "could not write the acceleration register") - } - vn.logger.CInfof(ctx, "Acceleration compensated with %1.6f %1.6f %1.6f ref accZ %1.6f", accMX, accMY, accMZ, accZ) - return nil -} - -func (vn *vectornav) compensateDVBias(ctx context.Context, smpSize uint) error { - var bX, bY, bZ float32 - _, err := vn.readRegisterSPI(ctx, deltaVDeltaTheta, 28) - if err != nil { - return errors.Wrap(err, "error reading dV register during bias compensation") - } - dt := 10 * time.Millisecond - if vn.polling > 0 { - s := 1.0 / float64(vn.polling) - dt = time.Duration(s * float64(time.Second)) - } - for j := uint(0); j < smpSize; j++ { - if !goutils.SelectContextOrWait(ctx, dt) { - return errors.New("error in context during Dv compensation") - } - dv, err := vn.readRegisterSPI(ctx, deltaVDeltaTheta, 28) - if err != nil { - return errors.Wrap(err, "error reading dV register during bias compensation") - } - bX += utils.Float32FromBytesLE(dv[16:20]) - bY += utils.Float32FromBytesLE(dv[20:24]) - bZ += utils.Float32FromBytesLE(dv[24:28]) - } - vn.bdVX = float64(bX) / float64(smpSize) - vn.bdVY = float64(bY) / float64(smpSize) - vn.bdVZ = float64(bZ) / float64(smpSize) - vn.logger.CInfof(ctx, "velocity bias compensated with %1.6f %1.6f %1.6f", - vn.bdVX, vn.bdVY, vn.bdVZ) - return nil -} - -func (vn *vectornav) Close(ctx context.Context) error { - vn.logger.CDebug(ctx, "closing vecnav imu") - vn.cancelFunc() - vn.busClosed = true - vn.activeBackgroundWorkers.Wait() - vn.logger.CDebug(ctx, "closed vecnav imu") - return nil -} diff --git a/components/movementsensor/imuvectornav/imu_nonlinux.go b/components/movementsensor/imuvectornav/imu_nonlinux.go deleted file mode 100644 index 1694ddb956c..00000000000 --- a/components/movementsensor/imuvectornav/imu_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package imuvectornav is unimplemented for Macs. -package imuvectornav diff --git a/components/movementsensor/imuwit/imu.go b/components/movementsensor/imuwit/imu.go deleted file mode 100644 index 9013bfb32eb..00000000000 --- a/components/movementsensor/imuwit/imu.go +++ /dev/null @@ -1,412 +0,0 @@ -// Package imuwit implements wit imus. -package imuwit - -/* -Sensor Manufacturer: Wit-motion -Supported Sensor Models: HWT901B, BWT901, BWT61CL -Supported OS: Linux -Tested Sensor Models and User Manuals: - - BWT61CL: https://drive.google.com/file/d/1cUTginKXArkHvwPB4LdqojG-ixm7PXCQ/view - BWT901: https://drive.google.com/file/d/18bScCGO5vVZYcEeNKjXNtjnT8OVlrHGI/view - HWT901B TTL: https://drive.google.com/file/d/10HW4MhvhJs4RP0ko7w2nnzwmzsFCKPs6/view - -This driver will connect to the sensor using a usb connection given as a serial path -using a default baud rate of 115200. We allow baud rate values of: 9600, 115200 -The default baud rate can be used to connect to all models. Only the HWT901B's baud rate is changeable. -We ask the user to refer to the datasheet if any baud rate changes are required for their application. - -Other models that connect over serial may work, but we ask the user to refer to wit-motion's datasheet -in that case as well. As of Feb 2023, Wit-motion has 48 gyro/inclinometer/imu models with varied levels of -driver commonality. - -Note: Model HWT905-TTL is not supported under the model name "imu-wit". Use the model name "imu-wit-hwt905" -for HWT905-TTL. -*/ - -import ( - "bufio" - "context" - "fmt" - "io" - "math" - "sync" - - "github.com/golang/geo/r3" - slib "github.com/jacobsa/go-serial/serial" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - rutils "go.viam.com/rdk/utils" -) - -var ( - model = resource.DefaultModelFamily.WithModel("imu-wit") - - // This is the dynamic integral cumulative error. - // Data acquired from datasheets of supported models. Links above. - compassAccuracy = 0.5 -) - -var baudRateList = []uint{115200, 9600, 0} - -// max tilt to use tilt compensation is 45 degrees. -var maxTiltInRad = rutils.DegToRad(45) - -// Config is used for converting a witmotion IMU MovementSensor config attributes. -type Config struct { - Port string `json:"serial_path"` - BaudRate uint `json:"serial_baud_rate,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - // Validating serial path - if cfg.Port == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "serial_path") - } - - // Validating baud rate - if !rutils.ValidateBaudRate(baudRateList, int(cfg.BaudRate)) { - return nil, resource.NewConfigValidationError(path, errors.Errorf("Baud rate is not in %v", baudRateList)) - } - - return nil, nil -} - -func init() { - resource.RegisterComponent(movementsensor.API, model, resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newWit, - }) -} - -type wit struct { - resource.Named - resource.AlwaysRebuild - angularVelocity spatialmath.AngularVelocity - orientation spatialmath.EulerAngles - acceleration r3.Vector - magnetometer r3.Vector - compassheading float64 - numBadReadings uint32 - err movementsensor.LastError - hasMagnetometer bool - mu sync.Mutex - reconfigMu sync.Mutex - port io.ReadWriteCloser - workers rutils.StoppableWorkers - logger logging.Logger - baudRate uint - serialPath string -} - -func (imu *wit) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - imu.reconfigMu.Lock() - defer imu.reconfigMu.Unlock() - - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - imu.baudRate = newConf.BaudRate - imu.serialPath = newConf.Port - - return nil -} - -func (imu *wit) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - imu.mu.Lock() - defer imu.mu.Unlock() - return imu.angularVelocity, imu.err.Get() -} - -func (imu *wit) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - imu.mu.Lock() - defer imu.mu.Unlock() - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearVelocity -} - -func (imu *wit) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - imu.mu.Lock() - defer imu.mu.Unlock() - return &imu.orientation, imu.err.Get() -} - -// LinearAcceleration returns linear acceleration in mm_per_sec_per_sec. -func (imu *wit) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - imu.mu.Lock() - defer imu.mu.Unlock() - return imu.acceleration, imu.err.Get() -} - -// getMagnetometer returns magnetic field in gauss. -func (imu *wit) getMagnetometer() (r3.Vector, error) { - imu.mu.Lock() - defer imu.mu.Unlock() - return imu.magnetometer, imu.err.Get() -} - -func (imu *wit) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - var err error - - imu.mu.Lock() - defer imu.mu.Unlock() - - // this will compensate for a tilted IMU if the tilt is less than 45 degrees - // do not let the imu near permanent magnets or things that make a strong magnetic field - imu.compassheading = imu.calculateCompassHeading() - - return imu.compassheading, err -} - -// Helper function to calculate compass heading with tilt compensation. -func (imu *wit) calculateCompassHeading() float64 { - pitch := imu.orientation.Pitch - roll := imu.orientation.Roll - - var x, y float64 - - // Tilt compensation only works if the pitch and roll are between -45 and 45 degrees. - if math.Abs(roll) <= maxTiltInRad && math.Abs(pitch) <= maxTiltInRad { - x, y = imu.calculateTiltCompensation(roll, pitch) - } else { - x = imu.magnetometer.X - y = imu.magnetometer.Y - } - - // calculate -180 to 180 heading from radians - // North (y) is 0 so the π/2 - atan2(y, x) identity is used - // directly - rad := math.Atan2(y, x) // -180 to 180 heading - compass := rutils.RadToDeg(rad) - compass = math.Mod(compass, 360) - compass = math.Mod(compass+360, 360) // compass 0 to 360 - - return compass -} - -func (imu *wit) calculateTiltCompensation(roll, pitch float64) (float64, float64) { - // calculate adjusted magnetometer readings. These get less accurate as the tilt angle increases. - xComp := imu.magnetometer.X*math.Cos(pitch) + imu.magnetometer.Z*math.Sin(pitch) - yComp := imu.magnetometer.X*math.Sin(roll)*math.Sin(pitch) + - imu.magnetometer.Y*math.Cos(roll) - imu.magnetometer.Z*math.Sin(roll)*math.Cos(pitch) - - return xComp, yComp -} - -func (imu *wit) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(0, 0), 0, movementsensor.ErrMethodUnimplementedPosition -} - -func (imu *wit) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error, -) { - // return the compass heading error from the datasheet (0.5) of the witIMU if - // the pitch angle is less than 45 degrees and the roll angle is near zero - // mag projects at angles over this threshold cannot be determined because of the larger contribution of other - // orientations to the true compass heading - // return NaN for compass accuracy otherwise. - imu.mu.Lock() - defer imu.mu.Unlock() - - roll := imu.orientation.Roll - pitch := imu.orientation.Pitch - - if math.Abs(roll) <= 1 && math.Abs(pitch) <= maxTiltInRad { - return &movementsensor.Accuracy{CompassDegreeError: float32(compassAccuracy)}, nil - } - - return &movementsensor.Accuracy{CompassDegreeError: float32(math.NaN())}, nil -} - -func (imu *wit) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - readings, err := movementsensor.DefaultAPIReadings(ctx, imu, extra) - if err != nil { - return nil, err - } - - mag, err := imu.getMagnetometer() - if err != nil { - return nil, err - } - readings["magnetometer"] = mag - - return readings, err -} - -func (imu *wit) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - imu.mu.Lock() - defer imu.mu.Unlock() - - return &movementsensor.Properties{ - AngularVelocitySupported: true, - OrientationSupported: true, - LinearAccelerationSupported: true, - CompassHeadingSupported: imu.hasMagnetometer, - }, nil -} - -// newWit creates a new Wit IMU. -func newWit( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - options := slib.OpenOptions{ - PortName: newConf.Port, - BaudRate: 115200, - DataBits: 8, - StopBits: 1, - MinimumReadSize: 1, - } - - if newConf.BaudRate > 0 { - options.BaudRate = newConf.BaudRate - } else { - logger.CWarnf(ctx, - "no valid serial_baud_rate set, setting to default of %d, baud rate of wit imus are: %v", options.BaudRate, baudRateList, - ) - } - - i := wit{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - err: movementsensor.NewLastError(1, 1), - } - logger.CDebugf(ctx, "initializing wit serial connection with parameters: %+v", options) - i.port, err = slib.Open(options) - if err != nil { - return nil, err - } - - portReader := bufio.NewReader(i.port) - i.startUpdateLoop(portReader, logger) - - return &i, nil -} - -func (imu *wit) startUpdateLoop(portReader *bufio.Reader, logger logging.Logger) { - imu.hasMagnetometer = false - imu.workers = rutils.NewStoppableWorkers(func(ctx context.Context) { - defer utils.UncheckedErrorFunc(func() error { - if imu.port != nil { - if err := imu.port.Close(); err != nil { - imu.port = nil - return err - } - imu.port = nil - } - return nil - }) - - for { - if ctx.Err() != nil { - return - } - select { - case <-ctx.Done(): - return - default: - } - - line, err := portReader.ReadString('U') - - func() { - imu.mu.Lock() - defer imu.mu.Unlock() - - switch { - case err != nil: - imu.err.Set(err) - imu.numBadReadings++ - if imu.numBadReadings < 20 { - logger.CError(ctx, err, "Check if wit imu is disconnected from port") - } else { - logger.CDebug(ctx, err) - } - - case len(line) != 11: - imu.numBadReadings++ - return - default: - imu.err.Set(imu.parseWIT(line)) - } - }() - } - }) -} - -func scale(a, b byte, r float64) float64 { - x := float64(int(b)<<8|int(a)) / 32768.0 // 0 -> 2 - x *= r // 0 -> 2r - x += r - x = math.Mod(x, r*2) - x -= r - return x -} - -func convertMagByteToTesla(a, b byte) float64 { - x := float64(int(int8(b))<<8 | int(a)) // 0 -> 2 - return x -} - -func (imu *wit) parseWIT(line string) error { - if line[0] == 0x52 { - if len(line) < 7 { - return fmt.Errorf("line is wrong for imu angularVelocity %d %v", len(line), line) - } - imu.angularVelocity.X = scale(line[1], line[2], 2000) - imu.angularVelocity.Y = scale(line[3], line[4], 2000) - imu.angularVelocity.Z = scale(line[5], line[6], 2000) - } - - if line[0] == 0x53 { - if len(line) < 7 { - return fmt.Errorf("line is wrong for imu orientation %d %v", len(line), line) - } - - imu.orientation.Roll = rutils.DegToRad(scale(line[1], line[2], 180)) - imu.orientation.Pitch = rutils.DegToRad(scale(line[3], line[4], 180)) - imu.orientation.Yaw = rutils.DegToRad(scale(line[5], line[6], 180)) - } - - if line[0] == 0x51 { - if len(line) < 7 { - return fmt.Errorf("line is wrong for imu acceleration %d %v", len(line), line) - } - imu.acceleration.X = scale(line[1], line[2], 16) * 9.80665 // converts to m_per_sec_per_sec in NYC - imu.acceleration.Y = scale(line[3], line[4], 16) * 9.80665 - imu.acceleration.Z = scale(line[5], line[6], 16) * 9.80665 - } - - if line[0] == 0x54 { - imu.hasMagnetometer = true - if len(line) < 7 { - return fmt.Errorf("line is wrong for imu magnetometer %d %v", len(line), line) - } - imu.magnetometer.X = convertMagByteToTesla(line[1], line[2]) // converts uT (micro Tesla) - imu.magnetometer.Y = convertMagByteToTesla(line[3], line[4]) - imu.magnetometer.Z = convertMagByteToTesla(line[5], line[6]) - } - - return nil -} - -// Close shuts down wit and closes imu.port. -func (imu *wit) Close(ctx context.Context) error { - imu.logger.CDebug(ctx, "Closing wit motion imu") - imu.workers.Stop() - imu.logger.CDebug(ctx, "Closed wit motion imu") - return imu.err.Get() -} diff --git a/components/movementsensor/imuwit/imuhwt905.go b/components/movementsensor/imuwit/imuhwt905.go deleted file mode 100644 index 7a53b5d33c5..00000000000 --- a/components/movementsensor/imuwit/imuhwt905.go +++ /dev/null @@ -1,132 +0,0 @@ -// Package imuwit implements wit imu -package imuwit - -/* -Sensor Manufacturer: Wit-motion -Supported Sensor Models: HWT905 -Supported OS: Linux -This driver only supports HWT905-TTL model of Wit imu. -Tested Sensor Models and User Manuals: - HWT905 TTL: https://drive.google.com/file/d/1RV7j8yzZjPsPmvQY--1UHr_FhBzc2YwO/view -*/ - -import ( - "bufio" - "context" - "errors" - "time" - - slib "github.com/jacobsa/go-serial/serial" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" -) - -var model905 = resource.DefaultModelFamily.WithModel("imu-wit-hwt905") - -func init() { - resource.RegisterComponent(movementsensor.API, model905, resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newWit905, - }) -} - -// newWit creates a new Wit IMU. -func newWit905( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - i := wit{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - err: movementsensor.NewLastError(1, 1), - baudRate: newConf.BaudRate, - serialPath: newConf.Port, - } - - options := slib.OpenOptions{ - PortName: i.serialPath, - BaudRate: i.baudRate, - DataBits: 8, - StopBits: 1, - MinimumReadSize: 1, - } - if err := i.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - logger.Debugf("initializing wit serial connection with parameters: %+v", options) - i.port, err = slib.Open(options) - if err != nil { - return nil, err - } - - portReader := bufio.NewReader(i.port) - i.start905UpdateLoop(portReader, logger) - - return &i, nil -} - -func (imu *wit) start905UpdateLoop(portReader *bufio.Reader, logger logging.Logger) { - imu.hasMagnetometer = false - imu.workers = utils.NewStoppableWorkers(func(cancelCtx context.Context) { - for { - if cancelCtx.Err() != nil { - return - } - - select { - case <-cancelCtx.Done(): - return - case <-time.After(10 * time.Second): - logger.Warn("ReadString timeout exceeded") - return - default: - line, err := readWithTimeout(portReader, 'U') - if err != nil { - logger.Error(err) - continue - } - - switch { - case len(line) != 11: - imu.numBadReadings++ - default: - imu.err.Set(imu.parseWIT(line)) - } - } - } - }) -} - -// readWithTimeout tries to read from the buffer until the delimiter is found or timeout occurs. -func readWithTimeout(r *bufio.Reader, delim byte) (string, error) { - lineChan := make(chan string) - errChan := make(chan error) - - go func() { - line, err := r.ReadString(delim) - if err != nil { - errChan <- err - return - } - lineChan <- line - }() - - select { - case line := <-lineChan: - return line, nil - case err := <-errChan: - return "", err - case <-time.After(10 * time.Second): - return "", errors.New("timeout exceeded while reading from serial port") - } -} diff --git a/components/movementsensor/merged/merged.go b/components/movementsensor/merged/merged.go deleted file mode 100644 index 1c2b77bee72..00000000000 --- a/components/movementsensor/merged/merged.go +++ /dev/null @@ -1,422 +0,0 @@ -// Package merged implements a movementsensor combining movement data from other sensors -package merged - -import ( - "context" - "fmt" - "math" - "sync" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.uber.org/multierr" - "golang.org/x/exp/maps" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -const errStrAccuracy = "_accuracy_err" - -var model = resource.DefaultModelFamily.WithModel("merged") - -// Config is the config of the merged movement_sensor model. -type Config struct { - Position []string `json:"position,omitempty"` - Orientation []string `json:"orientation,omitempty"` - CompassHeading []string `json:"compass_heading,omitempty"` - LinearVelocity []string `json:"linear_velocity,omitempty"` - AngularVelocity []string `json:"angular_velocity,omitempty"` - LinearAcceleration []string `json:"linear_acceleration,omitempty"` -} - -// Validate validates the merged model's configuration. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - deps = append(deps, cfg.Position...) - deps = append(deps, cfg.Orientation...) - deps = append(deps, cfg.CompassHeading...) - deps = append(deps, cfg.LinearVelocity...) - deps = append(deps, cfg.AngularVelocity...) - deps = append(deps, cfg.LinearAcceleration...) - return deps, nil -} - -type merged struct { - resource.Named - logger logging.Logger - - mu sync.Mutex - - ori movementsensor.MovementSensor - pos movementsensor.MovementSensor - compass movementsensor.MovementSensor - linVel movementsensor.MovementSensor - angVel movementsensor.MovementSensor - linAcc movementsensor.MovementSensor -} - -func init() { - resource.Register( - movementsensor.API, model, - resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newMergedModel, - }) -} - -func newMergedModel(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) ( - movementsensor.MovementSensor, error, -) { - m := merged{ - logger: logger, - Named: conf.ResourceName().AsNamed(), - } - - if err := m.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return &m, nil -} - -func (m *merged) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - m.mu.Lock() - defer m.mu.Unlock() - - firstGoodSensorWithProperties := func( - deps resource.Dependencies, names []string, logger logging.Logger, - want *movementsensor.Properties, propname string, - ) (movementsensor.MovementSensor, error) { - // check if the config names and dependencies have been passed at all - if len(names) == 0 || deps == nil { - return nil, nil - } - - for _, name := range names { - ms, err := movementsensor.FromDependencies(deps, name) - msName := ms.Name().ShortName() - if err != nil { - logger.CDebugf(ctx, "error getting sensor %v from dependencies", msName) - continue - } - - props, err := ms.Properties(ctx, nil) - if err != nil { - logger.CDebugf(ctx, "error in getting sensor %v properties", msName) - continue - } - - // check that the sensor matches the properties passed in - // if it doesn't, skip it and go on to the next sensor in the list - if want.OrientationSupported && !props.OrientationSupported { - continue - } - - if want.PositionSupported && !props.PositionSupported { - continue - } - - if want.CompassHeadingSupported && !props.CompassHeadingSupported { - continue - } - - if want.LinearVelocitySupported && !props.LinearVelocitySupported { - continue - } - - if want.AngularVelocitySupported && !props.AngularVelocitySupported { - continue - } - - if want.LinearAccelerationSupported && !props.LinearAccelerationSupported { - continue - } - - // we've found the sensor that reports everything we want - m.logger.Debugf("using sensor %v as %s sensor", msName, propname) - return ms, nil - } - - return nil, fmt.Errorf("%v not supported by any sensor in list %#v", propname, names) - } - - m.ori, err = firstGoodSensorWithProperties( - deps, newConf.Orientation, m.logger, - &movementsensor.Properties{OrientationSupported: true}, "orientation") - if err != nil { - return err - } - - m.pos, err = firstGoodSensorWithProperties( - deps, newConf.Position, m.logger, - &movementsensor.Properties{PositionSupported: true}, "position") - if err != nil { - return err - } - - m.compass, err = firstGoodSensorWithProperties( - deps, newConf.CompassHeading, m.logger, - &movementsensor.Properties{CompassHeadingSupported: true}, "compass_heading") - if err != nil { - return err - } - - m.linVel, err = firstGoodSensorWithProperties( - deps, newConf.LinearVelocity, m.logger, - &movementsensor.Properties{LinearVelocitySupported: true}, "linear_velocity") - if err != nil { - return err - } - - m.angVel, err = firstGoodSensorWithProperties( - deps, newConf.AngularVelocity, m.logger, - &movementsensor.Properties{AngularVelocitySupported: true}, "angular_velocity") - if err != nil { - return err - } - - m.linAcc, err = firstGoodSensorWithProperties( - deps, newConf.LinearAcceleration, m.logger, - &movementsensor.Properties{LinearAccelerationSupported: true}, "linear_acceleration") - if err != nil { - return err - } - - return nil -} - -func (m *merged) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.pos == nil { - return geo.NewPoint(math.NaN(), math.NaN()), math.NaN(), - movementsensor.ErrMethodUnimplementedPosition - } - return m.pos.Position(ctx, extra) -} - -func (m *merged) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.ori == nil { - nanOri := spatialmath.NewOrientationVector() - nanOri.OX = math.NaN() - nanOri.OY = math.NaN() - nanOri.OZ = math.NaN() - nanOri.Theta = math.NaN() - return nanOri, - movementsensor.ErrMethodUnimplementedOrientation - } - return m.ori.Orientation(ctx, extra) -} - -func (m *merged) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.compass == nil { - return math.NaN(), - movementsensor.ErrMethodUnimplementedCompassHeading - } - return m.compass.CompassHeading(ctx, extra) -} - -func (m *merged) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.linVel == nil { - return r3.Vector{X: math.NaN(), Y: math.NaN(), Z: math.NaN()}, - movementsensor.ErrMethodUnimplementedLinearVelocity - } - return m.linVel.LinearVelocity(ctx, extra) -} - -func (m *merged) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.angVel == nil { - return spatialmath.AngularVelocity{X: math.NaN(), Y: math.NaN(), Z: math.NaN()}, - movementsensor.ErrMethodUnimplementedAngularVelocity - } - return m.angVel.AngularVelocity(ctx, extra) -} - -func (m *merged) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.linAcc == nil { - return r3.Vector{X: math.NaN(), Y: math.NaN(), Z: math.NaN()}, - movementsensor.ErrMethodUnimplementedLinearAcceleration - } - return m.linAcc.LinearAcceleration(ctx, extra) -} - -func mapWithSensorName(name string, accMap map[string]float32) map[string]float32 { - result := map[string]float32{} - for k, v := range accMap { - result[name+"_"+k] = v - } - return result -} - -func (m *merged) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - m.mu.Lock() - defer m.mu.Unlock() - - accMap := make(map[string]float32) - var errs error - - if m.ori != nil { - oriAcc, err := m.ori.Accuracy(ctx, extra) - if err != nil { - // replace entire map with a map that shows that it has errors - errorAcc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{ - m.ori.Name().ShortName() + errStrAccuracy: float32(math.NaN()), - }, - } - oriAcc = errorAcc - errs = multierr.Combine(errs, err) - } - if oriAcc != nil { - maps.Copy(accMap, mapWithSensorName(m.ori.Name().ShortName(), oriAcc.AccuracyMap)) - } - } - - hdop := float32(math.NaN()) - vdop := float32(math.NaN()) - nmeaFix := int32(-1) - - if m.pos != nil { - posAcc, err := m.pos.Accuracy(ctx, extra) - if err != nil { - errorAcc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{ - m.pos.Name().ShortName() + errStrAccuracy: float32(math.NaN()), - }, - } - posAcc = errorAcc - errs = multierr.Combine(errs, err) - } - if posAcc != nil { - maps.Copy(accMap, mapWithSensorName(m.pos.Name().ShortName(), posAcc.AccuracyMap)) - } - hdop = posAcc.Hdop - vdop = posAcc.Vdop - nmeaFix = posAcc.NmeaFix - } - - compassDegreeError := float32(math.NaN()) - if m.compass != nil { - compassAcc, err := m.compass.Accuracy(ctx, extra) - if err != nil { - errorAcc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{ - m.compass.Name().ShortName() + errStrAccuracy: float32(math.NaN()), - }, - } - compassAcc = errorAcc - errs = multierr.Combine(errs, err) - } - if compassAcc != nil { - maps.Copy(accMap, mapWithSensorName(m.compass.Name().ShortName(), compassAcc.AccuracyMap)) - } - compassDegreeError = compassAcc.CompassDegreeError - } - - if m.linVel != nil { - linvelAcc, err := m.linVel.Accuracy(ctx, extra) - if err != nil { - errorAcc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{ - m.linVel.Name().ShortName() + errStrAccuracy: float32(math.NaN()), - }, - } - linvelAcc = errorAcc - errs = multierr.Combine(errs, err) - } - if linvelAcc != nil { - maps.Copy(accMap, mapWithSensorName(m.linVel.Name().ShortName(), linvelAcc.AccuracyMap)) - } - } - - if m.angVel != nil { - angvelAcc, err := m.angVel.Accuracy(ctx, extra) - if err != nil { - errorAcc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{ - m.angVel.Name().ShortName() + errStrAccuracy: float32(math.NaN()), - }, - } - angvelAcc = errorAcc - errs = multierr.Combine(errs, err) - } - if angvelAcc != nil { - maps.Copy(accMap, mapWithSensorName(m.angVel.Name().ShortName(), angvelAcc.AccuracyMap)) - } - } - - if m.linAcc != nil { - linaccAcc, err := m.linAcc.Accuracy(ctx, extra) - if err != nil { - errorAcc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{ - m.linAcc.Name().ShortName() + errStrAccuracy: float32(math.NaN()), - }, - } - linaccAcc = errorAcc - errs = multierr.Combine(errs, err) - } - if linaccAcc != nil { - maps.Copy(accMap, mapWithSensorName(m.linAcc.Name().ShortName(), linaccAcc.AccuracyMap)) - } - } - - acc := movementsensor.Accuracy{ - AccuracyMap: accMap, - Hdop: hdop, - Vdop: vdop, - NmeaFix: nmeaFix, - CompassDegreeError: compassDegreeError, - } - - return &acc, errs -} - -func (m *merged) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - m.mu.Lock() - defer m.mu.Unlock() - - return &movementsensor.Properties{ - PositionSupported: m.pos != nil, - OrientationSupported: m.ori != nil, - CompassHeadingSupported: m.compass != nil, - LinearVelocitySupported: m.linVel != nil, - AngularVelocitySupported: m.angVel != nil, - LinearAccelerationSupported: m.linAcc != nil, - }, nil -} - -func (m *merged) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - // we're already in lock in this driver - // don't lock the mutex again for the Readings call - return movementsensor.DefaultAPIReadings(ctx, m, extra) -} - -func (m *merged) Close(context.Context) error { - // we do not try to Close the movement sensors that this driver depends on - // we let their own drivers and modules close them - return nil -} diff --git a/components/movementsensor/merged/merged_test.go b/components/movementsensor/merged/merged_test.go deleted file mode 100644 index bc1dd43e8eb..00000000000 --- a/components/movementsensor/merged/merged_test.go +++ /dev/null @@ -1,361 +0,0 @@ -package merged - -import ( - "context" - "errors" - "math" - "strings" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testName = "testSensor" -) - -var ( - testlinvel = r3.Vector{X: 1, Y: 2, Z: 3} - testori = &spatialmath.OrientationVector{OX: 0, OY: 0, OZ: -1, Theta: 75} - testgeopoint = geo.NewPoint(43.4, -72.9) - testalt = 45.0 - testcompass = 75.0 - testangvel = spatialmath.AngularVelocity{X: 4, Y: 5, Z: 6} - testlinacc = r3.Vector{X: 7, Y: 8, Z: 9} - - errAccuracy = errors.New("no accuracy for you merged sensor") - errProps = errors.New("no properties for you merged sensor") - - emptySensors = []string{} - posSensors = []string{"goodPos", "unusedPos"} - oriSensors = []string{"goodOri", "unusedOri"} - compassSensors = []string{"badCompass", "goodCompass", "unusedCompass"} - linvelSensors = []string{"goodLinVel", "unusedLinVel"} - angvelSensors = []string{"goodAngVel", "unusedAngVel"} - linaccSensors = []string{"goodLinAcc", "unusedLinAcc"} - - emptyProps = movementsensor.Properties{} - oriProps = movementsensor.Properties{OrientationSupported: true} - posProps = movementsensor.Properties{PositionSupported: true} - compassProps = movementsensor.Properties{CompassHeadingSupported: true} - linvelProps = movementsensor.Properties{LinearVelocitySupported: true} - angvelProps = movementsensor.Properties{AngularVelocitySupported: true} - linaccProps = movementsensor.Properties{LinearAccelerationSupported: true} -) - -func setUpCfg(ori, pos, compass, linvel, angvel, linacc []string) resource.Config { - return resource.Config{ - Name: testName, - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - Orientation: ori, - Position: pos, - CompassHeading: compass, - LinearVelocity: linvel, - AngularVelocity: angvel, - LinearAcceleration: linacc, - }, - } -} - -func setupMovementSensor( - name string, prop movementsensor.Properties, - errAcc, oriPropsErr bool, -) movementsensor.MovementSensor { - ms := inject.NewMovementSensor(name) - ms.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}, - ) (*movementsensor.Properties, error) { - if oriPropsErr && strings.Contains(name, "goodOri") { - return &prop, errProps - } - return &prop, nil - } - ms.AccuracyFunc = func(ctx context.Context, exta map[string]interface{}) (*movementsensor.Accuracy, error, - ) { - if errAcc { - return nil, errAccuracy - } - acc := &movementsensor.Accuracy{ - AccuracyMap: map[string]float32{"accuracy": 32}, - Hdop: float32(math.NaN()), - Vdop: float32(math.NaN()), - NmeaFix: -1, - CompassDegreeError: float32(math.NaN()), - } - return acc, nil - } - - switch { - case strings.Contains(name, "Ori"): - ms.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return testori, nil - } - case strings.Contains(name, "Pos"): - ms.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return testgeopoint, testalt, nil - } - case strings.Contains(name, "Compass"): - ms.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return testcompass, nil - } - case strings.Contains(name, "AngVel"): - ms.AngularVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return testangvel, nil - } - case strings.Contains(name, "LinVel"): - ms.LinearVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return testlinvel, nil - } - case strings.Contains(name, "LinAcc"): - ms.LinearAccelerationFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return testlinacc, nil - } - } - - return ms -} - -func setupDependencies(t *testing.T, - depswithProps map[string]movementsensor.Properties, - errAcc, errProps bool, -) resource.Dependencies { - t.Helper() - result := make(resource.Dependencies) - for name, prop := range depswithProps { - ms := setupMovementSensor(name, prop, errAcc, errProps) - result[movementsensor.Named(name)] = ms - } - return result -} - -func TestValidate(t *testing.T) { - // doesn't pass validate - conf := setUpCfg( - emptySensors, emptySensors /*pos*/, emptySensors, /*compass*/ - emptySensors /*linvel*/, emptySensors /*angvel*/, emptySensors /*linacc*/) - implicits, err := conf.Validate("somepath", movementsensor.API.Type.Name) - test.That(t, err, test.ShouldBeNil) - test.That(t, implicits, test.ShouldBeNil) - - // doesn't pass configuration - conf = setUpCfg( - oriSensors, emptySensors /*pos*/, emptySensors, /*compass*/ - linvelSensors, emptySensors /*angvel*/, emptySensors /*linacc*/) - implicits, err = conf.Validate("somepath", movementsensor.API.Type.Name) - test.That(t, err, test.ShouldBeNil) - test.That(t, implicits, test.ShouldResemble, append(oriSensors, linvelSensors...)) - - conf = setUpCfg( - /*ori*/ emptySensors, emptySensors /*pos*/, emptySensors, /*comapss*/ - linvelSensors /*linval*/, angvelSensors /*angvel*/, emptySensors /*linacc*/) - implicits, err = conf.Validate("somepath", movementsensor.API.Type.Name) - test.That(t, err, test.ShouldBeNil) - test.That(t, implicits, test.ShouldResemble, append(linvelSensors, angvelSensors...)) -} - -func TestCreation(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // doesn't pass configuration - conf := setUpCfg( - oriSensors, emptySensors /*pos*/, emptySensors, /*compass*/ - linvelSensors, emptySensors /*angvel*/, emptySensors /*linacc*/) - - depmap := map[string]movementsensor.Properties{ - linvelSensors[0]: emptyProps, // empty - linvelSensors[1]: {AngularVelocitySupported: true}, // wrong properties - oriSensors[0]: {OrientationSupported: true}, // has correct properties but errors - oriSensors[1]: {LinearVelocitySupported: true}, // second sensor in a list, but wrong property - } - - deps := setupDependencies(t, depmap, false, true /* properties error */) - ms, err := newMergedModel(ctx, deps, conf, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "orientation not supported") - test.That(t, ms, test.ShouldBeNil) - - // first time passing configuration with two merged sensors - conf = setUpCfg( - /*ori*/ emptySensors, emptySensors /*pos*/, emptySensors, /*comapss*/ - linvelSensors /*linval*/, angvelSensors /*angvel*/, emptySensors /*linacc*/) - - depmap = map[string]movementsensor.Properties{ - linvelSensors[0]: linvelProps, - linvelSensors[1]: linvelProps, - angvelSensors[0]: angvelProps, - angvelSensors[1]: angvelProps, - } - - deps = setupDependencies(t, depmap, false, false) - - ms, err = newMergedModel(ctx, deps, conf, logger) - test.That(t, err, test.ShouldBeNil) - - compass, err := ms.CompassHeading(ctx, nil) - test.That(t, err, test.ShouldBeError, movementsensor.ErrMethodUnimplementedCompassHeading) - test.That(t, math.IsNaN(compass), test.ShouldBeTrue) - - pos, alt, err := ms.Position(ctx, nil) - test.That(t, err, test.ShouldBeError, movementsensor.ErrMethodUnimplementedPosition) - test.That(t, math.IsNaN(pos.Lat()), test.ShouldBeTrue) - test.That(t, math.IsNaN(pos.Lng()), test.ShouldBeTrue) - test.That(t, math.IsNaN(alt), test.ShouldBeTrue) - - linacc, err := ms.LinearAcceleration(ctx, nil) - test.That(t, err, test.ShouldBeError, movementsensor.ErrMethodUnimplementedLinearAcceleration) - test.That(t, math.IsNaN(linacc.X), test.ShouldBeTrue) - test.That(t, math.IsNaN(linacc.Y), test.ShouldBeTrue) - test.That(t, math.IsNaN(linacc.Z), test.ShouldBeTrue) - - ori, err := ms.Orientation(ctx, nil) - test.That(t, err, test.ShouldBeError, movementsensor.ErrMethodUnimplementedOrientation) - test.That(t, math.IsNaN(ori.OrientationVectorRadians().OX), test.ShouldBeTrue) - test.That(t, math.IsNaN(ori.OrientationVectorRadians().OY), test.ShouldBeTrue) - test.That(t, math.IsNaN(ori.OrientationVectorRadians().OZ), test.ShouldBeTrue) - test.That(t, math.IsNaN(ori.OrientationVectorRadians().Theta), test.ShouldBeTrue) - - linvel, err := ms.LinearVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, linvel, test.ShouldResemble, testlinvel) - - angvel, err := ms.AngularVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, angvel, test.ShouldResemble, testangvel) - - linacc, err = ms.LinearAcceleration(ctx, nil) - test.That(t, err, test.ShouldBeError, movementsensor.ErrMethodUnimplementedLinearAcceleration) - test.That(t, math.IsNaN(linacc.X), test.ShouldBeTrue) - test.That(t, math.IsNaN(linacc.Y), test.ShouldBeTrue) - test.That(t, math.IsNaN(linacc.Z), test.ShouldBeTrue) - - properties, err := ms.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.OrientationSupported, test.ShouldBeFalse) - test.That(t, properties.PositionSupported, test.ShouldBeFalse) - test.That(t, properties.CompassHeadingSupported, test.ShouldBeFalse) - test.That(t, properties.LinearAccelerationSupported, test.ShouldBeFalse) - test.That(t, properties.AngularVelocitySupported, test.ShouldBeTrue) - test.That(t, properties.LinearVelocitySupported, test.ShouldBeTrue) - - accuracies, err := ms.Accuracy(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, accuracies.AccuracyMap, test.ShouldResemble, - map[string]float32{ - "goodAngVel_accuracy": 32, - "goodLinVel_accuracy": 32, - }) - - readings, err := ms.Readings(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings, test.ShouldResemble, map[string]interface{}{ - "linear_velocity": linvel, - "angular_velocity": angvel, - }) - - conf = setUpCfg(oriSensors, posSensors, compassSensors, linvelSensors, angvelSensors, linaccSensors) - - depmap[linvelSensors[0]] = linvelProps - depmap[linvelSensors[1]] = linvelProps - depmap[angvelSensors[0]] = angvelProps - depmap[angvelSensors[1]] = angvelProps - depmap[oriSensors[0]] = oriProps - depmap[oriSensors[1]] = angvelProps // wrong properties for first compass sensor - depmap[posSensors[0]] = posProps - depmap[posSensors[1]] = emptyProps - depmap[compassSensors[0]] = emptyProps - depmap[compassSensors[1]] = compassProps - depmap[compassSensors[2]] = emptyProps - depmap[linaccSensors[0]] = linaccProps - depmap[linaccSensors[1]] = emptyProps - - deps = setupDependencies(t, depmap, false, false) - - // first reconfiguration with six sensors and no function errors - err = ms.Reconfigure(ctx, deps, conf) - test.That(t, err, test.ShouldBeNil) - - res := ms.Name() - test.That(t, res, test.ShouldNotBeNil) - - pos, alt, err = ms.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, testgeopoint) - test.That(t, alt, test.ShouldEqual, testalt) - - ori, err = ms.Orientation(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, ori, test.ShouldEqual, testori) - - compass, err = ms.CompassHeading(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, compass, test.ShouldEqual, testcompass) - - linvel, err = ms.LinearVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, linvel, test.ShouldResemble, testlinvel) - - angvel, err = ms.AngularVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, angvel, test.ShouldResemble, testangvel) - - linacc, err = ms.LinearAcceleration(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, linacc, test.ShouldResemble, testlinacc) - - properties, err = ms.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, properties.OrientationSupported, test.ShouldBeTrue) - test.That(t, properties.PositionSupported, test.ShouldBeTrue) - test.That(t, properties.CompassHeadingSupported, test.ShouldBeTrue) - test.That(t, properties.LinearAccelerationSupported, test.ShouldBeTrue) - test.That(t, properties.AngularVelocitySupported, test.ShouldBeTrue) - test.That(t, properties.LinearVelocitySupported, test.ShouldBeTrue) - - accuracies, err = ms.Accuracy(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, accuracies.AccuracyMap, test.ShouldResemble, - map[string]float32{ - "goodOri_accuracy": 32, - "goodPos_accuracy": 32, - "goodCompass_accuracy": 32, - "goodAngVel_accuracy": 32, - "goodLinVel_accuracy": 32, - "goodLinAcc_accuracy": 32, - }) - - readings, err = ms.Readings(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings, test.ShouldResemble, map[string]interface{}{ - "orientation": ori, - "position": pos, - "altitude": alt, - "compass": compass, - "linear_velocity": linvel, - "angular_velocity": angvel, - "linear_acceleration": linacc, - }) - - // second reconfiguration with six sensors but an error in accuracy - deps = setupDependencies(t, depmap, true /* accuracy error */, false) - err = ms.Reconfigure(ctx, deps, conf) - test.That(t, err, test.ShouldBeNil) - - accuracies, err = ms.Accuracy(ctx, nil) - test.That(t, err, test.ShouldNotBeNil) - for k, v := range accuracies.AccuracyMap { - test.That(t, k, test.ShouldContainSubstring, errStrAccuracy) - test.That(t, math.IsNaN(float64(v)), test.ShouldBeTrue) - } - - // close the sensor, this test is done - test.That(t, ms.Close(ctx), test.ShouldBeNil) -} diff --git a/components/movementsensor/movementsensor.go b/components/movementsensor/movementsensor.go deleted file mode 100644 index 1bbf20d4301..00000000000 --- a/components/movementsensor/movementsensor.go +++ /dev/null @@ -1,243 +0,0 @@ -// Package movementsensor defines the interfaces of a MovementSensor -package movementsensor - -import ( - "context" - "math" - "strings" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - pb "go.viam.com/api/component/movementsensor/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/spatialmath" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[MovementSensor]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterMovementSensorServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.MovementSensorService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: position.String(), - }, newPositionCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: linearVelocity.String(), - }, newLinearVelocityCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: angularVelocity.String(), - }, newAngularVelocityCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: compassHeading.String(), - }, newCompassHeadingCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: linearAcceleration.String(), - }, newLinearAccelerationCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: orientation.String(), - }, newOrientationCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: readings.String(), - }, newReadingsCollector) -} - -// SubtypeName is a constant that identifies the component resource API string "movement_sensor". -const SubtypeName = "movement_sensor" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named MovementSensor's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// A MovementSensor reports information about the robot's direction, position and speed. -// -// Position example: -// -// // Get the current position of the movement sensor above sea level in meters -// position, altitude, err := myMovementSensor.Position(context.Background(), nil) -// -// LinearVelocity example: -// -// // Get the current linear velocity of the movement sensor. -// linVel, err := myMovementSensor.LinearVelocity(context.Background(), nil) -// -// AngularVelocity example: -// -// // Get the current angular velocity of the movement sensor. -// angVel, err := myMovementSensor.AngularVelocity(context.Background(), nil) -// -// // Get the y component of angular velocity. -// yAngVel := angVel.Y -// -// LinearAcceleration example: -// -// // Get the current linear acceleration of the movement sensor. -// linVel, err := myMovementSensor.LinearVelocity(context.Background(), nil) -// -// CompassHeading example: -// -// // Get the current compass heading of the movement sensor. -// heading, err := myMovementSensor.CompassHeading(context.Background(), nil) -// -// Orientation example: -// -// // Get the current orientation of the movement sensor. -// sensorOrientation, err := myMovementSensor.Orientation(context.Background(), nil) -// -// // Get the orientation vector. -// orientation := sensorOrientation.OrientationVectorDegrees() -// -// // Print out the orientation vector. -// logger.Info("The x component of the orientation vector: ", orientation.0X) -// logger.Info("The y component of the orientation vector: ", orientation.0Y) -// logger.Info("The z component of the orientation vector: ", orientation.0Z) -// logger.Info("The number of degrees that the movement sensor is rotated about the vector: ", orientation.Theta) -// -// Properties example: -// -// // Get the supported properties of the movement sensor. -// properties, err := myMovementSensor.Properties(context.Background(), nil) -// -// Accuracy example: -// -// // Get the accuracy of the movement sensor. -// accuracy, err := myMovementSensor.Accuracy(context.Background(), nil) -type MovementSensor interface { - resource.Sensor - resource.Resource - // Position returns the current GeoPoint (latitude, longitude) and altitude of the movement sensor above sea level in meters. - // Supported by GPS models. - Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) // (lat, long), altitude (m) - - // LinearVelocity returns the current linear velocity as a 3D vector in meters per second. - LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) // m / sec - - // AngularVelcoity returns the current angular velocity as a 3D vector in degrees per second. - AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) // deg / sec - - // LinearAcceleration returns the current linear acceleration as a 3D vector in meters per second per second. - LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) - - // CompassHeading returns the current compass heading in degrees. - CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) // [0->360) - - // Orientation returns the current orientation of the movement sensor. - Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) - - // Properties returns the supported properties of the movement sensor. - Properties(ctx context.Context, extra map[string]interface{}) (*Properties, error) - - // Accuracy returns the reliability metrics of the movement sensor, - // including various parameters to access the sensor's accuracy and precision in different dimensions. - Accuracy(ctx context.Context, extra map[string]interface{}) (*Accuracy, error) -} - -// FromDependencies is a helper for getting the named movementsensor from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (MovementSensor, error) { - return resource.FromDependencies[MovementSensor](deps, Named(name)) -} - -// FromRobot is a helper for getting the named MovementSensor from the given Robot. -func FromRobot(r robot.Robot, name string) (MovementSensor, error) { - return robot.ResourceFromRobot[MovementSensor](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all MovementSensor names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// DefaultAPIReadings is a helper for getting all readings from a MovementSensor. -func DefaultAPIReadings(ctx context.Context, g MovementSensor, extra map[string]interface{}) (map[string]interface{}, error) { - readings := map[string]interface{}{} - - pos, altitude, err := g.Position(ctx, extra) - if err != nil { - if !strings.Contains(err.Error(), ErrMethodUnimplementedPosition.Error()) { - return nil, err - } - } else { - readings["position"] = pos - readings["altitude"] = altitude - } - - vel, err := g.LinearVelocity(ctx, extra) - if err != nil { - if !strings.Contains(err.Error(), ErrMethodUnimplementedLinearVelocity.Error()) { - return nil, err - } - } else { - readings["linear_velocity"] = vel - } - - la, err := g.LinearAcceleration(ctx, extra) - if err != nil { - if !strings.Contains(err.Error(), ErrMethodUnimplementedLinearAcceleration.Error()) { - return nil, err - } - } else { - readings["linear_acceleration"] = la - } - - avel, err := g.AngularVelocity(ctx, extra) - if err != nil { - if !strings.Contains(err.Error(), ErrMethodUnimplementedAngularVelocity.Error()) { - return nil, err - } - } else { - readings["angular_velocity"] = avel - } - - compass, err := g.CompassHeading(ctx, extra) - if err != nil { - if !strings.Contains(err.Error(), ErrMethodUnimplementedCompassHeading.Error()) { - return nil, err - } - } else { - readings["compass"] = compass - } - - ori, err := g.Orientation(ctx, extra) - if err != nil { - if !strings.Contains(err.Error(), ErrMethodUnimplementedOrientation.Error()) { - return nil, err - } - } else { - readings["orientation"] = ori - } - - return readings, nil -} - -// UnimplementedOptionalAccuracies returns accuracy values that will not show up on movement sensor's RC card -// or be useable for a caller of the GetAccuracies method. The RC card currently continuously polls accuracies, -// so a nil error must be rturned from the GetAccuracies call. -// It contains NaN definitiions for accuracies returned in floats, an invalid integer value for the NMEAFix of a gps -// and an empty map of other accuracies. -func UnimplementedOptionalAccuracies() *Accuracy { - nan32Bit := float32(math.NaN()) - nmeaInvalid := int32(-1) - - return &Accuracy{ - Hdop: nan32Bit, - Vdop: nan32Bit, - NmeaFix: nmeaInvalid, - CompassDegreeError: nan32Bit, - } -} diff --git a/components/movementsensor/mpu6050/mpu6050.go b/components/movementsensor/mpu6050/mpu6050.go deleted file mode 100644 index 8ef50a0b605..00000000000 --- a/components/movementsensor/mpu6050/mpu6050.go +++ /dev/null @@ -1,349 +0,0 @@ -//go:build linux - -// Package mpu6050 implements the movementsensor interface for an MPU-6050 6-axis accelerometer. A -// datasheet for this chip is at -// https://components101.com/sites/default/files/component_datasheet/MPU6050-DataSheet.pdf and a -// description of the I2C registers is at -// https://download.datasheets.com/pdfs/2015/3/19/8/3/59/59/invse_/manual/5rm-mpu-6000a-00v4.2.pdf -// -// We support reading the accelerometer, gyroscope, and thermometer data off of the chip. We do not -// yet support using the digital interrupt pin to notify on events (freefall, collision, etc.), -// nor do we yet support using the secondary I2C connection to add an external clock or -// magnetometer. -// -// The chip has two possible I2C addresses, which can be selected by wiring the AD0 pin to either -// hot or ground: -// - if AD0 is wired to ground, it uses the default I2C address of 0x68 -// - if AD0 is wired to hot, it uses the alternate I2C address of 0x69 -// -// If you use the alternate address, your config file for this component must set its -// "use_alternate_i2c_address" boolean to true. -package mpu6050 - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("gyro-mpu6050") - -const ( - defaultAddressRegister = 117 - expectedDefaultAddress = 0x68 - alternateAddress = 0x69 -) - -// Config is used to configure the attributes of the chip. -type Config struct { - I2cBus string `json:"i2c_bus"` - UseAlternateI2CAddress bool `json:"use_alt_i2c_address,omitempty"` -} - -// Validate ensures all parts of the config are valid, and then returns the list of things we -// depend on. -func (conf *Config) Validate(path string) ([]string, error) { - if conf.I2cBus == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "i2c_bus") - } - - var deps []string - return deps, nil -} - -func init() { - resource.RegisterComponent(movementsensor.API, model, resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newMpu6050, - }) -} - -type mpu6050 struct { - resource.Named - resource.AlwaysRebuild - bus buses.I2C - i2cAddress byte - mu sync.Mutex - - // The 3 things we can measure: lock the mutex before reading or writing these. - angularVelocity spatialmath.AngularVelocity - temperature float64 - linearAcceleration r3.Vector - // Stores the most recent error from the background goroutine - err movementsensor.LastError - - workers utils.StoppableWorkers - logger logging.Logger -} - -func addressReadError(err error, address byte, bus string) error { - msg := fmt.Sprintf("can't read from I2C address %d on bus %s", address, bus) - return errors.Wrap(err, msg) -} - -func unexpectedDeviceError(address, defaultAddress byte) error { - return errors.Errorf("unexpected non-MPU6050 device at address %d: response '%d'", - address, defaultAddress) -} - -// newMpu6050 constructs a new Mpu6050 object. -func newMpu6050( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - bus, err := buses.NewI2cBus(newConf.I2cBus) - if err != nil { - return nil, err - } - return makeMpu6050(ctx, deps, conf, logger, bus) -} - -// This function is separated from NewMpu6050 solely so you can inject a mock I2C bus in tests. -func makeMpu6050( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - bus buses.I2C, -) (movementsensor.MovementSensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - var address byte - if newConf.UseAlternateI2CAddress { - address = alternateAddress - } else { - address = expectedDefaultAddress - } - logger.CDebugf(ctx, "Using address %d for MPU6050 sensor", address) - - sensor := &mpu6050{ - Named: conf.ResourceName().AsNamed(), - bus: bus, - i2cAddress: address, - logger: logger, - // On overloaded boards, the I2C bus can become flaky. Only report errors if at least 5 of - // the last 10 attempts to talk to the device have failed. - err: movementsensor.NewLastError(10, 5), - } - - // To check that we're able to talk to the chip, we should be able to read register 117 and get - // back the device's non-alternative address (0x68) - defaultAddress, err := sensor.readByte(ctx, defaultAddressRegister) - if err != nil { - return nil, addressReadError(err, address, newConf.I2cBus) - } - if defaultAddress != expectedDefaultAddress { - return nil, unexpectedDeviceError(address, defaultAddress) - } - - // The chip starts out in standby mode (the Sleep bit in the power management register defaults - // to 1). Set it to measurement mode (by turning off the Sleep bit) so we can get data from it. - // To do this, we set register 107 to 0. - err = sensor.writeByte(ctx, 107, 0) - if err != nil { - return nil, errors.Errorf("Unable to wake up MPU6050: '%s'", err.Error()) - } - - // Now, turn on the background goroutine that constantly reads from the chip and stores data in - // the object we created. - sensor.workers = utils.NewStoppableWorkers(func(cancelCtx context.Context) { - // Reading data a thousand times per second is probably fast enough. - timer := time.NewTicker(time.Millisecond) - defer timer.Stop() - - for { - select { - case <-timer.C: - rawData, err := sensor.readBlock(cancelCtx, 59, 14) - // Record `err` no matter what: even if it's nil, that's useful information. - sensor.err.Set(err) - if err != nil { - sensor.logger.CErrorf(ctx, "error reading MPU6050 sensor: '%s'", err) - continue - } - - linearAcceleration := toLinearAcceleration(rawData[0:6]) - // Taken straight from the MPU6050 register map. Yes, these are weird constants. - temperature := float64(utils.Int16FromBytesBE(rawData[6:8]))/340.0 + 36.53 - angularVelocity := toAngularVelocity(rawData[8:14]) - - // Lock the mutex before modifying the state within the object. By keeping the mutex - // unlocked for everything else, we maximize the time when another thread can read the - // values. - sensor.mu.Lock() - sensor.linearAcceleration = linearAcceleration - sensor.temperature = temperature - sensor.angularVelocity = angularVelocity - sensor.mu.Unlock() - case <-cancelCtx.Done(): - return - } - } - }) - - return sensor, nil -} - -func (mpu *mpu6050) readByte(ctx context.Context, register byte) (byte, error) { - result, err := mpu.readBlock(ctx, register, 1) - if err != nil { - return 0, err - } - return result[0], err -} - -func (mpu *mpu6050) readBlock(ctx context.Context, register byte, length uint8) ([]byte, error) { - handle, err := mpu.bus.OpenHandle(mpu.i2cAddress) - if err != nil { - return nil, err - } - defer func() { - err := handle.Close() - if err != nil { - mpu.logger.CError(ctx, err) - } - }() - - results, err := handle.ReadBlockData(ctx, register, length) - return results, err -} - -func (mpu *mpu6050) writeByte(ctx context.Context, register, value byte) error { - handle, err := mpu.bus.OpenHandle(mpu.i2cAddress) - if err != nil { - return err - } - defer func() { - err := handle.Close() - if err != nil { - mpu.logger.CError(ctx, err) - } - }() - - return handle.WriteByteData(ctx, register, value) -} - -// Given a value, scales it so that the range of int16s becomes the range of +/- maxValue. -func setScale(value int, maxValue float64) float64 { - return float64(value) * maxValue / (1 << 15) -} - -// A helper function to abstract out shared code: takes 6 bytes and gives back AngularVelocity, in -// radians per second. -func toAngularVelocity(data []byte) spatialmath.AngularVelocity { - gx := int(utils.Int16FromBytesBE(data[0:2])) - gy := int(utils.Int16FromBytesBE(data[2:4])) - gz := int(utils.Int16FromBytesBE(data[4:6])) - - maxRotation := 250.0 // Maximum degrees per second measurable in the default configuration - return spatialmath.AngularVelocity{ - X: setScale(gx, maxRotation), - Y: setScale(gy, maxRotation), - Z: setScale(gz, maxRotation), - } -} - -// A helper function that takes 6 bytes and gives back linear acceleration. -func toLinearAcceleration(data []byte) r3.Vector { - x := int(utils.Int16FromBytesBE(data[0:2])) - y := int(utils.Int16FromBytesBE(data[2:4])) - z := int(utils.Int16FromBytesBE(data[4:6])) - - // The scale is +/- 2G's, but our units should be m/sec/sec. - maxAcceleration := 2.0 * 9.81 /* m/sec/sec */ - return r3.Vector{ - X: setScale(x, maxAcceleration), - Y: setScale(y, maxAcceleration), - Z: setScale(z, maxAcceleration), - } -} - -func (mpu *mpu6050) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - mpu.mu.Lock() - defer mpu.mu.Unlock() - return mpu.angularVelocity, mpu.err.Get() -} - -func (mpu *mpu6050) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearVelocity -} - -func (mpu *mpu6050) LinearAcceleration(ctx context.Context, exta map[string]interface{}) (r3.Vector, error) { - mpu.mu.Lock() - defer mpu.mu.Unlock() - - lastError := mpu.err.Get() - if lastError != nil { - return r3.Vector{}, lastError - } - return mpu.linearAcceleration, nil -} - -func (mpu *mpu6050) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return spatialmath.NewOrientationVector(), movementsensor.ErrMethodUnimplementedOrientation -} - -func (mpu *mpu6050) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, movementsensor.ErrMethodUnimplementedCompassHeading -} - -func (mpu *mpu6050) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(0, 0), 0, movementsensor.ErrMethodUnimplementedPosition -} - -func (mpu *mpu6050) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - return movementsensor.UnimplementedOptionalAccuracies(), nil -} - -func (mpu *mpu6050) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - mpu.mu.Lock() - defer mpu.mu.Unlock() - - readings := make(map[string]interface{}) - readings["linear_acceleration"] = mpu.linearAcceleration - readings["temperature_celsius"] = mpu.temperature - readings["angular_velocity"] = mpu.angularVelocity - - return readings, mpu.err.Get() -} - -func (mpu *mpu6050) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - AngularVelocitySupported: true, - LinearAccelerationSupported: true, - }, nil -} - -func (mpu *mpu6050) Close(ctx context.Context) error { - mpu.workers.Stop() - - mpu.mu.Lock() - defer mpu.mu.Unlock() - // Set the Sleep bit (bit 6) in the power control register (register 107). - err := mpu.writeByte(ctx, 107, 1<<6) - if err != nil { - mpu.logger.CError(ctx, err) - } - return err -} diff --git a/components/movementsensor/mpu6050/mpu6050_nonlinux.go b/components/movementsensor/mpu6050/mpu6050_nonlinux.go deleted file mode 100644 index 9087d5a9c18..00000000000 --- a/components/movementsensor/mpu6050/mpu6050_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package mpu6050 is only implemented for Linux systems. -package mpu6050 diff --git a/components/movementsensor/mpu6050/mpu6050_test.go b/components/movementsensor/mpu6050/mpu6050_test.go deleted file mode 100644 index 14452db59f3..00000000000 --- a/components/movementsensor/mpu6050/mpu6050_test.go +++ /dev/null @@ -1,263 +0,0 @@ -//go:build linux - -package mpu6050 - -import ( - "context" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func TestValidateConfig(t *testing.T) { - cfg := Config{} - deps, err := cfg.Validate("path") - expectedErr := resource.NewConfigValidationFieldRequiredError("path", "i2c_bus") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) -} - -func TestInitializationFailureOnChipCommunication(t *testing.T) { - logger := logging.NewTestLogger(t) - i2cName := "i2c" - - t.Run("fails on read error", func(t *testing.T) { - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - I2cBus: i2cName, - }, - } - i2cHandle := &inject.I2CHandle{} - readErr := errors.New("read error") - i2cHandle.ReadBlockDataFunc = func(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - if register == defaultAddressRegister { - return nil, readErr - } - return []byte{}, nil - } - i2cHandle.CloseFunc = func() error { return nil } - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - - deps := resource.Dependencies{} - sensor, err := makeMpu6050(context.Background(), deps, cfg, logger, i2c) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, addressReadError(readErr, expectedDefaultAddress, i2cName)) - test.That(t, sensor, test.ShouldBeNil) - }) - - t.Run("fails on unexpected address", func(t *testing.T) { - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - I2cBus: i2cName, - UseAlternateI2CAddress: true, - }, - } - i2cHandle := &inject.I2CHandle{} - i2cHandle.ReadBlockDataFunc = func(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - if register == defaultAddressRegister { - return []byte{0x64}, nil - } - return nil, errors.New("unexpected register") - } - i2cHandle.CloseFunc = func() error { return nil } - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - - deps := resource.Dependencies{} - sensor, err := makeMpu6050(context.Background(), deps, cfg, logger, i2c) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, unexpectedDeviceError(alternateAddress, 0x64)) - test.That(t, sensor, test.ShouldBeNil) - }) -} - -func TestSuccessfulInitializationAndClose(t *testing.T) { - logger := logging.NewTestLogger(t) - i2cName := "i2c" - - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - I2cBus: i2cName, - UseAlternateI2CAddress: true, - }, - } - i2cHandle := &inject.I2CHandle{} - i2cHandle.ReadBlockDataFunc = func(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - return []byte{expectedDefaultAddress}, nil - } - // the only write operations that the sensor implementation performs is - // the command to put it into either measurement mode or sleep mode, - // and measurement mode results from a write of 0, so if is closeWasCalled is toggled - // we know Close() was successfully called - closeWasCalled := false - i2cHandle.WriteByteDataFunc = func(ctx context.Context, register, data byte) error { - if data == 1<<6 { - closeWasCalled = true - } - return nil - } - i2cHandle.CloseFunc = func() error { return nil } - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - - deps := resource.Dependencies{} - sensor, err := makeMpu6050(context.Background(), deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - err = sensor.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, closeWasCalled, test.ShouldBeTrue) -} - -func setupDependencies(mockData []byte) (resource.Config, buses.I2C) { - i2cName := "i2c" - - cfg := resource.Config{ - Name: "movementsensor", - Model: model, - API: movementsensor.API, - ConvertedAttributes: &Config{ - I2cBus: i2cName, - UseAlternateI2CAddress: true, - }, - } - - i2cHandle := &inject.I2CHandle{} - i2cHandle.ReadBlockDataFunc = func(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - if register == defaultAddressRegister { - return []byte{expectedDefaultAddress}, nil - } - return mockData, nil - } - i2cHandle.WriteByteDataFunc = func(ctx context.Context, b1, b2 byte) error { - return nil - } - i2cHandle.CloseFunc = func() error { return nil } - i2c := &inject.I2C{} - i2c.OpenHandleFunc = func(addr byte) (buses.I2CHandle, error) { - return i2cHandle, nil - } - return cfg, i2c -} - -//nolint:dupl -func TestLinearAcceleration(t *testing.T) { - // linear acceleration, temperature, and angular velocity are all read - // sequentially from the same series of 16-bytes, so we need to fill in - // the mock data at the appropriate portion of the sequence - linearAccelMockData := make([]byte, 16) - // x-accel - linearAccelMockData[0] = 64 - linearAccelMockData[1] = 0 - expectedAccelX := 9.81 - // y-accel - linearAccelMockData[2] = 32 - linearAccelMockData[3] = 0 - expectedAccelY := 4.905 - // z-accel - linearAccelMockData[4] = 16 - linearAccelMockData[5] = 0 - expectedAccelZ := 2.4525 - - logger := logging.NewTestLogger(t) - deps := resource.Dependencies{} - cfg, i2c := setupDependencies(linearAccelMockData) - sensor, err := makeMpu6050(context.Background(), deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - defer sensor.Close(context.Background()) - testutils.WaitForAssertion(t, func(tb testing.TB) { - linAcc, err := sensor.LinearAcceleration(context.Background(), nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, linAcc, test.ShouldNotBeZeroValue) - }) - accel, err := sensor.LinearAcceleration(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, accel.X, test.ShouldEqual, expectedAccelX) - test.That(t, accel.Y, test.ShouldEqual, expectedAccelY) - test.That(t, accel.Z, test.ShouldEqual, expectedAccelZ) -} - -//nolint:dupl -func TestAngularVelocity(t *testing.T) { - // linear acceleration, temperature, and angular velocity are all read - // sequentially from the same series of 16-bytes, so we need to fill in - // the mock data at the appropriate portion of the sequence - angVelMockData := make([]byte, 16) - // x-vel - angVelMockData[8] = 64 - angVelMockData[9] = 0 - expectedAngVelX := 125.0 - // y-accel - angVelMockData[10] = 32 - angVelMockData[11] = 0 - expectedAngVelY := 62.5 - // z-accel - angVelMockData[12] = 16 - angVelMockData[13] = 0 - expectedAngVelZ := 31.25 - - logger := logging.NewTestLogger(t) - deps := resource.Dependencies{} - cfg, i2c := setupDependencies(angVelMockData) - sensor, err := makeMpu6050(context.Background(), deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - defer sensor.Close(context.Background()) - testutils.WaitForAssertion(t, func(tb testing.TB) { - angVel, err := sensor.AngularVelocity(context.Background(), nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, angVel, test.ShouldNotBeZeroValue) - }) - angVel, err := sensor.AngularVelocity(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, angVel.X, test.ShouldEqual, expectedAngVelX) - test.That(t, angVel.Y, test.ShouldEqual, expectedAngVelY) - test.That(t, angVel.Z, test.ShouldEqual, expectedAngVelZ) -} - -func TestTemperature(t *testing.T) { - // linear acceleration, temperature, and angular velocity are all read - // sequentially from the same series of 16-bytes, so we need to fill in - // the mock data at the appropriate portion of the sequence - temperatureMockData := make([]byte, 16) - temperatureMockData[6] = 231 - temperatureMockData[7] = 202 - expectedTemp := 18.3 - - logger := logging.NewTestLogger(t) - deps := resource.Dependencies{} - cfg, i2c := setupDependencies(temperatureMockData) - sensor, err := makeMpu6050(context.Background(), deps, cfg, logger, i2c) - test.That(t, err, test.ShouldBeNil) - defer sensor.Close(context.Background()) - testutils.WaitForAssertion(t, func(tb testing.TB) { - readings, err := sensor.Readings(context.Background(), nil) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, readings["temperature_celsius"], test.ShouldNotBeZeroValue) - }) - readings, err := sensor.Readings(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings["temperature_celsius"], test.ShouldAlmostEqual, expectedTemp, 0.001) -} diff --git a/components/movementsensor/properties.go b/components/movementsensor/properties.go deleted file mode 100644 index a50945ade3a..00000000000 --- a/components/movementsensor/properties.go +++ /dev/null @@ -1,46 +0,0 @@ -package movementsensor - -import pb "go.viam.com/api/component/movementsensor/v1" - -// Properties is a structure representing features -// of a movementsensor. -// The order is in terms of order of derivatives in time -// with position, orientation, compassheading at the top (zeroth derivative) -// linear and angular velocities next (first derivative) -// linear acceleration last (second derivative). -type Properties struct { - PositionSupported bool - OrientationSupported bool - CompassHeadingSupported bool - LinearVelocitySupported bool - AngularVelocitySupported bool - LinearAccelerationSupported bool -} - -// ProtoFeaturesToProperties takes a GetPropertiesResponse and returns -// an equivalent Properties struct. -func ProtoFeaturesToProperties(resp *pb.GetPropertiesResponse) *Properties { - return &Properties{ - PositionSupported: resp.PositionSupported, - OrientationSupported: resp.OrientationSupported, - CompassHeadingSupported: resp.CompassHeadingSupported, - LinearVelocitySupported: resp.LinearVelocitySupported, - AngularVelocitySupported: resp.AngularVelocitySupported, - LinearAccelerationSupported: resp.LinearAccelerationSupported, - } -} - -// PropertiesToProtoResponse takes a properties struct and converts it -// to a GetPropertiesResponse. -func PropertiesToProtoResponse( - features *Properties, -) (*pb.GetPropertiesResponse, error) { - return &pb.GetPropertiesResponse{ - PositionSupported: features.PositionSupported, - OrientationSupported: features.OrientationSupported, - CompassHeadingSupported: features.CompassHeadingSupported, - LinearVelocitySupported: features.LinearVelocitySupported, - AngularVelocitySupported: features.AngularVelocitySupported, - LinearAccelerationSupported: features.LinearAccelerationSupported, - }, nil -} diff --git a/components/movementsensor/register/register.go b/components/movementsensor/register/register.go deleted file mode 100644 index ff52ce5587c..00000000000 --- a/components/movementsensor/register/register.go +++ /dev/null @@ -1,18 +0,0 @@ -// Package register registers all relevant MovementSensors -package register - -import ( - // Load all movementsensors. - _ "go.viam.com/rdk/components/movementsensor/adxl345" - _ "go.viam.com/rdk/components/movementsensor/dualgps" - _ "go.viam.com/rdk/components/movementsensor/fake" - _ "go.viam.com/rdk/components/movementsensor/gpsnmea" - _ "go.viam.com/rdk/components/movementsensor/gpsrtkpmtk" - _ "go.viam.com/rdk/components/movementsensor/gpsrtkserial" - _ "go.viam.com/rdk/components/movementsensor/imuvectornav" - _ "go.viam.com/rdk/components/movementsensor/imuwit" - _ "go.viam.com/rdk/components/movementsensor/merged" - _ "go.viam.com/rdk/components/movementsensor/mpu6050" - _ "go.viam.com/rdk/components/movementsensor/replay" - _ "go.viam.com/rdk/components/movementsensor/wheeledodometry" -) diff --git a/components/movementsensor/replay/replay.go b/components/movementsensor/replay/replay.go deleted file mode 100644 index ef30cdc9fce..00000000000 --- a/components/movementsensor/replay/replay.go +++ /dev/null @@ -1,663 +0,0 @@ -// Package replay implements a replay movement sensor that can return motion data. -package replay - -import ( - "context" - "slices" - "strings" - "sync" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - datapb "go.viam.com/api/app/data/v1" - goutils "go.viam.com/utils" - "go.viam.com/utils/rpc" - "golang.org/x/exp/maps" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/internal/cloud" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils/contextutils" -) - -const ( - timeFormat = time.RFC3339 - grpcConnectionTimeout = 10 * time.Second - dataReceivedLoopWaitTime = time.Second - maxCacheSize = 1000 -) - -type method string - -const ( - position method = "Position" - linearVelocity method = "LinearVelocity" - angularVelocity method = "AngularVelocity" - linearAcceleration method = "LinearAcceleration" - compassHeading method = "CompassHeading" - orientation method = "Orientation" -) - -var ( - // model is the model of a replay movement sensor. - model = resource.DefaultModelFamily.WithModel("replay") - - // initializePropertiesTimeout defines the amount of time we allot to the attempt to initialize Properties. - initializePropertiesTimeout = 180 * time.Second - - // tabularDataByFilterTimeout defines the amount of time we allot to the call to TabularDataByFilter. - tabularDataByFilterTimeout = 20 * time.Second - - // ErrEndOfDataset represents that the replay sensor has reached the end of the dataset. - ErrEndOfDataset = errors.New("reached end of dataset") - - // errPropertiesFailedToInitialize represents that the properties failed to initialize. - errPropertiesFailedToInitialize = errors.New("Properties failed to initialize") - - // errCloudConnectionFailure represents that the attempt to connect to the cloud failed. - errCloudConnectionFailure = errors.New("failure to connect to the cloud") - - // errSessionClosed represents that the session has ended. - errSessionClosed = errors.New("session closed") - - // errBadData represents that the replay sensor data does not match the expected format. - errBadData = errors.New("data does not match expected format") - - // ererMessageNoDataAvailable indicates that no data was available for the given filter. - errMessageNoDataAvailable = "no data available for given filter" - - // methodList is a list of all the base methods possible for a movement sensor to implement. - methodList = []method{position, linearVelocity, angularVelocity, linearAcceleration, compassHeading, orientation} -) - -func init() { - resource.RegisterComponent(movementsensor.API, model, resource.Registration[movementsensor.MovementSensor, *Config]{ - Constructor: newReplayMovementSensor, - }) -} - -// Validate checks that the config attributes are valid for a replay movement sensor. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.Source == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "source") - } - - if cfg.RobotID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "robot_id") - } - - if cfg.LocationID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "location_id") - } - - if cfg.OrganizationID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "organization_id") - } - if cfg.APIKey == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "api_key") - } - if cfg.APIKeyID == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "api_key_id") - } - - var err error - var startTime time.Time - if cfg.Interval.Start != "" { - startTime, err = time.Parse(timeFormat, cfg.Interval.Start) - if err != nil { - return nil, errors.New("invalid time format for start time (UTC), use RFC3339") - } - } - - var endTime time.Time - if cfg.Interval.End != "" { - endTime, err = time.Parse(timeFormat, cfg.Interval.End) - if err != nil { - return nil, errors.New("invalid time format for end time (UTC), use RFC3339") - } - } - - if cfg.Interval.Start != "" && cfg.Interval.End != "" && startTime.After(endTime) { - return nil, errors.New("invalid config, end time (UTC) must be after start time (UTC)") - } - - if cfg.BatchSize != nil && (*cfg.BatchSize > uint64(maxCacheSize) || *cfg.BatchSize == 0) { - return nil, errors.Errorf("batch_size must be between 1 and %d", maxCacheSize) - } - - return []string{cloud.InternalServiceName.String()}, nil -} - -// Config describes how to configure the replay movement sensor. -type Config struct { - Source string `json:"source,omitempty"` - RobotID string `json:"robot_id,omitempty"` - LocationID string `json:"location_id,omitempty"` - OrganizationID string `json:"organization_id,omitempty"` - Interval TimeInterval `json:"time_interval,omitempty"` - BatchSize *uint64 `json:"batch_size,omitempty"` - APIKey string `json:"api_key,omitempty"` - APIKeyID string `json:"api_key_id,omitempty"` -} - -// TimeInterval holds the start and end time used to filter data. -type TimeInterval struct { - Start string `json:"start,omitempty"` - End string `json:"end,omitempty"` -} - -// cacheEntry stores data that was downloaded from a previous operation but has not yet been passed -// to the caller. -type cacheEntry struct { - data *structpb.Struct - timeRequested *timestamppb.Timestamp - timeReceived *timestamppb.Timestamp -} - -// replayMovementSensor is a movement sensor model that plays back pre-captured movement sensor data. -type replayMovementSensor struct { - resource.Named - logger logging.Logger - - APIKey string - APIKeyID string - cloudConnSvc cloud.ConnectionService - cloudConn rpc.ClientConn - dataClient datapb.DataServiceClient - - lastData map[method]string - limit uint64 - filter *datapb.Filter - - cache map[method][]*cacheEntry - - mu sync.RWMutex - closed bool - properties movementsensor.Properties -} - -// newReplayMovementSensor creates a new replay movement sensor based on the inputted config and dependencies. -func newReplayMovementSensor(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) ( - movementsensor.MovementSensor, error, -) { - replay := &replayMovementSensor{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - - if err := replay.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return replay, nil -} - -// Position returns the next position from the cache, in the form of a geo.Point and altitude. -func (replay *replayMovementSensor) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return nil, 0, errSessionClosed - } - - if !replay.properties.PositionSupported { - return nil, 0, movementsensor.ErrMethodUnimplementedPosition - } - - data, err := replay.getDataFromCache(ctx, position) - if err != nil { - return nil, 0, err - } - - coordStruct, ok := data.GetFields()["coordinate"] - if !ok { - return nil, 0, errBadData - } - altitude, ok := data.GetFields()["altitude_m"] - if !ok { - return nil, 0, errBadData - } - return geo.NewPoint( - coordStruct.GetStructValue().GetFields()["latitude"].GetNumberValue(), - coordStruct.GetStructValue().GetFields()["longitude"].GetNumberValue()), - altitude.GetNumberValue(), nil -} - -// LinearVelocity returns the next linear velocity from the cache in the form of an r3.Vector. -func (replay *replayMovementSensor) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return r3.Vector{}, errSessionClosed - } - - if !replay.properties.LinearVelocitySupported { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearVelocity - } - - dataStruct, err := replay.getDataFromCache(ctx, linearVelocity) - if err != nil { - return r3.Vector{}, err - } - data, ok := dataStruct.GetFields()["linear_velocity"] - if !ok { - return r3.Vector{}, errBadData - } - - return structToVector(data.GetStructValue()), nil -} - -// AngularVelocity returns the next angular velocity from the cache in the form of a spatialmath.AngularVelocity (r3.Vector). -func (replay *replayMovementSensor) AngularVelocity(ctx context.Context, extra map[string]interface{}) ( - spatialmath.AngularVelocity, error, -) { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return spatialmath.AngularVelocity{}, errSessionClosed - } - - if !replay.properties.AngularVelocitySupported { - return spatialmath.AngularVelocity{}, movementsensor.ErrMethodUnimplementedAngularVelocity - } - - dataStruct, err := replay.getDataFromCache(ctx, angularVelocity) - if err != nil { - return spatialmath.AngularVelocity{}, err - } - data, ok := dataStruct.GetFields()["angular_velocity"] - if !ok { - return spatialmath.AngularVelocity{}, errBadData - } - - return spatialmath.AngularVelocity{ - X: data.GetStructValue().GetFields()["x"].GetNumberValue(), - Y: data.GetStructValue().GetFields()["y"].GetNumberValue(), - Z: data.GetStructValue().GetFields()["z"].GetNumberValue(), - }, nil -} - -// LinearAcceleration returns the next linear acceleration from the cache in the form of an r3.Vector. -func (replay *replayMovementSensor) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return r3.Vector{}, errSessionClosed - } - - if !replay.properties.LinearAccelerationSupported { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearAcceleration - } - - dataStruct, err := replay.getDataFromCache(ctx, linearAcceleration) - if err != nil { - return r3.Vector{}, err - } - data, ok := dataStruct.GetFields()["linear_acceleration"] - if !ok { - return r3.Vector{}, errBadData - } - - return structToVector(data.GetStructValue()), nil -} - -// CompassHeading returns the next compass heading from the cache as a float64. -func (replay *replayMovementSensor) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return 0., errSessionClosed - } - - if !replay.properties.CompassHeadingSupported { - return 0., movementsensor.ErrMethodUnimplementedCompassHeading - } - - data, err := replay.getDataFromCache(ctx, compassHeading) - if err != nil { - return 0., err - } - value, ok := data.GetFields()["value"] - if !ok { - return 0, errBadData - } - - return value.GetNumberValue(), nil -} - -// Orientation returns the next orientation from the cache as a spatialmath.Orientation created from a spatialmath.OrientationVector. -func (replay *replayMovementSensor) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return nil, errSessionClosed - } - - if !replay.properties.OrientationSupported { - return nil, movementsensor.ErrMethodUnimplementedOrientation - } - - dataStruct, err := replay.getDataFromCache(ctx, orientation) - if err != nil { - return nil, err - } - data, ok := dataStruct.GetFields()["orientation"] - if !ok { - return nil, errBadData - } - - return &spatialmath.OrientationVectorDegrees{ - OX: data.GetStructValue().GetFields()["o_x"].GetNumberValue(), - OY: data.GetStructValue().GetFields()["o_y"].GetNumberValue(), - OZ: data.GetStructValue().GetFields()["o_z"].GetNumberValue(), - Theta: data.GetStructValue().GetFields()["theta"].GetNumberValue(), - }, nil -} - -// Properties returns the available properties for the given replay movement sensor. -func (replay *replayMovementSensor) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - replay.mu.Lock() - defer replay.mu.Unlock() - return &replay.properties, nil -} - -// Accuracy is currently not defined for replay movement sensors. -func (replay *replayMovementSensor) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error, -) { - return movementsensor.UnimplementedOptionalAccuracies(), nil -} - -// Close stops the replay movement sensor, closes its channels and its connections to the cloud. -func (replay *replayMovementSensor) Close(ctx context.Context) error { - replay.mu.Lock() - defer replay.mu.Unlock() - replay.closed = true - replay.closeCloudConnection(ctx) - - return nil -} - -// Readings returns all available data from the next entry stored in the cache. -func (replay *replayMovementSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return movementsensor.DefaultAPIReadings(ctx, replay, extra) -} - -// Reconfigure finishes the bring up of the replay movement sensor by evaluating given arguments and setting up the required cloud -// connection as well as updates all required parameters upon a reconfiguration attempt, restarting the cloud connection in the process. -func (replay *replayMovementSensor) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - replay.mu.Lock() - defer replay.mu.Unlock() - if replay.closed { - return errSessionClosed - } - - replayMovementSensorConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - replay.APIKey = replayMovementSensorConfig.APIKey - replay.APIKeyID = replayMovementSensorConfig.APIKeyID - - cloudConnSvc, err := resource.FromDependencies[cloud.ConnectionService](deps, cloud.InternalServiceName) - if err != nil { - return err - } - - // Update cloud connection if needed - if replay.cloudConnSvc != cloudConnSvc { - replay.closeCloudConnection(ctx) - replay.cloudConnSvc = cloudConnSvc - - if err := replay.initCloudConnection(ctx); err != nil { - replay.closeCloudConnection(ctx) - return errors.Wrap(err, errCloudConnectionFailure.Error()) - } - } - - if replayMovementSensorConfig.BatchSize == nil { - replay.limit = 1 - } else { - replay.limit = *replayMovementSensorConfig.BatchSize - } - - replay.cache = map[method][]*cacheEntry{} - for _, k := range methodList { - replay.cache[k] = nil - } - - replay.lastData = map[method]string{} - for _, k := range methodList { - replay.lastData[k] = "" - } - - replay.filter = &datapb.Filter{ - ComponentName: replayMovementSensorConfig.Source, - RobotId: replayMovementSensorConfig.RobotID, - LocationIds: []string{replayMovementSensorConfig.LocationID}, - OrganizationIds: []string{replayMovementSensorConfig.OrganizationID}, - Interval: &datapb.CaptureInterval{}, - } - - if replayMovementSensorConfig.Interval.Start != "" { - startTime, err := time.Parse(timeFormat, replayMovementSensorConfig.Interval.Start) - if err != nil { - replay.closeCloudConnection(ctx) - return errors.New("invalid time format for start time, missed during config validation") - } - replay.filter.Interval.Start = timestamppb.New(startTime) - } - - if replayMovementSensorConfig.Interval.End != "" { - endTime, err := time.Parse(timeFormat, replayMovementSensorConfig.Interval.End) - if err != nil { - replay.closeCloudConnection(ctx) - return errors.New("invalid time format for end time, missed during config validation") - } - replay.filter.Interval.End = timestamppb.New(endTime) - } - - ctxWithTimeout, cancel := context.WithTimeout(ctx, initializePropertiesTimeout) - defer cancel() - if err := replay.initializeProperties(ctxWithTimeout); err != nil { - err = errors.Wrap(err, errPropertiesFailedToInitialize.Error()) - if errors.Is(err, context.DeadlineExceeded) { - err = errors.Wrap(err, errMessageNoDataAvailable) - } - return err - } - - return nil -} - -// updateCache will update the cache with an additional batch of data downloaded from the cloud -// via TabularDataByFilter based on the given filter, and the last data accessed. -func (replay *replayMovementSensor) updateCache(ctx context.Context, method method) error { - filter := replay.filter - filter.Method = string(method) - - // Retrieve data from the cloud - resp, err := replay.dataClient.TabularDataByFilter(ctx, &datapb.TabularDataByFilterRequest{ - DataRequest: &datapb.DataRequest{ - Filter: filter, - Limit: replay.limit, - Last: replay.lastData[method], - SortOrder: datapb.Order_ORDER_ASCENDING, - }, - CountOnly: false, - }) - if err != nil { - return err - } - - // Check if data exists - if len(resp.GetData()) == 0 { - return ErrEndOfDataset - } - replay.lastData[method] = resp.GetLast() - // Add data to associated cache - for _, dataResponse := range resp.Data { - entry := &cacheEntry{ - data: dataResponse.Data, - timeRequested: dataResponse.GetTimeRequested(), - timeReceived: dataResponse.GetTimeReceived(), - } - replay.cache[method] = append(replay.cache[method], entry) - } - - return nil -} - -// addGRPCMetadata adds timestamps from the data response to the gRPC response header if one is found in the context. -func addGRPCMetadata(ctx context.Context, timeRequested, timeReceived *timestamppb.Timestamp) error { - if stream := grpc.ServerTransportStreamFromContext(ctx); stream != nil { - var grpcMetadata metadata.MD = make(map[string][]string) - if timeRequested != nil { - grpcMetadata.Set(contextutils.TimeRequestedMetadataKey, timeRequested.AsTime().Format(time.RFC3339Nano)) - } - if timeReceived != nil { - grpcMetadata.Set(contextutils.TimeReceivedMetadataKey, timeReceived.AsTime().Format(time.RFC3339Nano)) - } - if err := grpc.SetHeader(ctx, grpcMetadata); err != nil { - return err - } - } - - return nil -} - -func (replay *replayMovementSensor) setProperty(method method, supported bool) error { - switch method { - case position: - replay.properties.PositionSupported = supported - case linearVelocity: - replay.properties.LinearVelocitySupported = supported - case angularVelocity: - replay.properties.AngularVelocitySupported = supported - case linearAcceleration: - replay.properties.LinearAccelerationSupported = supported - case compassHeading: - replay.properties.CompassHeadingSupported = supported - case orientation: - replay.properties.OrientationSupported = supported - default: - return errors.New("can't set property, invalid method: " + string(method)) - } - return nil -} - -// attemptToGetData will try to update the cache for the provided method. Returns a bool that -// indicates whether or not the endpoint has data. -func (replay *replayMovementSensor) attemptToGetData(method method) (bool, error) { - if replay.closed { - return false, errSessionClosed - } - cancelCtx, cancel := context.WithTimeout(context.Background(), tabularDataByFilterTimeout) - defer cancel() - if err := replay.updateCache(cancelCtx, method); err != nil && !strings.Contains(err.Error(), ErrEndOfDataset.Error()) { - return false, errors.Wrap(err, "could not update the cache") - } - return len(replay.cache[method]) != 0, nil -} - -// initializeProperties will set the properties by repeatedly polling the cloud for data from -// the available methods until at least one returns data. The properties are set to -// `true` for the endpoints that returned data. -func (replay *replayMovementSensor) initializeProperties(ctx context.Context) error { - dataReceived := make(map[method]bool) - var err error - // Repeatedly attempt to poll data from the movement sensor for each method until at least - // one of the methods receives data. - for { - if !goutils.SelectContextOrWait(ctx, dataReceivedLoopWaitTime) { - return ctx.Err() - } - for _, method := range methodList { - if dataReceived[method], err = replay.attemptToGetData(method); err != nil { - return err - } - } - // If at least one method successfully managed to return data, we know - // that we can finish initializing the properties. - if slices.Contains(maps.Values(dataReceived), true) { - break - } - } - // Loop once more through all methods to ensure we didn't miss out on catching that they're supported - for _, method := range methodList { - if dataReceived[method], err = replay.attemptToGetData(method); err != nil { - return err - } - } - - for method, supported := range dataReceived { - if err := replay.setProperty(method, supported); err != nil { - return err - } - } - return nil -} - -// getDataFromCache retrieves the next cached data and removes it from the cache. It assumes the write lock is being held. -func (replay *replayMovementSensor) getDataFromCache(ctx context.Context, method method) (*structpb.Struct, error) { - // If no data remains in the cache, download a new batch of data - if len(replay.cache[method]) == 0 { - if err := replay.updateCache(ctx, method); err != nil { - return nil, errors.Wrapf(err, "could not update the cache") - } - } - - // Grab the next cached data and update the associated cache - methodCache := replay.cache[method] - entry := methodCache[0] - replay.cache[method] = methodCache[1:] - - if err := addGRPCMetadata(ctx, entry.timeRequested, entry.timeReceived); err != nil { - return nil, errors.Wrapf(err, "adding GRPC metadata failed") - } - - return entry.data, nil -} - -// closeCloudConnection closes all parts of the cloud connection used by the replay movement sensor. -func (replay *replayMovementSensor) closeCloudConnection(ctx context.Context) { - if replay.cloudConn != nil { - goutils.UncheckedError(replay.cloudConn.Close()) - } - - if replay.cloudConnSvc != nil { - goutils.UncheckedError(replay.cloudConnSvc.Close(ctx)) - } -} - -// initCloudConnection creates a rpc client connection and data service. -func (replay *replayMovementSensor) initCloudConnection(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, grpcConnectionTimeout) - defer cancel() - - _, conn, err := replay.cloudConnSvc.AcquireConnectionAPIKey(ctx, replay.APIKey, replay.APIKeyID) - if err != nil { - return err - } - dataServiceClient := datapb.NewDataServiceClient(conn) - - replay.cloudConn = conn - replay.dataClient = dataServiceClient - return nil -} - -func structToVector(data *structpb.Struct) r3.Vector { - return r3.Vector{ - X: data.GetFields()["x"].GetNumberValue(), - Y: data.GetFields()["y"].GetNumberValue(), - Z: data.GetFields()["z"].GetNumberValue(), - } -} diff --git a/components/movementsensor/replay/replay_test.go b/components/movementsensor/replay/replay_test.go deleted file mode 100644 index 26a193fd0d3..00000000000 --- a/components/movementsensor/replay/replay_test.go +++ /dev/null @@ -1,927 +0,0 @@ -package replay - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.viam.com/test" - "google.golang.org/grpc" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/internal/cloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/utils/contextutils" -) - -var ( - validSource = "source" - validRobotID = "robot_id" - validOrganizationID = "organization_id" - validLocationID = "location_id" - validAPIKey = "a key" - validAPIKeyID = "a key id" - - batchSizeZero = uint64(0) - batchSizeNonZero = uint64(5) - batchSize4 = uint64(4) - batchSizeOutOfBounds = uint64(50000) - - positionPointData = []*geo.Point{ - geo.NewPoint(0, 0), - geo.NewPoint(1, 0), - geo.NewPoint(.5, 0), - geo.NewPoint(0, .4), - geo.NewPoint(1, .4), - } - - positionAltitudeData = []float64{0, 1, 2, 3, 4} - - linearVelocityData = []r3.Vector{ - {X: 0, Y: 0, Z: 0}, - {X: 1, Y: 0, Z: 0}, - {X: 0, Y: 1, Z: 11}, - {X: 1, Y: 4, Z: 0}, - {X: 0, Y: 3, Z: 3}, - {X: 3, Y: 2, Z: 7}, - {X: 0, Y: 3, Z: 3}, - {X: 3, Y: 2, Z: 7}, - {X: 0, Y: 3, Z: 311}, - } - - angularVelocityData = []spatialmath.AngularVelocity{ - {X: 0, Y: 0, Z: 0}, - {X: 1, Y: 0, Z: 2}, - {X: 0, Y: 1, Z: 0}, - {X: 0, Y: 5, Z: 2}, - {X: 2, Y: 3, Z: 3}, - {X: 1, Y: 2, Z: 0}, - {X: 0, Y: 0, Z: 12}, - } - - linearAccelerationData = []r3.Vector{ - {X: 0, Y: 0, Z: 0}, - {X: 1, Y: 0, Z: 0}, - {X: 0, Y: 1, Z: 0}, - {X: 0, Y: 2, Z: 0}, - {X: 0, Y: 3, Z: 3}, - } - - compassHeadingData = []float64{0, 1, 2, 3, 4, 5, 6, 4, 3, 2, 1} - - orientationData = []*spatialmath.OrientationVectorDegrees{ - {OX: 1, OY: 0, OZ: 1, Theta: 0}, - {OX: 2, OY: 1, OZ: 1, Theta: 0}, - } - - allMethodsMaxDataLength = map[method]int{ - position: len(positionPointData), - linearVelocity: len(linearVelocityData), - angularVelocity: len(angularVelocityData), - linearAcceleration: len(linearAccelerationData), - compassHeading: len(compassHeadingData), - orientation: len(orientationData), - } - - allMethodsMinDataLength = map[method]int{ - position: 0, - linearVelocity: 0, - angularVelocity: 0, - linearAcceleration: 0, - compassHeading: 0, - orientation: 0, - } - - defaultReplayMovementSensorFunction = linearAcceleration - - allMethodsSupported = &movementsensor.Properties{ - PositionSupported: true, - LinearVelocitySupported: true, - AngularVelocitySupported: true, - LinearAccelerationSupported: true, - CompassHeadingSupported: true, - OrientationSupported: true, - } - - errPropertiesFailedToInitializeTest = errors.Wrap( - errors.Wrap(context.DeadlineExceeded, errPropertiesFailedToInitialize.Error()), - errMessageNoDataAvailable) -) - -func TestNewReplayMovementSensor(t *testing.T) { - ctx := context.Background() - - initializePropertiesTimeout = 2 * time.Second - tabularDataByFilterTimeout = 1 * time.Second - - cases := []struct { - description string - cfg *Config - validCloudConnection bool - expectedErr error - }{ - { - description: "Valid config with internal cloud service", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: true, - }, - { - description: "Bad internal cloud service", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: false, - expectedErr: errors.Wrap(errTestCloudConnection, errCloudConnectionFailure.Error()), - }, - { - description: "Bad start timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "bad timestamp", - }, - }, - validCloudConnection: true, - expectedErr: errors.New("invalid time format for start time, missed during config validation"), - }, - { - description: "Bad end timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - End: "bad timestamp", - }, - }, - validCloudConnection: true, - expectedErr: errors.New("invalid time format for end time, missed during config validation"), - }, - { - description: "Bad source, initialization of Properties fails", - cfg: &Config{ - Source: "bad_source", - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: true, - expectedErr: errPropertiesFailedToInitializeTest, - }, - { - description: "Bad robot_id, initialization of Properties fails", - cfg: &Config{ - Source: validSource, - RobotID: "bad_robot_id", - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: true, - expectedErr: errPropertiesFailedToInitializeTest, - }, - { - description: "Bad location_id, initialization of Properties fails", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: "bad_location_id", - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: true, - expectedErr: errPropertiesFailedToInitializeTest, - }, - { - description: "Bad organization_id, initialization of Properties fails", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: "bad_organization_id", - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - validCloudConnection: true, - expectedErr: errPropertiesFailedToInitializeTest, - }, - { - description: "Filter results in no data, initialization of Properties fails", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:30Z", - End: "2000-01-01T12:00:40Z", - }, - }, - validCloudConnection: true, - expectedErr: errPropertiesFailedToInitializeTest, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - replay, _, serverClose, err := createNewReplayMovementSensor(ctx, t, tt.cfg, tt.validCloudConnection, false) - if tt.expectedErr != nil { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, tt.expectedErr) - test.That(t, replay, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldBeNil) - test.That(t, replay, test.ShouldNotBeNil) - - err = replay.Close(ctx) - test.That(t, err, test.ShouldBeNil) - } - - if tt.validCloudConnection { - test.That(t, serverClose(), test.ShouldBeNil) - } - }) - } -} - -func TestReplayMovementSensorFunctions(t *testing.T) { - ctx := context.Background() - - initializePropertiesTimeout = 2 * time.Second - tabularDataByFilterTimeout = 1 * time.Second - - cases := []struct { - description string - cfg *Config - startFileNum map[method]int - endFileNum map[method]int - expectedMethodsErr map[method]error - expectedProperties *movementsensor.Properties - useBadDataMessages bool - }{ - { - description: "Calling method with valid filter, all methods are supported", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: allMethodsMinDataLength, - endFileNum: allMethodsMaxDataLength, - expectedProperties: allMethodsSupported, - }, - { - description: "Calling methods with end filter, all methods supported", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - Interval: TimeInterval{ - End: "2000-01-01T12:00:03Z", - }, - }, - startFileNum: allMethodsMinDataLength, - endFileNum: map[method]int{ - position: 3, - linearVelocity: 3, - angularVelocity: 3, - linearAcceleration: 3, - compassHeading: 3, - orientation: allMethodsMaxDataLength[orientation], - }, - expectedProperties: allMethodsSupported, - }, - { - description: "Calling methods with start filter starting at 2", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:02Z", - }, - }, - startFileNum: map[method]int{ - position: 2, - linearVelocity: 2, - angularVelocity: 2, - linearAcceleration: 2, - compassHeading: 2, - }, - endFileNum: map[method]int{ - position: allMethodsMaxDataLength[position], - linearVelocity: allMethodsMaxDataLength[linearVelocity], - angularVelocity: allMethodsMaxDataLength[angularVelocity], - linearAcceleration: allMethodsMaxDataLength[linearAcceleration], - compassHeading: allMethodsMaxDataLength[compassHeading], - }, - expectedMethodsErr: map[method]error{ - orientation: movementsensor.ErrMethodUnimplementedOrientation, - }, - expectedProperties: &movementsensor.Properties{ - PositionSupported: true, - LinearVelocitySupported: true, - AngularVelocitySupported: true, - LinearAccelerationSupported: true, - CompassHeadingSupported: true, - OrientationSupported: false, - }, - }, - { - description: "Calling methods with start filter starting at 6", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:06Z", - }, - }, - startFileNum: map[method]int{ - linearVelocity: 6, - angularVelocity: 6, - compassHeading: 6, - }, - endFileNum: map[method]int{ - linearVelocity: allMethodsMaxDataLength[linearVelocity], - angularVelocity: allMethodsMaxDataLength[angularVelocity], - compassHeading: allMethodsMaxDataLength[compassHeading], - }, - expectedMethodsErr: map[method]error{ - position: movementsensor.ErrMethodUnimplementedPosition, - linearAcceleration: movementsensor.ErrMethodUnimplementedLinearAcceleration, - orientation: movementsensor.ErrMethodUnimplementedOrientation, - }, - expectedProperties: &movementsensor.Properties{ - PositionSupported: false, - LinearVelocitySupported: true, - AngularVelocitySupported: true, - LinearAccelerationSupported: false, - CompassHeadingSupported: true, - OrientationSupported: false, - }, - }, - { - description: "Calling methods with start filter starting at 8", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:08Z", - }, - }, - startFileNum: map[method]int{ - linearVelocity: 8, - compassHeading: 8, - }, - endFileNum: map[method]int{ - linearVelocity: allMethodsMaxDataLength[linearVelocity], - compassHeading: allMethodsMaxDataLength[compassHeading], - }, - expectedMethodsErr: map[method]error{ - position: movementsensor.ErrMethodUnimplementedPosition, - angularVelocity: movementsensor.ErrMethodUnimplementedAngularVelocity, - linearAcceleration: movementsensor.ErrMethodUnimplementedLinearAcceleration, - orientation: movementsensor.ErrMethodUnimplementedOrientation, - }, - expectedProperties: &movementsensor.Properties{ - PositionSupported: false, - LinearVelocitySupported: true, - AngularVelocitySupported: false, - LinearAccelerationSupported: false, - CompassHeadingSupported: true, - OrientationSupported: false, - }, - }, - { - description: "Calling methods with start filter starting at 10", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:10Z", - }, - }, - startFileNum: map[method]int{ - compassHeading: 10, - }, - endFileNum: map[method]int{ - compassHeading: allMethodsMaxDataLength[compassHeading], - }, - expectedMethodsErr: map[method]error{ - position: movementsensor.ErrMethodUnimplementedPosition, - linearVelocity: movementsensor.ErrMethodUnimplementedLinearVelocity, - angularVelocity: movementsensor.ErrMethodUnimplementedAngularVelocity, - linearAcceleration: movementsensor.ErrMethodUnimplementedLinearAcceleration, - orientation: movementsensor.ErrMethodUnimplementedOrientation, - }, - expectedProperties: &movementsensor.Properties{ - PositionSupported: false, - LinearVelocitySupported: false, - AngularVelocitySupported: false, - LinearAccelerationSupported: false, - CompassHeadingSupported: true, - OrientationSupported: false, - }, - }, - { - description: "Calling methods with start and end filter", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:01Z", - End: "2000-01-01T12:00:03Z", - }, - }, - startFileNum: map[method]int{ - position: 1, - linearVelocity: 1, - angularVelocity: 1, - linearAcceleration: 1, - compassHeading: 1, - orientation: 1, - }, - endFileNum: map[method]int{ - position: 3, - linearVelocity: 3, - angularVelocity: 3, - linearAcceleration: 3, - compassHeading: 3, - orientation: allMethodsMaxDataLength[orientation], - }, - expectedProperties: allMethodsSupported, - }, - { - description: "Calling method with valid filter, all methods have invalid data", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - }, - startFileNum: allMethodsMinDataLength, - endFileNum: allMethodsMaxDataLength, - expectedMethodsErr: map[method]error{ - position: errBadData, - compassHeading: errBadData, - linearVelocity: errBadData, - angularVelocity: errBadData, - linearAcceleration: errBadData, - orientation: errBadData, - }, - expectedProperties: allMethodsSupported, - useBadDataMessages: true, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - replay, _, serverClose, err := createNewReplayMovementSensor(ctx, t, tt.cfg, true, tt.useBadDataMessages) - test.That(t, err, test.ShouldBeNil) - test.That(t, replay, test.ShouldNotBeNil) - - actualProperties, err := replay.Properties(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualProperties, test.ShouldResemble, tt.expectedProperties) - - for _, method := range methodList { - if tt.expectedMethodsErr[method] != nil { - testReplayMovementSensorMethodError(ctx, t, replay, method, tt.expectedMethodsErr[method]) - } else { - // Iterate through all files that meet the provided filter - if _, ok := tt.startFileNum[method]; ok { - for i := tt.startFileNum[method]; i < tt.endFileNum[method]; i++ { - testReplayMovementSensorMethodData(ctx, t, replay, method, i) - } - } - // Confirm the end of the dataset was reached when expected - testReplayMovementSensorMethodError(ctx, t, replay, method, ErrEndOfDataset) - } - } - - err = replay.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) - }) - } -} - -func TestReplayMovementSensorConfigValidation(t *testing.T) { - cases := []struct { - description string - cfg *Config - expectedDeps []string - expectedErr error - }{ - { - description: "Valid config and no timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{}, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Valid config with start timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - }, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Valid config with end timestamp", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - End: "2000-01-01T12:00:00Z", - }, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Valid config with start and end timestamps", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - End: "2000-01-01T12:00:01Z", - }, - }, - expectedDeps: []string{cloud.InternalServiceName.String()}, - }, - { - description: "Invalid config no source", - cfg: &Config{ - Interval: TimeInterval{}, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validSource), - }, - { - description: "Invalid config no robot_id", - cfg: &Config{ - Source: validSource, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{}, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validRobotID), - }, - { - description: "Invalid config no location_id", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - OrganizationID: validOrganizationID, - Interval: TimeInterval{}, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validLocationID), - }, - { - description: "Invalid config no organization_id", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - Interval: TimeInterval{}, - }, - expectedErr: resource.NewConfigValidationFieldRequiredError("", validOrganizationID), - }, - { - description: "Invalid config with bad start timestamp format", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "gibberish", - }, - }, - expectedErr: errors.New("invalid time format for start time (UTC), use RFC3339"), - }, - { - description: "Invalid config with bad end timestamp format", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - End: "gibberish", - }, - }, - expectedErr: errors.New("invalid time format for end time (UTC), use RFC3339"), - }, - { - description: "Invalid config with start after end timestamps", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:01Z", - End: "2000-01-01T12:00:00Z", - }, - }, - expectedErr: errors.New("invalid config, end time (UTC) must be after start time (UTC)"), - }, - { - description: "Invalid config with batch size of 0", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - End: "2000-01-01T12:00:01Z", - }, - BatchSize: &batchSizeZero, - }, - expectedErr: errors.New("batch_size must be between 1 and 1000"), - }, - { - description: "Invalid config with batch size above max", - cfg: &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - Interval: TimeInterval{ - Start: "2000-01-01T12:00:00Z", - End: "2000-01-01T12:00:01Z", - }, - BatchSize: &batchSizeOutOfBounds, - }, - expectedErr: errors.New("batch_size must be between 1 and 1000"), - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - deps, err := tt.cfg.Validate("") - if tt.expectedErr != nil { - test.That(t, err, test.ShouldBeError, tt.expectedErr) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, deps, test.ShouldResemble, tt.expectedDeps) - }) - } -} - -func TestUnimplementedFunctionAccuracy(t *testing.T) { - ctx := context.Background() - - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - } - replay, _, serverClose, err := createNewReplayMovementSensor(ctx, t, cfg, true, false) - test.That(t, err, test.ShouldBeNil) - - acc, err := replay.Accuracy(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, acc, test.ShouldNotBeNil) - - err = replay.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} - -func TestReplayMovementSensorReadings(t *testing.T) { - ctx := context.Background() - - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - } - replay, _, serverClose, err := createNewReplayMovementSensor(ctx, t, cfg, true, false) - test.That(t, err, test.ShouldBeNil) - test.That(t, replay, test.ShouldNotBeNil) - - // For loop depends on the data length of orientation as it has the fewest points of data - for i := 0; i < allMethodsMaxDataLength[orientation]; i++ { - readings, err := replay.Readings(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, readings["position"], test.ShouldResemble, positionPointData[i]) - test.That(t, readings["altitude"], test.ShouldResemble, positionAltitudeData[i]) - test.That(t, readings["linear_velocity"], test.ShouldResemble, linearVelocityData[i]) - test.That(t, readings["angular_velocity"], test.ShouldResemble, angularVelocityData[i]) - test.That(t, readings["linear_acceleration"], test.ShouldResemble, linearAccelerationData[i]) - test.That(t, readings["compass"], test.ShouldResemble, compassHeadingData[i]) - test.That(t, readings["orientation"], test.ShouldResemble, orientationData[i]) - } - - readings, err := replay.Readings(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, ErrEndOfDataset.Error()) - test.That(t, readings, test.ShouldBeNil) - - err = replay.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} - -func TestReplayMovementSensorTimestampsMetadata(t *testing.T) { - // Construct replay movement sensor. - ctx := context.Background() - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - BatchSize: &batchSizeNonZero, - } - replay, _, serverClose, err := createNewReplayMovementSensor(ctx, t, cfg, true, false) - test.That(t, err, test.ShouldBeNil) - test.That(t, replay, test.ShouldNotBeNil) - - // Repeatedly call the default method, checking for timestamps in the gRPC header. - for i := 0; i < allMethodsMaxDataLength[defaultReplayMovementSensorFunction]; i++ { - serverStream := testutils.NewServerTransportStream() - ctx = grpc.NewContextWithServerTransportStream(ctx, serverStream) - - testReplayMovementSensorMethodData(ctx, t, replay, defaultReplayMovementSensorFunction, i) - - expectedTimeReq := fmt.Sprintf(testTime, i) - expectedTimeRec := fmt.Sprintf(testTime, i+1) - - actualTimeReq := serverStream.Value(contextutils.TimeRequestedMetadataKey)[0] - actualTimeRec := serverStream.Value(contextutils.TimeReceivedMetadataKey)[0] - - test.That(t, expectedTimeReq, test.ShouldEqual, actualTimeReq) - test.That(t, expectedTimeRec, test.ShouldEqual, actualTimeRec) - } - - // Confirm the end of the dataset was reached when expected - testReplayMovementSensorMethodError(ctx, t, replay, defaultReplayMovementSensorFunction, ErrEndOfDataset) - - err = replay.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} - -func TestReplayMovementSensorReconfigure(t *testing.T) { - // Construct replay movement sensor - cfg := &Config{ - Source: validSource, - RobotID: validRobotID, - LocationID: validLocationID, - OrganizationID: validOrganizationID, - APIKey: validAPIKey, - APIKeyID: validAPIKeyID, - } - ctx := context.Background() - replay, deps, serverClose, err := createNewReplayMovementSensor(ctx, t, cfg, true, false) - test.That(t, err, test.ShouldBeNil) - test.That(t, replay, test.ShouldNotBeNil) - - // Call default movement sensor function to iterate through a few files - for i := 0; i < 3; i++ { - testReplayMovementSensorMethodData(ctx, t, replay, defaultReplayMovementSensorFunction, i) - } - - // Reconfigure with a new batch size - cfg = &Config{Source: validSource, BatchSize: &batchSize4} - replay.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - - // Call the default movement sensor function a couple more times, ensuring that we start over from - // the beginning of the dataset after calling Reconfigure - for i := 0; i < 5; i++ { - testReplayMovementSensorMethodData(ctx, t, replay, defaultReplayMovementSensorFunction, i) - } - - // Reconfigure again, batch size 1 - cfg = &Config{Source: validSource, BatchSize: &batchSizeNonZero} - replay.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - - // Again verify dataset starts from beginning - for i := 0; i < allMethodsMaxDataLength[defaultReplayMovementSensorFunction]; i++ { - testReplayMovementSensorMethodData(ctx, t, replay, defaultReplayMovementSensorFunction, i) - } - - // Confirm the end of the dataset was reached when expected - testReplayMovementSensorMethodError(ctx, t, replay, defaultReplayMovementSensorFunction, ErrEndOfDataset) - - err = replay.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, serverClose(), test.ShouldBeNil) -} diff --git a/components/movementsensor/replay/replay_utils_test.go b/components/movementsensor/replay/replay_utils_test.go deleted file mode 100644 index db9d23f6d36..00000000000 --- a/components/movementsensor/replay/replay_utils_test.go +++ /dev/null @@ -1,418 +0,0 @@ -package replay - -import ( - "context" - "fmt" - "math" - "net" - "strconv" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - datapb "go.viam.com/api/app/data/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/components/movementsensor" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/internal/cloud" - cloudinject "go.viam.com/rdk/internal/testutils/inject" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testTime = "2000-01-01T12:00:%02dZ" -) - -var errTestCloudConnection = errors.New("cloud connection error") - -// mockDataServiceServer is a struct that includes unimplemented versions of all the Data Service endpoints. These -// can be overwritten to allow developers to trigger desired behaviors during testing. -type mockDataServiceServer struct { - datapb.UnimplementedDataServiceServer - useBadDataMessages bool -} - -// TabularDataByFilter is a mocked version of the Data Service function of a similar name. It returns a response with -// data corresponding to the stored data associated with that function and index. -func (mDServer *mockDataServiceServer) TabularDataByFilter(ctx context.Context, req *datapb.TabularDataByFilterRequest, -) (*datapb.TabularDataByFilterResponse, error) { - filter := req.DataRequest.GetFilter() - last := req.DataRequest.GetLast() - limit := req.DataRequest.GetLimit() - - var dataset []*datapb.TabularData - var dataIndex int - var err error - for i := 0; i < int(limit); i++ { - dataIndex, err = getNextDataAfterFilter(filter, last) - if err != nil { - if i == 0 { - return nil, err - } - continue - } - var data *structpb.Struct - // Call desired function - if mDServer.useBadDataMessages { - data = createBadDataByMovementSensorMethod(method(filter.Method), dataIndex) - } else { - data = createDataByMovementSensorMethod(method(filter.Method), dataIndex) - } - - timeReq, timeRec, err := timestampsFromIndex(dataIndex) - if err != nil { - return nil, err - } - - last = fmt.Sprint(dataIndex) - - tabularData := &datapb.TabularData{ - Data: data, - TimeRequested: timeReq, - TimeReceived: timeRec, - } - dataset = append(dataset, tabularData) - } - - // Construct response - resp := &datapb.TabularDataByFilterResponse{ - Data: dataset, - Last: last, - } - - return resp, nil -} - -// timestampsFromIndex uses the index of the data to generate a timeReceived and timeRequested for testing. -func timestampsFromIndex(index int) (*timestamppb.Timestamp, *timestamppb.Timestamp, error) { - timeReq, err := time.Parse(time.RFC3339, fmt.Sprintf(testTime, index)) - if err != nil { - return nil, nil, errors.Wrap(err, "failed parsing time") - } - timeRec := timeReq.Add(time.Second) - return timestamppb.New(timeReq), timestamppb.New(timeRec), nil -} - -// getNextDataAfterFilter returns the index of the next data based on the provided. -func getNextDataAfterFilter(filter *datapb.Filter, last string) (int, error) { - // Basic component part (source) filter - if filter.ComponentName != "" && filter.ComponentName != validSource { - return 0, ErrEndOfDataset - } - - // Basic robot_id filter - if filter.RobotId != "" && filter.RobotId != validRobotID { - return 0, ErrEndOfDataset - } - - // Basic location_id filter - if len(filter.LocationIds) == 0 { - return 0, errors.New("issue occurred with transmitting LocationIds to the cloud") - } - if filter.LocationIds[0] != "" && filter.LocationIds[0] != validLocationID { - return 0, ErrEndOfDataset - } - - // Basic organization_id filter - if len(filter.OrganizationIds) == 0 { - return 0, errors.New("issue occurred with transmitting OrganizationIds to the cloud") - } - if filter.OrganizationIds[0] != "" && filter.OrganizationIds[0] != validOrganizationID { - return 0, ErrEndOfDataset - } - - // Apply the time-based filter based on the seconds value in the start and end fields. Because our mock data - // does not have timestamps associated with them but are ordered we can approximate the filtering - // by sorting for the data in the list whose index is after the start second count and before the end second count. - // For example, if there are 15 entries the start time is 2000-01-01T12:00:10Z and the end time is 2000-01-01T12:00:14Z, - // we will return data from indices 10 to 14. - startIntervalIndex := 0 - endIntervalIndex := math.MaxInt - availableDataNum := allMethodsMaxDataLength[method(filter.Method)] - - if filter.Interval.Start != nil { - startIntervalIndex = filter.Interval.Start.AsTime().Second() - } - if filter.Interval.End != nil { - endIntervalIndex = filter.Interval.End.AsTime().Second() - } - if last == "" { - return checkDataEndCondition(startIntervalIndex, endIntervalIndex, availableDataNum) - } - lastFileNum, err := strconv.Atoi(last) - if err != nil { - return 0, err - } - - return checkDataEndCondition(lastFileNum+1, endIntervalIndex, availableDataNum) -} - -// checkDataEndCondition will return the index of the data to be returned after checking the amount of data available and the end -// internal condition. -func checkDataEndCondition(i, endIntervalIndex, availableDataNum int) (int, error) { - if i < endIntervalIndex && i < availableDataNum { - return i, nil - } - return 0, ErrEndOfDataset -} - -// createMockCloudDependencies creates a mockDataServiceServer and rpc client connection to it which is then -// stored in a mockCloudConnectionService. -func createMockCloudDependencies(ctx context.Context, t *testing.T, logger logging.Logger, validCloudConnection, useBadDataMessages bool, -) (resource.Dependencies, func() error) { - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - test.That(t, rpcServer.RegisterServiceServer( - ctx, - &datapb.DataService_ServiceDesc, - &mockDataServiceServer{useBadDataMessages: useBadDataMessages}, - datapb.RegisterDataServiceHandlerFromEndpoint, - ), test.ShouldBeNil) - - go rpcServer.Serve(listener) - - conn, err := viamgrpc.Dial(ctx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - mockCloudConnectionService := &cloudinject.CloudConnectionService{ - Named: cloud.InternalServiceName.AsNamed(), - Conn: conn, - } - if !validCloudConnection { - mockCloudConnectionService.AcquireConnectionErr = errTestCloudConnection - } - - r := &inject.Robot{} - rs := map[resource.Name]resource.Resource{} - rs[cloud.InternalServiceName] = mockCloudConnectionService - r.MockResourcesFromMap(rs) - - return resourcesFromDeps(t, r, []string{cloud.InternalServiceName.String()}), rpcServer.Stop -} - -// createNewReplayMovementSensor will create a new replay movement sensor based on the provided config with either -// a valid or invalid data client. -func createNewReplayMovementSensor(ctx context.Context, t *testing.T, replayMovementSensorCfg *Config, - validCloudConnection, useBadDataMessages bool, -) (movementsensor.MovementSensor, resource.Dependencies, func() error, error) { - logger := logging.NewTestLogger(t) - - resources, closeRPCFunc := createMockCloudDependencies(ctx, t, logger, validCloudConnection, useBadDataMessages) - - cfg := resource.Config{ConvertedAttributes: replayMovementSensorCfg} - replay, err := newReplayMovementSensor(ctx, resources, cfg, logger) - - return replay, resources, closeRPCFunc, err -} - -// resourcesFromDeps returns a list of dependencies from the provided robot. -func resourcesFromDeps(t *testing.T, r robot.Robot, deps []string) resource.Dependencies { - t.Helper() - resources := resource.Dependencies{} - for _, dep := range deps { - resName, err := resource.NewFromString(dep) - test.That(t, err, test.ShouldBeNil) - res, err := r.ResourceByName(resName) - if err == nil { - // some resources are weakly linked - resources[resName] = res - } - } - return resources -} - -// createDataByMovementSensorMethod will create the mocked structpb.Struct containing the next data returned by calls in tabular data. -func createDataByMovementSensorMethod(method method, index int) *structpb.Struct { - var data structpb.Struct - switch method { - case position: - var coords structpb.Struct - coords.Fields = map[string]*structpb.Value{ - "latitude": structpb.NewNumberValue(positionPointData[index].Lat()), - "longitude": structpb.NewNumberValue(positionPointData[index].Lng()), - } - data.Fields = map[string]*structpb.Value{ - "coordinate": structpb.NewStructValue(&coords), - "altitude_m": structpb.NewNumberValue(positionAltitudeData[index]), - } - case linearVelocity: - var linVel structpb.Struct - linVel.Fields = map[string]*structpb.Value{ - "x": structpb.NewNumberValue(linearVelocityData[index].X), - "y": structpb.NewNumberValue(linearVelocityData[index].Y), - "z": structpb.NewNumberValue(linearVelocityData[index].Z), - } - data.Fields = map[string]*structpb.Value{ - "linear_velocity": structpb.NewStructValue(&linVel), - } - case angularVelocity: - var angVel structpb.Struct - angVel.Fields = map[string]*structpb.Value{ - "x": structpb.NewNumberValue(angularVelocityData[index].X), - "y": structpb.NewNumberValue(angularVelocityData[index].Y), - "z": structpb.NewNumberValue(angularVelocityData[index].Z), - } - data.Fields = map[string]*structpb.Value{ - "angular_velocity": structpb.NewStructValue(&angVel), - } - case linearAcceleration: - var linAcc structpb.Struct - linAcc.Fields = map[string]*structpb.Value{ - "x": structpb.NewNumberValue(linearAccelerationData[index].X), - "y": structpb.NewNumberValue(linearAccelerationData[index].Y), - "z": structpb.NewNumberValue(linearAccelerationData[index].Z), - } - data.Fields = map[string]*structpb.Value{ - "linear_acceleration": structpb.NewStructValue(&linAcc), - } - case compassHeading: - data.Fields = map[string]*structpb.Value{ - "value": structpb.NewNumberValue(compassHeadingData[index]), - } - case orientation: - var orient structpb.Struct - orient.Fields = map[string]*structpb.Value{ - "o_x": structpb.NewNumberValue(orientationData[index].OX), - "o_y": structpb.NewNumberValue(orientationData[index].OY), - "o_z": structpb.NewNumberValue(orientationData[index].OZ), - "theta": structpb.NewNumberValue(orientationData[index].Theta), - } - data.Fields = map[string]*structpb.Value{ - "orientation": structpb.NewStructValue(&orient), - } - } - return &data -} - -// createBadDataByMovementSensorMethod will create the mocked structpb.Struct containing the next data returned by calls in tabular data. -// The data returned will be formatted incorrectly to ensure the movement sensor errors. -func createBadDataByMovementSensorMethod(method method, index int) *structpb.Struct { - var data structpb.Struct - switch method { - case position: - var coords structpb.Struct - coords.Fields = map[string]*structpb.Value{ - "latitude": structpb.NewNumberValue(positionPointData[index].Lat()), - "longitude": structpb.NewNumberValue(positionPointData[index].Lng()), - } - data.Fields = map[string]*structpb.Value{ - "badcoordinate": structpb.NewStructValue(&coords), - "badaltitude_m": structpb.NewNumberValue(positionAltitudeData[index]), - } - case linearVelocity: - data.Fields = map[string]*structpb.Value{ - "x": structpb.NewNumberValue(linearVelocityData[index].X), - "y": structpb.NewNumberValue(linearVelocityData[index].Y), - "z": structpb.NewNumberValue(linearVelocityData[index].Z), - } - case angularVelocity: - data.Fields = map[string]*structpb.Value{ - "x": structpb.NewNumberValue(angularVelocityData[index].X), - "y": structpb.NewNumberValue(angularVelocityData[index].Y), - "z": structpb.NewNumberValue(angularVelocityData[index].Z), - } - case linearAcceleration: - data.Fields = map[string]*structpb.Value{ - "x": structpb.NewNumberValue(linearAccelerationData[index].X), - "y": structpb.NewNumberValue(linearAccelerationData[index].Y), - "z": structpb.NewNumberValue(linearAccelerationData[index].Z), - } - case compassHeading: - data.Fields = map[string]*structpb.Value{ - "badvalue": structpb.NewNumberValue(compassHeadingData[index]), - } - case orientation: - data.Fields = map[string]*structpb.Value{ - "o_x": structpb.NewNumberValue(orientationData[index].OX), - "o_y": structpb.NewNumberValue(orientationData[index].OY), - "o_z": structpb.NewNumberValue(orientationData[index].OZ), - "theta": structpb.NewNumberValue(orientationData[index].Theta), - } - } - return &data -} - -func testReplayMovementSensorMethodData(ctx context.Context, t *testing.T, replay movementsensor.MovementSensor, method method, - index int, -) { - var extra map[string]interface{} - switch method { - case position: - point, altitude, err := replay.Position(ctx, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, point, test.ShouldResemble, positionPointData[index]) - test.That(t, altitude, test.ShouldResemble, positionAltitudeData[index]) - case linearVelocity: - data, err := replay.LinearVelocity(ctx, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, data, test.ShouldResemble, linearVelocityData[index]) - case angularVelocity: - data, err := replay.AngularVelocity(ctx, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, data, test.ShouldResemble, angularVelocityData[index]) - case linearAcceleration: - data, err := replay.LinearAcceleration(ctx, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, data, test.ShouldResemble, linearAccelerationData[index]) - case compassHeading: - data, err := replay.CompassHeading(ctx, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, data, test.ShouldEqual, compassHeadingData[index]) - case orientation: - data, err := replay.Orientation(ctx, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, data, test.ShouldResemble, orientationData[index]) - } -} - -func testReplayMovementSensorMethodError(ctx context.Context, t *testing.T, replay movementsensor.MovementSensor, method method, - expectedErr error, -) { - var extra map[string]interface{} - switch method { - case position: - point, altitude, err := replay.Position(ctx, extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - test.That(t, point, test.ShouldBeNil) - test.That(t, altitude, test.ShouldEqual, 0) - case linearVelocity: - data, err := replay.LinearVelocity(ctx, extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - test.That(t, data, test.ShouldResemble, r3.Vector{}) - case angularVelocity: - data, err := replay.AngularVelocity(ctx, extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - test.That(t, data, test.ShouldResemble, spatialmath.AngularVelocity{}) - case linearAcceleration: - data, err := replay.LinearAcceleration(ctx, extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - test.That(t, data, test.ShouldResemble, r3.Vector{}) - case compassHeading: - data, err := replay.CompassHeading(ctx, extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - test.That(t, data, test.ShouldEqual, 0) - case orientation: - data, err := replay.Orientation(ctx, extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - test.That(t, data, test.ShouldBeNil) - } -} diff --git a/components/movementsensor/server.go b/components/movementsensor/server.go deleted file mode 100644 index df2794f1e50..00000000000 --- a/components/movementsensor/server.go +++ /dev/null @@ -1,191 +0,0 @@ -package movementsensor - -import ( - "context" - - "github.com/golang/geo/r3" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/movementsensor/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -type serviceServer struct { - pb.UnimplementedMovementSensorServiceServer - coll resource.APIResourceCollection[MovementSensor] -} - -// NewRPCServiceServer constructs an MovementSensor gRPC service serviceServer. -func NewRPCServiceServer(coll resource.APIResourceCollection[MovementSensor]) interface{} { - return &serviceServer{coll: coll} -} - -// GetReadings returns the most recent readings from the given Sensor. -func (s *serviceServer) GetReadings( - ctx context.Context, - req *commonpb.GetReadingsRequest, -) (*commonpb.GetReadingsResponse, error) { - sensorDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - readings, err := sensorDevice.Readings(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - m, err := protoutils.ReadingGoToProto(readings) - if err != nil { - return nil, err - } - return &commonpb.GetReadingsResponse{Readings: m}, nil -} - -func (s *serviceServer) GetPosition( - ctx context.Context, - req *pb.GetPositionRequest, -) (*pb.GetPositionResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - loc, altitide, err := msDevice.Position(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetPositionResponse{ - Coordinate: &commonpb.GeoPoint{Latitude: loc.Lat(), Longitude: loc.Lng()}, - AltitudeM: float32(altitide), - }, nil -} - -func (s *serviceServer) GetLinearVelocity( - ctx context.Context, - req *pb.GetLinearVelocityRequest, -) (*pb.GetLinearVelocityResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - vel, err := msDevice.LinearVelocity(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetLinearVelocityResponse{ - LinearVelocity: protoutils.ConvertVectorR3ToProto(vel), - }, nil -} - -func (s *serviceServer) GetAngularVelocity( - ctx context.Context, - req *pb.GetAngularVelocityRequest, -) (*pb.GetAngularVelocityResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - av, err := msDevice.AngularVelocity(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetAngularVelocityResponse{ - AngularVelocity: protoutils.ConvertVectorR3ToProto(r3.Vector(av)), - }, nil -} - -func (s *serviceServer) GetLinearAcceleration( - ctx context.Context, - req *pb.GetLinearAccelerationRequest, -) (*pb.GetLinearAccelerationResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - la, err := msDevice.LinearAcceleration(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetLinearAccelerationResponse{ - LinearAcceleration: protoutils.ConvertVectorR3ToProto(la), - }, nil -} - -func (s *serviceServer) GetCompassHeading( - ctx context.Context, - req *pb.GetCompassHeadingRequest, -) (*pb.GetCompassHeadingResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - ch, err := msDevice.CompassHeading(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetCompassHeadingResponse{ - Value: ch, - }, nil -} - -func (s *serviceServer) GetOrientation( - ctx context.Context, - req *pb.GetOrientationRequest, -) (*pb.GetOrientationResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - ori, err := msDevice.Orientation(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetOrientationResponse{ - Orientation: protoutils.ConvertOrientationToProto(ori), - }, nil -} - -func (s *serviceServer) GetProperties( - ctx context.Context, - req *pb.GetPropertiesRequest, -) (*pb.GetPropertiesResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - prop, err := msDevice.Properties(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return PropertiesToProtoResponse(prop) -} - -func (s *serviceServer) GetAccuracy( - ctx context.Context, - req *pb.GetAccuracyRequest, -) (*pb.GetAccuracyResponse, error) { - msDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - accuracy, err := msDevice.Accuracy(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - - uacc := UnimplementedOptionalAccuracies() - if accuracy != nil { - return accuracyToProtoResponse(accuracy) - } - return accuracyToProtoResponse(uacc) -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - msDevice, err := s.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, msDevice, req) -} diff --git a/components/movementsensor/server_test.go b/components/movementsensor/server_test.go deleted file mode 100644 index 9556be202ad..00000000000 --- a/components/movementsensor/server_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package movementsensor_test - -import ( - "context" - "errors" - "math" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/movementsensor/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -var errReadingsFailed = errors.New("can't get readings") - -func newServer() (pb.MovementSensorServiceServer, *inject.MovementSensor, *inject.MovementSensor, error) { - injectMovementSensor := &inject.MovementSensor{} - injectMovementSensor2 := &inject.MovementSensor{} - gpss := map[resource.Name]movementsensor.MovementSensor{ - movementsensor.Named(testMovementSensorName): injectMovementSensor, - movementsensor.Named(failMovementSensorName): injectMovementSensor2, - } - gpsSvc, err := resource.NewAPIResourceCollection(movementsensor.API, gpss) - if err != nil { - return nil, nil, nil, err - } - return movementsensor.NewRPCServiceServer(gpsSvc).(pb.MovementSensorServiceServer), injectMovementSensor, injectMovementSensor2, nil -} - -func TestServer(t *testing.T) { - gpsServer, injectMovementSensor, injectMovementSensor2, err := newServer() - test.That(t, err, test.ShouldBeNil) - - rs := map[string]interface{}{"a": 1.1, "b": 2.2} - - var extraCap map[string]interface{} - injectMovementSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - extraCap = extra - return rs, nil - } - - injectMovementSensor2.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return nil, errReadingsFailed - } - - t.Run("GetReadings", func(t *testing.T) { - expected := map[string]*structpb.Value{} - for k, v := range rs { - vv, err := structpb.NewValue(v) - test.That(t, err, test.ShouldBeNil) - expected[k] = vv - } - extra, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - - resp, err := gpsServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: testMovementSensorName, Extra: extra}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Readings, test.ShouldResemble, expected) - test.That(t, extraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errReadingsFailed.Error()) - - _, err = gpsServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - }) - - t.Run("GetPosition", func(t *testing.T) { - loc := geo.NewPoint(90, 1) - alt := 50.5 - injectMovementSensor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return loc, alt, nil - } - injectMovementSensor2.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return nil, 0, errLocation - } - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gpsServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: testMovementSensorName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Coordinate, test.ShouldResemble, &commonpb.GeoPoint{Latitude: loc.Lat(), Longitude: loc.Lng()}) - test.That(t, resp.AltitudeM, test.ShouldEqual, alt) - test.That(t, injectMovementSensor.PositionFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errLocation) - - _, err = gpsServer.GetPosition(context.Background(), &pb.GetPositionRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("GetLinearVelocity", func(t *testing.T) { - speed := 5.4 - injectMovementSensor.LinearVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{0, speed, 0}, nil - } - - injectMovementSensor2.LinearVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, errLinearVelocity - } - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gpsServer.GetLinearVelocity(context.Background(), &pb.GetLinearVelocityRequest{Name: testMovementSensorName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.LinearVelocity.Y, test.ShouldResemble, speed) - test.That(t, injectMovementSensor.LinearVelocityFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetLinearVelocity(context.Background(), &pb.GetLinearVelocityRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errLinearVelocity) - - _, err = gpsServer.GetLinearVelocity(context.Background(), &pb.GetLinearVelocityRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("GetAngularVelocity", func(t *testing.T) { - angZ := 1.1 - injectMovementSensor.AngularVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{0, 0, angZ}, nil - } - injectMovementSensor2.AngularVelocityFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - return spatialmath.AngularVelocity{}, errAngularVelocity - } - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gpsServer.GetAngularVelocity(context.Background(), &pb.GetAngularVelocityRequest{Name: testMovementSensorName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.AngularVelocity.Z, test.ShouldResemble, angZ) - test.That(t, injectMovementSensor.AngularVelocityFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetAngularVelocity(context.Background(), &pb.GetAngularVelocityRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errAngularVelocity) - - _, err = gpsServer.GetAngularVelocity(context.Background(), &pb.GetAngularVelocityRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("GetOrientation", func(t *testing.T) { - ori := spatialmath.NewEulerAngles() - ori.Roll = 1.1 - injectMovementSensor.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return ori, nil - } - injectMovementSensor2.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return nil, errOrientation - } - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gpsServer.GetOrientation(context.Background(), &pb.GetOrientationRequest{Name: testMovementSensorName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Orientation.OZ, test.ShouldResemble, ori.OrientationVectorDegrees().OZ) - test.That(t, injectMovementSensor.OrientationFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetOrientation(context.Background(), &pb.GetOrientationRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errOrientation) - - _, err = gpsServer.GetOrientation(context.Background(), &pb.GetOrientationRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("GetCompassHeading", func(t *testing.T) { - heading := 202. - injectMovementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { return heading, nil } - injectMovementSensor2.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0.0, errCompassHeading - } - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gpsServer.GetCompassHeading(context.Background(), &pb.GetCompassHeadingRequest{Name: testMovementSensorName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Value, test.ShouldResemble, heading) - test.That(t, injectMovementSensor.CompassHeadingFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetCompassHeading(context.Background(), &pb.GetCompassHeadingRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errCompassHeading) - - _, err = gpsServer.GetCompassHeading(context.Background(), &pb.GetCompassHeadingRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("GetProperties", func(t *testing.T) { - props := &movementsensor.Properties{LinearVelocitySupported: true} - injectMovementSensor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return props, nil - } - injectMovementSensor2.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return nil, errProperties - } - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gpsServer.GetProperties(context.Background(), &pb.GetPropertiesRequest{Name: testMovementSensorName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.LinearVelocitySupported, test.ShouldResemble, props.LinearVelocitySupported) - test.That(t, injectMovementSensor.PropertiesFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetProperties(context.Background(), &pb.GetPropertiesRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errProperties) - - _, err = gpsServer.GetProperties(context.Background(), &pb.GetPropertiesRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - }) - - t.Run("GetAccuracy", func(t *testing.T) { - acc := &movementsensor.Accuracy{AccuracyMap: map[string]float32{"x": 1.1}} - injectMovementSensor.AccuracyFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - return acc, nil - } - injectMovementSensor2.AccuracyFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - return nil, errAccuracy - } - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - resp, err := gpsServer.GetAccuracy(context.Background(), &pb.GetAccuracyRequest{Name: testMovementSensorName, Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Accuracy, test.ShouldResemble, acc.AccuracyMap) - test.That(t, injectMovementSensor.AccuracyFuncExtraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = gpsServer.GetAccuracy(context.Background(), &pb.GetAccuracyRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errAccuracy) - - _, err = gpsServer.GetAccuracy(context.Background(), &pb.GetAccuracyRequest{Name: missingMovementSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.IsNotFoundError(err), test.ShouldBeTrue) - - // test that server protext client against a nil accuracy return - injectMovementSensor2.AccuracyFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - //nolint:nilnil - return nil, nil - } - uacc, err := gpsServer.GetAccuracy(context.Background(), &pb.GetAccuracyRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, uacc.Accuracy, test.ShouldResemble, map[string]float32(nil)) - test.That(t, math.IsNaN(float64(*uacc.PositionHdop)), test.ShouldBeTrue) - test.That(t, math.IsNaN(float64(*uacc.PositionVdop)), test.ShouldBeTrue) - test.That(t, math.IsNaN(float64(*uacc.CompassDegreesError)), test.ShouldBeTrue) - test.That(t, *uacc.PositionNmeaGgaFix, test.ShouldResemble, int32(-1)) - - // check that server populates optionals correctly if they are undefined - injectMovementSensor2.AccuracyFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) { - nilOptionals := &movementsensor.Accuracy{AccuracyMap: map[string]float32{"foo": 1.1}} - return nilOptionals, nil - } - uacc, err = gpsServer.GetAccuracy(context.Background(), &pb.GetAccuracyRequest{Name: failMovementSensorName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, uacc.Accuracy, test.ShouldResemble, map[string]float32{"foo": 1.1}) - test.That(t, math.IsNaN(float64(*uacc.PositionHdop)), test.ShouldBeTrue) - test.That(t, math.IsNaN(float64(*uacc.PositionVdop)), test.ShouldBeTrue) - test.That(t, math.IsNaN(float64(*uacc.CompassDegreesError)), test.ShouldBeTrue) - // zero is an invlaid fix, and our default fix if accuracy is implemented - test.That(t, *uacc.PositionNmeaGgaFix, test.ShouldResemble, int32(0)) - }) -} diff --git a/components/movementsensor/utils.go b/components/movementsensor/utils.go deleted file mode 100644 index 35d83becbd7..00000000000 --- a/components/movementsensor/utils.go +++ /dev/null @@ -1,184 +0,0 @@ -package movementsensor - -import ( - "errors" - "math" - "sync" - - geo "github.com/kellydunn/golang-geo" -) - -var ( - // ErrMethodUnimplementedAccuracy returns error if the Accuracy method is unimplemented. - ErrMethodUnimplementedAccuracy = errors.New("Accuracy Unimplemented") - // ErrMethodUnimplementedPosition returns error if the Position method is unimplemented. - ErrMethodUnimplementedPosition = errors.New("Position Unimplemented") - // ErrMethodUnimplementedOrientation returns error if the Orientation method is unimplemented. - ErrMethodUnimplementedOrientation = errors.New("Orientation Unimplemented") - // ErrMethodUnimplementedLinearVelocity returns error if the LinearVelocity method is unimplemented. - ErrMethodUnimplementedLinearVelocity = errors.New("LinearVelocity Unimplemented") - // ErrMethodUnimplementedAngularVelocity returns error if the AngularVelocity method is unimplemented. - ErrMethodUnimplementedAngularVelocity = errors.New("AngularVelocity Unimplemented") - // ErrMethodUnimplementedCompassHeading returns error if the CompassHeading method is unimplemented. - ErrMethodUnimplementedCompassHeading = errors.New("CompassHeading Unimplemented") - // ErrMethodUnimplementedReadings returns error if the Readings method is unimplemented. - ErrMethodUnimplementedReadings = errors.New("Readings Unimplemented") - // ErrMethodUnimplementedProperties returns error if the Properties method is unimplemented. - ErrMethodUnimplementedProperties = errors.New("Properties Unimplemented") - // ErrMethodUnimplementedLinearAcceleration returns error if Linear Acceleration is unimplemented. - ErrMethodUnimplementedLinearAcceleration = errors.New("linear acceleration unimplemented") -) - -// LastError is an object that stores recent errors. If there have been sufficiently many recent -// errors, you can retrieve the most recent one. -type LastError struct { - // These values are immutable - size int // The length of errs, below - threshold int // How many items in errs must be non-nil for us to give back errors when asked - - // These values are mutable - mu sync.Mutex - errs []error // A list of recent errors, oldest to newest - count int // How many items in errs are non-nil -} - -// NewLastError creates a LastError object which will let you retrieve the most recent error if at -// least `threshold` of the most recent `size` items put into it are non-nil. -func NewLastError(size, threshold int) LastError { - return LastError{errs: make([]error, size), threshold: threshold, size: size} -} - -// Set stores an error to be retrieved later. -func (le *LastError) Set(err error) { - le.mu.Lock() - defer le.mu.Unlock() - - // Remove the oldest error, and add the newest one. - if le.errs[0] != nil { - le.count-- - } - if err != nil { - le.count++ - } - le.errs = append(le.errs[1:], err) -} - -// Get returns the most-recently-stored non-nil error if we've had enough recent errors. If we're -// going to return a non-nil error, we also wipe out all other data so we don't return the same -// error again next time. -func (le *LastError) Get() error { - le.mu.Lock() - defer le.mu.Unlock() - - if le.count < le.threshold { - // Keep our data, in case we're close to the threshold and will return an error next time. - return nil - } - - // Otherwise, find the most recent error, iterating through the list newest to oldest. Assuming - // the threshold is at least 1, there is definitely some error in here to find. - var errToReturn error - for i := 0; i < len(le.errs); i++ { - current := le.errs[len(le.errs)-1-i] - if current == nil { - continue - } - errToReturn = current - break - } - - // Wipe everything out - le.errs = make([]error, le.size) - le.count = 0 - return errToReturn -} - -// LastPosition stores the last position seen by the movement sensor. -type LastPosition struct { - lastposition *geo.Point - mu sync.Mutex -} - -// NewLastPosition creates a new point that's (NaN, NaN) -// go-staticcheck. -func NewLastPosition() LastPosition { - return LastPosition{lastposition: geo.NewPoint(math.NaN(), math.NaN())} -} - -// GetLastPosition returns the last known position. -func (lp *LastPosition) GetLastPosition() *geo.Point { - lp.mu.Lock() - defer lp.mu.Unlock() - return lp.lastposition -} - -// SetLastPosition updates the last known position. -func (lp *LastPosition) SetLastPosition(position *geo.Point) { - lp.mu.Lock() - defer lp.mu.Unlock() - lp.lastposition = position -} - -// ArePointsEqual checks if two geo.Point instances are equal. -func ArePointsEqual(p1, p2 *geo.Point) bool { - if p1 == nil || p2 == nil { - return p1 == p2 - } - return p1.Lng() == p2.Lng() && p1.Lat() == p2.Lat() -} - -// IsZeroPosition checks if a geo.Point represents the zero position (0, 0). -func IsZeroPosition(p *geo.Point) bool { - return p.Lng() == 0 && p.Lat() == 0 -} - -// IsPositionNaN checks if a geo.Point in math.NaN(). -func IsPositionNaN(p *geo.Point) bool { - return math.IsNaN(p.Lng()) && math.IsNaN(p.Lat()) -} - -// LastCompassHeading store the last valid compass heading seen by the movement sensor. -// This is really just an atomic float64, analogous to the atomic ints provided in the -// "sync/atomic" package. -type LastCompassHeading struct { - lastcompassheading float64 - mu sync.Mutex -} - -// NewLastCompassHeading create a new LastCompassHeading. -func NewLastCompassHeading() LastCompassHeading { - return LastCompassHeading{lastcompassheading: math.NaN()} -} - -// GetLastCompassHeading returns the last compass heading stored. -func (lch *LastCompassHeading) GetLastCompassHeading() float64 { - lch.mu.Lock() - defer lch.mu.Unlock() - return lch.lastcompassheading -} - -// SetLastCompassHeading sets lastcompassheading to the value given in the function. -func (lch *LastCompassHeading) SetLastCompassHeading(compassheading float64) { - lch.mu.Lock() - defer lch.mu.Unlock() - lch.lastcompassheading = compassheading -} - -// PMTKAddChk adds PMTK checksums to commands by XORing the bytes together. -func PMTKAddChk(data []byte) []byte { - chk := PMTKChecksum(data) - newCmd := []byte("$") - newCmd = append(newCmd, data...) - newCmd = append(newCmd, []byte("*")...) - newCmd = append(newCmd, chk) - return newCmd -} - -// PMTKChecksum calculates the checksum of a byte array by performing an XOR operation on each byte. -func PMTKChecksum(data []byte) byte { - var chk byte - for _, b := range data { - chk ^= b - } - return chk -} diff --git a/components/movementsensor/utils_test.go b/components/movementsensor/utils_test.go deleted file mode 100644 index 84d2754c432..00000000000 --- a/components/movementsensor/utils_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package movementsensor - -import ( - "errors" - "math" - "testing" - - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" -) - -var ( - testPos1 = geo.NewPoint(8.46696, -17.03663) - testPos2 = geo.NewPoint(65.35996, -17.03663) - zeroPos = geo.NewPoint(0, 0) - nanPos = geo.NewPoint(math.NaN(), math.NaN()) -) - -func TestNoErrors(t *testing.T) { - le := NewLastError(1, 1) - test.That(t, le.Get(), test.ShouldBeNil) -} - -func TestOneError(t *testing.T) { - le := NewLastError(1, 1) - - le.Set(errors.New("it's a test error")) - test.That(t, le.Get(), test.ShouldNotBeNil) - // We got the error, so it shouldn't be in here any more. - test.That(t, le.Get(), test.ShouldBeNil) -} - -func TestTwoErrors(t *testing.T) { - le := NewLastError(1, 1) - - le.Set(errors.New("first")) - le.Set(errors.New("second")) - - err := le.Get() - test.That(t, err.Error(), test.ShouldEqual, "second") -} - -func TestSetGetTwice(t *testing.T) { - le := NewLastError(1, 1) - - le.Set(errors.New("first")) - err := le.Get() - test.That(t, err.Error(), test.ShouldEqual, "first") - - le.Set(errors.New("second")) - err = le.Get() - test.That(t, err.Error(), test.ShouldEqual, "second") -} - -func TestSuppressRareErrors(t *testing.T) { - le := NewLastError(2, 2) // Only report if 2 of the last 2 are non-nil errors - - test.That(t, le.Get(), test.ShouldBeNil) - le.Set(nil) - test.That(t, le.Get(), test.ShouldBeNil) - le.Set(errors.New("one")) - test.That(t, le.Get(), test.ShouldBeNil) - le.Set(nil) - test.That(t, le.Get(), test.ShouldBeNil) - le.Set(errors.New("two")) - test.That(t, le.Get(), test.ShouldBeNil) - le.Set(errors.New("three")) // Two errors in a row! - - err := le.Get() - test.That(t, err.Error(), test.ShouldEqual, "three") - // and now that we've returned an error, the history is cleared out again. - test.That(t, le.Get(), test.ShouldBeNil) -} - -func TestLastPosition(t *testing.T) { - lp := NewLastPosition() - lp.SetLastPosition(testPos2) - test.That(t, lp.lastposition, test.ShouldEqual, testPos2) - - lp.SetLastPosition(testPos1) - getPos := lp.GetLastPosition() - test.That(t, getPos, test.ShouldEqual, testPos1) -} - -func TestPositionLogic(t *testing.T) { - test.That(t, ArePointsEqual(testPos2, testPos2), test.ShouldBeTrue) - test.That(t, ArePointsEqual(testPos2, testPos1), test.ShouldBeFalse) - - test.That(t, IsZeroPosition(zeroPos), test.ShouldBeTrue) - test.That(t, IsZeroPosition(testPos2), test.ShouldBeFalse) - - test.That(t, IsPositionNaN(nanPos), test.ShouldBeTrue) - test.That(t, IsPositionNaN(testPos1), test.ShouldBeFalse) -} - -func TestPMTKFunctions(t *testing.T) { - var ( - expectedValue = ([]uint8{36, 80, 77, 84, 75, 50, 50, 48, 44, 49, 48, 48, 48, 42, 31}) - testValue = ([]byte("PMTK220,1000")) - expectedChecksum = 31 - ) - test.That(t, PMTKChecksum(testValue), test.ShouldEqual, expectedChecksum) - test.That(t, PMTKAddChk(testValue), test.ShouldResemble, expectedValue) -} diff --git a/components/movementsensor/verify_main_test.go b/components/movementsensor/verify_main_test.go deleted file mode 100644 index 53d147fb028..00000000000 --- a/components/movementsensor/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package movementsensor - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/movementsensor/wheeledodometry/wheeledodometry.go b/components/movementsensor/wheeledodometry/wheeledodometry.go deleted file mode 100644 index 014541e60f5..00000000000 --- a/components/movementsensor/wheeledodometry/wheeledodometry.go +++ /dev/null @@ -1,507 +0,0 @@ -// Package wheeledodometry implements an odometery estimate from an encoder wheeled base. -package wheeledodometry - -import ( - "context" - "errors" - "fmt" - "math" - "sync" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("wheeled-odometry") - -const ( - defaultTimeIntervalMSecs = 500 - oneTurn = 2 * math.Pi - mToKm = 1e-3 - returnRelative = "return_relative_pos_m" - setLong = "setLong" - setLat = "setLat" - useCompass = "use_compass" - shiftPos = "shift_position" - resetShift = "reset" - moveX = "moveX" - moveY = "moveY" -) - -// Config is the config for a wheeledodometry MovementSensor. -type Config struct { - LeftMotors []string `json:"left_motors"` - RightMotors []string `json:"right_motors"` - Base string `json:"base"` - TimeIntervalMSecs float64 `json:"time_interval_msecs,omitempty"` -} - -type motorPair struct { - left motor.Motor - right motor.Motor -} - -type odometry struct { - resource.Named - resource.AlwaysRebuild - - lastLeftPos float64 - lastRightPos float64 - baseWidth float64 - wheelCircumference float64 - base base.Base - timeIntervalMSecs float64 - - motors []motorPair - - angularVelocity spatialmath.AngularVelocity - linearVelocity r3.Vector - position r3.Vector - orientation spatialmath.EulerAngles - coord *geo.Point - originCoord *geo.Point - - useCompass bool - shiftPos bool - - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup - mu sync.Mutex - logger logging.Logger -} - -func init() { - resource.RegisterComponent( - movementsensor.API, - model, - resource.Registration[movementsensor.MovementSensor, *Config]{Constructor: newWheeledOdometry}) -} - -// Validate ensures all parts of the config are valid. -func (cfg *Config) Validate(path string) ([]string, error) { - var deps []string - - if cfg.Base == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "base") - } - deps = append(deps, cfg.Base) - - if len(cfg.LeftMotors) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "left motors") - } - deps = append(deps, cfg.LeftMotors...) - - if len(cfg.RightMotors) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "right motors") - } - deps = append(deps, cfg.RightMotors...) - - if len(cfg.LeftMotors) != len(cfg.RightMotors) { - return nil, errors.New("mismatch number of left and right motors") - } - - // Temporary validation check until support for more than one left and right motor each is added. - if len(cfg.LeftMotors) > 1 || len(cfg.RightMotors) > 1 { - return nil, errors.New("wheeled odometry only supports one left and right motor each") - } - - return deps, nil -} - -// Reconfigure automatically reconfigures this movement sensor based on the updated config. -func (o *odometry) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - if len(o.motors) > 0 { - if err := o.motors[0].left.Stop(ctx, nil); err != nil { - return err - } - if err := o.motors[0].right.Stop(ctx, nil); err != nil { - return err - } - } - - if o.cancelFunc != nil { - o.cancelFunc() - o.activeBackgroundWorkers.Wait() - } - - o.mu.Lock() - defer o.mu.Unlock() - - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - // set the new timeIntervalMSecs - o.timeIntervalMSecs = newConf.TimeIntervalMSecs - if o.timeIntervalMSecs == 0 { - o.timeIntervalMSecs = defaultTimeIntervalMSecs - } - if o.timeIntervalMSecs > 1000 { - o.logger.CWarn(ctx, "if the time interval is more than 1000 ms, be sure to move the base slowly for better accuracy") - } - - // set baseWidth and wheelCircumference from the new base properties - newBase, err := base.FromDependencies(deps, newConf.Base) - if err != nil { - return err - } - props, err := newBase.Properties(ctx, nil) - if err != nil { - return err - } - o.baseWidth = props.WidthMeters - o.wheelCircumference = props.WheelCircumferenceMeters - if o.baseWidth == 0 || o.wheelCircumference == 0 { - return errors.New("base width or wheel circumference are 0, movement sensor cannot be created") - } - o.base = newBase - o.logger.Debugf("using base %v for wheeled_odometry sensor", newBase.Name().ShortName()) - - // check if new motors have been added, or the existing motors have been changed, and update the motorPairs accorodingly - for i := range newConf.LeftMotors { - var motorLeft, motorRight motor.Motor - - motorLeft, err = motor.FromDependencies(deps, newConf.LeftMotors[i]) - if err != nil { - return err - } - properties, err := motorLeft.Properties(ctx, nil) - if err != nil { - return err - } - if !properties.PositionReporting { - return motor.NewPropertyUnsupportedError(properties, newConf.LeftMotors[i]) - } - - motorRight, err = motor.FromDependencies(deps, newConf.RightMotors[i]) - if err != nil { - return err - } - properties, err = motorRight.Properties(ctx, nil) - if err != nil { - return err - } - if !properties.PositionReporting { - return motor.NewPropertyUnsupportedError(properties, newConf.LeftMotors[i]) - } - - // append if motors have been added, replace if motors have changed - thisPair := motorPair{left: motorLeft, right: motorRight} - if i >= len(o.motors) { - o.motors = append(o.motors, thisPair) - } else if (o.motors[i].left.Name().ShortName() != newConf.LeftMotors[i]) || - (o.motors[i].right.Name().ShortName() != newConf.RightMotors[i]) { - o.motors[i].left = motorLeft - o.motors[i].right = motorRight - } - o.logger.Debugf("using motors %v for wheeled odometery", - []string{motorLeft.Name().ShortName(), motorRight.Name().ShortName()}) - } - - if len(o.motors) > 1 { - o.logger.CWarn(ctx, "odometry will not be accurate if the left and right motors that are paired are not listed in the same order") - } - - o.orientation.Yaw = 0 - o.originCoord = geo.NewPoint(0, 0) - ctx, cancelFunc := context.WithCancel(context.Background()) - o.cancelFunc = cancelFunc - o.trackPosition(ctx) - - return nil -} - -// newWheeledOdometry returns a new wheeled encoder movement sensor defined by the given config. -func newWheeledOdometry( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (movementsensor.MovementSensor, error) { - o := &odometry{ - Named: conf.ResourceName().AsNamed(), - lastLeftPos: 0.0, - lastRightPos: 0.0, - originCoord: geo.NewPoint(0, 0), - logger: logger, - } - - if err := o.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return o, nil -} - -func (o *odometry) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - o.mu.Lock() - defer o.mu.Unlock() - return o.angularVelocity, nil -} - -func (o *odometry) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - return r3.Vector{}, movementsensor.ErrMethodUnimplementedLinearAcceleration -} - -func (o *odometry) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - o.mu.Lock() - defer o.mu.Unlock() - ov := &spatialmath.OrientationVector{Theta: o.orientation.Yaw, OX: 0, OY: 0, OZ: 1} - return ov, nil -} - -func (o *odometry) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - o.mu.Lock() - defer o.mu.Unlock() - - if o.useCompass { - return yawToCompassHeading(o.orientation.Yaw), nil - } - - return 0, movementsensor.ErrMethodUnimplementedCompassHeading -} - -// computes the compass heading in degrees from a yaw in radians, with 0 -> 360 and Z down. -func yawToCompassHeading(yaw float64) float64 { - yawDeg := rdkutils.RadToDeg(yaw) - if yawDeg < 0 { - yawDeg = 180 - yawDeg - } - return 360 - yawDeg -} - -func (o *odometry) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - o.mu.Lock() - defer o.mu.Unlock() - return o.linearVelocity, nil -} - -func (o *odometry) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - o.mu.Lock() - defer o.mu.Unlock() - - if relative, ok := extra[returnRelative]; ok { - if relative.(bool) { - return geo.NewPoint(o.position.Y, o.position.X), o.position.Z, nil - } - } - - return o.coord, o.position.Z, nil -} - -func (o *odometry) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - readings, err := movementsensor.DefaultAPIReadings(ctx, o, extra) - if err != nil { - return nil, err - } - - // movementsensor.Readings calls all the APIs with their owm mutex lock in this driver - // the lock has been released, so for the last two readings we lock again to append them to the readings map - o.mu.Lock() - defer o.mu.Unlock() - readings["position_meters_X"] = o.position.X - readings["position_meters_Y"] = o.position.Y - - return readings, nil -} - -func (o *odometry) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error, -) { - return movementsensor.UnimplementedOptionalAccuracies(), nil -} - -func (o *odometry) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - LinearVelocitySupported: true, - AngularVelocitySupported: true, - OrientationSupported: true, - PositionSupported: true, - CompassHeadingSupported: o.useCompass, - }, nil -} - -func (o *odometry) Close(ctx context.Context) error { - o.cancelFunc() - o.activeBackgroundWorkers.Wait() - return nil -} - -func (o *odometry) checkBaseProps(ctx context.Context) { - props, err := o.base.Properties(ctx, nil) - if err != nil { - if !errors.Is(err, context.Canceled) { - o.logger.Error(err) - return - } - } - if (o.baseWidth != props.WidthMeters) || (o.wheelCircumference != props.WheelCircumferenceMeters) { - o.baseWidth = props.WidthMeters - o.wheelCircumference = props.WheelCircumferenceMeters - o.logger.Warnf("Base %v's properties have changed: baseWidth = %v and wheelCirumference = %v.", - "Odometry can optionally be reset using DoCommand", - o.base.Name().ShortName(), o.baseWidth, o.wheelCircumference) - } -} - -// trackPosition uses the motor positions to calculate an estimation of the position, orientation, -// linear velocity, and angular velocity of the wheeled base. -// The estimations in this function are based on the math outlined in this article: -// https://stuff.mit.edu/afs/athena/course/6/6.186/OldFiles/2005/doc/odomtutorial/odomtutorial.pdf -func (o *odometry) trackPosition(ctx context.Context) { - if o.originCoord == nil { - o.originCoord = geo.NewPoint(0, 0) - } - - o.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer o.activeBackgroundWorkers.Done() - ticker := time.NewTicker(time.Duration(o.timeIntervalMSecs) * time.Millisecond) - for { - select { - case <-ctx.Done(): - return - default: - } - - // Sleep until it's time to update the position again. - select { - case <-ctx.Done(): - return - case <-ticker.C: - } - - // Use GetInParallel to ensure the left and right motors are polled at the same time. - positionFuncs := func() []rdkutils.FloatFunc { - fs := []rdkutils.FloatFunc{} - - // Always use the first pair until more than one pair of motors is supported in this model. - fs = append(fs, func(ctx context.Context) (float64, error) { return o.motors[0].left.Position(ctx, nil) }) - fs = append(fs, func(ctx context.Context) (float64, error) { return o.motors[0].right.Position(ctx, nil) }) - - return fs - } - - _, positions, err := rdkutils.GetInParallel(ctx, positionFuncs()) - if err != nil { - o.logger.CError(ctx, err) - continue - } - - // Current position of the left and right motors in revolutions. - if len(positions) != len(o.motors)*2 { - o.logger.CError(ctx, "error getting both motor positions, trying again") - continue - } - left := positions[0] - right := positions[1] - - // Base properties need to be checked every time because dependent components reconfiguring does not trigger - // the parent component to reconfigure. In this case, that means if the base properties change, the wheeled - // odometry movement sensor will not be aware of these changes and will continue to use the old values - o.checkBaseProps(ctx) - - // Difference in the left and right motors since the last iteration, in mm. - leftDist := (left - o.lastLeftPos) * o.wheelCircumference - rightDist := (right - o.lastRightPos) * o.wheelCircumference - - // Update lastLeftPos and lastRightPos to be the current position in mm. - o.lastLeftPos = left - o.lastRightPos = right - - // Linear and angular distance the center point has traveled. This works based on - // the assumption that the time interval between calulations is small enough that - // the inner angle of the rotation will be small enough that it can be accurately - // estimated using the below equations. - centerDist := (leftDist + rightDist) / 2 - centerAngle := (rightDist - leftDist) / o.baseWidth - - // Update the position and orientation values accordingly. - o.mu.Lock() - o.orientation.Yaw += centerAngle - - // Limit the yaw to a range of positive 0 to 360 degrees. - o.orientation.Yaw = math.Mod(o.orientation.Yaw, oneTurn) - o.orientation.Yaw = math.Mod(o.orientation.Yaw+oneTurn, oneTurn) - angle := o.orientation.Yaw - xFlip := -1.0 - if o.useCompass { - angle = rdkutils.DegToRad(yawToCompassHeading(o.orientation.Yaw)) - xFlip = 1.0 - } - o.position.X += xFlip * (centerDist * math.Sin(angle)) - o.position.Y += (centerDist * math.Cos(angle)) - - distance := math.Hypot(o.position.X, o.position.Y) - heading := rdkutils.RadToDeg(math.Atan2(o.position.X, o.position.Y)) - o.coord = o.originCoord.PointAtDistanceAndBearing(distance*mToKm, heading) - - // Update the linear and angular velocity values using the provided time interval. - o.linearVelocity.Y = centerDist / (o.timeIntervalMSecs / 1000) - o.angularVelocity.Z = centerAngle * (180 / math.Pi) / (o.timeIntervalMSecs / 1000) - - o.mu.Unlock() - } - }) -} - -func (o *odometry) DoCommand(ctx context.Context, - req map[string]interface{}, -) (map[string]interface{}, error) { - resp := make(map[string]interface{}) - - o.mu.Lock() - defer o.mu.Unlock() - cmd, ok := req[useCompass].(bool) - if ok { - o.useCompass = cmd - resp[useCompass] = fmt.Sprintf("using orientation as compass heading set to %v", cmd) - } - - reset, ok := req[resetShift].(bool) - if ok { - o.shiftPos = reset - o.originCoord = geo.NewPoint(0, 0) - o.position.X = 0 - o.position.Y = 0 - o.orientation.Yaw = 0 - - resp[resetShift] = fmt.Sprintf("resetting position and setting shift to %v", reset) - } - lat, okY := req[setLat].(float64) - long, okX := req[setLong].(float64) - if okY && okX { - o.originCoord = geo.NewPoint(lat, long) - o.shiftPos = true - - resp[setLat] = fmt.Sprintf("lat shifted to %.8f", lat) - resp[setLong] = fmt.Sprintf("lng shifted to %.8f", long) - } else if okY || okX { - // If only one value is given, return an error. - // This prevents errors when neither is given. - resp["bad shift"] = "need both lat and long shifts" - } - - xMove, okX := req[moveX].(float64) - yMove, okY := req[moveY].(float64) - if okX { - o.position.X += xMove - resp[moveX] = fmt.Sprintf("x position moved to %.8f", o.position.X) - } - if okY { - o.position.Y += yMove - resp[moveY] = fmt.Sprintf("y position shifted to %.8f", o.position.Y) - } - - return resp, nil -} diff --git a/components/movementsensor/wheeledodometry/wheeledodometry_test.go b/components/movementsensor/wheeledodometry/wheeledodometry_test.go deleted file mode 100644 index 833760088a5..00000000000 --- a/components/movementsensor/wheeledodometry/wheeledodometry_test.go +++ /dev/null @@ -1,501 +0,0 @@ -// Package wheeledodometry implements an odometery estimate from an encoder wheeled base -package wheeledodometry - -import ( - "context" - "errors" - "math" - "sync" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - leftMotorName = "left" - rightMotorName = "right" - baseName = "base" - testSensorName = "name" - newLeftMotorName = "new_left" - newRightMotorName = "new_right" - newBaseName = "new_base" -) - -type positions struct { - mu sync.Mutex - leftPos float64 - rightPos float64 -} - -var position = positions{ - leftPos: 0.0, - rightPos: 0.0, -} - -var relativePos = map[string]interface{}{returnRelative: true} - -func createFakeMotor(dir bool) motor.Motor { - return &inject.Motor{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{PositionReporting: true}, nil - }, - PositionFunc: func(ctx context.Context, extra map[string]interface{}) (float64, error) { - position.mu.Lock() - defer position.mu.Unlock() - if dir { - return position.leftPos, nil - } - return position.rightPos, nil - }, - IsMovingFunc: func(ctx context.Context) (bool, error) { - position.mu.Lock() - defer position.mu.Unlock() - if dir && math.Abs(position.leftPos) > 0 { - return true, nil - } - if !dir && math.Abs(position.rightPos) > 0 { - return true, nil - } - return false, nil - }, - ResetZeroPositionFunc: func(ctx context.Context, offset float64, extra map[string]interface{}) error { - position.mu.Lock() - defer position.mu.Unlock() - position.leftPos = 0 - position.rightPos = 0 - return nil - }, - StopFunc: func(ctx context.Context, extra map[string]interface{}) error { - return nil - }, - } -} - -func createFakeBase(circ, width, rad float64) base.Base { - return &inject.Base{ - PropertiesFunc: func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{WheelCircumferenceMeters: circ, WidthMeters: width, TurningRadiusMeters: rad}, nil - }, - } -} - -func setPositions(left, right float64) { - position.mu.Lock() - defer position.mu.Unlock() - position.leftPos += left - position.rightPos += right -} - -func TestNewWheeledOdometry(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - deps := make(resource.Dependencies) - deps[base.Named(baseName)] = createFakeBase(0.1, 0.1, 0.1) - deps[motor.Named(leftMotorName)] = createFakeMotor(true) - deps[motor.Named(rightMotorName)] = createFakeMotor(false) - - fakecfg := resource.Config{ - Name: testSensorName, - ConvertedAttributes: &Config{ - LeftMotors: []string{leftMotorName}, - RightMotors: []string{rightMotorName}, - Base: baseName, - TimeIntervalMSecs: 500, - }, - } - fakeSensor, err := newWheeledOdometry(ctx, deps, fakecfg, logger) - test.That(t, err, test.ShouldBeNil) - _, ok := fakeSensor.(*odometry) - test.That(t, ok, test.ShouldBeTrue) -} - -func TestReconfigure(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - deps := make(resource.Dependencies) - deps[base.Named(baseName)] = createFakeBase(0.1, 0.1, 0) - deps[motor.Named(leftMotorName)] = createFakeMotor(true) - deps[motor.Named(rightMotorName)] = createFakeMotor(false) - - fakecfg := resource.Config{ - Name: testSensorName, - ConvertedAttributes: &Config{ - LeftMotors: []string{leftMotorName}, - RightMotors: []string{rightMotorName}, - Base: baseName, - TimeIntervalMSecs: 500, - }, - } - fakeSensor, err := newWheeledOdometry(ctx, deps, fakecfg, logger) - test.That(t, err, test.ShouldBeNil) - od, ok := fakeSensor.(*odometry) - test.That(t, ok, test.ShouldBeTrue) - - newDeps := make(resource.Dependencies) - newDeps[base.Named(newBaseName)] = createFakeBase(0.2, 0.2, 0) - newDeps[motor.Named(newLeftMotorName)] = createFakeMotor(true) - newDeps[motor.Named(rightMotorName)] = createFakeMotor(false) - - newconf := resource.Config{ - Name: testSensorName, - ConvertedAttributes: &Config{ - LeftMotors: []string{newLeftMotorName}, - RightMotors: []string{rightMotorName}, - Base: newBaseName, - TimeIntervalMSecs: 500, - }, - } - - err = fakeSensor.Reconfigure(ctx, newDeps, newconf) - test.That(t, err, test.ShouldBeNil) - test.That(t, od.timeIntervalMSecs, test.ShouldEqual, 500) - props, err := od.base.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, props.WidthMeters, test.ShouldEqual, 0.2) - test.That(t, props.WheelCircumferenceMeters, test.ShouldEqual, 0.2) - - newDeps = make(resource.Dependencies) - newDeps[base.Named(newBaseName)] = createFakeBase(0.2, 0.2, 0) - newDeps[motor.Named(newLeftMotorName)] = createFakeMotor(true) - newDeps[motor.Named(newRightMotorName)] = createFakeMotor(false) - - newconf = resource.Config{ - Name: testSensorName, - ConvertedAttributes: &Config{ - LeftMotors: []string{newLeftMotorName}, - RightMotors: []string{newRightMotorName}, - Base: newBaseName, - TimeIntervalMSecs: 200, - }, - } - - err = fakeSensor.Reconfigure(ctx, newDeps, newconf) - test.That(t, err, test.ShouldBeNil) - test.That(t, od.timeIntervalMSecs, test.ShouldEqual, 200) - props, err = od.base.Properties(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, props.WidthMeters, test.ShouldEqual, 0.2) - test.That(t, props.WheelCircumferenceMeters, test.ShouldEqual, 0.2) -} - -func TestValidateConfig(t *testing.T) { - cfg := Config{ - LeftMotors: []string{leftMotorName}, - RightMotors: []string{rightMotorName}, - Base: "", - TimeIntervalMSecs: 500, - } - - deps, err := cfg.Validate("path") - expectedErr := resource.NewConfigValidationFieldRequiredError("path", "base") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) - - cfg = Config{ - LeftMotors: []string{}, - RightMotors: []string{rightMotorName}, - Base: baseName, - TimeIntervalMSecs: 500, - } - - deps, err = cfg.Validate("path") - expectedErr = resource.NewConfigValidationFieldRequiredError("path", "left motors") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) - - cfg = Config{ - LeftMotors: []string{leftMotorName}, - RightMotors: []string{}, - Base: baseName, - TimeIntervalMSecs: 500, - } - - deps, err = cfg.Validate("path") - expectedErr = resource.NewConfigValidationFieldRequiredError("path", "right motors") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) - - cfg = Config{ - LeftMotors: []string{leftMotorName, leftMotorName}, - RightMotors: []string{rightMotorName}, - Base: baseName, - TimeIntervalMSecs: 500, - } - - deps, err = cfg.Validate("path") - expectedErr = errors.New("mismatch number of left and right motors") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) - - cfg = Config{ - LeftMotors: []string{leftMotorName, leftMotorName}, - RightMotors: []string{rightMotorName, rightMotorName}, - Base: baseName, - TimeIntervalMSecs: 500, - } - - deps, err = cfg.Validate("path") - expectedErr = errors.New("wheeled odometry only supports one left and right motor each") - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, deps, test.ShouldBeEmpty) -} - -func TestSpin(t *testing.T) { - left := createFakeMotor(true) - right := createFakeMotor(false) - base := createFakeBase(0.2, 0.2, 0.1) - ctx := context.Background() - _ = left.ResetZeroPosition(ctx, 0, nil) - _ = right.ResetZeroPosition(ctx, 0, nil) - - od := &odometry{ - lastLeftPos: 0, - lastRightPos: 0, - wheelCircumference: 0.2, - baseWidth: 0.2, - base: base, - timeIntervalMSecs: 500, - } - od.motors = append(od.motors, motorPair{left, right}) - od.trackPosition(context.Background()) - - // turn 90 degrees - setPositions(-1*(math.Pi/4), 1*(math.Pi/4)) - // sleep for slightly longer than time interval to ensure trackPosition has enough time to run - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, _ := od.Position(ctx, nil) - or, err := od.Orientation(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 90, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 0, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 0, 0.1) - - // turn negative 180 degrees - setPositions(1*(math.Pi/2), -1*(math.Pi/2)) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, _ = od.Position(ctx, nil) - or, err = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 270, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 0, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 0, 0.1) - - // turn another 360 degrees - setPositions(-1*math.Pi, 1*math.Pi) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, _ = od.Position(ctx, nil) - or, err = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 270, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 0, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 0, 0.1) -} - -func TestMoveStraight(t *testing.T) { - left := createFakeMotor(true) - right := createFakeMotor(false) - base := createFakeBase(1, 1, 0.1) - ctx := context.Background() - _ = left.ResetZeroPosition(ctx, 0, nil) - _ = right.ResetZeroPosition(ctx, 0, nil) - - od := &odometry{ - lastLeftPos: 0, - lastRightPos: 0, - wheelCircumference: 1, - baseWidth: 1, - base: base, - timeIntervalMSecs: 500, - } - od.motors = append(od.motors, motorPair{left, right}) - od.trackPosition(context.Background()) - - // move straight 5 m - setPositions(5, 5) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err := od.Position(ctx, relativePos) - or, _ := od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 0, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 5, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 0, 0.1) - - // move backwards 10 m - setPositions(-10, -10) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err = od.Position(ctx, relativePos) - or, _ = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 0, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, -5, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 0, 0.1) -} - -func TestComplicatedPath(t *testing.T) { - left := createFakeMotor(true) - right := createFakeMotor(false) - base := createFakeBase(1, 1, 0.1) - ctx := context.Background() - _ = left.ResetZeroPosition(ctx, 0, nil) - _ = right.ResetZeroPosition(ctx, 0, nil) - - od := &odometry{ - lastLeftPos: 0, - lastRightPos: 0, - wheelCircumference: 1, - baseWidth: 1, - base: base, - timeIntervalMSecs: 500, - } - od.motors = append(od.motors, motorPair{left, right}) - od.trackPosition(context.Background()) - - // move straight 5 m - setPositions(5, 5) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err := od.Position(ctx, relativePos) - test.That(t, err, test.ShouldBeNil) - or, _ := od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 0, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 5, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 0, 0.1) - - // spin negative 90 degrees - setPositions(1*(math.Pi/4), -1*(math.Pi/4)) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err = od.Position(ctx, relativePos) - test.That(t, err, test.ShouldBeNil) - or, err = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 270, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 5, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 0, 0.1) - - // move forward another 5 m - setPositions(5, 5) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err = od.Position(ctx, relativePos) - test.That(t, err, test.ShouldBeNil) - or, err = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 270, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 5, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 5, 0.1) - - // spin positive 45 degrees - setPositions(-1*(math.Pi/8), 1*(math.Pi/8)) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err = od.Position(ctx, relativePos) - test.That(t, err, test.ShouldBeNil) - or, err = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 315, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 5, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 5, 0.1) - - // move forward 2 m - setPositions(2, 2) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err = od.Position(ctx, relativePos) - test.That(t, err, test.ShouldBeNil) - or, err = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 315, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 6.4, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 6.4, 0.1) - - // travel in an arc - setPositions(1*(math.Pi/4), 2*(math.Pi/4)) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - pos, _, err = od.Position(ctx, relativePos) - test.That(t, err, test.ShouldBeNil) - or, err = od.Orientation(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, or.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 0, 0.1) - test.That(t, pos.Lat(), test.ShouldAlmostEqual, 7.6, 0.1) - test.That(t, pos.Lng(), test.ShouldAlmostEqual, 6.4, 0.1) -} - -func TestVelocities(t *testing.T) { - left := createFakeMotor(true) - right := createFakeMotor(false) - base := createFakeBase(1, 1, 0.1) - ctx := context.Background() - _ = left.ResetZeroPosition(ctx, 0, nil) - _ = right.ResetZeroPosition(ctx, 0, nil) - - od := &odometry{ - lastLeftPos: 0, - lastRightPos: 0, wheelCircumference: 1, - baseWidth: 1, - base: base, - timeIntervalMSecs: 500, - } - od.motors = append(od.motors, motorPair{left, right}) - od.trackPosition(context.Background()) - - // move forward 10 m - setPositions(10, 10) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - linVel, err := od.LinearVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - angVel, err := od.AngularVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, linVel.Y, test.ShouldAlmostEqual, 20, 0.1) - test.That(t, angVel.Z, test.ShouldAlmostEqual, 0, 0.1) - - // spin 45 degrees - setPositions(-1*(math.Pi/8), 1*(math.Pi/8)) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - linVel, err = od.LinearVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - angVel, err = od.AngularVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, linVel.Y, test.ShouldAlmostEqual, 0, 0.1) - test.That(t, angVel.Z, test.ShouldAlmostEqual, 90, 0.1) - - // spin back 45 degrees - setPositions(1*(math.Pi/8), -1*(math.Pi/8)) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - linVel, err = od.LinearVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - angVel, err = od.AngularVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, linVel.Y, test.ShouldAlmostEqual, 0, 0.1) - test.That(t, angVel.Z, test.ShouldAlmostEqual, -90, 0.1) - - // move backwards 5 m - setPositions(-5, -5) - time.Sleep(time.Duration(od.timeIntervalMSecs*1.15) * time.Millisecond) - - linVel, err = od.LinearVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - angVel, err = od.AngularVelocity(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, linVel.Y, test.ShouldAlmostEqual, -10, 0.1) - test.That(t, angVel.Z, test.ShouldAlmostEqual, 0, 0.1) -} diff --git a/components/posetracker/client.go b/components/posetracker/client.go deleted file mode 100644 index 761815a582c..00000000000 --- a/components/posetracker/client.go +++ /dev/null @@ -1,72 +0,0 @@ -package posetracker - -import ( - "context" - - pb "go.viam.com/api/component/posetracker/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" -) - -// client implements PoseTrackerServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.PoseTrackerServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (PoseTracker, error) { - c := pb.NewPoseTrackerServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - }, nil -} - -func (c *client) Poses( - ctx context.Context, bodyNames []string, extra map[string]interface{}, -) (BodyToPoseInFrame, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - req := &pb.GetPosesRequest{ - Name: c.name, - BodyNames: bodyNames, - Extra: ext, - } - resp, err := c.client.GetPoses(ctx, req) - if err != nil { - return nil, err - } - result := BodyToPoseInFrame{} - for key, pf := range resp.GetBodyPoses() { - result[key] = referenceframe.ProtobufToPoseInFrame(pf) - } - return result, nil -} - -func (c *client) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return Readings(ctx, c) -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/components/posetracker/client_test.go b/components/posetracker/client_test.go deleted file mode 100644 index 803fd8744b9..00000000000 --- a/components/posetracker/client_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package posetracker_test - -import ( - "context" - "math" - "net" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/posetracker" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - zeroPoseBody = "zeroBody" - nonZeroPoseBody = "body2" - nonZeroPoseBody2 = "body3" - otherBodyFrame = "bodyFrame2" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - workingPT := &inject.PoseTracker{} - failingPT := &inject.PoseTracker{} - - pose := spatialmath.NewPose(r3.Vector{X: 2, Y: 4, Z: 6}, &spatialmath.R4AA{Theta: math.Pi, RX: 0, RY: 0, RZ: 1}) - pose2 := spatialmath.NewPose(r3.Vector{X: 1, Y: 2, Z: 3}, &spatialmath.R4AA{Theta: math.Pi, RX: 0, RY: 0, RZ: 1}) - zeroPose := spatialmath.NewZeroPose() - allBodiesToPoseInFrames := posetracker.BodyToPoseInFrame{ - zeroPoseBody: referenceframe.NewPoseInFrame(bodyFrame, zeroPose), - nonZeroPoseBody: referenceframe.NewPoseInFrame(bodyFrame, pose), - nonZeroPoseBody2: referenceframe.NewPoseInFrame(otherBodyFrame, pose2), - } - var extraOptions map[string]interface{} - poseTester := func( - t *testing.T, receivedPoseInFrames posetracker.BodyToPoseInFrame, - bodyName string, - ) { - t.Helper() - poseInFrame, ok := receivedPoseInFrames[bodyName] - test.That(t, ok, test.ShouldBeTrue) - expectedPoseInFrame := allBodiesToPoseInFrames[bodyName] - test.That(t, poseInFrame.Parent(), test.ShouldEqual, expectedPoseInFrame.Parent()) - poseEqualToExpected := spatialmath.PoseAlmostEqual(poseInFrame.Pose(), expectedPoseInFrame.Pose()) - test.That(t, poseEqualToExpected, test.ShouldBeTrue) - } - - workingPT.PosesFunc = func(ctx context.Context, bodyNames []string, extra map[string]interface{}) ( - posetracker.BodyToPoseInFrame, error, - ) { - extraOptions = extra - return allBodiesToPoseInFrames, nil - } - - failingPT.PosesFunc = func(ctx context.Context, bodyNames []string, extra map[string]interface{}) ( - posetracker.BodyToPoseInFrame, error, - ) { - return nil, errPoseFailed - } - - resourceMap := map[resource.Name]posetracker.PoseTracker{ - posetracker.Named(workingPTName): workingPT, - posetracker.Named(failingPTName): failingPT, - } - ptSvc, err := resource.NewAPIResourceCollection(posetracker.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[posetracker.PoseTracker](posetracker.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, ptSvc), test.ShouldBeNil) - - workingPT.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingPTClient, err := posetracker.NewClientFromConn(context.Background(), conn, "", posetracker.Named(workingPTName), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests for working pose tracker", func(t *testing.T) { - bodyToPoseInFrame, err := workingPTClient.Poses( - context.Background(), []string{zeroPoseBody, nonZeroPoseBody}, map[string]interface{}{"foo": "Poses"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "Poses"}) - - // DoCommand - resp, err := workingPTClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - poseTester(t, bodyToPoseInFrame, zeroPoseBody) - poseTester(t, bodyToPoseInFrame, nonZeroPoseBody) - poseTester(t, bodyToPoseInFrame, nonZeroPoseBody2) - }) - - t.Run("dialed client tests for working pose tracker", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := resourceAPI.RPCClient(context.Background(), conn, "", posetracker.Named(workingPTName), logger) - test.That(t, err, test.ShouldBeNil) - bodyToPoseInFrame, err := client.Poses(context.Background(), []string{}, map[string]interface{}{"foo": "PosesDialed"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "PosesDialed"}) - - poseTester(t, bodyToPoseInFrame, nonZeroPoseBody2) - poseTester(t, bodyToPoseInFrame, nonZeroPoseBody) - poseTester(t, bodyToPoseInFrame, zeroPoseBody) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client tests for failing pose tracker", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingPTDialedClient, err := posetracker.NewClientFromConn(context.Background(), conn, "", posetracker.Named(failingPTName), logger) - test.That(t, err, test.ShouldBeNil) - - bodyToPoseInFrame, err := failingPTDialedClient.Poses(context.Background(), []string{}, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPoseFailed.Error()) - test.That(t, bodyToPoseInFrame, test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - test.That(t, conn.Close(), test.ShouldBeNil) -} diff --git a/components/posetracker/pose_tracker.go b/components/posetracker/pose_tracker.go deleted file mode 100644 index 7d3e9586d00..00000000000 --- a/components/posetracker/pose_tracker.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package posetracker contains the interface and gRPC infrastructure -// for a pose tracker component -package posetracker - -import ( - "context" - - pb "go.viam.com/api/component/posetracker/v1" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[PoseTracker]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterPoseTrackerServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.PoseTrackerService_ServiceDesc, - RPCClient: NewClientFromConn, - }) -} - -// SubtypeName is a constant that identifies the component resource API string "posetracker". -const SubtypeName = "pose_tracker" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named PoseTracker's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// BodyToPoseInFrame represents a map of body names to PoseInFrames. -type BodyToPoseInFrame map[string]*referenceframe.PoseInFrame - -// A PoseTracker represents a robot component that can observe bodies in an -// environment and provide their respective poses in space. These poses are -// given in the context of the PoseTracker's frame of reference. -type PoseTracker interface { - sensor.Sensor - Poses(ctx context.Context, bodyNames []string, extra map[string]interface{}) (BodyToPoseInFrame, error) -} - -// FromRobot is a helper for getting the named force matrix sensor from the given Robot. -func FromRobot(r robot.Robot, name string) (PoseTracker, error) { - return robot.ResourceFromRobot[PoseTracker](r, Named(name)) -} - -// Readings is a helper for getting all readings from a PoseTracker. -func Readings(ctx context.Context, poseTracker PoseTracker) (map[string]interface{}, error) { - poseLookup, err := poseTracker.Poses(ctx, []string{}, map[string]interface{}{}) - if err != nil { - return nil, err - } - result := map[string]interface{}{} - for bodyName, poseInFrame := range poseLookup { - result[bodyName] = poseInFrame - } - return result, nil -} diff --git a/components/posetracker/server.go b/components/posetracker/server.go deleted file mode 100644 index ab0c250e214..00000000000 --- a/components/posetracker/server.go +++ /dev/null @@ -1,56 +0,0 @@ -package posetracker - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/posetracker/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" -) - -type serviceServer struct { - pb.UnimplementedPoseTrackerServiceServer - coll resource.APIResourceCollection[PoseTracker] -} - -// NewRPCServiceServer constructs a pose tracker gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[PoseTracker]) interface{} { - return &serviceServer{coll: coll} -} - -func (server *serviceServer) GetPoses( - ctx context.Context, - req *pb.GetPosesRequest, -) (*pb.GetPosesResponse, error) { - poseTracker, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - framedPoses, err := poseTracker.Poses(ctx, req.GetBodyNames(), req.Extra.AsMap()) - if err != nil { - return nil, err - } - poseInFrameProtoStructs := map[string]*commonpb.PoseInFrame{} - for key, framedPose := range framedPoses { - framedPoseProto := referenceframe.PoseInFrameToProtobuf(framedPose) - poseInFrameProtoStructs[key] = framedPoseProto - } - return &pb.GetPosesResponse{ - BodyPoses: poseInFrameProtoStructs, - }, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - poseTracker, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, poseTracker, req) -} diff --git a/components/posetracker/server_test.go b/components/posetracker/server_test.go deleted file mode 100644 index 4e89b456c04..00000000000 --- a/components/posetracker/server_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package posetracker_test - -import ( - "context" - "errors" - "testing" - - pb "go.viam.com/api/component/posetracker/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/posetracker" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -var errPoseFailed = errors.New("failure to get poses") - -const ( - workingPTName = "workingPT" - failingPTName = "failingPT" - bodyName = "body1" - bodyFrame = "bodyFrame" -) - -func newServer() (pb.PoseTrackerServiceServer, *inject.PoseTracker, *inject.PoseTracker, error) { - injectPT1 := &inject.PoseTracker{} - injectPT2 := &inject.PoseTracker{} - - resourceMap := map[resource.Name]posetracker.PoseTracker{ - posetracker.Named(workingPTName): injectPT1, - posetracker.Named(failingPTName): injectPT2, - } - - injectSvc, err := resource.NewAPIResourceCollection(posetracker.API, resourceMap) - if err != nil { - return nil, nil, nil, err - } - return posetracker.NewRPCServiceServer(injectSvc).(pb.PoseTrackerServiceServer), injectPT1, injectPT2, nil -} - -func TestGetPoses(t *testing.T) { - ptServer, workingPT, failingPT, err := newServer() - test.That(t, err, test.ShouldBeNil) - - var extraOptions map[string]interface{} - workingPT.PosesFunc = func(ctx context.Context, bodyNames []string, extra map[string]interface{}) ( - posetracker.BodyToPoseInFrame, error, - ) { - extraOptions = extra - zeroPose := spatialmath.NewZeroPose() - return posetracker.BodyToPoseInFrame{ - bodyName: referenceframe.NewPoseInFrame(bodyFrame, zeroPose), - }, nil - } - - failingPT.PosesFunc = func(ctx context.Context, bodyNames []string, extra map[string]interface{}) ( - posetracker.BodyToPoseInFrame, error, - ) { - return nil, errPoseFailed - } - - t.Run("get poses fails on failing pose tracker", func(t *testing.T) { - req := pb.GetPosesRequest{ - Name: failingPTName, BodyNames: []string{bodyName}, - } - resp, err := ptServer.GetPoses(context.Background(), &req) - test.That(t, err, test.ShouldBeError, errPoseFailed) - test.That(t, resp, test.ShouldBeNil) - }) - - ext, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "GetPosesRequest"}) - test.That(t, err, test.ShouldBeNil) - req := pb.GetPosesRequest{ - Name: workingPTName, BodyNames: []string{bodyName}, Extra: ext, - } - req2 := pb.GetPosesRequest{ - Name: workingPTName, - } - resp1, err := ptServer.GetPoses(context.Background(), &req) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{"foo": "GetPosesRequest"}) - resp2, err := ptServer.GetPoses(context.Background(), &req2) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{}) - - workingTestCases := []struct { - testStr string - resp *pb.GetPosesResponse - }{ - { - testStr: "get poses succeeds with working pose tracker and body names passed", - resp: resp1, - }, - { - testStr: "get poses succeeds with working pose tracker but no body names passed", - resp: resp2, - }, - } - for _, tc := range workingTestCases { - t.Run(tc.testStr, func(t *testing.T) { - framedPoses := tc.resp.GetBodyPoses() - poseInFrame, ok := framedPoses[bodyName] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, poseInFrame.GetReferenceFrame(), test.ShouldEqual, bodyFrame) - pose := poseInFrame.GetPose() - test.That(t, pose.GetX(), test.ShouldEqual, 0) - test.That(t, pose.GetY(), test.ShouldEqual, 0) - test.That(t, pose.GetZ(), test.ShouldEqual, 0) - test.That(t, pose.GetTheta(), test.ShouldEqual, 0) - test.That(t, pose.GetOX(), test.ShouldEqual, 0) - test.That(t, pose.GetOY(), test.ShouldEqual, 0) - test.That(t, pose.GetOZ(), test.ShouldEqual, 1) - }) - } -} diff --git a/components/powersensor/client_test.go b/components/powersensor/client_test.go deleted file mode 100644 index e3f1358fafa..00000000000 --- a/components/powersensor/client_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// package powersensor_test contains tests for powersensor -package powersensor_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/powersensor" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - testVolts := 4.8 - testAmps := 3.8 - testWatts := 2.8 - testIsAC := false - - workingPowerSensor := &inject.PowerSensor{} - failingPowerSensor := &inject.PowerSensor{} - - workingPowerSensor.VoltageFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return testVolts, testIsAC, nil - } - - workingPowerSensor.CurrentFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return testAmps, testIsAC, nil - } - - workingPowerSensor.PowerFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return testWatts, nil - } - - failingPowerSensor.VoltageFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return 0, false, errVoltageFailed - } - - failingPowerSensor.CurrentFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return 0, false, errCurrentFailed - } - - failingPowerSensor.PowerFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, errPowerFailed - } - - resourceMap := map[resource.Name]powersensor.PowerSensor{ - motor.Named(workingPowerSensorName): workingPowerSensor, - motor.Named(failingPowerSensorName): failingPowerSensor, - } - powersensorSvc, err := resource.NewAPIResourceCollection(powersensor.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[powersensor.PowerSensor](powersensor.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, powersensorSvc), test.ShouldBeNil) - - workingPowerSensor.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing client - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := powersensor.NewClientFromConn(context.Background(), conn, "", motor.Named(workingPowerSensorName), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests with working power sensor", func(t *testing.T) { - // DoCommand - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - volts, isAC, err := client.Voltage(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldBeNil) - test.That(t, volts, test.ShouldEqual, testVolts) - test.That(t, isAC, test.ShouldEqual, testIsAC) - - amps, isAC, err := client.Current(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldBeNil) - test.That(t, amps, test.ShouldEqual, testAmps) - test.That(t, isAC, test.ShouldEqual, testIsAC) - - watts, err := client.Power(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldBeNil) - test.That(t, watts, test.ShouldEqual, testWatts) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - conn, err = viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err = powersensor.NewClientFromConn(context.Background(), conn, "", powersensor.Named(failingPowerSensorName), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests with failing power sensor", func(t *testing.T) { - volts, isAC, err := client.Voltage(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errVoltageFailed.Error()) - test.That(t, volts, test.ShouldEqual, 0) - test.That(t, isAC, test.ShouldEqual, false) - - amps, isAC, err := client.Current(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errCurrentFailed.Error()) - test.That(t, amps, test.ShouldEqual, 0) - test.That(t, isAC, test.ShouldEqual, false) - - watts, err := client.Power(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPowerFailed.Error()) - test.That(t, watts, test.ShouldEqual, 0) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/powersensor/collectors_test.go b/components/powersensor/collectors_test.go deleted file mode 100644 index 79dbd754738..00000000000 --- a/components/powersensor/collectors_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package powersensor_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - pb "go.viam.com/api/component/powersensor/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/powersensor" - "go.viam.com/rdk/data" - du "go.viam.com/rdk/data/testutils" - "go.viam.com/rdk/logging" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - componentName = "powersensor" - captureInterval = time.Second - numRetries = 5 -) - -var readingMap = map[string]any{"reading1": false, "reading2": "test"} - -func TestPowerSensorCollectors(t *testing.T) { - tests := []struct { - name string - collector data.CollectorConstructor - expected map[string]any - }{ - { - name: "Power sensor voltage collector should write a voltage response", - collector: powersensor.NewVoltageCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetVoltageResponse{ - Volts: 1.0, - IsAc: false, - }), - }, - { - name: "Power sensor current collector should write a current response", - collector: powersensor.NewCurrentCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetCurrentResponse{ - Amperes: 1.0, - IsAc: false, - }), - }, - { - name: "Power sensor power collector should write a power response", - collector: powersensor.NewPowerCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(pb.GetPowerResponse{ - Watts: 1.0, - }), - }, - { - name: "Power sensor readings collector should write a readings response", - collector: powersensor.NewReadingsCollector, - expected: tu.ToProtoMapIgnoreOmitEmpty(du.GetExpectedReadingsStruct(readingMap).AsMap()), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: componentName, - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Clock: mockClock, - Target: &buf, - } - - pwrSens := newPowerSensor() - col, err := tc.collector(pwrSens, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, tc.expected) - }) - } -} - -func newPowerSensor() powersensor.PowerSensor { - p := &inject.PowerSensor{} - p.VoltageFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return 1.0, false, nil - } - p.CurrentFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return 1.0, false, nil - } - p.PowerFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 1.0, nil - } - p.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return readingMap, nil - } - return p -} diff --git a/components/powersensor/export_collectors_test.go b/components/powersensor/export_collectors_test.go deleted file mode 100644 index a1f28d632c5..00000000000 --- a/components/powersensor/export_collectors_test.go +++ /dev/null @@ -1,10 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package powersensor - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var ( - NewVoltageCollector = newVoltageCollector - NewCurrentCollector = newCurrentCollector - NewPowerCollector = newPowerCollector - NewReadingsCollector = newReadingsCollector -) diff --git a/components/powersensor/server_test.go b/components/powersensor/server_test.go deleted file mode 100644 index 6e8e0265078..00000000000 --- a/components/powersensor/server_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package powersensor_test - -import ( - "context" - "errors" - "testing" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/powersensor/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/components/powersensor" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - workingPowerSensorName = "workingPS" - failingPowerSensorName = "failingPS" - missingPowerSensorName = "missingPS" - errVoltageFailed = errors.New("can't get voltage") - errCurrentFailed = errors.New("can't get current") - errPowerFailed = errors.New("can't get power") - errPowerSensorNotFound = errors.New("not found") - errReadingsFailed = errors.New("can't get readings") -) - -func newServer() (pb.PowerSensorServiceServer, *inject.PowerSensor, *inject.PowerSensor, error) { - workingPowerSensor := &inject.PowerSensor{} - failingPowerSensor := &inject.PowerSensor{} - powerSensors := map[resource.Name]powersensor.PowerSensor{ - powersensor.Named(workingPowerSensorName): workingPowerSensor, - powersensor.Named(failingPowerSensorName): failingPowerSensor, - } - - powerSensorSvc, err := resource.NewAPIResourceCollection(sensor.API, powerSensors) - if err != nil { - return nil, nil, nil, err - } - - server := powersensor.NewRPCServiceServer(powerSensorSvc).(pb.PowerSensorServiceServer) - - return server, workingPowerSensor, failingPowerSensor, nil -} - -//nolint:dupl -func TestServerGetVoltage(t *testing.T) { - powerSensorServer, testPowerSensor, failingPowerSensor, err := newServer() - test.That(t, err, test.ShouldBeNil) - volts := 4.8 - isAC := false - - // successful - testPowerSensor.VoltageFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return volts, isAC, nil - } - req := &pb.GetVoltageRequest{Name: workingPowerSensorName} - resp, err := powerSensorServer.GetVoltage(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Volts, test.ShouldEqual, volts) - test.That(t, resp.IsAc, test.ShouldEqual, isAC) - - // fails on bad power sensor - failingPowerSensor.VoltageFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return 0, false, errVoltageFailed - } - req = &pb.GetVoltageRequest{Name: failingPowerSensorName} - resp, err = powerSensorServer.GetVoltage(context.Background(), req) - test.That(t, err, test.ShouldBeError, errVoltageFailed) - test.That(t, resp, test.ShouldBeNil) - - // missing power sensor - req = &pb.GetVoltageRequest{Name: missingPowerSensorName} - resp, err = powerSensorServer.GetVoltage(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPowerSensorNotFound.Error()) - test.That(t, resp, test.ShouldBeNil) -} - -//nolint:dupl -func TestServerGetCurrent(t *testing.T) { - powerSensorServer, testPowerSensor, failingPowerSensor, err := newServer() - test.That(t, err, test.ShouldBeNil) - amps := 4.8 - isAC := false - - // successful - testPowerSensor.CurrentFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return amps, isAC, nil - } - req := &pb.GetCurrentRequest{Name: workingPowerSensorName} - resp, err := powerSensorServer.GetCurrent(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Amperes, test.ShouldEqual, amps) - test.That(t, resp.IsAc, test.ShouldEqual, isAC) - - // fails on bad power sensor - failingPowerSensor.CurrentFunc = func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) { - return 0, false, errCurrentFailed - } - req = &pb.GetCurrentRequest{Name: failingPowerSensorName} - resp, err = powerSensorServer.GetCurrent(context.Background(), req) - test.That(t, err, test.ShouldBeError, errCurrentFailed) - test.That(t, resp, test.ShouldBeNil) - - // missing power sensor - req = &pb.GetCurrentRequest{Name: missingPowerSensorName} - resp, err = powerSensorServer.GetCurrent(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPowerSensorNotFound.Error()) - test.That(t, resp, test.ShouldBeNil) -} - -func TestServerGetPower(t *testing.T) { - powerSensorServer, testPowerSensor, failingPowerSensor, err := newServer() - test.That(t, err, test.ShouldBeNil) - watts := 4.8 - - // successful - testPowerSensor.PowerFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return watts, nil - } - req := &pb.GetPowerRequest{Name: workingPowerSensorName} - resp, err := powerSensorServer.GetPower(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Watts, test.ShouldEqual, watts) - - // fails on bad power sensor - failingPowerSensor.PowerFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, errPowerFailed - } - req = &pb.GetPowerRequest{Name: failingPowerSensorName} - resp, err = powerSensorServer.GetPower(context.Background(), req) - test.That(t, err, test.ShouldBeError, errPowerFailed) - test.That(t, resp, test.ShouldBeNil) - - // missing power sensor - req = &pb.GetPowerRequest{Name: missingPowerSensorName} - resp, err = powerSensorServer.GetPower(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPowerSensorNotFound.Error()) - test.That(t, resp, test.ShouldBeNil) -} - -func TestServerGetReadings(t *testing.T) { - powerSensorServer, testPowerSensor, failingPowerSensor, err := newServer() - test.That(t, err, test.ShouldBeNil) - - rs := map[string]interface{}{"a": 1.1, "b": 2.2} - - var extraCap map[string]interface{} - testPowerSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - extraCap = extra - return rs, nil - } - - failingPowerSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return nil, errReadingsFailed - } - - expected := map[string]*structpb.Value{} - for k, v := range rs { - vv, err := structpb.NewValue(v) - test.That(t, err, test.ShouldBeNil) - expected[k] = vv - } - extra, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - - resp, err := powerSensorServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: workingPowerSensorName, Extra: extra}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Readings, test.ShouldResemble, expected) - test.That(t, extraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = powerSensorServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: failingPowerSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errReadingsFailed.Error()) - - _, err = powerSensorServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: missingPowerSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") -} diff --git a/components/register/all.go b/components/register/all.go index 34b15da1470..5c471b8577f 100644 --- a/components/register/all.go +++ b/components/register/all.go @@ -3,17 +3,7 @@ package register import ( // register components. - _ "go.viam.com/rdk/components/board/register" - _ "go.viam.com/rdk/components/camera/register" - _ "go.viam.com/rdk/components/encoder/register" - _ "go.viam.com/rdk/components/gantry/register" _ "go.viam.com/rdk/components/generic/register" - _ "go.viam.com/rdk/components/input/register" - _ "go.viam.com/rdk/components/motor/register" - _ "go.viam.com/rdk/components/movementsensor/register" // register APIs without implementations directly. - _ "go.viam.com/rdk/components/posetracker" _ "go.viam.com/rdk/components/powersensor/register" - _ "go.viam.com/rdk/components/sensor/register" - _ "go.viam.com/rdk/components/servo/register" ) diff --git a/components/register/all_cgo.go b/components/register/all_cgo.go index 3bb07594e91..b9db2542adf 100644 --- a/components/register/all_cgo.go +++ b/components/register/all_cgo.go @@ -5,7 +5,5 @@ package register import ( // blank import registration pattern. _ "go.viam.com/rdk/components/arm/register" - _ "go.viam.com/rdk/components/audioinput/register" - _ "go.viam.com/rdk/components/base/register" _ "go.viam.com/rdk/components/gripper/register" ) diff --git a/components/sensor/bme280/bme280.go b/components/sensor/bme280/bme280.go deleted file mode 100644 index 928d9825d3b..00000000000 --- a/components/sensor/bme280/bme280.go +++ /dev/null @@ -1,586 +0,0 @@ -//go:build linux - -// Package bme280 implements a bme280 sensor for temperature, humidity, and pressure. -// Code based on https://github.com/sparkfun/SparkFun_bme280_Arduino_Library (MIT license) -// and also https://github.com/rm-hull/bme280 (MIT License) -package bme280 - -import ( - "context" - "encoding/binary" - "errors" - "fmt" - "math" - "time" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("bme280") - -const ( - defaultI2Caddr = 0x77 - - // When mode is set to 0, sensors are off. - // When mode is set to 1 or 2, sensors are read once and then turn off again. - // When mode is set to 3, sensors are on and continuously read. - activeMode = 0b11 - - // Addresses of bme280 registers. - bme280T1LSBReg = 0x88 - bme280T1MSBReg = 0x89 - bme280T2LSBReg = 0x8A - bme280T2MSBReg = 0x8B - bme280T3LSBReg = 0x8C - bme280T3MSBReg = 0x8D - bme280P1LSBReg = 0x8E - bme280P1MSBReg = 0x8F - bme280P2LSBReg = 0x90 - bme280P2MSBReg = 0x91 - bme280P3LSBReg = 0x92 - bme280P3MSBReg = 0x93 - bme280P4LSBReg = 0x94 - bme280P4MSBReg = 0x95 - bme280P5LSBReg = 0x96 - bme280P5MSBReg = 0x97 - bme280P6LSBReg = 0x98 - bme280P6MSBReg = 0x99 - bme280P7LSBReg = 0x9A - bme280P7MSBReg = 0x9B - bme280P8LSBReg = 0x9C - bme280P8MSBReg = 0x9D - bme280P9LSBReg = 0x9E - bme280P9MSBReg = 0x9F - bme280H1Reg = 0xA1 - bme280CHIPIDReg = 0xD0 // Chip ID - bme280RSTReg = 0xE0 // Softreset Reg - bme280H2LSBReg = 0xE1 - bme280H2MSBReg = 0xE2 - bme280H3Reg = 0xE3 - bme280H4MSBReg = 0xE4 - bme280H4LSBReg = 0xE5 - bme280H5MSBReg = 0xE6 - bme280H6Reg = 0xE7 - bme280CTRLHumidityReg = 0xF2 // Ctrl Humidity Reg - bme280STATReg = 0xF3 // Status Reg - bme280CTRLMEASReg = 0xF4 // Ctrl Measure Reg - bme280ConfigReg = 0xF5 // Configuration Reg - bme280MeasurementsReg = 0xF7 // Measurements register start - bme280PressureMSBReg = 0xF7 // Pressure MSB - bme280PressureLSBReg = 0xF8 // Pressure LSB - bme280PressureXLSBReg = 0xF9 // Pressure XLSB - bme280TemperatureMSBReg = 0xFA // Temperature MSB - bme280TemperatureLSBReg = 0xFB // Temperature LSB - bme280TemperatureXLSBReg = 0xFC // Temperature XLSB - bme280HumidityMSBReg = 0xFD // Humidity MSB - bme280HumidityLSBReg = 0xFE // Humidity LSB -) - -// Config is used for converting config attributes. -type Config struct { - // The I2C bus is almost certainly numeric (e.g., the "7" in /dev/i2c-7), but it is nonetheless - // possible for the OS to give its I2C buses a non-numeric identifier, so we store it as a - // string. - I2CBus string `json:"i2c_bus"` - I2cAddr int `json:"i2c_addr,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - if len(conf.I2CBus) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "i2c bus") - } - return deps, nil -} - -func init() { - resource.RegisterComponent( - sensor.API, - model, - resource.Registration[sensor.Sensor, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (sensor.Sensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - return newSensor(ctx, deps, conf.ResourceName(), newConf, logger) - }, - }) -} - -func newSensor( - ctx context.Context, - _ resource.Dependencies, - name resource.Name, - conf *Config, - logger logging.Logger, -) (sensor.Sensor, error) { - i2cbus, err := buses.NewI2cBus(conf.I2CBus) - if err != nil { - return nil, fmt.Errorf("bme280 init: failed to open i2c bus %s: %w", - conf.I2CBus, err) - } - - addr := conf.I2cAddr - if addr == 0 { - addr = defaultI2Caddr - logger.CWarn(ctx, "using i2c address : 0x77") - } - - s := &bme280{ - Named: name.AsNamed(), - logger: logger, - bus: i2cbus, - addr: byte(addr), - lastTemp: -999, // initialize to impossible temp - } - - err = s.reset(ctx) - if err != nil { - return nil, err - } - // After sending the reset signal above, it takes the chip a short time to be ready to receive commands again - time.Sleep(100 * time.Millisecond) - s.calibration = map[string]int{} - err = s.setupCalibration(ctx) - if err != nil { - return nil, err - } - err = s.setMode(ctx, activeMode) - if err != nil { - return nil, err - } - - err = s.setStandbyTime(ctx, 0) - if err != nil { - return nil, err - } - err = s.setFilter(ctx, 0) - if err != nil { - return nil, err - } - - // Oversample means "read the sensor this many times and average the results". 1 is generally fine. - // Chip inits to 0 for all, which means "do not read this sensor"/ - // humidity - err = s.setOverSample(ctx, bme280CTRLHumidityReg, 0, 1) // Default of 1x oversample - if err != nil { - return nil, err - } - // pressure - err = s.setOverSample(ctx, bme280CTRLMEASReg, 2, 1) // Default of 1x oversample - if err != nil { - return nil, err - } - // temperature - err = s.setOverSample(ctx, bme280CTRLMEASReg, 5, 1) // Default of 1x oversample - if err != nil { - return nil, err - } - - return s, nil -} - -// bme280 is a i2c sensor device. -type bme280 struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - logger logging.Logger - - bus buses.I2C - addr byte - calibration map[string]int - lastTemp float64 // Store raw data from temp for humidity calculations -} - -// Readings returns a list containing single item (current temperature). -func (s *bme280) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - s.logger.CErrorf(ctx, "can't open bme280 i2c %s", err) - return nil, err - } - err = handle.Write(ctx, []byte{byte(bme280MeasurementsReg)}) - if err != nil { - s.logger.CDebug(ctx, "Failed to request temperature") - } - buffer, err := handle.Read(ctx, 8) - if err != nil { - return nil, err - } - if len(buffer) != 8 { - return nil, errors.New("i2c read did not get 8 bytes") - } - - pressure := s.readPressure(buffer) - temp := s.readTemperatureCelsius(buffer) - humid := s.readHumidity(buffer) - dewPt := s.calculateDewPoint(temp, humid) - return map[string]interface{}{ - "temperature_celsius": temp, - "dew_point_celsius": dewPt, - "temperature_fahrenheit": temp*1.8 + 32, - "dew_point_fahrenheit": dewPt*1.8 + 32, - "relative_humidity_pct": humid, - "pressure_mpa": pressure, - }, handle.Close() -} - -// readPressure returns current pressure in mPa. -func (s *bme280) readPressure(buffer []byte) float64 { - adc := float64((int(buffer[0])<<16 | int(buffer[1])<<8 | int(buffer[2])) >> 4) - - // Need temp to calculate humidity - if s.lastTemp == -999 { - s.readTemperatureCelsius(buffer) - } - - v1 := s.lastTemp/2. - 64000. - v2 := v1 * v1 * float64(s.calibration["digP6"]) / 32768.0 - v2 += v1 * float64(s.calibration["digP5"]) * 2. - v2 = v2/4. + float64(s.calibration["digP4"])*65536. - v1 = (float64(s.calibration["digP3"])*v1*v1/524288.0 + float64(s.calibration["digP2"])*v1) / 524288.0 - v1 = (1.0 + v1/32768.0) * float64(s.calibration["digP1"]) - - if v1 == 0 { - return 0 - } - - res := 1048576.0 - adc - res = ((res - v2/4096.0) * 6250.0) / v1 - v1 = float64(s.calibration["digP9"]) * res * res / 2147483648.0 - v2 = res * float64(s.calibration["digP8"]) / 32768.0 - res += (v1 + v2 + float64(s.calibration["digP7"])) / 16.0 - return res / 100. -} - -// readTemperatureCelsius returns current temperature in celsius. -func (s *bme280) readTemperatureCelsius(buffer []byte) float64 { - adc := float64((int(buffer[3])<<16 | int(buffer[4])<<8 | int(buffer[5])) >> 4) - var1 := (adc/16382 - float64(s.calibration["digT1"])/1024) * float64(s.calibration["digT2"]) - var2 := math.Pow((adc/131072-float64(s.calibration["digT1"])/8192), 2) * float64(s.calibration["digT3"]) - - tFine := var1 + var2 - s.lastTemp = tFine - output := tFine / 5120. - return output -} - -// readHumidity returns current humidity as %RH. -func (s *bme280) readHumidity(buffer []byte) float64 { - adc := float64(int(buffer[6])<<8 | int(buffer[7])) - - // Need temp to calculate humidity - if s.lastTemp == -999 { - s.readTemperatureCelsius(buffer) - } - - var1 := s.lastTemp - 76800. - var1 = (adc - (float64(s.calibration["digH4"])*64.0 + float64(s.calibration["digH5"])/16384.0*var1)) * - (float64(s.calibration["digH2"]) / 65536.0 * (1.0 + float64(s.calibration["digH6"])/67108864.0*var1*(1.0+ - float64(s.calibration["digH3"])/67108864.0*var1))) - var1 *= (1.0 - (float64(s.calibration["digH1"]) * var1 / 524288.0)) - return math.Max(0., math.Min(var1, 100.)) -} - -// calculateDewPoint returns current dew point in degrees C. -func (s *bme280) calculateDewPoint(temp, humid float64) float64 { - ratio := 373.15 / (273.15 + temp) - rhs := -7.90298 * (ratio - 1) - rhs += 5.02808 * math.Log10(ratio) - rhs += -1.3816e-7 * (math.Pow(10, (11.344*(1-1/ratio))) - 1) - rhs += 8.1328e-3 * (math.Pow(10, (-3.49149*(ratio-1))) - 1) - rhs += math.Log10(1013.246) - - // factor -3 is to adjust units - Vapor Pressure SVP * humidity - vp := math.Pow(10, rhs-3) * humid - // (2) DEWPOINT = F(Vapor Pressure) - t := math.Log(vp / 0.61078) // temp var - denominator := 17.558 - t - if denominator == 0 { - // should be impossible - return 999 - } - return (241.88 * t) / denominator -} - -func (s *bme280) reset(ctx context.Context) error { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return err - } - err = handle.WriteByteData(ctx, bme280RSTReg, 0xB6) - if err != nil { - return err - } - return handle.Close() -} - -// Mode 00 = Sleep -// 01 and 10 = Forced -// 11 = Normal mode. -func (s *bme280) setMode(ctx context.Context, mode int) error { - if mode > activeMode { - mode = 0 // Error check. Default to sleep mode - } - - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return err - } - controlDataByte, err := handle.ReadByteData(ctx, bme280CTRLMEASReg) - if err != nil { - return err - } - controlData := int(controlDataByte) - - controlData |= mode // Set - err = handle.WriteByteData(ctx, bme280CTRLMEASReg, byte(controlData)) - if err != nil { - return err - } - return handle.Close() -} - -func (s *bme280) currentMode(ctx context.Context) (int, error) { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return -1, err - } - controlDataByte, err := handle.ReadByteData(ctx, bme280CTRLMEASReg) - if err != nil { - return -1, err - } - - return (int(controlDataByte) & 0b00000011), handle.Close() -} - -func (s *bme280) IsMeasuring(ctx context.Context) (bool, error) { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return false, err - } - stat, err := handle.ReadByteData(ctx, bme280STATReg) - if err != nil { - return false, err - } - return stat&(1<<3) == 1, handle.Close() -} - -func (s *bme280) setStandbyTime(ctx context.Context, val byte) error { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return err - } - - if val > 0b111 { - val = 0 - } - controlData, err := handle.ReadByteData(ctx, bme280ConfigReg) - if err != nil { - return err - } - controlData &= ^((byte(1) << 7) | (byte(1) << 6) | (byte(1) << 5)) - controlData |= (val << 5) - - err = handle.WriteByteData(ctx, bme280ConfigReg, controlData) - if err != nil { - return err - } - return handle.Close() -} - -func (s *bme280) setFilter(ctx context.Context, val byte) error { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return err - } - - if val > 0b111 { - val = 0 - } - controlData, err := handle.ReadByteData(ctx, bme280ConfigReg) - if err != nil { - return err - } - controlData &= ^((byte(1) << 4) | (byte(1) << 3) | (byte(1) << 2)) - controlData |= (val << 2) - - err = handle.WriteByteData(ctx, bme280ConfigReg, controlData) - if err != nil { - return err - } - return handle.Close() -} - -func (s *bme280) setOverSample(ctx context.Context, addr, offset, val byte) error { - mode, err := s.currentMode(ctx) - if err != nil { - return err - } - - if err = s.setMode(ctx, 0b00); err != nil { - return err - } - - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return err - } - - controlData, err := handle.ReadByteData(ctx, addr) - if err != nil { - return err - } - controlData &= ^((byte(1) << (offset + 2)) | (byte(1) << (offset + 1)) | (byte(1) << offset)) - controlData |= (val << offset) - - if err = handle.WriteByteData(ctx, addr, controlData); err != nil { - return err - } - - if err := handle.Close(); err != nil { - return err - } - - if err = s.setMode(ctx, mode); err != nil { - return err - } - - return nil -} - -// setupCalibration sets up all calibration data for the chip. -func (s *bme280) setupCalibration(ctx context.Context) error { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - return err - } - - // A helper function to read 2 bytes from the handle and interpret it as a word - readWord := func(register byte) (uint16, error) { - rd, err := handle.ReadBlockData(ctx, register, 2) - if err != nil { - return 0, err - } - return binary.LittleEndian.Uint16(rd), nil - } - - // Note, some are signed, others are unsigned - if calib, err := readWord(bme280T1LSBReg); err == nil { - s.calibration["digT1"] = int(calib) - } else { - return err - } - if calib, err := readWord(bme280T2LSBReg); err == nil { - s.calibration["digT2"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280T3LSBReg); err == nil { - s.calibration["digT3"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P1LSBReg); err == nil { - s.calibration["digP1"] = int(calib) - } else { - return err - } - if calib, err := readWord(bme280P2LSBReg); err == nil { - s.calibration["digP2"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P3LSBReg); err == nil { - s.calibration["digP3"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P4LSBReg); err == nil { - s.calibration["digP4"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P5LSBReg); err == nil { - s.calibration["digP5"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P6LSBReg); err == nil { - s.calibration["digP6"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P7LSBReg); err == nil { - s.calibration["digP7"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P8LSBReg); err == nil { - s.calibration["digP8"] = int(int16(calib)) - } else { - return err - } - if calib, err := readWord(bme280P9LSBReg); err == nil { - s.calibration["digP9"] = int(int16(calib)) - } else { - return err - } - - calib, err := handle.ReadByteData(ctx, bme280H1Reg) - if err != nil { - return err - } - s.calibration["digH1"] = int(calib) - - if calib, err := readWord(bme280H2LSBReg); err == nil { - s.calibration["digH2"] = int(int16(calib)) - } else { - return err - } - - calib, err = handle.ReadByteData(ctx, bme280H3Reg) - if err != nil { - return err - } - s.calibration["digH3"] = int(calib) - - calib, err = handle.ReadByteData(ctx, bme280H6Reg) - if err != nil { - return err - } - s.calibration["digH6"] = int(calib) - - r1byte, err := handle.ReadByteData(ctx, bme280H4MSBReg) - if err != nil { - return err - } - r2byte, err := handle.ReadByteData(ctx, bme280H4LSBReg) - if err != nil { - return err - } - s.calibration["digH4"] = (int(r1byte) << 4) + int(r2byte&0x0f) - - r1byte, err = handle.ReadByteData(ctx, bme280H5MSBReg) - if err != nil { - return err - } - r2byte, err = handle.ReadByteData(ctx, bme280H4LSBReg) - if err != nil { - return err - } - s.calibration["digH5"] = (int(r1byte) << 4) + int((r2byte>>4)&0x0f) - - return handle.Close() -} diff --git a/components/sensor/bme280/bme280_nonlinux.go b/components/sensor/bme280/bme280_nonlinux.go deleted file mode 100644 index 9efaef42722..00000000000 --- a/components/sensor/bme280/bme280_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package bme280 is only available on Linux. -package bme280 diff --git a/components/sensor/client.go b/components/sensor/client.go deleted file mode 100644 index b46ae0a1d99..00000000000 --- a/components/sensor/client.go +++ /dev/null @@ -1,62 +0,0 @@ -// Package sensor contains a gRPC based sensor client. -package sensor - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/sensor/v1" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// client implements SensorServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.SensorServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Sensor, error) { - c := pb.NewSensorServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - }, nil -} - -func (c *client) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - ext, err := structpb.NewStruct(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetReadings(ctx, &commonpb.GetReadingsRequest{ - Name: c.name, - Extra: ext, - }) - if err != nil { - return nil, err - } - - return protoutils.ReadingProtoToGo(resp.Readings) -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return protoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/components/sensor/client_test.go b/components/sensor/client_test.go deleted file mode 100644 index 0db38b88190..00000000000 --- a/components/sensor/client_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package sensor_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/sensor" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/sensors" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testSensorName = "sensor1" - failSensorName = "sensor2" - missingSensorName = "sensor4" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - rs := map[string]interface{}{"a": 1.1, "b": 2.2} - - var extraCap map[string]interface{} - injectSensor := &inject.Sensor{} - injectSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - extraCap = extra - return rs, nil - } - - injectSensor2 := &inject.Sensor{} - injectSensor2.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return nil, errReadingsFailed - } - - sensorSvc, err := resource.NewAPIResourceCollection( - sensors.API, - map[resource.Name]sensor.Sensor{sensor.Named(testSensorName): injectSensor, sensor.Named(failSensorName): injectSensor2}, - ) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[sensor.Sensor](sensor.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, sensorSvc), test.ShouldBeNil) - - injectSensor.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("Sensor client 1", func(t *testing.T) { - // working - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - sensor1Client, err := sensor.NewClientFromConn(context.Background(), conn, "", sensor.Named(testSensorName), logger) - test.That(t, err, test.ShouldBeNil) - - // DoCommand - resp, err := sensor1Client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - rs1, err := sensor1Client.Readings(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs1, test.ShouldResemble, rs) - test.That(t, extraCap, test.ShouldResemble, make(map[string]interface{})) - - // With extra params - rs1, err = sensor1Client.Readings(context.Background(), map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, rs1, test.ShouldResemble, rs) - test.That(t, extraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - test.That(t, sensor1Client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("Sensor client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", sensor.Named(failSensorName), logger) - test.That(t, err, test.ShouldBeNil) - - _, err = client2.Readings(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errReadingsFailed.Error()) - - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/sensor/collector.go b/components/sensor/collector.go deleted file mode 100644 index 07f34e6880a..00000000000 --- a/components/sensor/collector.go +++ /dev/null @@ -1,63 +0,0 @@ -package sensor - -import ( - "context" - "errors" - - pb "go.viam.com/api/common/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/protoutils" -) - -type method int64 - -const ( - readings method = iota -) - -func (m method) String() string { - if m == readings { - return "Readings" - } - return "Unknown" -} - -// newReadingsCollector returns a collector to register a sensor reading method. If one is already registered -// with the same MethodMetadata it will panic. -func newReadingsCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - sensorResource, err := assertSensor(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, arg map[string]*anypb.Any) (interface{}, error) { - values, err := sensorResource.Readings(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, readings.String(), err) - } - readings, err := protoutils.ReadingGoToProto(values) - if err != nil { - return nil, err - } - return pb.GetReadingsResponse{ - Readings: readings, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -func assertSensor(resource interface{}) (Sensor, error) { - sensorResource, ok := resource.(Sensor) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - - return sensorResource, nil -} diff --git a/components/sensor/collector_test.go b/components/sensor/collector_test.go deleted file mode 100644 index 752f256c1e3..00000000000 --- a/components/sensor/collector_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package sensor_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - "go.viam.com/test" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/data" - du "go.viam.com/rdk/data/testutils" - "go.viam.com/rdk/logging" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - captureInterval = time.Second - numRetries = 5 -) - -var readingMap = map[string]any{"reading1": false, "reading2": "test"} - -func TestSensorCollector(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: "sensor", - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Target: &buf, - Clock: mockClock, - } - - sens := newSensor() - col, err := sensor.NewReadingsCollector(sens, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, du.GetExpectedReadingsStruct(readingMap).AsMap()) -} - -func newSensor() sensor.Sensor { - s := &inject.Sensor{} - s.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return readingMap, nil - } - return s -} diff --git a/components/sensor/ds18b20/ds18b20.go b/components/sensor/ds18b20/ds18b20.go deleted file mode 100644 index 65a900bd796..00000000000 --- a/components/sensor/ds18b20/ds18b20.go +++ /dev/null @@ -1,96 +0,0 @@ -// Package ds18b20 implements a 1-wire temperature sensor -package ds18b20 - -import ( - "context" - "errors" - "fmt" - "math" - "os" - "path/filepath" - "strconv" - "strings" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("ds18b20") - -// Config is used for converting config attributes. -type Config struct { - resource.TriviallyValidateConfig - UniqueID string `json:"unique_id"` -} - -func init() { - resource.RegisterComponent( - sensor.API, - model, - resource.Registration[sensor.Sensor, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (sensor.Sensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - return newSensor(conf.ResourceName(), newConf.UniqueID, logger), nil - }, - }) -} - -func newSensor(name resource.Name, id string, logger logging.Logger) sensor.Sensor { - // temp sensors are in family 28 - return &Sensor{ - Named: name.AsNamed(), - logger: logger, - OneWireID: id, - OneWireFamily: "28", - } -} - -// Sensor is a 1-wire Sensor device. -type Sensor struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - OneWireID string - OneWireFamily string - logger logging.Logger -} - -// ReadTemperatureCelsius returns current temperature in celsius. -func (s *Sensor) ReadTemperatureCelsius(ctx context.Context) (float64, error) { - // logic here is specific to 1-wire protocol, could be abstracted next time we - // want to build support for a different 1-wire device, - // or look at support via periph (or other library) - devPath := fmt.Sprintf("/sys/bus/w1/devices/%s-%s/w1_slave", s.OneWireFamily, s.OneWireID) - dat, err := os.ReadFile(filepath.Clean(devPath)) - if err != nil { - return math.NaN(), err - } - tempString := strings.TrimSuffix(string(dat), "\n") - splitString := strings.Split(tempString, "t=") - if len(splitString) == 2 { - tempMili, err := strconv.ParseFloat(splitString[1], 32) - if err != nil { - return math.NaN(), err - } - return tempMili / 1000, nil - } - return math.NaN(), errors.New("temperature could not be read") -} - -// Readings returns a list containing single item (current temperature). -func (s *Sensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - temp, err := s.ReadTemperatureCelsius(ctx) - if err != nil { - return nil, err - } - return map[string]interface{}{"degrees_celsius": temp}, nil -} diff --git a/components/sensor/export_collectors_test.go b/components/sensor/export_collectors_test.go deleted file mode 100644 index feb79e603ba..00000000000 --- a/components/sensor/export_collectors_test.go +++ /dev/null @@ -1,5 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package sensor - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var NewReadingsCollector = newReadingsCollector diff --git a/components/sensor/fake/sensor.go b/components/sensor/fake/sensor.go deleted file mode 100644 index fa19cd942da..00000000000 --- a/components/sensor/fake/sensor.go +++ /dev/null @@ -1,48 +0,0 @@ -// Package fake implements a fake Sensor. -package fake - -import ( - "context" - "sync" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func init() { - resource.RegisterComponent( - sensor.API, - resource.DefaultModelFamily.WithModel("fake"), - resource.Registration[sensor.Sensor, resource.NoNativeConfig]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (sensor.Sensor, error) { - return newSensor(conf.ResourceName(), logger), nil - }}) -} - -func newSensor(name resource.Name, logger logging.Logger) sensor.Sensor { - return &Sensor{ - Named: name.AsNamed(), - logger: logger, - } -} - -// Sensor is a fake Sensor device that always returns the set location. -type Sensor struct { - mu sync.Mutex - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - logger logging.Logger -} - -// Readings always returns the set values. -func (s *Sensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - s.mu.Lock() - defer s.mu.Unlock() - return map[string]interface{}{"a": 1, "b": 2, "c": 3}, nil -} diff --git a/components/sensor/register/register.go b/components/sensor/register/register.go deleted file mode 100644 index 7d4f5a991e0..00000000000 --- a/components/sensor/register/register.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package register registers all relevant Sensors -package register - -import ( - // for Sensors. - _ "go.viam.com/rdk/components/sensor/bme280" - _ "go.viam.com/rdk/components/sensor/ds18b20" - _ "go.viam.com/rdk/components/sensor/fake" - _ "go.viam.com/rdk/components/sensor/sht3xd" - _ "go.viam.com/rdk/components/sensor/ultrasonic" -) diff --git a/components/sensor/sensor.go b/components/sensor/sensor.go deleted file mode 100644 index 2df2b048d2e..00000000000 --- a/components/sensor/sensor.go +++ /dev/null @@ -1,57 +0,0 @@ -// Package sensor defines an abstract sensing device that can provide measurement readings. -package sensor - -import ( - pb "go.viam.com/api/component/sensor/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Sensor]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterSensorServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.SensorService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: readings.String(), - }, newReadingsCollector) -} - -// SubtypeName is a constant that identifies the component resource API string "Sensor". -const SubtypeName = "sensor" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// Named is a helper for getting the named Sensor's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// A Sensor represents a general purpose sensors that can give arbitrary readings -// of some thing that it is sensing. -type Sensor interface { - resource.Resource - resource.Sensor -} - -// FromDependencies is a helper for getting the named sensor from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Sensor, error) { - return resource.FromDependencies[Sensor](deps, Named(name)) -} - -// FromRobot is a helper for getting the named Sensor from the given Robot. -func FromRobot(r robot.Robot, name string) (Sensor, error) { - return robot.ResourceFromRobot[Sensor](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all sensor names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} diff --git a/components/sensor/server.go b/components/sensor/server.go deleted file mode 100644 index 6239d42b288..00000000000 --- a/components/sensor/server.go +++ /dev/null @@ -1,54 +0,0 @@ -// Package sensor contains a gRPC based Sensor service serviceServer. -package sensor - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/sensor/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// serviceServer implements the SensorService from sensor.proto. -type serviceServer struct { - pb.UnimplementedSensorServiceServer - coll resource.APIResourceCollection[Sensor] -} - -// NewRPCServiceServer constructs an sensor gRPC service serviceServer. -func NewRPCServiceServer(coll resource.APIResourceCollection[Sensor]) interface{} { - return &serviceServer{coll: coll} -} - -// GetReadings returns the most recent readings from the given Sensor. -func (s *serviceServer) GetReadings( - ctx context.Context, - req *commonpb.GetReadingsRequest, -) (*commonpb.GetReadingsResponse, error) { - sensorDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - readings, err := sensorDevice.Readings(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - m, err := protoutils.ReadingGoToProto(readings) - if err != nil { - return nil, err - } - return &commonpb.GetReadingsResponse{Readings: m}, nil -} - -// DoCommand receives arbitrary commands. -func (s *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - sensorDevice, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, sensorDevice, req) -} diff --git a/components/sensor/server_test.go b/components/sensor/server_test.go deleted file mode 100644 index 3c56617a027..00000000000 --- a/components/sensor/server_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package sensor_test - -import ( - "context" - "errors" - "testing" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/sensor/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var errReadingsFailed = errors.New("can't get readings") - -func newServer() (pb.SensorServiceServer, *inject.Sensor, *inject.Sensor, error) { - injectSensor := &inject.Sensor{} - injectSensor2 := &inject.Sensor{} - sensors := map[resource.Name]sensor.Sensor{ - sensor.Named(testSensorName): injectSensor, - sensor.Named(failSensorName): injectSensor2, - } - sensorSvc, err := resource.NewAPIResourceCollection(sensor.API, sensors) - if err != nil { - return nil, nil, nil, err - } - return sensor.NewRPCServiceServer(sensorSvc).(pb.SensorServiceServer), injectSensor, injectSensor2, nil -} - -func TestServer(t *testing.T) { - sensorServer, injectSensor, injectSensor2, err := newServer() - test.That(t, err, test.ShouldBeNil) - - rs := map[string]interface{}{"a": 1.1, "b": 2.2} - - var extraCap map[string]interface{} - injectSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - extraCap = extra - return rs, nil - } - - injectSensor2.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return nil, errReadingsFailed - } - - t.Run("GetReadings", func(t *testing.T) { - expected := map[string]*structpb.Value{} - for k, v := range rs { - vv, err := structpb.NewValue(v) - test.That(t, err, test.ShouldBeNil) - expected[k] = vv - } - extra, err := protoutils.StructToStructPb(map[string]interface{}{"foo": "bar"}) - test.That(t, err, test.ShouldBeNil) - - resp, err := sensorServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: testSensorName, Extra: extra}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Readings, test.ShouldResemble, expected) - test.That(t, extraCap, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - _, err = sensorServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: failSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errReadingsFailed.Error()) - - _, err = sensorServer.GetReadings(context.Background(), &commonpb.GetReadingsRequest{Name: missingSensorName}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - }) -} diff --git a/components/sensor/sht3xd/sht3xd.go b/components/sensor/sht3xd/sht3xd.go deleted file mode 100644 index 0119847a0ef..00000000000 --- a/components/sensor/sht3xd/sht3xd.go +++ /dev/null @@ -1,168 +0,0 @@ -//go:build linux - -// Package sht3xd implements a sht3x-d sensor for temperature and humidity -// datasheet can be found at: https://cdn-shop.adafruit.com/product-files/2857/Sensirion_Humidity_SHT3x_Datasheet_digital-767294.pdf -// example repo: https://github.com/esphome/esphome/tree/dev/esphome/components/sht3xd -package sht3xd - -import ( - "context" - "encoding/binary" - "fmt" - "time" - - "go.uber.org/multierr" - - "go.viam.com/rdk/components/board/genericlinux/buses" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("sensirion-sht3xd") - -const ( - defaultI2Caddr = 0x44 - // Addresses of sht3xd registers. - sht3xdCOMMANDSOFTRESET1 = 0x30 - sht3xdCOMMANDSOFTRESET2 = 0xA2 - sht3xdCOMMANDPOLLINGH1 = 0x24 - sht3xdCOMMANDPOLLINGH2 = 0x00 -) - -// Config is used for converting config attributes. -type Config struct { - I2cBus string `json:"i2c_bus"` - I2cAddr int `json:"i2c_addr,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - if conf.I2cBus == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "i2c_bus") - } - return deps, nil -} - -func init() { - resource.RegisterComponent( - sensor.API, - model, - resource.Registration[sensor.Sensor, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (sensor.Sensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - return newSensor(ctx, deps, conf.ResourceName(), newConf, logger) - }, - }) -} - -func newSensor( - ctx context.Context, - _ resource.Dependencies, - name resource.Name, - conf *Config, - logger logging.Logger, -) (sensor.Sensor, error) { - i2cbus, err := buses.NewI2cBus(conf.I2cBus) - if err != nil { - return nil, fmt.Errorf("sht3xd init: failed to find i2c bus %s", conf.I2cBus) - } - - addr := conf.I2cAddr - if addr == 0 { - addr = defaultI2Caddr - logger.CWarn(ctx, "using i2c address : 0x44") - } - - s := &sht3xd{ - Named: name.AsNamed(), - logger: logger, - bus: i2cbus, - addr: byte(addr), - } - - err = s.reset(ctx) - if err != nil { - return nil, err - } - - return s, nil -} - -// sht3xd is a i2c sensor device that reports temperature and humidity. -type sht3xd struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - logger logging.Logger - - bus buses.I2C - addr byte -} - -// Readings returns a list containing two items (current temperature and humidity). -func (s *sht3xd) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - tryRead := func() ([]byte, error) { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - s.logger.CErrorf(ctx, "can't open sht3xd i2c %s", err) - return nil, err - } - err = handle.Write(ctx, []byte{sht3xdCOMMANDPOLLINGH1, sht3xdCOMMANDPOLLINGH2}) - if err != nil { - s.logger.CDebug(ctx, "Failed to request temperature") - return nil, multierr.Append(err, handle.Close()) - } - buffer, err := handle.Read(ctx, 2) - if err != nil { - return nil, multierr.Append(err, handle.Close()) - } - return buffer, handle.Close() - } - buffer, err := tryRead() - if err != nil { - // If error, do a soft reset and try again - err = s.reset(ctx) - if err != nil { - return nil, err - } - buffer, err = tryRead() - if err != nil { - return nil, err - } - } - if len(buffer) != 2 { - return nil, fmt.Errorf("expected 2 bytes from sht3xd i2c, got %d", len(buffer)) - } - tempRaw := binary.LittleEndian.Uint16([]byte{0, buffer[0]}) - humidRaw := binary.LittleEndian.Uint16([]byte{0, buffer[1]}) - - temp := 175.0*float64(tempRaw)/65535.0 - 45.0 - humid := 100.0 * float64(humidRaw) / 65535.0 - return map[string]interface{}{ - "temperature_celsius": temp, - "relative_humidity_pct": humid, // TODO(RSDK-1903) - }, nil -} - -// reset will reset the sensor. -func (s *sht3xd) reset(ctx context.Context) error { - handle, err := s.bus.OpenHandle(s.addr) - if err != nil { - s.logger.CErrorf(ctx, "can't open sht3xd i2c %s", err) - return err - } - err = handle.Write(ctx, []byte{sht3xdCOMMANDSOFTRESET1, sht3xdCOMMANDSOFTRESET2}) - // wait for chip reset cycle to complete - time.Sleep(1 * time.Millisecond) - return multierr.Append(err, handle.Close()) -} diff --git a/components/sensor/sht3xd/sht3xd_nonlinux.go b/components/sensor/sht3xd/sht3xd_nonlinux.go deleted file mode 100644 index e8c2ca97902..00000000000 --- a/components/sensor/sht3xd/sht3xd_nonlinux.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package sht3xd is only supported on Linux machines. -package sht3xd diff --git a/components/sensor/ultrasonic/ultrasonic.go b/components/sensor/ultrasonic/ultrasonic.go deleted file mode 100644 index 4e60579aa78..00000000000 --- a/components/sensor/ultrasonic/ultrasonic.go +++ /dev/null @@ -1,195 +0,0 @@ -// Package ultrasonic implements an ultrasonic sensor based of the yahboom ultrasonic sensor -package ultrasonic - -import ( - "context" - "math" - "sync" - "time" - - "github.com/pkg/errors" - "go.uber.org/multierr" - rdkutils "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -var model = resource.DefaultModelFamily.WithModel("ultrasonic") - -// Config is used for converting config attributes. -type Config struct { - TriggerPin string `json:"trigger_pin"` - EchoInterrupt string `json:"echo_interrupt_pin"` - Board string `json:"board"` - TimeoutMs uint `json:"timeout_ms,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - if len(conf.Board) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - deps = append(deps, conf.Board) - if len(conf.TriggerPin) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "trigger pin") - } - if len(conf.EchoInterrupt) == 0 { - return nil, resource.NewConfigValidationFieldRequiredError(path, "echo interrupt pin") - } - return deps, nil -} - -func init() { - resource.RegisterComponent( - sensor.API, - model, - resource.Registration[sensor.Sensor, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (sensor.Sensor, error) { - newConf, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - return NewSensor(ctx, deps, conf.ResourceName(), newConf, logger) - }, - }) -} - -// NewSensor creates and configures a new ultrasonic sensor. -func NewSensor(ctx context.Context, deps resource.Dependencies, - name resource.Name, config *Config, logger logging.Logger, -) (sensor.Sensor, error) { - s := &Sensor{ - Named: name.AsNamed(), - logger: logger, - config: config, - } - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - s.cancelCtx = cancelCtx - s.cancelFunc = cancelFunc - - res, ok := deps[board.Named(config.Board)] - if !ok { - return nil, errors.Errorf("ultrasonic: board %q missing from dependencies", config.Board) - } - - b, ok := res.(board.Board) - if !ok { - return nil, errors.Errorf("ultrasonic: cannot find board %q", config.Board) - } - s.board = b - - if config.TimeoutMs > 0 { - s.timeoutMs = config.TimeoutMs - } else { - // default to 1 sec - s.timeoutMs = 1000 - } - - s.ticksChan = make(chan board.Tick, 2) - - // Set the trigger pin to low, so it's ready for later. - triggerPin, err := b.GPIOPinByName(config.TriggerPin) - if err != nil { - return nil, errors.Wrapf(err, "ultrasonic: cannot grab gpio %q", config.TriggerPin) - } - if err := triggerPin.Set(ctx, false, nil); err != nil { - return nil, errors.Wrap(err, "ultrasonic: cannot set trigger pin to low") - } - - return s, nil -} - -// Sensor ultrasonic sensor. -type Sensor struct { - resource.Named - resource.AlwaysRebuild - mu sync.Mutex - config *Config - board board.Board - ticksChan chan board.Tick - timeoutMs uint - cancelCtx context.Context - cancelFunc func() - logger logging.Logger -} - -func (s *Sensor) namedError(err error) error { - return errors.Wrapf( - err, "Error in ultrasonic sensor with name %s: ", s.Name(), - ) -} - -// Readings returns the calculated distance. -func (s *Sensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - s.mu.Lock() - defer s.mu.Unlock() - - // Grab the 2 pins from the board. We don't just get these once during setup, in case the board - // reconfigures itself because someone decided to rewire things. - echoInterrupt, err := s.board.DigitalInterruptByName(s.config.EchoInterrupt) - if err != nil { - return nil, multierr.Combine(errors.Errorf("ultrasonic: cannot grab digital interrupt %q", s.config.EchoInterrupt), err) - } - triggerPin, err := s.board.GPIOPinByName(s.config.TriggerPin) - if err != nil { - return nil, errors.Wrapf(err, "ultrasonic: cannot grab gpio %q", s.config.TriggerPin) - } - - err = s.board.StreamTicks(ctx, []board.DigitalInterrupt{echoInterrupt}, s.ticksChan, nil) - if err != nil { - return nil, errors.Wrap(err, "ultrasonic: error getting digital interrupt ticks") - } - - // we send a high and a low to the trigger pin 10 microseconds - // apart to signal the sensor to begin sending the sonic pulse - if err := triggerPin.Set(ctx, true, nil); err != nil { - return nil, s.namedError(errors.Wrap(err, "ultrasonic cannot set trigger pin to high")) - } - rdkutils.SelectContextOrWait(ctx, time.Microsecond*10) - if err := triggerPin.Set(ctx, false, nil); err != nil { - return nil, s.namedError(errors.Wrap(err, "ultrasonic cannot set trigger pin to low")) - } - // the first signal from the interrupt indicates that the sonic - // pulse has been sent and the second indicates that the echo has been received - var tick board.Tick - ticks := make([]board.Tick, 2) - for i := 0; i < 2; i++ { - var signalStr string - if i == 0 { - signalStr = "sound pulse was emitted" - } else { - signalStr = "echo was received" - } - select { - case tick = <-s.ticksChan: - ticks[i] = tick - case <-s.cancelCtx.Done(): - return nil, s.namedError(errors.New("ultrasonic: context canceled")) - case <-time.After(time.Millisecond * time.Duration(s.timeoutMs)): - return nil, s.namedError(errors.Errorf("timed out waiting for signal that %s", signalStr)) - } - } - timeB := ticks[0].TimestampNanosec - timeA := ticks[1].TimestampNanosec - // we calculate the distance to the nearest object based - // on the time interval between the sound and its echo - // and the speed of sound (343 m/s) - secondsElapsed := float64(timeA-timeB) / math.Pow10(9) - distMeters := secondsElapsed * 343.0 / 2.0 - return map[string]interface{}{"distance": distMeters}, nil -} - -// Close remove interrupt callback of ultrasonic sensor. -func (s *Sensor) Close(ctx context.Context) error { - s.cancelFunc() - return nil -} diff --git a/components/sensor/ultrasonic/ultrasonic_test.go b/components/sensor/ultrasonic/ultrasonic_test.go deleted file mode 100644 index df4736035cb..00000000000 --- a/components/sensor/ultrasonic/ultrasonic_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package ultrasonic - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testSensorName = "ultrasonic1" - triggerPin = "some-pin" - echoInterrupt = "some-echo-interrupt" - board1 = "some-board" -) - -func setupDependencies(t *testing.T) resource.Dependencies { - t.Helper() - - deps := make(resource.Dependencies) - - actualBoard := inject.NewBoard(board1) - actualBoard.DigitalInterruptNamesFunc = func() []string { - return []string{echoInterrupt} - } - injectDigi := &inject.DigitalInterrupt{} - actualBoard.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - return injectDigi, nil - } - pin := &inject.GPIOPin{} - pin.SetFunc = func(ctx context.Context, high bool, extra map[string]interface{}) error { - return nil - } - actualBoard.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - return pin, nil - } - deps[board.Named(board1)] = actualBoard - - return deps -} - -func TestValidate(t *testing.T) { - fakecfg := &Config{} - _, err := fakecfg.Validate("path") - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "board") - - fakecfg.Board = board1 - _, err = fakecfg.Validate("path") - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "trigger pin") - - fakecfg.TriggerPin = triggerPin - _, err = fakecfg.Validate("path") - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "echo interrupt pin") - - fakecfg.EchoInterrupt = echoInterrupt - _, err = fakecfg.Validate("path") - test.That(t, err, test.ShouldBeNil) -} - -func TestNewSensor(t *testing.T) { - fakecfg := &Config{TriggerPin: triggerPin, EchoInterrupt: echoInterrupt, Board: board1} - ctx := context.Background() - deps := setupDependencies(t) - logger := logging.NewTestLogger(t) - - _, err := NewSensor(ctx, deps, sensor.Named(testSensorName), fakecfg, logger) - test.That(t, err, test.ShouldBeNil) -} diff --git a/components/sensor/verify_main_test.go b/components/sensor/verify_main_test.go deleted file mode 100644 index 6c4e81f72a4..00000000000 --- a/components/sensor/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package sensor - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/components/servo/client.go b/components/servo/client.go deleted file mode 100644 index a6696443dac..00000000000 --- a/components/servo/client.go +++ /dev/null @@ -1,87 +0,0 @@ -// Package servo contains a gRPC bases servo client -package servo - -import ( - "context" - - pb "go.viam.com/api/component/servo/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// client implements ServoServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.ServoServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Servo, error) { - c := pb.NewServoServiceClient(conn) - return &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - logger: logger, - }, nil -} - -func (c *client) Move(ctx context.Context, angleDeg uint32, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - req := &pb.MoveRequest{AngleDeg: angleDeg, Name: c.name, Extra: ext} - if _, err := c.client.Move(ctx, req); err != nil { - return err - } - return nil -} - -func (c *client) Position(ctx context.Context, extra map[string]interface{}) (uint32, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return 0, err - } - req := &pb.GetPositionRequest{Name: c.name, Extra: ext} - resp, err := c.client.GetPosition(ctx, req) - if err != nil { - return 0, err - } - return resp.PositionDeg, nil -} - -func (c *client) Stop(ctx context.Context, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - _, err = c.client.Stop(ctx, &pb.StopRequest{Name: c.name, Extra: ext}) - return err -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} - -func (c *client) IsMoving(ctx context.Context) (bool, error) { - resp, err := c.client.IsMoving(ctx, &pb.IsMovingRequest{Name: c.name}) - if err != nil { - return false, err - } - return resp.IsMoving, nil -} diff --git a/components/servo/client_test.go b/components/servo/client_test.go deleted file mode 100644 index d3f9cf70eb7..00000000000 --- a/components/servo/client_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package servo_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/servo" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testServoName = "servo1" - failServoName = "servo2" - fakeServoName = "servo3" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - var actualExtra map[string]interface{} - - workingServo := &inject.Servo{} - failingServo := &inject.Servo{} - - workingServo.MoveFunc = func(ctx context.Context, angle uint32, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - workingServo.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - actualExtra = extra - return 20, nil - } - workingServo.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - - failingServo.MoveFunc = func(ctx context.Context, angle uint32, extra map[string]interface{}) error { - return errMoveFailed - } - failingServo.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - return 0, errPositionUnreadable - } - failingServo.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopFailed - } - - resourceMap := map[resource.Name]servo.Servo{ - servo.Named(testServoName): workingServo, - servo.Named(failServoName): failingServo, - } - servoSvc, err := resource.NewAPIResourceCollection(servo.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[servo.Servo](servo.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, servoSvc), test.ShouldBeNil) - - workingServo.DoFunc = testutils.EchoFunc - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("client tests for working servo", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingServoClient, err := servo.NewClientFromConn(context.Background(), conn, "", servo.Named(testServoName), logger) - test.That(t, err, test.ShouldBeNil) - - // DoCommand - resp, err := workingServoClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - err = workingServoClient.Move(context.Background(), 20, map[string]interface{}{"foo": "Move"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, map[string]interface{}{"foo": "Move"}) - - currentDeg, err := workingServoClient.Position(context.Background(), map[string]interface{}{"foo": "Position"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, currentDeg, test.ShouldEqual, 20) - test.That(t, actualExtra, test.ShouldResemble, map[string]interface{}{"foo": "Position"}) - - test.That(t, workingServoClient.Stop(context.Background(), map[string]interface{}{"foo": "Stop"}), test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, map[string]interface{}{"foo": "Stop"}) - - test.That(t, workingServoClient.Close(context.Background()), test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("client tests for failing servo", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingServoClient, err := servo.NewClientFromConn(context.Background(), conn, "", servo.Named(failServoName), logger) - test.That(t, err, test.ShouldBeNil) - - err = failingServoClient.Move(context.Background(), 20, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errMoveFailed.Error()) - - _, err = failingServoClient.Position(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPositionUnreadable.Error()) - - err = failingServoClient.Stop(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopFailed.Error()) - - test.That(t, failingServoClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client tests for working servo", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := resourceAPI.RPCClient(context.Background(), conn, "", servo.Named(testServoName), logger) - test.That(t, err, test.ShouldBeNil) - - err = client.Move(context.Background(), 20, nil) - test.That(t, err, test.ShouldBeNil) - - currentDeg, err := client.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, currentDeg, test.ShouldEqual, 20) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/components/servo/collectors.go b/components/servo/collectors.go deleted file mode 100644 index b073b122fde..00000000000 --- a/components/servo/collectors.go +++ /dev/null @@ -1,57 +0,0 @@ -package servo - -import ( - "context" - "errors" - - pb "go.viam.com/api/component/servo/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" -) - -type method int64 - -const ( - position method = iota -) - -func (m method) String() string { - if m == position { - return "Position" - } - return "Unknown" -} - -// newPositionCollector returns a collector to register a position method. If one is already registered -// with the same MethodMetadata it will panic. -func newPositionCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - servo, err := assertServo(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - pos, err := servo.Position(ctx, data.FromDMExtraMap) - if err != nil { - // A modular filter component can be created to filter the readings from a component. The error ErrNoCaptureToStore - // is used in the datamanager to exclude readings from being captured and stored. - if errors.Is(err, data.ErrNoCaptureToStore) { - return nil, err - } - return nil, data.FailedToReadErr(params.ComponentName, position.String(), err) - } - return pb.GetPositionResponse{ - PositionDeg: pos, - }, nil - }) - return data.NewCollector(cFunc, params) -} - -func assertServo(resource interface{}) (Servo, error) { - servo, ok := resource.(Servo) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - return servo, nil -} diff --git a/components/servo/collectors_test.go b/components/servo/collectors_test.go deleted file mode 100644 index 6e253f730ef..00000000000 --- a/components/servo/collectors_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package servo_test - -import ( - "context" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - pb "go.viam.com/api/component/servo/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/data" - "go.viam.com/rdk/logging" - tu "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - captureInterval = time.Second - numRetries = 5 -) - -func TestServoCollector(t *testing.T) { - mockClock := clk.NewMock() - buf := tu.MockBuffer{} - params := data.CollectorParams{ - ComponentName: "servo", - Interval: captureInterval, - Logger: logging.NewTestLogger(t), - Target: &buf, - Clock: mockClock, - } - - serv := newServo() - col, err := servo.NewPositionCollector(serv, params) - test.That(t, err, test.ShouldBeNil) - - defer col.Close() - col.Collect() - mockClock.Add(captureInterval) - - tu.Retry(func() bool { - return buf.Length() != 0 - }, numRetries) - test.That(t, buf.Length(), test.ShouldBeGreaterThan, 0) - test.That(t, buf.Writes[0].GetStruct().AsMap(), test.ShouldResemble, - tu.ToProtoMapIgnoreOmitEmpty(pb.GetPositionResponse{ - PositionDeg: 1.0, - })) -} - -func newServo() servo.Servo { - s := &inject.Servo{} - s.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - return 1.0, nil - } - return s -} diff --git a/components/servo/export_collectors_test.go b/components/servo/export_collectors_test.go deleted file mode 100644 index 3463e07ee32..00000000000 --- a/components/servo/export_collectors_test.go +++ /dev/null @@ -1,5 +0,0 @@ -// export_collectors_test.go adds functionality to the package that we only want to use and expose during testing. -package servo - -// Exported variables for testing collectors, see unexported collectors for implementation details. -var NewPositionCollector = newPositionCollector diff --git a/components/servo/fake/servo.go b/components/servo/fake/servo.go deleted file mode 100644 index 90f23736c2e..00000000000 --- a/components/servo/fake/servo.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package fake implements a fake servo. -package fake - -import ( - "context" - - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func init() { - resource.RegisterComponent( - servo.API, - resource.DefaultModelFamily.WithModel("fake"), - resource.Registration[servo.Servo, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, _ resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (servo.Servo, error) { - return &Servo{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - }, nil - }, - }) -} - -// A Servo allows setting and reading a single angle. -type Servo struct { - angle uint32 - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - logger logging.Logger -} - -// Move sets the given angle. -func (s *Servo) Move(ctx context.Context, angleDeg uint32, extra map[string]interface{}) error { - s.angle = angleDeg - return nil -} - -// Position returns the set angle. -func (s *Servo) Position(ctx context.Context, extra map[string]interface{}) (uint32, error) { - return s.angle, nil -} - -// Stop doesn't do anything for a fake servo. -func (s *Servo) Stop(ctx context.Context, extra map[string]interface{}) error { - return nil -} - -// IsMoving is always false for a fake servo. -func (s *Servo) IsMoving(ctx context.Context) (bool, error) { - return false, nil -} diff --git a/components/servo/gpio/servo.go b/components/servo/gpio/servo.go deleted file mode 100644 index f2b44ff8fd3..00000000000 --- a/components/servo/gpio/servo.go +++ /dev/null @@ -1,397 +0,0 @@ -// Package gpio implements a pin based servo -package gpio - -import ( - "context" - "math" - "sync" - "time" - - "github.com/pkg/errors" - viamutils "go.viam.com/utils" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/resource" -) - -const ( - defaultMinDeg float64 = 0.0 - defaultMaxDeg float64 = 180.0 - minWidthUs uint = 500 // absolute minimum PWM width - maxWidthUs uint = 2500 // absolute maximum PWM width - defaultFreq uint = 300 -) - -// We want to distinguish values that are 0 because the user set them to 0 from ones that are 0 -// because that's the default when the user didn't set them. Consequently, all numerical fields in -// this struct are pointers. They'll be nil if they were unset, and point to a value (possibly 0!) -// if they were set. -type servoConfig struct { - Pin string `json:"pin"` // Pin is a GPIO pin with PWM capabilities. - Board string `json:"board"` // Board is a board that exposes GPIO pins. - // MinDeg is the minimum angle the servo can reach. Note this doesn't affect PWM calculation. - MinDeg *float64 `json:"min_angle_deg,omitempty"` - // MaxDeg is the maximum angle the servo can reach. Note this doesn't affect PWM calculation. - MaxDeg *float64 `json:"max_angle_deg,omitempty"` - // StartPos is the starting position of the servo in degrees. - StartPos *float64 `json:"starting_position_deg,omitempty"` - // Frequency at which to drive the PWM - Frequency *uint `json:"frequency_hz,omitempty"` - // Resolution of the PWM driver (eg number of ticks for a full period). If omitted or 0, the - // driver will attempt to estimate the resolution. - Resolution *uint `json:"pwm_resolution,omitempty"` - // MinWidthUs overrides the safe minimum PWM width in microseconds. - MinWidthUs *uint `json:"min_width_us,omitempty"` - // MaxWidthUs overrides the safe maximum PWM width in microseconds. - MaxWidthUs *uint `json:"max_width_us,omitempty"` -} - -// Validate ensures all parts of the config are valid. -func (config *servoConfig) Validate(path string) ([]string, error) { - var deps []string - if config.Board == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "board") - } - deps = append(deps, config.Board) - if config.Pin == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "pin") - } - - if config.StartPos != nil { - minDeg := defaultMinDeg - maxDeg := defaultMaxDeg - if config.MinDeg != nil { - minDeg = *config.MinDeg - } - if config.MaxDeg != nil { - maxDeg = *config.MaxDeg - } - if *config.StartPos < minDeg || *config.StartPos > maxDeg { - return nil, resource.NewConfigValidationError(path, - errors.Errorf("starting_position_deg should be between minimum (%.1f) and maximum (%.1f) positions", minDeg, maxDeg)) - } - } - - if config.MinDeg != nil && *config.MinDeg < 0 { - return nil, resource.NewConfigValidationError(path, errors.New("min_angle_deg cannot be lower than 0")) - } - if config.MinWidthUs != nil && *config.MinWidthUs < minWidthUs { - return nil, resource.NewConfigValidationError(path, errors.Errorf("min_width_us cannot be lower than %d", minWidthUs)) - } - if config.MaxWidthUs != nil && *config.MaxWidthUs > maxWidthUs { - return nil, resource.NewConfigValidationError(path, errors.Errorf("max_width_us cannot be higher than %d", maxWidthUs)) - } - return deps, nil -} - -var model = resource.DefaultModelFamily.WithModel("gpio") - -func init() { - resource.RegisterComponent(servo.API, model, - resource.Registration[servo.Servo, *servoConfig]{ - Constructor: newGPIOServo, - }) -} - -type servoGPIO struct { - resource.Named - resource.TriviallyCloseable - pin board.GPIOPin - minDeg float64 - maxDeg float64 - logger logging.Logger - opMgr *operation.SingleOperationManager - frequency uint - minUs uint - maxUs uint - pwmRes uint - currPct float64 - mu sync.Mutex -} - -func newGPIOServo( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (servo.Servo, error) { - servo := &servoGPIO{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - opMgr: operation.NewSingleOperationManager(), - currPct: 0, - } - - // reconfigure - if err := servo.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return servo, nil -} - -func (s *servoGPIO) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - s.mu.Lock() - defer s.mu.Unlock() - - newConf, err := resource.NativeConfig[*servoConfig](conf) - if err != nil { - return err - } - - boardName := newConf.Board - - b, err := board.FromDependencies(deps, boardName) - if err != nil { - return errors.Wrap(err, "board doesn't exist") - } - - s.pin, err = b.GPIOPinByName(newConf.Pin) - if err != nil { - return errors.Wrap(err, "couldn't get servo pin") - } - - s.minDeg = defaultMinDeg - if newConf.MinDeg != nil { - s.minDeg = *newConf.MinDeg - } - startPos := 0.0 - if newConf.StartPos != nil { - startPos = *newConf.StartPos - } - - s.maxDeg = defaultMaxDeg - if newConf.MaxDeg != nil { - s.maxDeg = *newConf.MaxDeg - } - - s.minUs = minWidthUs - if newConf.MinWidthUs != nil { - s.minUs = *newConf.MinWidthUs - } - - s.maxUs = maxWidthUs - if newConf.MaxWidthUs != nil { - s.maxUs = *newConf.MaxWidthUs - } - - // If the frequency isn't specified in the config, we'll use whatever it's currently set to - // instead. If it's currently set to 0, we'll default to using 300 Hz. - s.frequency, err = s.pin.PWMFreq(ctx, nil) - if err != nil { - return errors.Wrap(err, "couldn't get servo pin pwm frequency") - } - - if s.frequency == 0 { - s.frequency = defaultFreq - } - - if newConf.Frequency != nil { - if *newConf.Frequency > 450 || *newConf.Frequency < 50 { - return errors.Errorf( - "PWM frequencies should not be above 450Hz or below 50, have %d", newConf.Frequency) - } - - s.frequency = *newConf.Frequency - } - - // We need the pin to be high for up to maxUs microseconds, plus the motor's deadband width - // time spent low before going high again. The deadband width is usually at least 1 - // microsecond, but rarely over 10. Call it 50 microseconds just to be safe. - const maxDeadbandWidthUs = 50 - if maxFrequency := 1e6 / (s.maxUs + maxDeadbandWidthUs); s.frequency > maxFrequency { - s.logger.CWarnf(ctx, "servo frequency (%f.1) is above maximum (%f.1), setting to max instead", - s.frequency, maxFrequency) - s.frequency = maxFrequency - } - - if err := s.pin.SetPWMFreq(ctx, s.frequency, nil); err != nil { - return errors.Wrap(err, "error setting servo pin frequency") - } - - // Try to detect the PWM resolution. - if err := s.Move(ctx, uint32(startPos), nil); err != nil { - return errors.Wrap(err, "couldn't move servo to start position") - } - - if err := s.findPWMResolution(ctx); err != nil { - return errors.Wrap(err, "failed to guess the pwm resolution") - } - - if err := s.Move(ctx, uint32(startPos), nil); err != nil { - return errors.Wrap(err, "couldn't move servo back to start position") - } - - return nil -} - -// Given minUs, maxUs, deg, and frequency attempt to calculate the corresponding duty cycle pct. -func mapDegToDutyCylePct(minUs, maxUs uint, minDeg, maxDeg, deg float64, frequency uint) float64 { - period := 1.0 / float64(frequency) - degRange := maxDeg - minDeg - uSRange := float64(maxUs - minUs) - - usPerDeg := uSRange / degRange - - pwmWidthUs := float64(minUs) + (deg-minDeg)*usPerDeg - return (pwmWidthUs / (1000 * 1000)) / period -} - -// Given minUs, maxUs, duty cycle pct, and frequency returns the position in degrees. -func mapDutyCylePctToDeg(minUs, maxUs uint, minDeg, maxDeg, pct float64, frequency uint) float64 { - period := 1.0 / float64(frequency) - pwmWidthUs := pct * period * 1000 * 1000 - degRange := maxDeg - minDeg - uSRange := float64(maxUs - minUs) - - pwmWidthUs = math.Max(float64(minUs), pwmWidthUs) - pwmWidthUs = math.Min(float64(maxUs), pwmWidthUs) - - degsPerUs := degRange / uSRange - returnVal := math.Round(minDeg + (pwmWidthUs-float64(minUs))*degsPerUs) - return returnVal -} - -// Attempt to find the PWM resolution assuming a hardware PWM -// -// 1. assume a resolution of any 16,15,14,12,or 8 bit timer -// -// 2. Starting from the current PWM duty cycle we increase the duty cycle by -// 1/(1< rDist { - dir = -1.0 - } - - if realPct != currPct { - if err := s.pin.SetPWM(ctx, realPct, nil); err != nil { - return errors.Wrap(err, "couldn't set PWM to realPct") - } - r2, err := s.pin.PWM(ctx, nil) - if err != nil { - return errors.Wrap(err, "couldn't find PWM resolution") - } - if r2 == realPct { - currPct = r2 - } else { - return errors.Errorf("giving up searching for the resolution tried to match %.7f but got %.7f", realPct, r2) - } - } - - resolution := []int{16, 15, 14, 12, 8} - for _, r := range resolution { - val := (1 << r) - 1 - pct := currPct + dir/float64(val) - err := s.pin.SetPWM(ctx, pct, nil) - if err != nil { - return errors.Wrap(err, "couldn't search for PWM resolution") - } - if !viamutils.SelectContextOrWait(ctx, 3*time.Millisecond) { - return errors.New("context canceled while looking for servo's PWM resolution") - } - realPct, err := s.pin.PWM(ctx, nil) - s.logger.CDebugf(ctx, "starting step %d currPct %.7f target Pct %.14f realPct %.14f", val, currPct, pct, realPct) - if err != nil { - return errors.Wrap(err, "couldn't find servo PWM resolution") - } - if realPct != currPct { - if realPct == pct { - s.pwmRes = uint(val) - } else { - val = int(math.Abs(math.Round(1 / (currPct - realPct)))) - s.logger.CDebugf(ctx, "the servo moved but the expected duty cyle (%.7f) is not the one reported (%.7f) we are guessing %d", - pct, realPct, val) - s.pwmRes = uint(val) - } - break - } - } - return nil -} - -// Move moves the servo to the given angle (0-180 degrees) -// This will block until done or a new operation cancels this one. -func (s *servoGPIO) Move(ctx context.Context, ang uint32, extra map[string]interface{}) error { - ctx, done := s.opMgr.New(ctx) - defer done() - - angle := float64(ang) - - if angle < s.minDeg { - angle = s.minDeg - } - if angle > s.maxDeg { - angle = s.maxDeg - } - - pct := mapDegToDutyCylePct(s.minUs, s.maxUs, s.minDeg, s.maxDeg, angle, s.frequency) - if s.pwmRes != 0 { - realTick := math.Round(pct * float64(s.pwmRes)) - pct = realTick / float64(s.pwmRes) - } - - if err := s.pin.SetPWM(ctx, pct, nil); err != nil { - return errors.Wrap(err, "couldn't move the servo") - } - - s.currPct = pct - return nil -} - -// Position returns the current set angle (degrees) of the servo. -func (s *servoGPIO) Position(ctx context.Context, extra map[string]interface{}) (uint32, error) { - pct, err := s.pin.PWM(ctx, nil) - if err != nil { - return 0, errors.Wrap(err, "couldn't get servo pin duty cycle") - } - // Since Stop() sets the dutyCycle to 0.0 in order to maintain the position of the servo, - // we are setting the dutyCycle back to the last known dutyCycle to prevent the servo - // from going back to zero position. - if pct == 0 { - pct = s.currPct - err := s.pin.SetPWM(ctx, pct, extra) - if err != nil { - return 0, errors.Wrap(err, "couldn't get servo pin duty cycle") - } - } - - return uint32(mapDutyCylePctToDeg(s.minUs, s.maxUs, s.minDeg, s.maxDeg, pct, s.frequency)), nil -} - -// Stop stops the servo. It is assumed the servo stops immediately. -func (s *servoGPIO) Stop(ctx context.Context, extra map[string]interface{}) error { - ctx, done := s.opMgr.New(ctx) - defer done() - // Turning the pin all the way off (i.e., setting the duty cycle to 0%) will cut power to the - // motor. If you wanted to send it to position 0, you should set it to `minUs` instead. - if err := s.pin.SetPWM(ctx, 0.0, nil); err != nil { - return errors.Wrap(err, "couldn't stop servo") - } - return nil -} - -// IsMoving returns whether or not the servo is moving. -func (s *servoGPIO) IsMoving(ctx context.Context) (bool, error) { - res, err := s.pin.PWM(ctx, nil) - if err != nil { - return false, errors.Wrap(err, "servo error while checking if moving") - } - if int(res) == 0 { - return false, nil - } - return s.opMgr.OpRunning(), nil -} diff --git a/components/servo/gpio/servo_test.go b/components/servo/gpio/servo_test.go deleted file mode 100644 index 00401e42f70..00000000000 --- a/components/servo/gpio/servo_test.go +++ /dev/null @@ -1,207 +0,0 @@ -// Package gpio implements a pin based servo -package gpio - -import ( - "context" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -func ptr[T any](v T) *T { - return &v -} - -func TestValidate(t *testing.T) { - cfg := servoConfig{ - Pin: "a", - Board: "b", - MinDeg: ptr(1.5), - MaxDeg: ptr(90.0), - StartPos: ptr(3.5), - MinWidthUs: ptr(uint(501)), - MaxWidthUs: ptr(uint(2499)), - } - - deps, err := cfg.Validate("test") - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldContain, "b") - test.That(t, len(deps), test.ShouldEqual, 1) - - cfg.MinDeg = ptr(-1.5) - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "min_angle_deg cannot be lower than 0") - cfg.MinDeg = ptr(1.5) - - cfg.MaxDeg = ptr(90.0) - - cfg.MinWidthUs = ptr(uint(450)) - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "min_width_us cannot be lower than 500") - cfg.MinWidthUs = ptr(uint(501)) - - cfg.MaxWidthUs = ptr(uint(2520)) - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "max_width_us cannot be higher than 2500") - cfg.MaxWidthUs = ptr(uint(2499)) - - cfg.StartPos = ptr(91.0) - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), - test.ShouldContainSubstring, - "starting_position_deg should be between minimum (1.5) and maximum (90.0) positions") - - cfg.StartPos = ptr(1.0) - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), - test.ShouldContainSubstring, - "starting_position_deg should be between minimum (1.5) and maximum (90.0) positions") - - cfg.StartPos = ptr(199.0) - cfg.MaxDeg = nil - cfg.MinDeg = nil - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), - test.ShouldContainSubstring, - "starting_position_deg should be between minimum (0.0) and maximum (180.0) positions") - - cfg.StartPos = ptr(0.0) - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldBeNil) - - cfg.Board = "" - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "board") - cfg.Board = "b" - - cfg.Pin = "" - _, err = cfg.Validate("test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "pin") -} - -func setupDependencies(t *testing.T) resource.Dependencies { - t.Helper() - - deps := make(resource.Dependencies) - board1 := inject.NewBoard("mock") - - innerTick1, innerTick2 := 0, 0 - scale1, scale2 := 255, 4095 - - pin0 := &inject.GPIOPin{} - pin0.PWMFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - pct := float64(innerTick1) / float64(scale1) - return pct, nil - } - pin0.PWMFreqFunc = func(ctx context.Context, extra map[string]interface{}) (uint, error) { - return 50, nil - } - pin0.SetPWMFunc = func(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - innerTick1 = utils.ScaleByPct(scale1, dutyCyclePct) - return nil - } - pin0.SetPWMFreqFunc = func(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - return nil - } - - pin1 := &inject.GPIOPin{} - pin1.PWMFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - pct := float64(innerTick2) / float64(scale2) - return pct, nil - } - pin1.PWMFreqFunc = func(ctx context.Context, extra map[string]interface{}) (uint, error) { - return 50, nil - } - pin1.SetPWMFunc = func(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - innerTick2 = utils.ScaleByPct(scale2, dutyCyclePct) - return nil - } - pin1.SetPWMFreqFunc = func(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - return nil - } - - board1.GPIOPinByNameFunc = func(name string) (board.GPIOPin, error) { - switch name { - case "0": - return pin0, nil - case "1": - return pin1, nil - default: - return nil, errors.New("bad pin") - } - } - deps[board.Named("mock")] = board1 - return deps -} - -func TestServoMove(t *testing.T) { - logger := logging.NewTestLogger(t) - deps := setupDependencies(t) - - ctx := context.Background() - - conf := servoConfig{ - Pin: "0", - Board: "mock", - StartPos: ptr(0.0), - } - - cfg := resource.Config{ - ConvertedAttributes: &conf, - } - servo, err := newGPIOServo(ctx, deps, cfg, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, servo, test.ShouldNotBeNil) - realServo, ok := servo.(*servoGPIO) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, realServo.pwmRes, test.ShouldEqual, 255) - pos, err := realServo.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - - err = realServo.Move(ctx, 63, nil) - test.That(t, err, test.ShouldBeNil) - pos, err = realServo.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 61) - - conf = servoConfig{ - Pin: "1", - Board: "mock", - StartPos: ptr(0.0), - } - - cfg = resource.Config{ - ConvertedAttributes: &conf, - } - servo, err = newGPIOServo(ctx, deps, cfg, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, servo, test.ShouldNotBeNil) - realServo, ok = servo.(*servoGPIO) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, realServo.pwmRes, test.ShouldEqual, 4095) - pos, err = realServo.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 0) - - err = realServo.Move(ctx, 63, nil) - test.That(t, err, test.ShouldBeNil) - pos, err = realServo.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 63) -} diff --git a/components/servo/register/register.go b/components/servo/register/register.go deleted file mode 100644 index 2b434fd3819..00000000000 --- a/components/servo/register/register.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package register registers all relevant servos -package register - -import ( - // for servos. - _ "go.viam.com/rdk/components/servo/fake" - _ "go.viam.com/rdk/components/servo/gpio" -) diff --git a/components/servo/server.go b/components/servo/server.go deleted file mode 100644 index 4338b263de8..00000000000 --- a/components/servo/server.go +++ /dev/null @@ -1,81 +0,0 @@ -// Package servo contains a gRPC based servo service server -package servo - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/component/servo/v1" - - "go.viam.com/rdk/operation" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -type serviceServer struct { - pb.UnimplementedServoServiceServer - coll resource.APIResourceCollection[Servo] -} - -// NewRPCServiceServer constructs a servo gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Servo]) interface{} { - return &serviceServer{coll: coll} -} - -func (server *serviceServer) Move(ctx context.Context, req *pb.MoveRequest) (*pb.MoveResponse, error) { - operation.CancelOtherWithLabel(ctx, req.GetName()) - servo, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return &pb.MoveResponse{}, servo.Move(ctx, req.GetAngleDeg(), req.Extra.AsMap()) -} - -func (server *serviceServer) GetPosition( - ctx context.Context, - req *pb.GetPositionRequest, -) (*pb.GetPositionResponse, error) { - servo, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - angleDeg, err := servo.Position(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetPositionResponse{PositionDeg: angleDeg}, nil -} - -func (server *serviceServer) Stop(ctx context.Context, req *pb.StopRequest) (*pb.StopResponse, error) { - operation.CancelOtherWithLabel(ctx, req.Name) - servo, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return &pb.StopResponse{}, servo.Stop(ctx, req.Extra.AsMap()) -} - -// IsMoving queries of a component is in motion. -func (server *serviceServer) IsMoving(ctx context.Context, req *pb.IsMovingRequest) (*pb.IsMovingResponse, error) { - servo, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - moving, err := servo.IsMoving(ctx) - if err != nil { - return nil, err - } - return &pb.IsMovingResponse{IsMoving: moving}, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - servo, err := server.coll.Resource(req.GetName()) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, servo, req) -} diff --git a/components/servo/server_test.go b/components/servo/server_test.go deleted file mode 100644 index 83813c7ec01..00000000000 --- a/components/servo/server_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package servo_test - -import ( - "context" - "testing" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/servo/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -var ( - errMoveFailed = errors.New("move failed") - errPositionUnreadable = errors.New("current angle not readable") - errStopFailed = errors.New("stop failed") -) - -func newServer() (pb.ServoServiceServer, *inject.Servo, *inject.Servo, error) { - injectServo := &inject.Servo{} - injectServo2 := &inject.Servo{} - resourceMap := map[resource.Name]servo.Servo{ - servo.Named(testServoName): injectServo, - servo.Named(failServoName): injectServo2, - } - injectSvc, err := resource.NewAPIResourceCollection(servo.API, resourceMap) - if err != nil { - return nil, nil, nil, err - } - return servo.NewRPCServiceServer(injectSvc).(pb.ServoServiceServer), injectServo, injectServo2, nil -} - -func TestServoMove(t *testing.T) { - servoServer, workingServo, failingServo, err := newServer() - test.That(t, err, test.ShouldBeNil) - - var actualExtra map[string]interface{} - - workingServo.MoveFunc = func(ctx context.Context, angle uint32, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - failingServo.MoveFunc = func(ctx context.Context, angle uint32, extra map[string]interface{}) error { - return errMoveFailed - } - - extra := map[string]interface{}{"foo": "Move"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := pb.MoveRequest{Name: testServoName, Extra: ext} - resp, err := servoServer.Move(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, extra) - - req = pb.MoveRequest{Name: failServoName} - resp, err = servoServer.Move(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errMoveFailed.Error()) - - req = pb.MoveRequest{Name: fakeServoName} - resp, err = servoServer.Move(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestServoGetPosition(t *testing.T) { - servoServer, workingServo, failingServo, _ := newServer() - - var actualExtra map[string]interface{} - - workingServo.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - actualExtra = extra - return 20, nil - } - failingServo.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - return 0, errPositionUnreadable - } - - extra := map[string]interface{}{"foo": "Move"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := pb.GetPositionRequest{Name: testServoName, Extra: ext} - resp, err := servoServer.GetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, extra) - - req = pb.GetPositionRequest{Name: failServoName} - resp, err = servoServer.GetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errPositionUnreadable.Error()) - - req = pb.GetPositionRequest{Name: fakeServoName} - resp, err = servoServer.GetPosition(context.Background(), &req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestServoStop(t *testing.T) { - servoServer, workingServo, failingServo, _ := newServer() - - var actualExtra map[string]interface{} - - workingServo.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - actualExtra = extra - return nil - } - failingServo.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - return errStopFailed - } - - extra := map[string]interface{}{"foo": "Move"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := pb.StopRequest{Name: testServoName, Extra: ext} - _, err = servoServer.Stop(context.Background(), &req) - test.That(t, err, test.ShouldBeNil) - test.That(t, actualExtra, test.ShouldResemble, extra) - - req = pb.StopRequest{Name: failServoName} - _, err = servoServer.Stop(context.Background(), &req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errStopFailed.Error()) - - req = pb.StopRequest{Name: fakeServoName} - _, err = servoServer.Stop(context.Background(), &req) - test.That(t, err, test.ShouldNotBeNil) -} diff --git a/components/servo/servo.go b/components/servo/servo.go deleted file mode 100644 index 56cff2e5201..00000000000 --- a/components/servo/servo.go +++ /dev/null @@ -1,91 +0,0 @@ -package servo - -import ( - "context" - - pb "go.viam.com/api/component/servo/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Servo]{ - Status: resource.StatusFunc(CreateStatus), - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterServoServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.ServoService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: position.String(), - }, newPositionCollector) -} - -// SubtypeName is a constant that identifies the component resource API string "servo". -const SubtypeName = "servo" - -// API is a variable that identifies the component resource API. -var API = resource.APINamespaceRDK.WithComponentType(SubtypeName) - -// A Servo represents a physical servo connected to a board. -// -// Move example: -// -// // Move the servo from its origin to the desired angle of 30 degrees. -// myServoComponent.Move(context.Background(), 30, nil) -// -// Position example: -// -// // Get the current set angle of the servo. -// pos1, err := myServoComponent.Position(context.Background(), nil) -// -// // Move the servo from its origin to the desired angle of 20 degrees. -// myServoComponent.Move(context.Background(), 20, nil) -// -// // Get the current set angle of the servo. -// pos2, err := myServoComponent.Position(context.Background(), nil) -// -// logger.Info("Position 1: ", pos1) -// logger.Info("Position 2: ", pos2) -type Servo interface { - resource.Resource - resource.Actuator - - // Move moves the servo to the given angle (0-180 degrees) - // This will block until done or a new operation cancels this one - Move(ctx context.Context, angleDeg uint32, extra map[string]interface{}) error - - // Position returns the current set angle (degrees) of the servo. - Position(ctx context.Context, extra map[string]interface{}) (uint32, error) -} - -// Named is a helper for getting the named Servo's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named servo from the given Robot. -func FromRobot(r robot.Robot, name string) (Servo, error) { - return robot.ResourceFromRobot[Servo](r, Named(name)) -} - -// NamesFromRobot is a helper for getting all servo names from the given Robot. -func NamesFromRobot(r robot.Robot) []string { - return robot.NamesByAPI(r, API) -} - -// CreateStatus creates a status from the servo. -func CreateStatus(ctx context.Context, s Servo) (*pb.Status, error) { - position, err := s.Position(ctx, nil) - if err != nil { - return nil, err - } - isMoving, err := s.IsMoving(ctx) - if err != nil { - return nil, err - } - return &pb.Status{PositionDeg: position, IsMoving: isMoving}, nil -} diff --git a/components/servo/servo_test.go b/components/servo/servo_test.go deleted file mode 100644 index 7bbfeeb70f1..00000000000 --- a/components/servo/servo_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package servo_test - -import ( - "context" - "testing" - - "github.com/pkg/errors" - pb "go.viam.com/api/component/servo/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func TestCreateStatus(t *testing.T) { - status := &pb.Status{PositionDeg: uint32(8), IsMoving: true} - - injectServo := &inject.Servo{} - injectServo.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - return status.PositionDeg, nil - } - injectServo.IsMovingFunc = func(context.Context) (bool, error) { - return true, nil - } - - t.Run("working", func(t *testing.T) { - status1, err := servo.CreateStatus(context.Background(), injectServo) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status) - - resourceAPI, ok, err := resource.LookupAPIRegistration[servo.Servo](servo.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - status2, err := resourceAPI.Status(context.Background(), injectServo) - test.That(t, err, test.ShouldBeNil) - test.That(t, status2, test.ShouldResemble, status) - }) - - t.Run("not moving", func(t *testing.T) { - injectServo.IsMovingFunc = func(context.Context) (bool, error) { - return false, nil - } - - status2 := &pb.Status{PositionDeg: uint32(8), IsMoving: false} - status1, err := servo.CreateStatus(context.Background(), injectServo) - test.That(t, err, test.ShouldBeNil) - test.That(t, status1, test.ShouldResemble, status2) - }) - - t.Run("fail on Position", func(t *testing.T) { - errFail := errors.New("can't get position") - injectServo.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - return 0, errFail - } - _, err := servo.CreateStatus(context.Background(), injectServo) - test.That(t, err, test.ShouldBeError, errFail) - }) -} diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index d784444f1d1..00000000000 --- a/config/config_test.go +++ /dev/null @@ -1,1220 +0,0 @@ -package config_test - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "encoding/json" - "fmt" - "net" - "os" - "path/filepath" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/lestrrat-go/jwx/jwk" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/jwks" - "go.viam.com/utils/pexec" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/board" - fakeboard "go.viam.com/rdk/components/board/fake" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/encoder/incremental" - fakemotor "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/shell" - "go.viam.com/rdk/spatialmath" - rutils "go.viam.com/rdk/utils" -) - -func TestConfigRobot(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/robot.json", logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, cfg.Components, test.ShouldHaveLength, 3) - test.That(t, len(cfg.Remotes), test.ShouldEqual, 2) - test.That(t, cfg.Remotes[0].Name, test.ShouldEqual, "one") - test.That(t, cfg.Remotes[0].Address, test.ShouldEqual, "foo") - test.That(t, cfg.Remotes[1].Name, test.ShouldEqual, "two") - test.That(t, cfg.Remotes[1].Address, test.ShouldEqual, "bar") - - var foundArm, foundCam bool - for _, comp := range cfg.Components { - if comp.API == arm.API && comp.Model == resource.DefaultModelFamily.WithModel("ur") { - foundArm = true - } - if comp.API == camera.API && comp.Model == resource.DefaultModelFamily.WithModel("url") { - foundCam = true - } - } - test.That(t, foundArm, test.ShouldBeTrue) - test.That(t, foundCam, test.ShouldBeTrue) - - // test that gripper geometry is being added correctly - component := cfg.FindComponent("pieceGripper") - bc, _ := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{4, 5, 6}), r3.Vector{1, 2, 3}, "") - newBc, err := component.Frame.Geometry.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, newBc, test.ShouldResemble, bc) -} - -func TestConfig3(t *testing.T) { - logger := logging.NewTestLogger(t) - - test.That(t, os.Setenv("TEST_THING_FOO", "5"), test.ShouldBeNil) - cfg, err := config.Read(context.Background(), "data/config3.json", logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(cfg.Components), test.ShouldEqual, 4) - test.That(t, cfg.Components[0].Attributes.Int("foo", 0), test.ShouldEqual, 5) - test.That(t, cfg.Components[0].Attributes.Bool("foo2", false), test.ShouldEqual, true) - test.That(t, cfg.Components[0].Attributes.Bool("foo3", false), test.ShouldEqual, false) - test.That(t, cfg.Components[0].Attributes.Bool("xxxx", true), test.ShouldEqual, true) - test.That(t, cfg.Components[0].Attributes.Bool("xxxx", false), test.ShouldEqual, false) - test.That(t, cfg.Components[0].Attributes.String("foo4"), test.ShouldEqual, "no") - test.That(t, cfg.Components[0].Attributes.String("xxxx"), test.ShouldEqual, "") - test.That(t, cfg.Components[0].Attributes.Has("foo"), test.ShouldEqual, true) - test.That(t, cfg.Components[0].Attributes.Has("xxxx"), test.ShouldEqual, false) - test.That(t, cfg.Components[0].Attributes.Float64("bar5", 1.1), test.ShouldEqual, 5.17) - test.That(t, cfg.Components[0].Attributes.Float64("bar5-no", 1.1), test.ShouldEqual, 1.1) - - test.That(t, cfg.Components[1].ConvertedAttributes, test.ShouldResemble, &fakeboard.Config{ - AnalogReaders: []board.AnalogReaderConfig{ - {Name: "analog1", Pin: "0"}, - }, - DigitalInterrupts: []board.DigitalInterruptConfig{ - {Name: "encoder", Pin: "14"}, - }, - }) - - test.That(t, cfg.Components[2].ConvertedAttributes, test.ShouldResemble, &fakemotor.Config{ - Pins: fakemotor.PinConfig{ - Direction: "io17", - PWM: "io18", - }, - Encoder: "encoder1", - MaxPowerPct: 0.5, - TicksPerRotation: 10000, - }) - test.That(t, cfg.Components[2].AssociatedResourceConfigs, test.ShouldHaveLength, 1) - test.That(t, cfg.Components[2].AssociatedResourceConfigs[0], test.ShouldResemble, resource.AssociatedResourceConfig{ - API: resource.APINamespaceRDK.WithServiceType("data_manager"), - Attributes: rutils.AttributeMap{ - "hi": 1.1, - "friend": 2.2, - }, - }) - - test.That(t, cfg.Components[3].ConvertedAttributes, test.ShouldResemble, &incremental.Config{ - Pins: incremental.Pins{ - A: "encoder-steering-b", - B: "encoder-steering-a", - }, - BoardName: "board1", - }) - - test.That(t, cfg.Network.Sessions.HeartbeatWindow, test.ShouldEqual, 5*time.Second) - test.That(t, cfg.Remotes, test.ShouldHaveLength, 1) - test.That(t, cfg.Remotes[0].ConnectionCheckInterval, test.ShouldEqual, 12*time.Second) - test.That(t, cfg.Remotes[0].ReconnectInterval, test.ShouldEqual, 3*time.Second) - test.That(t, cfg.Remotes[0].AssociatedResourceConfigs, test.ShouldHaveLength, 2) - test.That(t, cfg.Remotes[0].AssociatedResourceConfigs[0], test.ShouldResemble, resource.AssociatedResourceConfig{ - API: resource.APINamespaceRDK.WithServiceType("data_manager"), - Attributes: rutils.AttributeMap{ - "hi": 3.3, - "friend": 4.4, - }, - RemoteName: "rem1", - }) - test.That(t, cfg.Remotes[0].AssociatedResourceConfigs[1], test.ShouldResemble, resource.AssociatedResourceConfig{ - API: resource.APINamespaceRDK.WithServiceType("some_type"), - Attributes: rutils.AttributeMap{ - "hi": 5.5, - "friend": 6.6, - }, - RemoteName: "rem1", - }) -} - -func TestConfigWithLogDeclarations(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/config_with_log.json", logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(cfg.Components), test.ShouldEqual, 4) - // The board log level is explicitly configured as `Info`. - test.That(t, cfg.Components[0].Name, test.ShouldEqual, "board1") - test.That(t, cfg.Components[0].LogConfiguration.Level, test.ShouldEqual, logging.INFO) - - // The left motor is explicitly configured as `debug`. Note the lower case. - test.That(t, cfg.Components[1].Name, test.ShouldEqual, "left_motor") - test.That(t, cfg.Components[1].LogConfiguration.Level, test.ShouldEqual, logging.DEBUG) - - // The right motor is left unconfigured. The default log level is `Info`. However, the global - // log configure for builtin fake motors would apply for a log level of `warn`. This "overlayed" - // log level is not applied at config parsing time. - test.That(t, cfg.Components[2].Name, test.ShouldEqual, "right_motor") - test.That(t, cfg.Components[2].LogConfiguration.Level, test.ShouldEqual, logging.INFO) - - // The wheeled base is also left unconfigured. The global log configuration for things - // implementing the `base` API is `error`. This "overlayed" log level is not applied at config - // parsing time. - test.That(t, cfg.Components[3].Name, test.ShouldEqual, "wheeley") - test.That(t, cfg.Components[3].LogConfiguration.Level, test.ShouldEqual, logging.INFO) - - test.That(t, len(cfg.Services), test.ShouldEqual, 2) - // The slam service has a log level of `WARN`. Note the upper case. - test.That(t, cfg.Services[0].Name, test.ShouldEqual, "slam1") - test.That(t, cfg.Services[0].LogConfiguration.Level, test.ShouldEqual, logging.WARN) - - // The data manager service is left unconfigured. - test.That(t, cfg.Services[1].Name, test.ShouldEqual, "dm") - test.That(t, cfg.Services[1].LogConfiguration.Level, test.ShouldEqual, logging.INFO) - - test.That(t, len(cfg.GlobalLogConfig), test.ShouldEqual, 2) - // The first global configuration is to default `base`s to `error`. - test.That(t, cfg.GlobalLogConfig[0].API.String(), test.ShouldEqual, "rdk:component:base") - test.That(t, cfg.GlobalLogConfig[0].Level, test.ShouldEqual, logging.ERROR) - - // The second global configuration is to default `motor`s of the builtin fake variety to `warn`. - test.That(t, cfg.GlobalLogConfig[1].API.String(), test.ShouldEqual, "rdk:component:motor") - test.That(t, cfg.GlobalLogConfig[1].Level, test.ShouldEqual, logging.WARN) -} - -func TestConfigEnsure(t *testing.T) { - logger := logging.NewTestLogger(t) - var emptyConfig config.Config - test.That(t, emptyConfig.Ensure(false, logger), test.ShouldBeNil) - - invalidCloud := config.Config{ - Cloud: &config.Cloud{}, - } - err := invalidCloud.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `cloud`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "id") - invalidCloud.Cloud.ID = "some_id" - err = invalidCloud.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "secret") - err = invalidCloud.Ensure(true, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "fqdn") - invalidCloud.Cloud.Secret = "my_secret" - test.That(t, invalidCloud.Ensure(false, logger), test.ShouldBeNil) - test.That(t, invalidCloud.Ensure(true, logger), test.ShouldNotBeNil) - invalidCloud.Cloud.Secret = "" - invalidCloud.Cloud.FQDN = "wooself" - err = invalidCloud.Ensure(true, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "local_fqdn") - invalidCloud.Cloud.LocalFQDN = "yeeself" - - invalidRemotes := config.Config{ - DisablePartialStart: true, - Remotes: []config.Remote{{}}, - } - err = invalidRemotes.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `remotes.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - invalidRemotes.Remotes[0] = config.Remote{ - Name: "foo", - } - err = invalidRemotes.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "address") - invalidRemotes.Remotes[0] = config.Remote{ - Name: "foo", - Address: "bar", - } - test.That(t, invalidRemotes.Ensure(false, logger), test.ShouldBeNil) - - invalidComponents := config.Config{ - DisablePartialStart: true, - Components: []resource.Config{{}}, - } - err = invalidComponents.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `components.0`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - invalidComponents.Components[0] = resource.Config{ - Name: "foo", - API: base.API, - Model: fakeModel, - } - - test.That(t, invalidComponents.Ensure(false, logger), test.ShouldBeNil) - - c1 := resource.Config{ - Name: "c1", - API: base.API, - Model: resource.DefaultModelFamily.WithModel("c1"), - } - c2 := resource.Config{ - Name: "c2", - API: base.API, - DependsOn: []string{"c1"}, - Model: resource.DefaultModelFamily.WithModel("c2"), - } - c3 := resource.Config{ - Name: "c3", - API: base.API, - DependsOn: []string{"c1", "c2"}, - Model: resource.DefaultModelFamily.WithModel("c3"), - } - c4 := resource.Config{ - Name: "c4", - API: base.API, - DependsOn: []string{"c1", "c3"}, - Model: resource.DefaultModelFamily.WithModel("c4"), - } - c5 := resource.Config{ - Name: "c5", - API: base.API, - DependsOn: []string{"c2", "c4"}, - Model: resource.DefaultModelFamily.WithModel("c5"), - } - c6 := resource.Config{ - Name: "c6", - API: base.API, - Model: resource.DefaultModelFamily.WithModel("c6"), - } - c7 := resource.Config{ - Name: "c7", - API: base.API, - DependsOn: []string{"c6", "c4"}, - Model: resource.DefaultModelFamily.WithModel("c7"), - } - components := config.Config{ - DisablePartialStart: true, - Components: []resource.Config{c7, c6, c5, c3, c4, c1, c2}, - } - err = components.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - - invalidProcesses := config.Config{ - DisablePartialStart: true, - Processes: []pexec.ProcessConfig{{}}, - } - err = invalidProcesses.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `processes.0`) - test.That(t, err.Error(), test.ShouldContainSubstring, `"id" is required`) - invalidProcesses = config.Config{ - DisablePartialStart: true, - Processes: []pexec.ProcessConfig{{ID: "bar"}}, - } - err = invalidProcesses.Ensure(false, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, `"name" is required`) - invalidProcesses = config.Config{ - DisablePartialStart: true, - Processes: []pexec.ProcessConfig{{ID: "bar", Name: "foo"}}, - } - test.That(t, invalidProcesses.Ensure(false, logger), test.ShouldBeNil) - - invalidNetwork := config.Config{ - Network: config.NetworkConfig{ - NetworkConfigData: config.NetworkConfigData{ - TLSCertFile: "hey", - }, - }, - } - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `network`) - test.That(t, err.Error(), test.ShouldContainSubstring, `both tls`) - - invalidNetwork.Network.TLSCertFile = "" - invalidNetwork.Network.TLSKeyFile = "hey" - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `network`) - test.That(t, err.Error(), test.ShouldContainSubstring, `both tls`) - - invalidNetwork.Network.TLSCertFile = "dude" - test.That(t, invalidNetwork.Ensure(false, logger), test.ShouldBeNil) - - invalidNetwork.Network.TLSCertFile = "" - invalidNetwork.Network.TLSKeyFile = "" - test.That(t, invalidNetwork.Ensure(false, logger), test.ShouldBeNil) - - test.That(t, invalidNetwork.Network.Sessions.HeartbeatWindow, test.ShouldNotBeNil) - test.That(t, invalidNetwork.Network.Sessions.HeartbeatWindow, test.ShouldEqual, config.DefaultSessionHeartbeatWindow) - - invalidNetwork.Network.Sessions.HeartbeatWindow = time.Millisecond - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `heartbeat_window`) - test.That(t, err.Error(), test.ShouldContainSubstring, `between`) - - invalidNetwork.Network.Sessions.HeartbeatWindow = 2 * time.Minute - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `heartbeat_window`) - test.That(t, err.Error(), test.ShouldContainSubstring, `between`) - - invalidNetwork.Network.Sessions.HeartbeatWindow = 30 * time.Millisecond - test.That(t, invalidNetwork.Ensure(false, logger), test.ShouldBeNil) - - invalidNetwork.Network.BindAddress = "woop" - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `bind_address`) - test.That(t, err.Error(), test.ShouldContainSubstring, `missing port`) - - invalidNetwork.Network.BindAddress = "woop" - invalidNetwork.Network.Listener = &net.TCPListener{} - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `only set one of`) - - invalidAuthConfig := config.Config{ - Auth: config.AuthConfig{}, - } - test.That(t, invalidAuthConfig.Ensure(false, logger), test.ShouldBeNil) - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - {Type: rpc.CredentialsTypeAPIKey}, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.0`) - test.That(t, err.Error(), test.ShouldContainSubstring, `required`) - test.That(t, err.Error(), test.ShouldContainSubstring, `key`) - - validAPIKeyHandler := config.AuthHandlerConfig{ - Type: rpc.CredentialsTypeAPIKey, - Config: rutils.AttributeMap{ - "key": "foo", - }, - } - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - validAPIKeyHandler, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.1`) - test.That(t, err.Error(), test.ShouldContainSubstring, `duplicate`) - test.That(t, err.Error(), test.ShouldContainSubstring, `api-key`) - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - {Type: "unknown"}, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.1`) - test.That(t, err.Error(), test.ShouldContainSubstring, `do not know how`) - test.That(t, err.Error(), test.ShouldContainSubstring, `unknown`) - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - } - test.That(t, invalidAuthConfig.Ensure(false, logger), test.ShouldBeNil) - - validAPIKeyHandler.Config = rutils.AttributeMap{ - "keys": []string{}, - } - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.0`) - test.That(t, err.Error(), test.ShouldContainSubstring, `required`) - test.That(t, err.Error(), test.ShouldContainSubstring, `key`) - - validAPIKeyHandler.Config = rutils.AttributeMap{ - "keys": []string{"one", "two"}, - } - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - } - - test.That(t, invalidAuthConfig.Ensure(false, logger), test.ShouldBeNil) -} - -func TestConfigEnsurePartialStart(t *testing.T) { - logger := logging.NewTestLogger(t) - var emptyConfig config.Config - test.That(t, emptyConfig.Ensure(false, logger), test.ShouldBeNil) - - invalidCloud := config.Config{ - Cloud: &config.Cloud{}, - } - err := invalidCloud.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `cloud`) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "id") - invalidCloud.Cloud.ID = "some_id" - err = invalidCloud.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "secret") - err = invalidCloud.Ensure(true, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "fqdn") - invalidCloud.Cloud.Secret = "my_secret" - test.That(t, invalidCloud.Ensure(false, logger), test.ShouldBeNil) - test.That(t, invalidCloud.Ensure(true, logger), test.ShouldNotBeNil) - invalidCloud.Cloud.Secret = "" - invalidCloud.Cloud.FQDN = "wooself" - err = invalidCloud.Ensure(true, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "local_fqdn") - invalidCloud.Cloud.LocalFQDN = "yeeself" - - test.That(t, invalidCloud.Ensure(true, logger), test.ShouldBeNil) - - invalidRemotes := config.Config{ - Remotes: []config.Remote{{}}, - } - err = invalidRemotes.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - invalidRemotes.Remotes[0].Name = "foo" - err = invalidRemotes.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - invalidRemotes.Remotes[0].Address = "bar" - test.That(t, invalidRemotes.Ensure(false, logger), test.ShouldBeNil) - - invalidComponents := config.Config{ - Components: []resource.Config{{}}, - } - err = invalidComponents.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - invalidComponents.Components[0].Name = "foo" - - c1 := resource.Config{Name: "c1"} - c2 := resource.Config{Name: "c2", DependsOn: []string{"c1"}} - c3 := resource.Config{Name: "c3", DependsOn: []string{"c1", "c2"}} - c4 := resource.Config{Name: "c4", DependsOn: []string{"c1", "c3"}} - c5 := resource.Config{Name: "c5", DependsOn: []string{"c2", "c4"}} - c6 := resource.Config{Name: "c6"} - c7 := resource.Config{Name: "c7", DependsOn: []string{"c6", "c4"}} - components := config.Config{ - Components: []resource.Config{c7, c6, c5, c3, c4, c1, c2}, - } - err = components.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - - invalidProcesses := config.Config{ - Processes: []pexec.ProcessConfig{{}}, - } - err = invalidProcesses.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - invalidProcesses.Processes[0].Name = "foo" - test.That(t, invalidProcesses.Ensure(false, logger), test.ShouldBeNil) - - cloudErr := "bad cloud err doing validation" - invalidModules := config.Config{ - Modules: []config.Module{{ - Name: "testmodErr", - ExePath: ".", - LogLevel: "debug", - Type: config.ModuleTypeRegistry, - ModuleID: "mod:testmodErr", - Environment: map[string]string{}, - Status: &config.AppValidationStatus{ - Error: cloudErr, - }, - }}, - } - invalidModules.DisablePartialStart = true - err = invalidModules.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, cloudErr) - - invalidModules.DisablePartialStart = false - err = invalidModules.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - - invalidPackges := config.Config{ - Packages: []config.PackageConfig{{ - Name: "testPackage", - Type: config.PackageTypeMlModel, - Package: "hi/package/test", - Status: &config.AppValidationStatus{ - Error: cloudErr, - }, - }}, - } - - invalidModules.DisablePartialStart = true - err = invalidModules.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, cloudErr) - - invalidModules.DisablePartialStart = false - err = invalidPackges.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) - - invalidNetwork := config.Config{ - Network: config.NetworkConfig{ - NetworkConfigData: config.NetworkConfigData{ - TLSCertFile: "hey", - }, - }, - } - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `network`) - test.That(t, err.Error(), test.ShouldContainSubstring, `both tls`) - - invalidNetwork.Network.TLSCertFile = "" - invalidNetwork.Network.TLSKeyFile = "hey" - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `network`) - test.That(t, err.Error(), test.ShouldContainSubstring, `both tls`) - - invalidNetwork.Network.TLSCertFile = "dude" - test.That(t, invalidNetwork.Ensure(false, logger), test.ShouldBeNil) - - invalidNetwork.Network.TLSCertFile = "" - invalidNetwork.Network.TLSKeyFile = "" - test.That(t, invalidNetwork.Ensure(false, logger), test.ShouldBeNil) - - invalidNetwork.Network.BindAddress = "woop" - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `bind_address`) - test.That(t, err.Error(), test.ShouldContainSubstring, `missing port`) - - invalidNetwork.Network.BindAddress = "woop" - invalidNetwork.Network.Listener = &net.TCPListener{} - err = invalidNetwork.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `only set one of`) - - invalidAuthConfig := config.Config{ - Auth: config.AuthConfig{}, - } - test.That(t, invalidAuthConfig.Ensure(false, logger), test.ShouldBeNil) - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - {Type: rpc.CredentialsTypeAPIKey}, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.0`) - test.That(t, err.Error(), test.ShouldContainSubstring, `required`) - test.That(t, err.Error(), test.ShouldContainSubstring, `key`) - - validAPIKeyHandler := config.AuthHandlerConfig{ - Type: rpc.CredentialsTypeAPIKey, - Config: rutils.AttributeMap{ - "key": "foo", - }, - } - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - validAPIKeyHandler, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.1`) - test.That(t, err.Error(), test.ShouldContainSubstring, `duplicate`) - test.That(t, err.Error(), test.ShouldContainSubstring, `api-key`) - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - {Type: "unknown"}, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.1`) - test.That(t, err.Error(), test.ShouldContainSubstring, `do not know how`) - test.That(t, err.Error(), test.ShouldContainSubstring, `unknown`) - - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - } - test.That(t, invalidAuthConfig.Ensure(false, logger), test.ShouldBeNil) - - validAPIKeyHandler.Config = rutils.AttributeMap{ - "keys": []string{}, - } - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - } - err = invalidAuthConfig.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `auth.handlers.0`) - test.That(t, err.Error(), test.ShouldContainSubstring, `required`) - test.That(t, err.Error(), test.ShouldContainSubstring, `key`) - - validAPIKeyHandler.Config = rutils.AttributeMap{ - "keys": []string{"one", "two"}, - } - invalidAuthConfig.Auth.Handlers = []config.AuthHandlerConfig{ - validAPIKeyHandler, - } - - test.That(t, invalidAuthConfig.Ensure(false, logger), test.ShouldBeNil) -} - -func TestRemoteValidate(t *testing.T) { - t.Run("remote invalid name", func(t *testing.T) { - lc := &referenceframe.LinkConfig{ - Parent: "parent", - } - validRemote := config.Remote{ - Name: "foo-_remote", - Address: "address", - Frame: lc, - } - - _, err := validRemote.Validate("path") - test.That(t, err, test.ShouldBeNil) - - validRemote = config.Remote{ - Name: "foo.remote", - Address: "address", - Frame: lc, - } - _, err = validRemote.Validate("path") - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - "must start with a letter or number and must only contain letters, numbers, dashes, and underscores", - ) - }) -} - -func TestCopyOnlyPublicFields(t *testing.T) { - t.Run("copy sample config", func(t *testing.T) { - content, err := os.ReadFile("data/robot.json") - test.That(t, err, test.ShouldBeNil) - var cfg config.Config - json.Unmarshal(content, &cfg) - - cfgCopy, err := cfg.CopyOnlyPublicFields() - test.That(t, err, test.ShouldBeNil) - - test.That(t, *cfgCopy, test.ShouldResemble, cfg) - }) - - t.Run("should not copy unexported json fields", func(t *testing.T) { - cfg := &config.Config{ - Cloud: &config.Cloud{ - TLSCertificate: "abc", - }, - Network: config.NetworkConfig{ - NetworkConfigData: config.NetworkConfigData{ - TLSConfig: &tls.Config{ - Time: time.Now().UTC, - }, - }, - }, - } - - cfgCopy, err := cfg.CopyOnlyPublicFields() - test.That(t, err, test.ShouldBeNil) - - test.That(t, cfgCopy.Cloud.TLSCertificate, test.ShouldEqual, cfg.Cloud.TLSCertificate) - test.That(t, cfgCopy.Network.TLSConfig, test.ShouldBeNil) - }) -} - -func TestNewTLSConfig(t *testing.T) { - for _, tc := range []struct { - TestName string - Config *config.Config - HasTLSConfig bool - }{ - {TestName: "no cloud", Config: &config.Config{}, HasTLSConfig: false}, - {TestName: "cloud but no cert", Config: &config.Config{Cloud: &config.Cloud{TLSCertificate: ""}}, HasTLSConfig: false}, - {TestName: "cloud and cert", Config: &config.Config{Cloud: &config.Cloud{TLSCertificate: "abc"}}, HasTLSConfig: true}, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed := config.NewTLSConfig(tc.Config) - if tc.HasTLSConfig { - test.That(t, observed.MinVersion, test.ShouldEqual, tls.VersionTLS12) - } else { - test.That(t, observed, test.ShouldResemble, &config.TLSConfig{}) - } - }) - } -} - -func TestUpdateCert(t *testing.T) { - t.Run("cert update", func(t *testing.T) { - cfg := &config.Config{ - Cloud: &config.Cloud{ - TLSCertificate: `-----BEGIN CERTIFICATE----- -MIIBCzCBtgIJAIuXZJ6ZiHraMA0GCSqGSIb3DQEBCwUAMA0xCzAJBgNVBAYTAnVz -MB4XDTIyMDQwNTE5MTMzNVoXDTIzMDQwNTE5MTMzNVowDTELMAkGA1UEBhMCdXMw -XDANBgkqhkiG9w0BAQEFAANLADBIAkEAyiHLgbZFf5UNAue0HAdQfv1Z15n8ldkI -bi4Owm5Iwb9IGGdkQNniEgveue536vV/ugAdt8ZxLuM1vzYFSApxXwIDAQABMA0G -CSqGSIb3DQEBCwUAA0EAOYH+xj8NuneL6w5D/FlW0+qUwBaS+/J3nL+PW1MQqjs8 -1AHgPDxOtY7dUXK2E8SYia75JjtK9/FnpaFVHdQ9jQ== ------END CERTIFICATE-----`, - TLSPrivateKey: `-----BEGIN PRIVATE KEY----- -MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAyiHLgbZFf5UNAue0 -HAdQfv1Z15n8ldkIbi4Owm5Iwb9IGGdkQNniEgveue536vV/ugAdt8ZxLuM1vzYF -SApxXwIDAQABAkAEY412qI2DwqnAqWVIwoPl7fxYaRiJ7Gd5dPiPEjP0OPglB7eJ -VuSJeiPi3XSFXE9tw//Lpe2oOITF6OBCZURBAiEA7oZslGO+24+leOffb8PpceNm -EgHnAdibedkHD7ZprX8CIQDY8NASxuaEMa6nH7b9kkx/KaOo0/dOkW+sWb5PeIbs -IQIgOUd6p5/UY3F5cTFtjK9lTf4nssdWLDFSFM6zTWimtA0CIHwhFj2YN2/uaYvQ -1siyfDjKn41Lc5cuGmLYms8oHLNhAiBxeGqLlEyHdk+Trp99+nK+pFi4cj5NZSFh -ph2C/7IgjA== ------END PRIVATE KEY-----`, - }, - } - cert, err := tls.X509KeyPair([]byte(cfg.Cloud.TLSCertificate), []byte(cfg.Cloud.TLSPrivateKey)) - test.That(t, err, test.ShouldBeNil) - - tlsCfg := config.NewTLSConfig(cfg) - err = tlsCfg.UpdateCert(cfg) - test.That(t, err, test.ShouldBeNil) - - observed, err := tlsCfg.GetCertificate(&tls.ClientHelloInfo{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, observed, test.ShouldResemble, &cert) - }) - t.Run("cert error", func(t *testing.T) { - cfg := &config.Config{Cloud: &config.Cloud{TLSCertificate: "abcd", TLSPrivateKey: "abcd"}} - tlsCfg := &config.TLSConfig{} - err := tlsCfg.UpdateCert(cfg) - test.That(t, err, test.ShouldBeError, errors.New("tls: failed to find any PEM data in certificate input")) - }) -} - -func TestProcessConfig(t *testing.T) { - cloud := &config.Cloud{ - ManagedBy: "acme", - SignalingAddress: "abc", - ID: "def", - Secret: "ghi", - TLSCertificate: "", - } - cloudWTLS := &config.Cloud{ - ManagedBy: "acme", - SignalingAddress: "abc", - ID: "def", - Secret: "ghi", - TLSCertificate: `-----BEGIN CERTIFICATE----- -MIIBCzCBtgIJAIuXZJ6ZiHraMA0GCSqGSIb3DQEBCwUAMA0xCzAJBgNVBAYTAnVz -MB4XDTIyMDQwNTE5MTMzNVoXDTIzMDQwNTE5MTMzNVowDTELMAkGA1UEBhMCdXMw -XDANBgkqhkiG9w0BAQEFAANLADBIAkEAyiHLgbZFf5UNAue0HAdQfv1Z15n8ldkI -bi4Owm5Iwb9IGGdkQNniEgveue536vV/ugAdt8ZxLuM1vzYFSApxXwIDAQABMA0G -CSqGSIb3DQEBCwUAA0EAOYH+xj8NuneL6w5D/FlW0+qUwBaS+/J3nL+PW1MQqjs8 -1AHgPDxOtY7dUXK2E8SYia75JjtK9/FnpaFVHdQ9jQ== ------END CERTIFICATE-----`, - TLSPrivateKey: `-----BEGIN PRIVATE KEY----- -MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEAyiHLgbZFf5UNAue0 -HAdQfv1Z15n8ldkIbi4Owm5Iwb9IGGdkQNniEgveue536vV/ugAdt8ZxLuM1vzYF -SApxXwIDAQABAkAEY412qI2DwqnAqWVIwoPl7fxYaRiJ7Gd5dPiPEjP0OPglB7eJ -VuSJeiPi3XSFXE9tw//Lpe2oOITF6OBCZURBAiEA7oZslGO+24+leOffb8PpceNm -EgHnAdibedkHD7ZprX8CIQDY8NASxuaEMa6nH7b9kkx/KaOo0/dOkW+sWb5PeIbs -IQIgOUd6p5/UY3F5cTFtjK9lTf4nssdWLDFSFM6zTWimtA0CIHwhFj2YN2/uaYvQ -1siyfDjKn41Lc5cuGmLYms8oHLNhAiBxeGqLlEyHdk+Trp99+nK+pFi4cj5NZSFh -ph2C/7IgjA== ------END PRIVATE KEY-----`, - } - - remoteAuth := config.RemoteAuth{ - Credentials: &rpc.Credentials{rutils.CredentialsTypeRobotSecret, "xyz"}, - Managed: false, - SignalingServerAddress: "xyz", - SignalingAuthEntity: "xyz", - } - remote := config.Remote{ - ManagedBy: "acme", - Auth: remoteAuth, - } - remoteDiffManager := config.Remote{ - ManagedBy: "viam", - Auth: remoteAuth, - } - noCloudCfg := &config.Config{Remotes: []config.Remote{}} - cloudCfg := &config.Config{Cloud: cloud, Remotes: []config.Remote{}} - cloudWTLSCfg := &config.Config{Cloud: cloudWTLS, Remotes: []config.Remote{}} - remotesNoCloudCfg := &config.Config{Remotes: []config.Remote{remote, remoteDiffManager}} - remotesCloudCfg := &config.Config{Cloud: cloud, Remotes: []config.Remote{remote, remoteDiffManager}} - remotesCloudWTLSCfg := &config.Config{Cloud: cloudWTLS, Remotes: []config.Remote{remote, remoteDiffManager}} - - expectedRemoteAuthNoCloud := remoteAuth - expectedRemoteAuthNoCloud.SignalingCreds = expectedRemoteAuthNoCloud.Credentials - - expectedRemoteAuthCloud := remoteAuth - expectedRemoteAuthCloud.Managed = true - expectedRemoteAuthCloud.SignalingServerAddress = cloud.SignalingAddress - expectedRemoteAuthCloud.SignalingAuthEntity = cloud.ID - expectedRemoteAuthCloud.SignalingCreds = &rpc.Credentials{rutils.CredentialsTypeRobotSecret, cloud.Secret} - - expectedRemoteNoCloud := remote - expectedRemoteNoCloud.Auth = expectedRemoteAuthNoCloud - expectedRemoteCloud := remote - expectedRemoteCloud.Auth = expectedRemoteAuthCloud - - expectedRemoteDiffManagerNoCloud := remoteDiffManager - expectedRemoteDiffManagerNoCloud.Auth = expectedRemoteAuthNoCloud - - tlsCfg := &config.TLSConfig{} - err := tlsCfg.UpdateCert(cloudWTLSCfg) - test.That(t, err, test.ShouldBeNil) - - expectedCloudWTLSCfg := &config.Config{Cloud: cloudWTLS, Remotes: []config.Remote{}} - expectedCloudWTLSCfg.Network.TLSConfig = tlsCfg.Config - - expectedRemotesCloudWTLSCfg := &config.Config{Cloud: cloudWTLS, Remotes: []config.Remote{expectedRemoteCloud, remoteDiffManager}} - expectedRemotesCloudWTLSCfg.Network.TLSConfig = tlsCfg.Config - - for _, tc := range []struct { - TestName string - Config *config.Config - Expected *config.Config - }{ - {TestName: "no cloud", Config: noCloudCfg, Expected: noCloudCfg}, - {TestName: "cloud but no cert", Config: cloudCfg, Expected: cloudCfg}, - {TestName: "cloud and cert", Config: cloudWTLSCfg, Expected: expectedCloudWTLSCfg}, - { - TestName: "remotes no cloud", - Config: remotesNoCloudCfg, - Expected: &config.Config{Remotes: []config.Remote{expectedRemoteNoCloud, expectedRemoteDiffManagerNoCloud}}, - }, - { - TestName: "remotes cloud but no cert", - Config: remotesCloudCfg, - Expected: &config.Config{Cloud: cloud, Remotes: []config.Remote{expectedRemoteCloud, remoteDiffManager}}, - }, - {TestName: "remotes cloud and cert", Config: remotesCloudWTLSCfg, Expected: expectedRemotesCloudWTLSCfg}, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed, err := config.ProcessConfig(tc.Config, &config.TLSConfig{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, observed, test.ShouldResemble, tc.Expected) - }) - } - - t.Run("cert error", func(t *testing.T) { - cfg := &config.Config{Cloud: &config.Cloud{TLSCertificate: "abcd", TLSPrivateKey: "abcd"}} - _, err := config.ProcessConfig(cfg, &config.TLSConfig{}) - test.That(t, err, test.ShouldBeError, errors.New("tls: failed to find any PEM data in certificate input")) - }) -} - -func TestAuthConfigEnsure(t *testing.T) { - t.Run("unknown handler", func(t *testing.T) { - logger := logging.NewTestLogger(t) - config := config.Config{ - Auth: config.AuthConfig{ - Handlers: []config.AuthHandlerConfig{ - { - Type: "some-type", - Config: rutils.AttributeMap{"key": "abc123"}, - }, - }, - }, - } - - err := config.Ensure(true, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "do not know how to handle auth for \"some-type\"") - }) - - t.Run("api-key handler", func(t *testing.T) { - logger := logging.NewTestLogger(t) - config := config.Config{ - Auth: config.AuthConfig{ - Handlers: []config.AuthHandlerConfig{ - { - Type: rpc.CredentialsTypeAPIKey, - Config: rutils.AttributeMap{"key": "abc123"}, - }, - }, - }, - } - - err := config.Ensure(true, logger) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("external auth with invalid keyset", func(t *testing.T) { - logger := logging.NewTestLogger(t) - config := config.Config{ - Auth: config.AuthConfig{ - ExternalAuthConfig: &config.ExternalAuthConfig{}, - }, - } - - err := config.Ensure(true, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "failed to parse jwks") - }) - - t.Run("external auth valid config", func(t *testing.T) { - logger := logging.NewTestLogger(t) - algTypes := map[string]bool{ - "RS256": true, - "RS384": true, - "RS512": true, - } - - for alg := range algTypes { - keyset := jwk.NewSet() - privKeyForWebAuth, err := rsa.GenerateKey(rand.Reader, 256) - test.That(t, err, test.ShouldBeNil) - publicKeyForWebAuth, err := jwk.New(privKeyForWebAuth.PublicKey) - test.That(t, err, test.ShouldBeNil) - publicKeyForWebAuth.Set("alg", alg) - publicKeyForWebAuth.Set(jwk.KeyIDKey, "key-id-1") - test.That(t, keyset.Add(publicKeyForWebAuth), test.ShouldBeTrue) - - config := config.Config{ - Auth: config.AuthConfig{ - ExternalAuthConfig: &config.ExternalAuthConfig{ - JSONKeySet: keysetToAttributeMap(t, keyset), - }, - }, - } - - err = config.Ensure(true, logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, config.Auth.ExternalAuthConfig.ValidatedKeySet, test.ShouldNotBeNil) - _, ok := config.Auth.ExternalAuthConfig.ValidatedKeySet.LookupKeyID("key-id-1") - test.That(t, ok, test.ShouldBeTrue) - } - }) - - t.Run("web-oauth invalid alg type", func(t *testing.T) { - logger := logging.NewTestLogger(t) - badTypes := []string{"invalid", "", "nil"} // nil is a special case and is not set. - for _, badType := range badTypes { - t.Run(fmt.Sprintf(" with %s", badType), func(t *testing.T) { - keyset := jwk.NewSet() - privKeyForWebAuth, err := rsa.GenerateKey(rand.Reader, 256) - test.That(t, err, test.ShouldBeNil) - publicKeyForWebAuth, err := jwk.New(privKeyForWebAuth.PublicKey) - test.That(t, err, test.ShouldBeNil) - - if badType != "nil" { - publicKeyForWebAuth.Set("alg", badType) - } - - publicKeyForWebAuth.Set(jwk.KeyIDKey, "key-id-1") - test.That(t, keyset.Add(publicKeyForWebAuth), test.ShouldBeTrue) - - config := config.Config{ - Auth: config.AuthConfig{ - ExternalAuthConfig: &config.ExternalAuthConfig{ - JSONKeySet: keysetToAttributeMap(t, keyset), - }, - }, - } - - err = config.Ensure(true, logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid alg") - }) - } - }) - - t.Run("external auth no keys", func(t *testing.T) { - logger := logging.NewTestLogger(t) - config := config.Config{ - Auth: config.AuthConfig{ - ExternalAuthConfig: &config.ExternalAuthConfig{ - JSONKeySet: keysetToAttributeMap(t, jwk.NewSet()), - }, - }, - } - - err := config.Ensure(true, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "must contain at least 1 key") - }) -} - -func TestValidateUniqueNames(t *testing.T) { - logger := logging.NewTestLogger(t) - component := resource.Config{ - Name: "custom", - Model: fakeModel, - API: arm.API, - } - service := resource.Config{ - Name: "custom", - Model: fakeModel, - API: shell.API, - } - package1 := config.PackageConfig{ - Package: "package1", - Name: "package1", - Type: config.PackageTypeMlModel, - } - module1 := config.Module{ - Name: "m1", - LogLevel: "info", - ExePath: ".", - } - - process1 := pexec.ProcessConfig{ - ID: "process1", Name: "process1", - } - - remote1 := config.Remote{ - Name: "remote1", - Address: "test", - } - config1 := config.Config{ - Components: []resource.Config{component, component}, - } - config2 := config.Config{ - Services: []resource.Config{service, service}, - } - - config3 := config.Config{ - Packages: []config.PackageConfig{package1, package1}, - } - config4 := config.Config{ - Modules: []config.Module{module1, module1}, - } - config5 := config.Config{ - Processes: []pexec.ProcessConfig{process1, process1}, - } - - config6 := config.Config{ - Remotes: []config.Remote{remote1, remote1}, - } - allConfigs := []config.Config{config1, config2, config3, config4, config5, config6} - - for _, config := range allConfigs { - // returns an error instead of logging it - config.DisablePartialStart = true - // test that the logger returns an error after the ensure method is done - err := config.Ensure(false, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "duplicate resource") - - observedLogger, logs := logging.NewObservedTestLogger(t) - // now test it with logging enabled - config.DisablePartialStart = false - err = config.Ensure(false, observedLogger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, logs.FilterMessageSnippet("duplicate resource").Len(), test.ShouldBeGreaterThan, 0) - } - - // mix components and services with the same name -- no error as use triplets - config7 := config.Config{ - Components: []resource.Config{component}, - Services: []resource.Config{service}, - Modules: []config.Module{module1}, - Remotes: []config.Remote{ - { - Name: module1.Name, - Address: "test1", - }, - }, - } - config7.DisablePartialStart = true - err := config7.Ensure(false, logger) - test.That(t, err, test.ShouldBeNil) -} - -func keysetToAttributeMap(t *testing.T, keyset jwks.KeySet) rutils.AttributeMap { - t.Helper() - - // hack around marshaling the KeySet into pb.Struct. Passing interface directly - // does not work. - jwksAsJSON, err := json.Marshal(keyset) - test.That(t, err, test.ShouldBeNil) - - jwksAsInterface := rutils.AttributeMap{} - err = json.Unmarshal(jwksAsJSON, &jwksAsInterface) - test.That(t, err, test.ShouldBeNil) - - return jwksAsInterface -} - -func TestPackageConfig(t *testing.T) { - homeDir, _ := os.UserHomeDir() - viamDotDir := filepath.Join(homeDir, ".viam") - - packageTests := []struct { - config config.PackageConfig - shouldFailValidation bool - expectedRealFilePath string - }{ - { - config: config.PackageConfig{ - Name: "my_package", - Package: "my_org/my_package", - Version: "0", - }, - shouldFailValidation: true, - }, - { - config: config.PackageConfig{ - Name: "my_module", - Type: config.PackageTypeModule, - Package: "my_org/my_module", - Version: "1.2", - }, - expectedRealFilePath: filepath.Join(viamDotDir, "packages", "data", "module", "my_org-my_module-1_2"), - }, - { - config: config.PackageConfig{ - Name: "my_ml_model", - Type: config.PackageTypeMlModel, - Package: "my_org/my_ml_model", - Version: "latest", - }, - expectedRealFilePath: filepath.Join(viamDotDir, "packages", "data", "ml_model", "my_org-my_ml_model-latest"), - }, - { - config: config.PackageConfig{ - Name: "my_slam_map", - Type: config.PackageTypeSlamMap, - Package: "my_org/my_slam_map", - Version: "latest", - }, - expectedRealFilePath: filepath.Join(viamDotDir, "packages", "data", "slam_map", "my_org-my_slam_map-latest"), - }, - { - config: config.PackageConfig{ - Name: "::::", - Type: config.PackageTypeMlModel, - Package: "my_org/my_ml_model", - Version: "latest", - }, - shouldFailValidation: true, - }, - { - config: config.PackageConfig{ - Name: "my_ml_model", - Type: config.PackageType("willfail"), - Package: "my_org/my_ml_model", - Version: "latest", - }, - shouldFailValidation: true, - }, - } - - for _, pt := range packageTests { - err := pt.config.Validate("") - if pt.shouldFailValidation { - test.That(t, err, test.ShouldBeError) - continue - } - test.That(t, err, test.ShouldBeNil) - actualFilepath := pt.config.LocalDataDirectory(filepath.Join(viamDotDir, "packages")) - test.That(t, actualFilepath, test.ShouldEqual, pt.expectedRealFilePath) - } -} diff --git a/config/diff_test.go b/config/diff_test.go deleted file mode 100644 index d65c484a175..00000000000 --- a/config/diff_test.go +++ /dev/null @@ -1,723 +0,0 @@ -package config_test - -import ( - "context" - "crypto/tls" - "fmt" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/pexec" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/board" - fakeboard "go.viam.com/rdk/components/board/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" -) - -var ( - fakeModel = resource.DefaultModelFamily.WithModel("fake") - extModel = resource.NewModel("acme", "test", "model") -) - -func TestDiffConfigs(t *testing.T) { - config1 := config.Config{ - Modules: []config.Module{ - { - Name: "my-module", - ExePath: ".", - LogLevel: "info", - }, - }, - Remotes: []config.Remote{ - { - Name: "remote1", - Address: "addr1", - }, - { - Name: "remote2", - Address: "addr2", - }, - }, - Components: []resource.Config{ - { - Name: "arm1", - - API: arm.API, - Model: fakeModel, - Attributes: utils.AttributeMap{ - "one": float64(1), - }, - }, - { - Name: "base1", - - API: base.API, - Model: fakeModel, - Attributes: utils.AttributeMap{ - "two": float64(2), - }, - }, - { - Name: "board1", - Model: fakeModel, - - API: board.API, - Attributes: utils.AttributeMap{ - "analogs": []interface{}{ - map[string]interface{}{ - "name": "analog1", - "pin": "0", - }, - }, - "digital_interrupts": []interface{}{ - map[string]interface{}{ - "name": "encoder", - "pin": "14", - }, - }, - }, - ConvertedAttributes: &fakeboard.Config{ - AnalogReaders: []board.AnalogReaderConfig{ - { - Name: "analog1", - Pin: "0", - }, - }, DigitalInterrupts: []board.DigitalInterruptConfig{ - { - Name: "encoder", - Pin: "14", - }, - }, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "echo", - Args: []string{"hello", "world"}, - OneShot: true, - }, - { - ID: "2", - Name: "bash", - Args: []string{"-c", "trap \"exit 0\" SIGINT; while true; do echo hey; sleep 2; done"}, - Log: true, - }, - }, - } - - config2 := config.ModifiedConfigDiff{ - Modules: []config.Module{ - { - Name: "my-module", - ExePath: "..", - LogLevel: "debug", - }, - }, - Remotes: []config.Remote{ - { - Name: "remote1", - Address: "addr3", - }, - { - Name: "remote2", - Address: "addr4", - }, - }, - Components: []resource.Config{ - { - Name: "arm1", - - API: arm.API, - Model: fakeModel, - Attributes: utils.AttributeMap{ - "two": float64(2), - }, - }, - { - Name: "base1", - - API: base.API, - Model: extModel, - Attributes: utils.AttributeMap{ - "three": float64(3), - }, - }, - { - Name: "board1", - Model: fakeModel, - - API: board.API, - Attributes: utils.AttributeMap{ - "analogs": []interface{}{ - map[string]interface{}{ - "name": "analog1", - "pin": "1", - }, - }, - "digital_interrupts": []interface{}{ - map[string]interface{}{ - "name": "encoder", - "pin": "15", - }, - }, - }, - ConvertedAttributes: &fakeboard.Config{ - AnalogReaders: []board.AnalogReaderConfig{ - { - Name: "analog1", - Pin: "1", - }, - }, DigitalInterrupts: []board.DigitalInterruptConfig{ - { - Name: "encoder", - Pin: "15", - }, - }, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "echo", - Args: []string{"hello", "world", "again"}, - OneShot: true, - }, - { - ID: "2", - Name: "bash", - Args: []string{"-c", "trap \"exit 0\" SIGINT; while true; do echo hello; sleep 2; done"}, - Log: true, - }, - }, - } - - for _, tc := range []struct { - Name string - LeftFile string - RightFile string - Expected config.Diff - }{ - { - "empty", - "data/diff_config_empty.json", - "data/diff_config_empty.json", - config.Diff{ - Added: &config.Config{}, - Modified: &config.ModifiedConfigDiff{}, - Removed: &config.Config{}, - ResourcesEqual: true, - NetworkEqual: true, - }, - }, - { - "equal", - "data/diff_config_1.json", - "data/diff_config_1.json", - config.Diff{ - Added: &config.Config{}, - Modified: &config.ModifiedConfigDiff{}, - Removed: &config.Config{}, - ResourcesEqual: true, - NetworkEqual: true, - }, - }, - { - "only additions", - "data/diff_config_empty.json", - "data/diff_config_1.json", - config.Diff{ - Added: &config1, - Modified: &config.ModifiedConfigDiff{}, - Removed: &config.Config{}, - ResourcesEqual: false, - NetworkEqual: true, - }, - }, - { - "only removals", - "data/diff_config_1.json", - "data/diff_config_empty.json", - config.Diff{ - Added: &config.Config{}, - Removed: &config1, - Modified: &config.ModifiedConfigDiff{}, - ResourcesEqual: false, - NetworkEqual: true, - }, - }, - { - "only modifications", - "data/diff_config_1.json", - "data/diff_config_2.json", - config.Diff{ - Added: &config.Config{}, - Removed: &config.Config{}, - Modified: &config2, - ResourcesEqual: false, - NetworkEqual: true, - }, - }, - { - "mixed", - "data/diff_config_1.json", - "data/diff_config_3.json", - config.Diff{ - Added: &config.Config{ - Components: []resource.Config{ - { - Name: "base2", - - API: base.API, - Model: fakeModel, - }, - { - Name: "board2", - - API: board.API, - Model: fakeModel, - Attributes: utils.AttributeMap{ - "digital_interrupts": []interface{}{ - map[string]interface{}{ - "name": "encoder2", - "pin": "16", - }, - }, - }, - ConvertedAttributes: &fakeboard.Config{ - DigitalInterrupts: []board.DigitalInterruptConfig{{Name: "encoder2", Pin: "16"}}, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "3", - Name: "bash", - Args: []string{"-c", "trap \"exit 0\" SIGINT; while true; do echo world; sleep 2; done"}, - Log: true, - }, - }, - }, - Modified: &config.ModifiedConfigDiff{ - Remotes: []config.Remote{ - { - Name: "remote1", - Address: "addr3", - }, - { - Name: "remote2", - Address: "addr4", - }, - }, - Components: []resource.Config{ - { - Name: "arm1", - - API: arm.API, - Model: extModel, - Attributes: utils.AttributeMap{ - "two": float64(2), - }, - }, - { - Name: "board1", - - API: board.API, - Model: fakeModel, - Attributes: utils.AttributeMap{ - "analogs": []interface{}{ - map[string]interface{}{ - "name": "analog1", - "pin": "1", - }, - }, - }, - ConvertedAttributes: &fakeboard.Config{ - AnalogReaders: []board.AnalogReaderConfig{{Name: "analog1", Pin: "1"}}, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "echo", - Args: []string{"hello", "world", "again"}, - OneShot: true, - }, - }, - }, - Removed: &config.Config{ - Modules: []config.Module{ - { - Name: "my-module", - ExePath: ".", - LogLevel: "info", - }, - }, - Components: []resource.Config{ - { - Name: "base1", - - API: base.API, - Model: fakeModel, - Attributes: utils.AttributeMap{ - "two": float64(2), - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "2", - Name: "bash", - Args: []string{"-c", "trap \"exit 0\" SIGINT; while true; do echo hey; sleep 2; done"}, - Log: true, - }, - }, - }, - ResourcesEqual: false, - NetworkEqual: true, - }, - }, - } { - // test with revealSensitiveConfigDiffs = true - t.Run(tc.Name, func(t *testing.T) { - logger := logging.NewTestLogger(t) - // ensure parts are valid for components, services, modules, and remotes - test.That(t, tc.Expected.Added.Ensure(false, logger), test.ShouldBeNil) - test.That(t, tc.Expected.Removed.Ensure(false, logger), test.ShouldBeNil) - test.That(t, modifiedConfigDiffValidate(tc.Expected.Modified), test.ShouldBeNil) - tc.Expected.Added.Network = config.NetworkConfig{} - tc.Expected.Removed.Network = config.NetworkConfig{} - - for _, revealSensitiveConfigDiffs := range []bool{true, false} { - t.Run(fmt.Sprintf("revealSensitiveConfigDiffs=%t", revealSensitiveConfigDiffs), func(t *testing.T) { - logger.Infof("Test name: %v LeftFile: `%v` RightFile: `%v`", tc.Name, tc.LeftFile, tc.RightFile) - logger := logging.NewTestLogger(t) - left, err := config.Read(context.Background(), tc.LeftFile, logger) - test.That(t, err, test.ShouldBeNil) - right, err := config.Read(context.Background(), tc.RightFile, logger) - test.That(t, err, test.ShouldBeNil) - - diff, err := config.DiffConfigs(*left, *right, revealSensitiveConfigDiffs) - test.That(t, err, test.ShouldBeNil) - test.That(t, diff.Left, test.ShouldResemble, left) - test.That(t, diff.Right, test.ShouldResemble, right) - if tc.Expected.ResourcesEqual || !revealSensitiveConfigDiffs { - test.That(t, diff.PrettyDiff, test.ShouldBeEmpty) - } else { - test.That(t, diff.PrettyDiff, test.ShouldNotBeEmpty) - } - diff.PrettyDiff = "" - tc.Expected.Left = diff.Left - tc.Expected.Right = diff.Right - - test.That(t, diff.Added, test.ShouldResemble, tc.Expected.Added) - test.That(t, diff.Removed, test.ShouldResemble, tc.Expected.Removed) - test.That(t, diff.Modified, test.ShouldResemble, tc.Expected.Modified) - test.That(t, diff.ResourcesEqual, test.ShouldEqual, tc.Expected.ResourcesEqual) - test.That(t, diff.NetworkEqual, test.ShouldEqual, tc.Expected.NetworkEqual) - }) - } - }) - } -} - -func TestDiffConfigHeterogenousTypes(t *testing.T) { - for _, tc := range []struct { - Name string - LeftFile string - RightFile string - Expected string - }{ - { - "component model", - "data/diff_config_1.json", - "data/diff_config_1_component_model.json", - "", - }, - } { - t.Run(tc.Name, func(t *testing.T) { - logger := logging.NewTestLogger(t) - left, err := config.Read(context.Background(), tc.LeftFile, logger) - test.That(t, err, test.ShouldBeNil) - right, err := config.Read(context.Background(), tc.RightFile, logger) - test.That(t, err, test.ShouldBeNil) - - _, err = config.DiffConfigs(*left, *right, true) - if tc.Expected == "" { - test.That(t, err, test.ShouldBeNil) - return - } - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Expected) - }) - } -} - -func TestDiffNetworkingCfg(t *testing.T) { - network1 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{FQDN: "abc"}} - network2 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{FQDN: "xyz"}} - - tls1 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}}} - tls2 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{TLSConfig: &tls.Config{MinVersion: tls.VersionTLS10}}} - - tlsCfg3 := &tls.Config{ - MinVersion: tls.VersionTLS12, - GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("abc")}}, nil - }, - GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("abc")}}, nil - }, - } - tls3 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{TLSConfig: tlsCfg3}} - tlsCfg4 := &tls.Config{ - MinVersion: tls.VersionTLS12, - GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("abc")}}, nil - }, - GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("abc")}}, nil - }, - } - tls4 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{TLSConfig: tlsCfg4}} - tlsCfg5 := &tls.Config{ - MinVersion: tls.VersionTLS12, - GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("xyz")}}, nil - }, - GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("abc")}}, nil - }, - } - tls5 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{TLSConfig: tlsCfg5}} - tlsCfg6 := &tls.Config{ - MinVersion: tls.VersionTLS12, - GetCertificate: func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("abcd")}}, nil - }, - GetClientCertificate: func(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { - return &tls.Certificate{Certificate: [][]byte{[]byte("xyz")}}, nil - }, - } - tls6 := config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{TLSConfig: tlsCfg6}} - - cloud1 := &config.Cloud{ID: "1"} - cloud2 := &config.Cloud{ID: "2"} - - auth1 := config.AuthConfig{ - Handlers: []config.AuthHandlerConfig{{Config: utils.AttributeMap{"key": "value"}}}, - } - auth2 := config.AuthConfig{ - Handlers: []config.AuthHandlerConfig{{Config: utils.AttributeMap{"key2": "value2"}}}, - } - for _, tc := range []struct { - Name string - LeftCfg config.Config - RightCfg config.Config - NetworkEqual bool - }{ - { - "same", - config.Config{Network: network1, Cloud: cloud1, Auth: auth1}, - config.Config{Network: network1, Cloud: cloud1, Auth: auth1}, - true, - }, - { - "diff network", - config.Config{Network: network1}, - config.Config{Network: network2}, - false, - }, - { - "same tls", - config.Config{Network: tls3}, - config.Config{Network: tls4}, - true, - }, - { - "diff tls", - config.Config{Network: tls1}, - config.Config{Network: tls2}, - false, - }, - { - "diff tls cert", - config.Config{Network: tls3}, - config.Config{Network: tls5}, - false, - }, - { - "diff tls client cert", - config.Config{Network: tls3}, - config.Config{Network: tls6}, - false, - }, - { - "diff cloud", - config.Config{Cloud: cloud1}, - config.Config{Cloud: cloud2}, - false, - }, - { - "diff auth", - config.Config{Auth: auth1}, - config.Config{Auth: auth2}, - false, - }, - } { - t.Run(tc.Name, func(t *testing.T) { - diff, err := config.DiffConfigs(tc.LeftCfg, tc.RightCfg, true) - test.That(t, err, test.ShouldBeNil) - - test.That(t, diff.NetworkEqual, test.ShouldEqual, tc.NetworkEqual) - }) - } -} - -func TestDiffSanitize(t *testing.T) { - cloud1 := &config.Cloud{ - ID: "1", - Secret: "hello", - LocationSecret: "world", - LocationSecrets: []config.LocationSecret{ - {ID: "id1", Secret: "sec1"}, - {ID: "id2", Secret: "sec2"}, - }, - TLSCertificate: "foo", - TLSPrivateKey: "bar", - } - - // cloud - // remotes.remote.auth.creds - // remotes.remote.auth.signaling creds - // remote.secret - // .auth.handlers.config *** - - auth1 := config.AuthConfig{ - Handlers: []config.AuthHandlerConfig{ - {Config: utils.AttributeMap{"key1": "value1"}}, - {Config: utils.AttributeMap{"key2": "value2"}}, - }, - } - remotes1 := []config.Remote{ - { - Secret: "remsecret1", - Auth: config.RemoteAuth{ - Credentials: &rpc.Credentials{ - Type: "remauthtype1", - Payload: "payload1", - }, - SignalingCreds: &rpc.Credentials{ - Type: "remauthtypesig1", - Payload: "payloadsig1", - }, - }, - }, - { - Secret: "remsecret2", - Auth: config.RemoteAuth{ - Credentials: &rpc.Credentials{ - Type: "remauthtype2", - Payload: "payload2", - }, - SignalingCreds: &rpc.Credentials{ - Type: "remauthtypesig2", - Payload: "payloadsig2", - }, - }, - }, - } - - left := config.Config{} - leftOrig := config.Config{} - right := config.Config{ - Cloud: cloud1, - Auth: auth1, - Remotes: remotes1, - } - rightOrig := config.Config{ - Cloud: cloud1, - Auth: auth1, - Remotes: remotes1, - } - - diff, err := config.DiffConfigs(left, right, true) - test.That(t, err, test.ShouldBeNil) - - // verify secrets did not change - test.That(t, left, test.ShouldResemble, leftOrig) - test.That(t, right, test.ShouldResemble, rightOrig) - - diffStr := diff.String() - test.That(t, diffStr, test.ShouldContainSubstring, cloud1.ID) - test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.Secret) - test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.LocationSecret) - test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.LocationSecrets[0].Secret) - test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.LocationSecrets[1].Secret) - test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.TLSCertificate) - test.That(t, diffStr, test.ShouldNotContainSubstring, cloud1.TLSPrivateKey) - for _, hdlr := range auth1.Handlers { - for _, value := range hdlr.Config { - test.That(t, diffStr, test.ShouldNotContainSubstring, value) - } - } - for _, rem := range remotes1 { - test.That(t, diffStr, test.ShouldContainSubstring, string(rem.Auth.Credentials.Type)) - test.That(t, diffStr, test.ShouldNotContainSubstring, rem.Secret) - test.That(t, diffStr, test.ShouldNotContainSubstring, rem.Auth.Credentials.Payload) - test.That(t, diffStr, test.ShouldNotContainSubstring, rem.Auth.SignalingCreds.Payload) - } -} - -func modifiedConfigDiffValidate(c *config.ModifiedConfigDiff) error { - for idx := 0; idx < len(c.Remotes); idx++ { - if _, err := c.Remotes[idx].Validate(fmt.Sprintf("%s.%d", "remotes", idx)); err != nil { - return err - } - } - - for idx := 0; idx < len(c.Components); idx++ { - dependsOn, err := c.Components[idx].Validate(fmt.Sprintf("%s.%d", "components", idx), resource.APITypeComponentName) - if err != nil { - return err - } - c.Components[idx].ImplicitDependsOn = dependsOn - } - - for idx := 0; idx < len(c.Processes); idx++ { - if err := c.Processes[idx].Validate(fmt.Sprintf("%s.%d", "processes", idx)); err != nil { - return err - } - } - - for idx := 0; idx < len(c.Services); idx++ { - dependsOn, err := c.Services[idx].Validate(fmt.Sprintf("%s.%d", "services", idx), resource.APITypeServiceName) - if err != nil { - return err - } - c.Services[idx].ImplicitDependsOn = dependsOn - } - - for idx := 0; idx < len(c.Packages); idx++ { - if err := c.Packages[idx].Validate(fmt.Sprintf("%s.%d", "packages", idx)); err != nil { - return err - } - } - - for idx := 0; idx < len(c.Modules); idx++ { - if err := c.Modules[idx].Validate(fmt.Sprintf("%s.%d", "modules", idx)); err != nil { - return err - } - } - - return nil -} diff --git a/config/logging_level_test.go b/config/logging_level_test.go deleted file mode 100644 index 1afef429b52..00000000000 --- a/config/logging_level_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package config - -import ( - "testing" - - "go.uber.org/zap" - "go.viam.com/test" - - "go.viam.com/rdk/logging" -) - -func TestConfigDebugFlag(t *testing.T) { - logConfig := logging.NewZapLoggerConfig() - logger := logging.FromZapCompatible(zap.Must(logConfig.Build()).Sugar()) - - for _, cmdLineValue := range []bool{true, false} { - for _, fileDebugValue := range []bool{true, false} { - for _, cloudDebugValue := range []bool{true, false} { - InitLoggingSettings(logger, cmdLineValue) - UpdateFileConfigDebug(fileDebugValue) - UpdateCloudConfigDebug(cloudDebugValue) - - expectedDebugEnabled := cmdLineValue || fileDebugValue || cloudDebugValue - if expectedDebugEnabled { - test.That(t, logger.Level().Enabled(zap.DebugLevel), test.ShouldBeTrue) - } - test.That(t, logger.Level().Enabled(zap.InfoLevel), test.ShouldBeTrue) - } - } - } -} diff --git a/config/placeholder_replacement_test.go b/config/placeholder_replacement_test.go deleted file mode 100644 index e66a9ba7e92..00000000000 --- a/config/placeholder_replacement_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package config_test - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" -) - -func TestPlaceholderReplacement(t *testing.T) { - homeDir, _ := os.UserHomeDir() - viamPackagesDir := filepath.Join(homeDir, ".viam", "packages") - t.Run("package placeholder replacement", func(t *testing.T) { - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "m", - Attributes: utils.AttributeMap{ - "should_equal_1": "${packages.coolpkg}", - "should_equal_2": "${packages.ml_model.coolpkg}", - "mid_string_replace": "Hello ${packages.coolpkg} Friends!", - "module_replace": "${packages.module.coolmod}", - "multi_replace": "${packages.coolpkg} ${packages.module.coolmod}", - }, - }, - }, - Services: []resource.Config{ - { - Name: "m", - Attributes: utils.AttributeMap{ - "apply_to_services_too": "${packages.coolpkg}", - }, - }, - }, - Modules: []config.Module{ - { - ExePath: "${packages.module.coolmod}/bin", - }, - }, - Packages: []config.PackageConfig{ - { - Name: "coolpkg", - Package: "orgid/pkg", - Type: "ml_model", - Version: "0.4.0", - }, - { - Name: "coolmod", - Package: "orgid/mod", - Type: "module", - Version: "0.5.0", - }, - }, - } - err := cfg.ReplacePlaceholders() - test.That(t, err, test.ShouldBeNil) - dirForMlModel := cfg.Packages[0].LocalDataDirectory(viamPackagesDir) - dirForModule := cfg.Packages[1].LocalDataDirectory(viamPackagesDir) - // components - attrMap := cfg.Components[0].Attributes - test.That(t, attrMap["should_equal_1"], test.ShouldResemble, attrMap["should_equal_2"]) - test.That(t, attrMap["should_equal_1"], test.ShouldResemble, dirForMlModel) - test.That(t, attrMap["mid_string_replace"], test.ShouldResemble, fmt.Sprintf("Hello %s Friends!", dirForMlModel)) - test.That(t, attrMap["module_replace"], test.ShouldResemble, dirForModule) - test.That(t, attrMap["multi_replace"], test.ShouldResemble, fmt.Sprintf("%s %s", dirForMlModel, dirForModule)) - // services - attrMap = cfg.Services[0].Attributes - test.That(t, attrMap["apply_to_services_too"], test.ShouldResemble, dirForMlModel) - // module - exePath := cfg.Modules[0].ExePath - test.That(t, exePath, test.ShouldResemble, fmt.Sprintf("%s/bin", dirForModule)) - }) - t.Run("package placeholder typos", func(t *testing.T) { - // Unknown type of placeholder - cfg := &config.Config{ - Components: []resource.Config{ - { - Attributes: utils.AttributeMap{ - "a": "${invalidplaceholder}", - }, - }, - }, - } - err := cfg.ReplacePlaceholders() - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "invalidplaceholder") - // Test that the attribute is left unchanged if replacement failed - test.That(t, cfg.Components[0].Attributes["a"], test.ShouldResemble, "${invalidplaceholder}") - - // Empy placeholder - cfg = &config.Config{ - Components: []resource.Config{ - { - Attributes: utils.AttributeMap{ - "a": "${}", - }, - }, - }, - } - err = cfg.ReplacePlaceholders() - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "invalid placeholder") - // Test that the attribute is left unchanged if replacement failed - test.That(t, cfg.Components[0].Attributes["a"], test.ShouldResemble, "${}") - - // Package placeholder with no equivalent pkg - cfg = &config.Config{ - Components: []resource.Config{ - { - Attributes: utils.AttributeMap{ - "a": "${packages.ml_model.chicken}", - }, - }, - }, - } - err = cfg.ReplacePlaceholders() - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "package named \"chicken\"") - // Test that the attribute is left unchanged if replacement failed - test.That(t, cfg.Components[0].Attributes["a"], test.ShouldResemble, "${packages.ml_model.chicken}") - - // Package placeholder with wrong type - cfg = &config.Config{ - Components: []resource.Config{ - { - Attributes: utils.AttributeMap{ - "a": "${packages.ml_model.chicken}", - }, - }, - }, - Packages: []config.PackageConfig{ - { - Name: "chicken", - Package: "orgid/pkg", - Type: "module", - Version: "0.4.0", - }, - }, - } - err = cfg.ReplacePlaceholders() - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "looking for a package of type \"ml_model\"") - - // Half successful string replacement - cfg = &config.Config{ - Components: []resource.Config{ - { - Attributes: utils.AttributeMap{ - "a": "${packages.module.chicken}/${invalidplaceholder}", - }, - }, - }, - Packages: []config.PackageConfig{ - { - Name: "chicken", - Package: "orgid/pkg", - Type: "module", - Version: "0.4.0", - }, - }, - } - err = cfg.ReplacePlaceholders() - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "invalidplaceholder") - test.That(t, fmt.Sprint(err), test.ShouldNotContainSubstring, "chicken") - - test.That(t, cfg.Components[0].Attributes["a"], test.ShouldResemble, - fmt.Sprintf("%s/${invalidplaceholder}", cfg.Packages[0].LocalDataDirectory(viamPackagesDir))) - }) - t.Run("environment variable placeholder replacement", func(t *testing.T) { - // test success - cfg := &config.Config{ - Components: []resource.Config{ - { - Attributes: utils.AttributeMap{ - "a": "${environment.HOME}", - }, - }, - }, - Modules: []config.Module{ - { - Environment: map[string]string{ - "PATH": "${environment.PATH}", - }, - }, - }, - } - err := cfg.ReplacePlaceholders() - test.That(t, err, test.ShouldBeNil) - test.That(t, cfg.Components[0].Attributes["a"], test.ShouldEqual, os.Getenv("HOME")) - test.That(t, cfg.Modules[0].Environment["PATH"], test.ShouldEqual, os.Getenv("PATH")) - - // test failure - cfg = &config.Config{ - Components: []resource.Config{ - { - Attributes: utils.AttributeMap{ - "a": "${environment.VIAM_UNDEFINED_TEST_VAR}", - }, - }, - }, - } - err = cfg.ReplacePlaceholders() - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "VIAM_UNDEFINED_TEST_VAR") - }) -} diff --git a/config/proto_conversions_test.go b/config/proto_conversions_test.go deleted file mode 100644 index f3b565ea4da..00000000000 --- a/config/proto_conversions_test.go +++ /dev/null @@ -1,985 +0,0 @@ -package config - -import ( - "crypto/rand" - "crypto/rsa" - "encoding/json" - "fmt" - "syscall" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/lestrrat-go/jwx/jwk" - packagespb "go.viam.com/api/app/packages/v1" - pb "go.viam.com/api/app/v1" - "go.viam.com/test" - "go.viam.com/utils/jwks" - "go.viam.com/utils/pexec" - "go.viam.com/utils/rpc" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var testOrientation, _ = spatial.NewOrientationConfig(spatial.NewZeroOrientation()) - -var testFrame = &referenceframe.LinkConfig{ - Parent: "world", - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Orientation: testOrientation, - Geometry: &spatial.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, -} - -var testComponent = resource.Config{ - Name: "some-name", - API: resource.NewAPI("some-namespace", "component", "some-type"), - Model: resource.DefaultModelFamily.WithModel("some-model"), - DependsOn: []string{"dep1", "dep2"}, - Attributes: utils.AttributeMap{ - "attr1": 1, - "attr2": "attr-string", - }, - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{ - // these will get rewritten in tests to simulate API data - { - // will resemble the same but become "foo:bar:baz" - API: resource.NewAPI("foo", "bar", "baz"), - Attributes: utils.AttributeMap{ - "attr1": 1, - }, - }, - { - // will stay the same - API: resource.APINamespaceRDK.WithServiceType("some-type-2"), - Attributes: utils.AttributeMap{ - "attr1": 2, - }, - }, - { - // will resemble the same but be just "some-type-3" - API: resource.APINamespaceRDK.WithServiceType("some-type-3"), - Attributes: utils.AttributeMap{ - "attr1": 3, - }, - }, - }, - Frame: testFrame, -} - -var testRemote = Remote{ - Name: "some-name", - Address: "localohst:8080", - Frame: testFrame, - Auth: RemoteAuth{ - Entity: "some-entity", - Credentials: &rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: "payload", - }, - }, - ManagedBy: "managed-by", - Insecure: true, - ConnectionCheckInterval: 1000000000, - ReconnectInterval: 2000000000, - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{ - { - API: resource.APINamespaceRDK.WithServiceType("some-type-1"), - Attributes: utils.AttributeMap{ - "attr1": 1, - }, - }, - { - API: resource.APINamespaceRDK.WithServiceType("some-type-2"), - Attributes: utils.AttributeMap{ - "attr1": 1, - }, - }, - }, -} - -var testService = resource.Config{ - Name: "some-name", - API: resource.NewAPI("some-namespace", "service", "some-type"), - Model: resource.DefaultModelFamily.WithModel("some-model"), - Attributes: utils.AttributeMap{ - "attr1": 1, - }, - DependsOn: []string{"some-depends-on"}, - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{ - { - API: resource.APINamespaceRDK.WithServiceType("some-type-1"), - Attributes: utils.AttributeMap{ - "attr1": 1, - }, - }, - }, -} - -var testProcessConfig = pexec.ProcessConfig{ - ID: "Some-id", - Name: "Some-name", - Args: []string{"arg1", "arg2"}, - CWD: "/home", - Environment: map[string]string{"SOME_VAR": "value"}, - OneShot: true, - Log: true, - StopSignal: syscall.SIGINT, - StopTimeout: time.Second, -} - -var testNetworkConfig = NetworkConfig{ - NetworkConfigData: NetworkConfigData{ - FQDN: "some.fqdn", - BindAddress: "0.0.0.0:1234", - TLSCertFile: "./cert.pub", - TLSKeyFile: "./cert.private", - Sessions: SessionsConfig{ - HeartbeatWindow: 5 * time.Second, - }, - }, -} - -var testAuthConfig = AuthConfig{ - Handlers: []AuthHandlerConfig{ - { - Type: rpc.CredentialsTypeAPIKey, - Config: utils.AttributeMap{ - "config-1": 1, - }, - }, - { - Type: rpc.CredentialsTypeAPIKey, - Config: utils.AttributeMap{ - "config-2": 2, - }, - }, - }, - TLSAuthEntities: []string{"tls1", "tls2"}, -} - -var testCloudConfig = Cloud{ - ID: "some-id", - Secret: "some-secret", - LocationSecret: "other-secret", - LocationSecrets: []LocationSecret{ - {ID: "id1", Secret: "abc1"}, - {ID: "id2", Secret: "abc2"}, - }, - ManagedBy: "managed-by", - FQDN: "some.fqdn", - LocalFQDN: "local.fqdn", - SignalingAddress: "0.0.0.0:8080", - SignalingInsecure: true, -} - -var testModule = Module{ - Name: "testmod", - ExePath: "/tmp/test.mod", - LogLevel: "debug", - Type: ModuleTypeLocal, - ModuleID: "a:b", - Environment: map[string]string{"SOME_VAR": "value"}, -} - -var testModuleWithError = Module{ - Name: "testmodErr", - ExePath: "/tmp/test.mod", - LogLevel: "debug", - Type: ModuleTypeRegistry, - ModuleID: "mod:testmodErr", - Environment: map[string]string{}, - Status: &AppValidationStatus{ - Error: "i have a bad error ah!", - }, -} - -var testPackageConfig = PackageConfig{ - Name: "package-name", - Package: "some/package", - Version: "v1", - Type: PackageTypeModule, -} - -var ( - testInvalidModule = Module{} - testInvalidComponent = resource.Config{ - API: resource.NewAPI("", "component", ""), - } - testInvalidRemote = Remote{} - testInvalidProcessConfig = pexec.ProcessConfig{} - testInvalidService = resource.Config{ - API: resource.NewAPI("", "service", ""), - } - testInvalidPackage = PackageConfig{} -) - -func init() { - if _, err := testComponent.Validate("", resource.APITypeComponentName); err != nil { - panic(err) - } - if _, err := testService.Validate("", resource.APITypeServiceName); err != nil { - panic(err) - } -} - -//nolint:thelper -func validateModule(t *testing.T, actual, expected Module) { - test.That(t, actual.Name, test.ShouldEqual, expected.Name) - test.That(t, actual.ExePath, test.ShouldEqual, expected.ExePath) - test.That(t, actual.LogLevel, test.ShouldEqual, expected.LogLevel) - test.That(t, actual, test.ShouldResemble, expected) -} - -func TestPackageConfigConversions(t *testing.T) { - proto, err := PackageConfigToProto(&testPackageConfig) - test.That(t, err, test.ShouldBeNil) - - out, err := PackageConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - test.That(t, testPackageConfig, test.ShouldResemble, *out) - - // test package with error - pckWithErr := &PackageConfig{ - Name: "testErr", - Package: "package/test/me", - Version: "1.0.0", - Type: PackageTypeMlModel, - Status: &AppValidationStatus{ - Error: "help me error!", - }, - } - - proto, err = PackageConfigToProto(pckWithErr) - test.That(t, err, test.ShouldBeNil) - - out, err = PackageConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - test.That(t, pckWithErr, test.ShouldResemble, out) -} - -func TestModuleConfigToProto(t *testing.T) { - proto, err := ModuleConfigToProto(&testModule) - test.That(t, err, test.ShouldBeNil) - test.That(t, proto.Status, test.ShouldBeNil) - - out, err := ModuleConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - - validateModule(t, *out, testModule) - - protoWithErr, err := ModuleConfigToProto(&testModuleWithError) - test.That(t, err, test.ShouldBeNil) - test.That(t, protoWithErr.Status, test.ShouldNotBeNil) - - out, err = ModuleConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - - validateModule(t, *out, testModule) -} - -//nolint:thelper -func validateComponent(t *testing.T, actual, expected resource.Config) { - test.That(t, actual.Name, test.ShouldEqual, expected.Name) - test.That(t, actual.API, test.ShouldResemble, expected.API) - test.That(t, actual.Model, test.ShouldResemble, expected.Model) - test.That(t, actual.DependsOn, test.ShouldResemble, expected.DependsOn) - test.That(t, actual.Attributes.Int("attr1", 0), test.ShouldEqual, expected.Attributes.Int("attr1", -1)) - test.That(t, actual.Attributes.String("attr2"), test.ShouldEqual, expected.Attributes.String("attr2")) - - test.That(t, actual.AssociatedResourceConfigs, test.ShouldHaveLength, 3) - test.That(t, actual.AssociatedResourceConfigs[0].API, test.ShouldResemble, expected.AssociatedResourceConfigs[0].API) - test.That(t, - actual.AssociatedResourceConfigs[0].Attributes.Int("attr1", 0), - test.ShouldEqual, - expected.AssociatedResourceConfigs[0].Attributes.Int("attr1", -1)) - test.That(t, - actual.AssociatedResourceConfigs[1].API, - test.ShouldResemble, - expected.AssociatedResourceConfigs[1].API) - test.That(t, - actual.AssociatedResourceConfigs[1].Attributes.Int("attr1", 0), - test.ShouldEqual, - expected.AssociatedResourceConfigs[1].Attributes.Int("attr1", -1)) - test.That(t, - actual.AssociatedResourceConfigs[2].API, - test.ShouldResemble, - expected.AssociatedResourceConfigs[2].API) - test.That(t, - actual.AssociatedResourceConfigs[2].Attributes.Int("attr1", 0), - test.ShouldEqual, - expected.AssociatedResourceConfigs[2].Attributes.Int("attr1", -1)) - - f1, err := actual.Frame.ParseConfig() - test.That(t, err, test.ShouldBeNil) - f2, err := testComponent.Frame.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, f1, test.ShouldResemble, f2) -} - -func rewriteTestComponentProto(conf *pb.ComponentConfig) { - conf.ServiceConfigs[0].Type = "foo:bar:baz" - conf.ServiceConfigs[2].Type = "some-type-3" -} - -func TestComponentConfigToProto(t *testing.T) { - proto, err := ComponentConfigToProto(&testComponent) - test.That(t, err, test.ShouldBeNil) - rewriteTestComponentProto(proto) - - out, err := ComponentConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - - validateComponent(t, *out, testComponent) - - for _, tc := range []struct { - Name string - Conf resource.Config - }{ - { - Name: "basic component with internal API", - Conf: resource.Config{ - Name: "foo", - API: resource.APINamespaceRDK.WithComponentType("base"), - Model: resource.DefaultModelFamily.WithModel("fake"), - }, - }, - { - Name: "basic component with external API", - Conf: resource.Config{ - Name: "foo", - API: resource.NewAPI("acme", "component", "gizmo"), - Model: resource.DefaultModelFamily.WithModel("fake"), - }, - }, - { - Name: "basic component with external model", - Conf: resource.Config{ - Name: "foo", - API: resource.NewAPI("acme", "component", "gizmo"), - Model: resource.NewModel("acme", "test", "model"), - }, - }, - } { - t.Run(tc.Name, func(t *testing.T) { - _, err := tc.Conf.Validate("", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - proto, err := ComponentConfigToProto(&tc.Conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, proto.Api, test.ShouldEqual, tc.Conf.API.String()) - test.That(t, proto.Model, test.ShouldEqual, tc.Conf.Model.String()) - test.That(t, proto.Namespace, test.ShouldEqual, tc.Conf.API.Type.Namespace) - test.That(t, proto.Type, test.ShouldEqual, tc.Conf.API.SubtypeName) - out, err := ComponentConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - _, err = out.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - test.That(t, out, test.ShouldResemble, &tc.Conf) - test.That(t, out.API, test.ShouldResemble, out.API) - test.That(t, out.Model, test.ShouldResemble, out.Model) - }) - } -} - -func TestFrameConfigFromProto(t *testing.T) { - expectedFrameWithOrientation := func(or spatial.Orientation) *referenceframe.LinkConfig { - orCfg, err := spatial.NewOrientationConfig(or) - test.That(t, err, test.ShouldBeNil) - return &referenceframe.LinkConfig{ - Parent: "world", - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Orientation: orCfg, - } - } - createNewFrame := func(or *pb.Orientation) *pb.Frame { - return &pb.Frame{ - Parent: "world", - Translation: &pb.Translation{ - X: 1, - Y: 2, - Z: 3, - }, - Orientation: or, - } - } - - orRadians := spatial.NewOrientationVector() - orRadians.OX = 1 - orRadians.OY = 2 - orRadians.OZ = 3 - orRadians.Theta = 4 - - orDegress := spatial.NewOrientationVectorDegrees() - orDegress.OX = 1 - orDegress.OY = 2 - orDegress.OZ = 3 - orDegress.Theta = 4 - - orR4AA := spatial.NewR4AA() - orR4AA.RX = 1 - orR4AA.RY = 2 - orR4AA.RZ = 3 - orR4AA.Theta = 4 - - orEulerAngles := spatial.NewEulerAngles() - orEulerAngles.Roll = 1 - orEulerAngles.Pitch = 2 - orEulerAngles.Yaw = 3 - - testCases := []struct { - name string - expectedFrame *referenceframe.LinkConfig - inputFrame *pb.Frame - }{ - { - "with orientation vector radians", - expectedFrameWithOrientation(orRadians), - createNewFrame(&pb.Orientation{ - Type: &pb.Orientation_VectorRadians{VectorRadians: &pb.Orientation_OrientationVectorRadians{Theta: 4, X: 1, Y: 2, Z: 3}}, - }), - }, - { - "with orientation vector degrees", - expectedFrameWithOrientation(orDegress), - createNewFrame(&pb.Orientation{ - Type: &pb.Orientation_VectorDegrees{VectorDegrees: &pb.Orientation_OrientationVectorDegrees{Theta: 4, X: 1, Y: 2, Z: 3}}, - }), - }, - { - "with orientation R4AA", - expectedFrameWithOrientation(orR4AA), - createNewFrame(&pb.Orientation{ - Type: &pb.Orientation_AxisAngles_{AxisAngles: &pb.Orientation_AxisAngles{Theta: 4, X: 1, Y: 2, Z: 3}}, - }), - }, - { - "with orientation EulerAngles", - expectedFrameWithOrientation(orEulerAngles), - createNewFrame(&pb.Orientation{ - Type: &pb.Orientation_EulerAngles_{EulerAngles: &pb.Orientation_EulerAngles{Roll: 1, Pitch: 2, Yaw: 3}}, - }), - }, - { - "with orientation Quaternion", - expectedFrameWithOrientation(&spatial.Quaternion{Real: 1, Imag: 2, Jmag: 3, Kmag: 4}), - createNewFrame(&pb.Orientation{ - Type: &pb.Orientation_Quaternion_{Quaternion: &pb.Orientation_Quaternion{W: 1, X: 2, Y: 3, Z: 4}}, - }), - }, - { - "with no orientation", - expectedFrameWithOrientation(nil), - createNewFrame(nil), - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - frameOut, err := FrameConfigFromProto(testCase.inputFrame) - test.That(t, err, test.ShouldBeNil) - f1, err := frameOut.ParseConfig() - test.That(t, err, test.ShouldBeNil) - f2, err := testCase.expectedFrame.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, f1, test.ShouldResemble, f2) - }) - } -} - -//nolint:thelper -func validateRemote(t *testing.T, actual, expected Remote) { - test.That(t, actual.Name, test.ShouldEqual, expected.Name) - test.That(t, actual.Address, test.ShouldEqual, expected.Address) - test.That(t, actual.ManagedBy, test.ShouldEqual, expected.ManagedBy) - test.That(t, actual.Insecure, test.ShouldEqual, expected.Insecure) - test.That(t, actual.ReconnectInterval, test.ShouldEqual, expected.ReconnectInterval) - test.That(t, actual.ConnectionCheckInterval, test.ShouldEqual, expected.ConnectionCheckInterval) - test.That(t, actual.Auth, test.ShouldResemble, expected.Auth) - f1, err := actual.Frame.ParseConfig() - test.That(t, err, test.ShouldBeNil) - f2, err := testComponent.Frame.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, f1, test.ShouldResemble, f2) - - test.That(t, actual.AssociatedResourceConfigs, test.ShouldHaveLength, 2) - test.That(t, - actual.AssociatedResourceConfigs[0].API, - test.ShouldResemble, - expected.AssociatedResourceConfigs[0].API) - test.That(t, - actual.AssociatedResourceConfigs[0].Attributes.Int("attr1", 0), - test.ShouldEqual, - expected.AssociatedResourceConfigs[0].Attributes.Int("attr1", -1)) - test.That(t, - actual.AssociatedResourceConfigs[1].API, - test.ShouldResemble, - expected.AssociatedResourceConfigs[1].API) - test.That(t, - actual.AssociatedResourceConfigs[1].Attributes.Int("attr1", 0), - test.ShouldEqual, - expected.AssociatedResourceConfigs[1].Attributes.Int("attr1", -1)) -} - -func TestRemoteConfigToProto(t *testing.T) { - t.Run("With RemoteAuth", func(t *testing.T) { - proto, err := RemoteConfigToProto(&testRemote) - test.That(t, err, test.ShouldBeNil) - - out, err := RemoteConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - validateRemote(t, *out, testRemote) - }) - - t.Run("Without RemoteAuth", func(t *testing.T) { - proto := pb.RemoteConfig{ - Name: "some-name", - Address: "localohst:8080", - } - - out, err := RemoteConfigFromProto(&proto) - test.That(t, err, test.ShouldBeNil) - - test.That(t, out.Name, test.ShouldEqual, proto.Name) - test.That(t, out.Address, test.ShouldEqual, proto.Address) - test.That(t, out.Auth, test.ShouldResemble, RemoteAuth{}) - }) -} - -//nolint:thelper -func validateService(t *testing.T, actual, expected resource.Config) { - test.That(t, actual.Name, test.ShouldEqual, expected.Name) - test.That(t, actual.API, test.ShouldResemble, expected.API) - test.That(t, actual.Model, test.ShouldResemble, expected.Model) - test.That(t, actual.DependsOn, test.ShouldResemble, expected.DependsOn) - test.That(t, actual.Attributes.Int("attr1", 0), test.ShouldEqual, expected.Attributes.Int("attr1", -1)) - test.That(t, actual.Attributes.String("attr2"), test.ShouldEqual, expected.Attributes.String("attr2")) -} - -func TestServiceConfigToProto(t *testing.T) { - proto, err := ServiceConfigToProto(&testService) - test.That(t, err, test.ShouldBeNil) - - out, err := ServiceConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - validateService(t, *out, testService) - - for _, tc := range []struct { - Name string - Conf resource.Config - }{ - { - Name: "basic component with internal API", - Conf: resource.Config{ - Name: "foo", - API: resource.APINamespaceRDK.WithServiceType("base"), - Model: resource.DefaultModelFamily.WithModel("fake"), - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{}, - }, - }, - { - Name: "basic component with external API", - Conf: resource.Config{ - Name: "foo", - API: resource.NewAPI("acme", "service", "gizmo"), - Model: resource.DefaultModelFamily.WithModel("fake"), - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{}, - }, - }, - { - Name: "basic component with external model", - Conf: resource.Config{ - Name: "foo", - API: resource.NewAPI("acme", "service", "gizmo"), - Model: resource.NewModel("acme", "test", "model"), - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{}, - }, - }, - { - Name: "empty model name", - Conf: resource.Config{ - Name: "foo", - API: resource.NewAPI("acme", "service", "gizmo"), - Model: resource.Model{}, - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{}, - }, - }, - { - Name: "associated service config", - Conf: resource.Config{ - Name: "foo", - API: resource.NewAPI("acme", "service", "gizmo"), - Model: resource.Model{}, - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{ - { - API: resource.APINamespaceRDK.WithServiceType("some-type-1"), - Attributes: utils.AttributeMap{ - "attr1": 1, - }, - }, - }, - }, - }, - } { - t.Run(tc.Name, func(t *testing.T) { - proto, err := ServiceConfigToProto(&tc.Conf) - test.That(t, err, test.ShouldBeNil) - - out, err := ServiceConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - - test.That(t, out.String(), test.ShouldResemble, tc.Conf.String()) - _, err = out.Validate("test", resource.APITypeServiceName) - test.That(t, err, test.ShouldBeNil) - }) - } -} - -func TestServiceConfigWithEmptyModelName(t *testing.T) { - servicesConfigJSON := ` - { - "type": "base_remote_control", - "attributes": {}, - "depends_on": [], - "name": "base_rc" - }` - - var fromJSON resource.Config - err := json.Unmarshal([]byte(servicesConfigJSON), &fromJSON) - test.That(t, err, test.ShouldBeNil) - - // should have an empty model - test.That(t, fromJSON.Model, test.ShouldResemble, resource.Model{}) - - proto, err := ServiceConfigToProto(&fromJSON) - test.That(t, err, test.ShouldBeNil) - - out, err := ServiceConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - - test.That(t, out.Model, test.ShouldResemble, fromJSON.Model) - test.That(t, out.Model.Validate(), test.ShouldBeNil) - test.That(t, out.Model, test.ShouldResemble, resource.DefaultServiceModel) - _, err = out.Validate("...", resource.APITypeServiceName) - test.That(t, err, test.ShouldBeNil) -} - -func TestProcessConfigToProto(t *testing.T) { - proto, err := ProcessConfigToProto(&testProcessConfig) - test.That(t, err, test.ShouldBeNil) - out, err := ProcessConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - test.That(t, *out, test.ShouldResemble, testProcessConfig) -} - -func TestNetworkConfigToProto(t *testing.T) { - proto, err := NetworkConfigToProto(&testNetworkConfig) - test.That(t, err, test.ShouldBeNil) - out, err := NetworkConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - test.That(t, *out, test.ShouldResemble, testNetworkConfig) -} - -//nolint:thelper -func validateAuthConfig(t *testing.T, actual, expected AuthConfig) { - test.That(t, actual.TLSAuthEntities, test.ShouldResemble, expected.TLSAuthEntities) - test.That(t, actual.Handlers, test.ShouldHaveLength, 2) - test.That(t, actual.Handlers[0].Type, test.ShouldEqual, expected.Handlers[0].Type) - test.That(t, actual.Handlers[0].Config.Int("config-1", 0), test.ShouldEqual, expected.Handlers[0].Config.Int("config-1", -1)) - test.That(t, actual.Handlers[1].Type, test.ShouldEqual, expected.Handlers[1].Type) - test.That(t, actual.Handlers[1].Config.Int("config-2", 0), test.ShouldEqual, expected.Handlers[1].Config.Int("config-2", -1)) -} - -func TestAuthConfigToProto(t *testing.T) { - t.Run("api-key auth handler", func(t *testing.T) { - proto, err := AuthConfigToProto(&testAuthConfig) - test.That(t, err, test.ShouldBeNil) - out, err := AuthConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - validateAuthConfig(t, *out, testAuthConfig) - }) - - t.Run("external auth config", func(t *testing.T) { - keyset := jwk.NewSet() - privKeyForWebAuth, err := rsa.GenerateKey(rand.Reader, 4096) - test.That(t, err, test.ShouldBeNil) - publicKeyForWebAuth, err := jwk.New(privKeyForWebAuth.PublicKey) - test.That(t, err, test.ShouldBeNil) - publicKeyForWebAuth.Set(jwk.KeyIDKey, "key-id-1") - test.That(t, keyset.Add(publicKeyForWebAuth), test.ShouldBeTrue) - - authConfig := AuthConfig{ - TLSAuthEntities: []string{"tls1", "tls2"}, - ExternalAuthConfig: &ExternalAuthConfig{ - JSONKeySet: keysetToInterface(t, keyset).AsMap(), - }, - } - - proto, err := AuthConfigToProto(&authConfig) - test.That(t, err, test.ShouldBeNil) - out, err := AuthConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - test.That(t, out.ExternalAuthConfig, test.ShouldResemble, authConfig.ExternalAuthConfig) - }) -} - -func keysetToInterface(t *testing.T, keyset jwks.KeySet) *structpb.Struct { - t.Helper() - - // hack around marshaling the KeySet into pb.Struct. Passing interface directly - // does not work. - jwksAsJSON, err := json.Marshal(keyset) - test.That(t, err, test.ShouldBeNil) - - jwksAsInterface := map[string]interface{}{} - err = json.Unmarshal(jwksAsJSON, &jwksAsInterface) - test.That(t, err, test.ShouldBeNil) - - jwksAsStruct, err := structpb.NewStruct(jwksAsInterface) - test.That(t, err, test.ShouldBeNil) - - return jwksAsStruct -} - -func TestCloudConfigToProto(t *testing.T) { - proto, err := CloudConfigToProto(&testCloudConfig) - test.That(t, err, test.ShouldBeNil) - out, err := CloudConfigFromProto(proto) - test.That(t, err, test.ShouldBeNil) - - test.That(t, *out, test.ShouldResemble, testCloudConfig) -} - -func TestFromProto(t *testing.T) { - logger := logging.NewTestLogger(t) - cloudConfig, err := CloudConfigToProto(&testCloudConfig) - test.That(t, err, test.ShouldBeNil) - - remoteConfig, err := RemoteConfigToProto(&testRemote) - test.That(t, err, test.ShouldBeNil) - - moduleConfig, err := ModuleConfigToProto(&testModule) - test.That(t, err, test.ShouldBeNil) - - componentConfig, err := ComponentConfigToProto(&testComponent) - test.That(t, err, test.ShouldBeNil) - rewriteTestComponentProto(componentConfig) - - processConfig, err := ProcessConfigToProto(&testProcessConfig) - test.That(t, err, test.ShouldBeNil) - - serviceConfig, err := ServiceConfigToProto(&testService) - test.That(t, err, test.ShouldBeNil) - - networkConfig, err := NetworkConfigToProto(&testNetworkConfig) - test.That(t, err, test.ShouldBeNil) - - authConfig, err := AuthConfigToProto(&testAuthConfig) - test.That(t, err, test.ShouldBeNil) - - packageConfig, err := PackageConfigToProto(&testPackageConfig) - test.That(t, err, test.ShouldBeNil) - - debug := true - - input := &pb.RobotConfig{ - Cloud: cloudConfig, - Remotes: []*pb.RemoteConfig{remoteConfig}, - Modules: []*pb.ModuleConfig{moduleConfig}, - Components: []*pb.ComponentConfig{componentConfig}, - Processes: []*pb.ProcessConfig{processConfig}, - Services: []*pb.ServiceConfig{serviceConfig}, - Packages: []*pb.PackageConfig{packageConfig}, - Network: networkConfig, - Auth: authConfig, - Debug: &debug, - } - - out, err := FromProto(input, logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, *out.Cloud, test.ShouldResemble, testCloudConfig) - test.That(t, out.Remotes, test.ShouldHaveLength, 1) - validateRemote(t, out.Remotes[0], testRemote) - test.That(t, out.Modules, test.ShouldHaveLength, 1) - validateModule(t, out.Modules[0], testModule) - test.That(t, out.Components, test.ShouldHaveLength, 1) - validateComponent(t, out.Components[0], testComponent) - test.That(t, out.Processes, test.ShouldHaveLength, 1) - test.That(t, out.Processes[0], test.ShouldResemble, testProcessConfig) - test.That(t, out.Services, test.ShouldHaveLength, 1) - validateService(t, out.Services[0], testService) - test.That(t, out.Network, test.ShouldResemble, testNetworkConfig) - validateAuthConfig(t, out.Auth, testAuthConfig) - test.That(t, out.Debug, test.ShouldEqual, debug) - test.That(t, out.Packages, test.ShouldHaveLength, 1) - test.That(t, out.Packages[0], test.ShouldResemble, testPackageConfig) -} - -func TestPartialStart(t *testing.T) { - logger := logging.NewTestLogger(t) - cloudConfig, err := CloudConfigToProto(&testCloudConfig) - test.That(t, err, test.ShouldBeNil) - - remoteConfig, err := RemoteConfigToProto(&testRemote) - test.That(t, err, test.ShouldBeNil) - - moduleConfig, err := ModuleConfigToProto(&testModule) - test.That(t, err, test.ShouldBeNil) - - componentConfig, err := ComponentConfigToProto(&testComponent) - test.That(t, err, test.ShouldBeNil) - rewriteTestComponentProto(componentConfig) - - processConfig, err := ProcessConfigToProto(&testProcessConfig) - test.That(t, err, test.ShouldBeNil) - - serviceConfig, err := ServiceConfigToProto(&testService) - test.That(t, err, test.ShouldBeNil) - - networkConfig, err := NetworkConfigToProto(&testNetworkConfig) - test.That(t, err, test.ShouldBeNil) - - authConfig, err := AuthConfigToProto(&testAuthConfig) - test.That(t, err, test.ShouldBeNil) - - packageConfig, err := PackageConfigToProto(&testPackageConfig) - test.That(t, err, test.ShouldBeNil) - - remoteInvalidConfig, err := RemoteConfigToProto(&testInvalidRemote) - test.That(t, err, test.ShouldBeNil) - - moduleInvalidConfig, err := ModuleConfigToProto(&testInvalidModule) - test.That(t, err, test.ShouldBeNil) - - componentInvalidConfig, err := ComponentConfigToProto(&testInvalidComponent) - test.That(t, err, test.ShouldBeNil) - - processInvalidConfig, err := ProcessConfigToProto(&testInvalidProcessConfig) - test.That(t, err, test.ShouldBeNil) - - serviceInvalidConfig, err := ServiceConfigToProto(&testInvalidService) - test.That(t, err, test.ShouldBeNil) - - packageInvalidConfig, err := PackageConfigToProto(&testInvalidPackage) - test.That(t, err, test.ShouldBeNil) - - debug := true - disablePartialStart := false - - input := &pb.RobotConfig{ - Cloud: cloudConfig, - Remotes: []*pb.RemoteConfig{remoteConfig, remoteInvalidConfig}, - Modules: []*pb.ModuleConfig{moduleConfig, moduleInvalidConfig}, - Components: []*pb.ComponentConfig{componentConfig, componentInvalidConfig}, - Processes: []*pb.ProcessConfig{processConfig, processInvalidConfig}, - Services: []*pb.ServiceConfig{serviceConfig, serviceInvalidConfig}, - Packages: []*pb.PackageConfig{packageConfig, packageInvalidConfig}, - Network: networkConfig, - Auth: authConfig, - Debug: &debug, - DisablePartialStart: &disablePartialStart, - } - - out, err := FromProto(input, logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, *out.Cloud, test.ShouldResemble, testCloudConfig) - validateRemote(t, out.Remotes[0], testRemote) - test.That(t, out.Remotes[1].Name, test.ShouldEqual, "") - validateModule(t, out.Modules[0], testModule) - test.That(t, out.Modules[1], test.ShouldResemble, testInvalidModule) - test.That(t, len(out.Components), test.ShouldEqual, 1) - validateComponent(t, out.Components[0], testComponent) - // there should only be one valid component and service in our list - test.That(t, out.Processes[0], test.ShouldResemble, testProcessConfig) - test.That(t, out.Processes[1], test.ShouldResemble, testInvalidProcessConfig) - test.That(t, len(out.Services), test.ShouldEqual, 1) - validateService(t, out.Services[0], testService) - test.That(t, out.Network, test.ShouldResemble, testNetworkConfig) - validateAuthConfig(t, out.Auth, testAuthConfig) - test.That(t, out.Debug, test.ShouldEqual, debug) - test.That(t, out.Packages[0], test.ShouldResemble, testPackageConfig) - test.That(t, out.Packages[1], test.ShouldResemble, testInvalidPackage) -} - -func TestDisablePartialStart(t *testing.T) { - logger := logging.NewTestLogger(t) - cloudConfig, err := CloudConfigToProto(&testCloudConfig) - test.That(t, err, test.ShouldBeNil) - - remoteConfig, err := RemoteConfigToProto(&testRemote) - test.That(t, err, test.ShouldBeNil) - - moduleConfig, err := ModuleConfigToProto(&testModule) - test.That(t, err, test.ShouldBeNil) - - componentInvalidConfig, err := ComponentConfigToProto(&testInvalidComponent) - test.That(t, err, test.ShouldBeNil) - - processConfig, err := ProcessConfigToProto(&testProcessConfig) - test.That(t, err, test.ShouldBeNil) - - serviceConfig, err := ServiceConfigToProto(&testService) - test.That(t, err, test.ShouldBeNil) - - networkConfig, err := NetworkConfigToProto(&testNetworkConfig) - test.That(t, err, test.ShouldBeNil) - - authConfig, err := AuthConfigToProto(&testAuthConfig) - test.That(t, err, test.ShouldBeNil) - - debug := true - disablePartialStart := true - - input := &pb.RobotConfig{ - Cloud: cloudConfig, - Remotes: []*pb.RemoteConfig{remoteConfig}, - Modules: []*pb.ModuleConfig{moduleConfig}, - Components: []*pb.ComponentConfig{componentInvalidConfig}, - Processes: []*pb.ProcessConfig{processConfig}, - Services: []*pb.ServiceConfig{serviceConfig}, - Network: networkConfig, - Auth: authConfig, - Debug: &debug, - DisablePartialStart: &disablePartialStart, - } - - out, err := FromProto(input, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, out, test.ShouldBeNil) -} - -func TestPackageTypeConversion(t *testing.T) { - emptyType := PackageType("") - _, err := PackageTypeToProto(emptyType) - test.That(t, err, test.ShouldNotBeNil) - - moduleType := PackageType("module") - converted, err := PackageTypeToProto(moduleType) - test.That(t, err, test.ShouldBeNil) - test.That(t, converted, test.ShouldResemble, packagespb.PackageType_PACKAGE_TYPE_MODULE.Enum()) - - badType := PackageType("invalid-package-type") - converted, err = PackageTypeToProto(badType) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "invalid-package-type") - test.That(t, converted, test.ShouldResemble, packagespb.PackageType_PACKAGE_TYPE_UNSPECIFIED.Enum()) -} diff --git a/config/reader_ext_test.go b/config/reader_ext_test.go deleted file mode 100644 index dd265d92eee..00000000000 --- a/config/reader_ext_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package config_test - -import ( - "context" - "errors" - "strings" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -func TestFromReaderValidate(t *testing.T) { - logger := logging.NewTestLogger(t) - _, err := config.FromReader(context.Background(), "somepath", strings.NewReader(""), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "json: EOF") - - _, err = config.FromReader(context.Background(), "somepath", strings.NewReader(`{"cloud": 1}`), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "unmarshal") - - conf, err := config.FromReader(context.Background(), "somepath", strings.NewReader(`{}`), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, conf, test.ShouldResemble, &config.Config{ - ConfigFilePath: "somepath", - Network: config.NetworkConfig{ - NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - BindAddressDefaultSet: true, - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }, - }, - }) - - _, err = config.FromReader(context.Background(), "somepath", strings.NewReader(`{"cloud": {}}`), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "id") - - _, err = config.FromReader(context.Background(), - "somepath", strings.NewReader(`{"disable_partial_start":true,"components": [{}]}`), logger) - test.That(t, err, test.ShouldNotBeNil) - var fre resource.FieldRequiredError - test.That(t, errors.As(err, &fre), test.ShouldBeTrue) - test.That(t, fre.Path, test.ShouldEqual, "components.0") - test.That(t, fre.Field, test.ShouldEqual, "name") - - conf, err = config.FromReader(context.Background(), - "somepath", - strings.NewReader(`{"components": [{"name": "foo", "type": "arm", "model": "foo"}]}`), - logger) - test.That(t, err, test.ShouldBeNil) - expected := &config.Config{ - ConfigFilePath: "somepath", - Components: []resource.Config{ - { - Name: "foo", - API: arm.API, - Model: resource.DefaultModelFamily.WithModel("foo"), - }, - }, - Network: config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - BindAddressDefaultSet: true, - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }}, - } - test.That(t, expected.Ensure(false, logger), test.ShouldBeNil) - test.That(t, conf, test.ShouldResemble, expected) -} diff --git a/config/reader_test.go b/config/reader_test.go deleted file mode 100644 index 87f6c2ae3a8..00000000000 --- a/config/reader_test.go +++ /dev/null @@ -1,369 +0,0 @@ -package config - -import ( - "context" - "errors" - "fmt" - "io/fs" - "os" - "strings" - "testing" - "time" - - "github.com/google/uuid" - pb "go.viam.com/api/app/v1" - "go.viam.com/test" - - "go.viam.com/rdk/config/testutils" - "go.viam.com/rdk/logging" -) - -func TestFromReader(t *testing.T) { - const ( - robotPartID = "forCachingTest" - secret = testutils.FakeCredentialPayLoad - ) - var ( - logger = logging.NewTestLogger(t) - ctx = context.Background() - ) - - // clear cache - setupClearCache := func(t *testing.T) { - t.Helper() - clearCache(robotPartID) - _, err := readFromCache(robotPartID) - test.That(t, os.IsNotExist(err), test.ShouldBeTrue) - } - - t.Run("online", func(t *testing.T) { - setupClearCache(t) - - fakeServer, cleanup := testutils.NewFakeCloudServer(t, ctx, logger) - defer cleanup() - - cloudResponse := &Cloud{ - ManagedBy: "acme", - SignalingAddress: "abc", - ID: robotPartID, - Secret: secret, - FQDN: "fqdn", - LocalFQDN: "localFqdn", - LocationSecrets: []LocationSecret{}, - LocationID: "the-location", - PrimaryOrgID: "the-primary-org", - MachineID: "the-machine", - } - certProto := &pb.CertificateResponse{ - TlsCertificate: "cert", - TlsPrivateKey: "key", - } - - cloudConfProto, err := CloudConfigToProto(cloudResponse) - test.That(t, err, test.ShouldBeNil) - protoConfig := &pb.RobotConfig{Cloud: cloudConfProto} - fakeServer.StoreDeviceConfig(robotPartID, protoConfig, certProto) - - appAddress := fmt.Sprintf("http://%s", fakeServer.Addr().String()) - cfgText := fmt.Sprintf(`{"cloud":{"id":%q,"app_address":%q,"secret":%q}}`, robotPartID, appAddress, secret) - gotCfg, err := FromReader(ctx, "", strings.NewReader(cfgText), logger) - defer clearCache(robotPartID) - test.That(t, err, test.ShouldBeNil) - - expectedCloud := *cloudResponse - expectedCloud.AppAddress = appAddress - expectedCloud.TLSCertificate = certProto.TlsCertificate - expectedCloud.TLSPrivateKey = certProto.TlsPrivateKey - expectedCloud.RefreshInterval = time.Duration(10000000000) - test.That(t, gotCfg.Cloud, test.ShouldResemble, &expectedCloud) - - cachedCfg, err := readFromCache(robotPartID) - test.That(t, err, test.ShouldBeNil) - expectedCloud.AppAddress = "" - test.That(t, cachedCfg.Cloud, test.ShouldResemble, &expectedCloud) - }) - - t.Run("offline with cached config", func(t *testing.T) { - setupClearCache(t) - - cachedCloud := &Cloud{ - ManagedBy: "acme", - SignalingAddress: "abc", - ID: robotPartID, - Secret: secret, - FQDN: "fqdn", - LocalFQDN: "localFqdn", - TLSCertificate: "cert", - TLSPrivateKey: "key", - LocationID: "the-location", - PrimaryOrgID: "the-primary-org", - MachineID: "the-machine", - } - cachedConf := &Config{Cloud: cachedCloud} - err := storeToCache(robotPartID, cachedConf) - test.That(t, err, test.ShouldBeNil) - defer clearCache(robotPartID) - - fakeServer, cleanup := testutils.NewFakeCloudServer(t, ctx, logger) - defer cleanup() - fakeServer.FailOnConfigAndCertsWith(context.DeadlineExceeded) - fakeServer.StoreDeviceConfig(robotPartID, nil, nil) - - appAddress := fmt.Sprintf("http://%s", fakeServer.Addr().String()) - cfgText := fmt.Sprintf(`{"cloud":{"id":%q,"app_address":%q,"secret":%q}}`, robotPartID, appAddress, secret) - gotCfg, err := FromReader(ctx, "", strings.NewReader(cfgText), logger) - test.That(t, err, test.ShouldBeNil) - - expectedCloud := *cachedCloud - expectedCloud.AppAddress = appAddress - expectedCloud.TLSCertificate = "cert" - expectedCloud.TLSPrivateKey = "key" - expectedCloud.RefreshInterval = time.Duration(10000000000) - test.That(t, gotCfg.Cloud, test.ShouldResemble, &expectedCloud) - }) - - t.Run("online with insecure signaling", func(t *testing.T) { - setupClearCache(t) - - fakeServer, cleanup := testutils.NewFakeCloudServer(t, ctx, logger) - defer cleanup() - - cloudResponse := &Cloud{ - ManagedBy: "acme", - SignalingAddress: "abc", - SignalingInsecure: true, - ID: robotPartID, - Secret: secret, - FQDN: "fqdn", - LocalFQDN: "localFqdn", - LocationSecrets: []LocationSecret{}, - LocationID: "the-location", - PrimaryOrgID: "the-primary-org", - MachineID: "the-machine", - } - certProto := &pb.CertificateResponse{} - - cloudConfProto, err := CloudConfigToProto(cloudResponse) - test.That(t, err, test.ShouldBeNil) - protoConfig := &pb.RobotConfig{Cloud: cloudConfProto} - fakeServer.StoreDeviceConfig(robotPartID, protoConfig, certProto) - - appAddress := fmt.Sprintf("http://%s", fakeServer.Addr().String()) - cfgText := fmt.Sprintf(`{"cloud":{"id":%q,"app_address":%q,"secret":%q}}`, robotPartID, appAddress, secret) - gotCfg, err := FromReader(ctx, "", strings.NewReader(cfgText), logger) - defer clearCache(robotPartID) - test.That(t, err, test.ShouldBeNil) - - expectedCloud := *cloudResponse - expectedCloud.AppAddress = appAddress - expectedCloud.RefreshInterval = time.Duration(10000000000) - test.That(t, gotCfg.Cloud, test.ShouldResemble, &expectedCloud) - - cachedCfg, err := readFromCache(robotPartID) - test.That(t, err, test.ShouldBeNil) - expectedCloud.AppAddress = "" - test.That(t, cachedCfg.Cloud, test.ShouldResemble, &expectedCloud) - }) -} - -func TestStoreToCache(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - cfg, err := FromReader(ctx, "", strings.NewReader(`{}`), logger) - - test.That(t, err, test.ShouldBeNil) - - cloud := &Cloud{ - ManagedBy: "acme", - SignalingAddress: "abc", - ID: "forCachingTest", - Secret: "ghi", - FQDN: "fqdn", - LocalFQDN: "localFqdn", - TLSCertificate: "cert", - TLSPrivateKey: "key", - AppAddress: "https://app.viam.dev:443", - LocationID: "the-location", - PrimaryOrgID: "the-primary-org", - MachineID: "the-machine", - } - cfg.Cloud = cloud - - // store our config to the cloud - err = storeToCache(cfg.Cloud.ID, cfg) - test.That(t, err, test.ShouldBeNil) - - // read config from cloud, confirm consistency - cloudCfg, err := readFromCloud(ctx, cfg, nil, true, false, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, cloudCfg, test.ShouldResemble, cfg) - - // Modify our config - newRemote := Remote{Name: "test", Address: "foo"} - cfg.Remotes = append(cfg.Remotes, newRemote) - - // read config from cloud again, confirm that the cached config differs from cfg - cloudCfg2, err := readFromCloud(ctx, cfg, nil, true, false, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, cloudCfg2, test.ShouldNotResemble, cfg) - - // store the updated config to the cloud - err = storeToCache(cfg.Cloud.ID, cfg) - test.That(t, err, test.ShouldBeNil) - - test.That(t, cfg.Ensure(true, logger), test.ShouldBeNil) - - // read updated cloud config, confirm that it now matches our updated cfg - cloudCfg3, err := readFromCloud(ctx, cfg, nil, true, false, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, cloudCfg3, test.ShouldResemble, cfg) -} - -func TestCacheInvalidation(t *testing.T) { - id := uuid.New().String() - // store invalid config in cache - cachePath := getCloudCacheFilePath(id) - err := os.WriteFile(cachePath, []byte("invalid-json"), 0o750) - test.That(t, err, test.ShouldBeNil) - - // read from cache, should return parse error and remove file - _, err = readFromCache(id) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot parse the cached config as json") - - // read from cache again and file should not exist - _, err = readFromCache(id) - test.That(t, os.IsNotExist(err), test.ShouldBeTrue) -} - -func TestShouldCheckForCert(t *testing.T) { - cloud1 := Cloud{ - ManagedBy: "acme", - SignalingAddress: "abc", - ID: "forCachingTest", - Secret: "ghi", - FQDN: "fqdn", - LocalFQDN: "localFqdn", - TLSCertificate: "cert", - TLSPrivateKey: "key", - LocationID: "the-location", - PrimaryOrgID: "the-primary-org", - MachineID: "the-machine", - LocationSecrets: []LocationSecret{ - {ID: "id1", Secret: "secret1"}, - {ID: "id2", Secret: "secret2"}, - }, - } - cloud2 := cloud1 - test.That(t, shouldCheckForCert(&cloud1, &cloud2), test.ShouldBeFalse) - - cloud2.TLSCertificate = "abc" - test.That(t, shouldCheckForCert(&cloud1, &cloud2), test.ShouldBeFalse) - - cloud2 = cloud1 - cloud2.LocationSecret = "something else" - test.That(t, shouldCheckForCert(&cloud1, &cloud2), test.ShouldBeTrue) - - cloud2 = cloud1 - cloud2.LocationSecrets = []LocationSecret{ - {ID: "id1", Secret: "secret1"}, - {ID: "id2", Secret: "secret3"}, - } - test.That(t, shouldCheckForCert(&cloud1, &cloud2), test.ShouldBeTrue) -} - -func TestProcessConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - unprocessedConfig := Config{ - ConfigFilePath: "path", - } - - cfg, err := processConfig(&unprocessedConfig, true, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, *cfg, test.ShouldResemble, unprocessedConfig) -} - -func TestReadTLSFromCache(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - cfg, err := FromReader(ctx, "", strings.NewReader(`{}`), logger) - test.That(t, err, test.ShouldBeNil) - - robotPartID := "forCachingTest" - t.Run("no cached config", func(t *testing.T) { - clearCache(robotPartID) - test.That(t, err, test.ShouldBeNil) - - tls := tlsConfig{} - err = tls.readFromCache(robotPartID, logger) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("cache config without cloud", func(t *testing.T) { - defer clearCache(robotPartID) - cfg.Cloud = nil - - err = storeToCache(robotPartID, cfg) - test.That(t, err, test.ShouldBeNil) - - tls := tlsConfig{} - err = tls.readFromCache(robotPartID, logger) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("invalid cached TLS", func(t *testing.T) { - defer clearCache(robotPartID) - cloud := &Cloud{ - TLSPrivateKey: "key", - } - cfg.Cloud = cloud - - err = storeToCache(robotPartID, cfg) - test.That(t, err, test.ShouldBeNil) - - tls := tlsConfig{} - err = tls.readFromCache(robotPartID, logger) - test.That(t, err, test.ShouldNotBeNil) - - _, err = readFromCache(robotPartID) - test.That(t, errors.Is(err, fs.ErrNotExist), test.ShouldBeTrue) - }) - - t.Run("invalid cached TLS but insecure signaling", func(t *testing.T) { - defer clearCache(robotPartID) - cloud := &Cloud{ - TLSPrivateKey: "key", - SignalingInsecure: true, - } - cfg.Cloud = cloud - - err = storeToCache(robotPartID, cfg) - test.That(t, err, test.ShouldBeNil) - - tls := tlsConfig{} - err = tls.readFromCache(robotPartID, logger) - test.That(t, err, test.ShouldBeNil) - - _, err = readFromCache(robotPartID) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("valid cached TLS", func(t *testing.T) { - defer clearCache(robotPartID) - cloud := &Cloud{ - TLSCertificate: "cert", - TLSPrivateKey: "key", - } - cfg.Cloud = cloud - - err = storeToCache(robotPartID, cfg) - test.That(t, err, test.ShouldBeNil) - - // the config is missing several fields required to start the robot, but this - // should not prevent us from reading TLS information from it. - _, err = processConfigFromCloud(cfg, logger) - test.That(t, err, test.ShouldNotBeNil) - tls := tlsConfig{} - err = tls.readFromCache(robotPartID, logger) - test.That(t, err, test.ShouldBeNil) - }) -} diff --git a/config/verify_main_test.go b/config/verify_main_test.go deleted file mode 100644 index ceccb889f97..00000000000 --- a/config/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package config - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/config/watcher_test.go b/config/watcher_test.go deleted file mode 100644 index 3cb7df459a0..00000000000 --- a/config/watcher_test.go +++ /dev/null @@ -1,364 +0,0 @@ -package config_test - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "os" - "testing" - "time" - - "go.mongodb.org/mongo-driver/bson/primitive" - pb "go.viam.com/api/app/v1" - "go.viam.com/test" - "go.viam.com/utils/pexec" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/config" - "go.viam.com/rdk/config/testutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - rutils "go.viam.com/rdk/utils" -) - -func TestNewWatcherNoop(t *testing.T) { - logger := logging.NewTestLogger(t) - watcher, err := config.NewWatcher(context.Background(), &config.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - - timer := time.NewTimer(time.Second) - defer timer.Stop() - select { - case c := <-watcher.Config(): - test.That(t, c, test.ShouldBeNil) - case <-timer.C: - } - - test.That(t, watcher.Close(), test.ShouldBeNil) -} - -func TestNewWatcherFile(t *testing.T) { - logger := logging.NewTestLogger(t) - - temp, err := os.CreateTemp(t.TempDir(), "*.json") - test.That(t, err, test.ShouldBeNil) - defer os.Remove(temp.Name()) - - watcher, err := config.NewWatcher(context.Background(), &config.Config{ConfigFilePath: temp.Name()}, logger) - test.That(t, err, test.ShouldBeNil) - - writeConf := func(conf *config.Config) { - md, err := json.Marshal(&conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, os.WriteFile(temp.Name(), md, 0o755), test.ShouldBeNil) - for { - rd, err := os.ReadFile(temp.Name()) - test.That(t, err, test.ShouldBeNil) - if bytes.Equal(rd, md) { - break - } - time.Sleep(time.Second) - } - } - - confToWrite := config.Config{ - ConfigFilePath: temp.Name(), - Components: []resource.Config{ - { - API: arm.API, - Name: "hello", - Model: resource.DefaultModelFamily.WithModel("hello"), - Attributes: rutils.AttributeMap{ - "world": 1.0, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "echo", - }, - }, - Network: config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }}, - } - writeConf(&confToWrite) - test.That(t, confToWrite.Ensure(false, logger), test.ShouldBeNil) - - newConf := <-watcher.Config() - test.That(t, newConf, test.ShouldResemble, &confToWrite) - - confToWrite = config.Config{ - ConfigFilePath: temp.Name(), - Components: []resource.Config{ - { - API: arm.API, - Name: "world", - Model: resource.DefaultModelFamily.WithModel("world"), - Attributes: rutils.AttributeMap{ - "hello": 1.0, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "2", - Name: "bar", - }, - }, - Network: config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }}, - } - writeConf(&confToWrite) - test.That(t, confToWrite.Ensure(false, logger), test.ShouldBeNil) - - newConf = <-watcher.Config() - test.That(t, newConf, test.ShouldResemble, &confToWrite) - - go func() { - f, err := os.OpenFile(temp.Name(), os.O_RDWR|os.O_CREATE, 0o755) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, f.Close(), test.ShouldBeNil) - }() - _, err = f.WriteString("blahblah") - test.That(t, err, test.ShouldBeNil) - test.That(t, f.Sync(), test.ShouldBeNil) - }() - - timer := time.NewTimer(time.Second) - defer timer.Stop() - select { - case c := <-watcher.Config(): - test.That(t, c, test.ShouldBeNil) - case <-timer.C: - } - - confToWrite = config.Config{ - ConfigFilePath: temp.Name(), - Components: []resource.Config{ - { - API: arm.API, - Name: "woo", - Model: resource.DefaultModelFamily.WithModel("woo"), - Attributes: rutils.AttributeMap{ - "wah": 1.0, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "wee", - Name: "mah", - }, - }, - Network: config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }}, - } - writeConf(&confToWrite) - test.That(t, confToWrite.Ensure(false, logger), test.ShouldBeNil) - - newConf = <-watcher.Config() - test.That(t, newConf, test.ShouldResemble, &confToWrite) - - test.That(t, watcher.Close(), test.ShouldBeNil) -} - -func TestNewWatcherCloud(t *testing.T) { - logger := logging.NewTestLogger(t) - - certsToReturn := config.Cloud{ - TLSCertificate: "hello", - TLSPrivateKey: "world", - } - - deviceID := primitive.NewObjectID().Hex() - - fakeServer, cleanup := testutils.NewFakeCloudServer(t, context.Background(), logger) - defer cleanup() - - storeConfigInServer := func(cfg config.Config) { - cloudConfProto, err := config.CloudConfigToProto(cfg.Cloud) - test.That(t, err, test.ShouldBeNil) - - componentConfProto, err := config.ComponentConfigToProto(&cfg.Components[0]) - test.That(t, err, test.ShouldBeNil) - - proccessConfProto, err := config.ProcessConfigToProto(&cfg.Processes[0]) - test.That(t, err, test.ShouldBeNil) - - networkConfProto, err := config.NetworkConfigToProto(&cfg.Network) - test.That(t, err, test.ShouldBeNil) - - protoConfig := &pb.RobotConfig{ - Cloud: cloudConfProto, - Components: []*pb.ComponentConfig{componentConfProto}, - Processes: []*pb.ProcessConfig{proccessConfProto}, - Network: networkConfProto, - } - - fakeServer.Clear() - fakeServer.StoreDeviceConfig(deviceID, protoConfig, &pb.CertificateResponse{ - TlsCertificate: certsToReturn.TLSCertificate, - TlsPrivateKey: certsToReturn.TLSPrivateKey, - }) - } - - var confToReturn config.Config - newCloudConf := func() *config.Cloud { - return &config.Cloud{ - AppAddress: fmt.Sprintf("http://%s", fakeServer.Addr().String()), - ID: deviceID, - Secret: testutils.FakeCredentialPayLoad, - FQDN: "woo", - LocalFQDN: "yee", - RefreshInterval: time.Second, - LocationSecrets: []config.LocationSecret{{ID: "1", Secret: "secret"}}, - PrimaryOrgID: "the-primary-org", - LocationID: "the-location", - MachineID: "the-machine", - } - } - - confToReturn = config.Config{ - Cloud: newCloudConf(), - Components: []resource.Config{ - { - API: arm.API, - Name: "hello", - Model: resource.DefaultModelFamily.WithModel("hello"), - Attributes: rutils.AttributeMap{ - "world": 1.0, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "echo", - }, - }, - Network: config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }}, - } - - storeConfigInServer(confToReturn) - - watcher, err := config.NewWatcher(context.Background(), &config.Config{Cloud: newCloudConf()}, logger) - test.That(t, err, test.ShouldBeNil) - - confToExpect := confToReturn - confToExpect.Cloud.TLSCertificate = certsToReturn.TLSCertificate - confToExpect.Cloud.TLSPrivateKey = certsToReturn.TLSPrivateKey - test.That(t, confToExpect.Ensure(true, logger), test.ShouldBeNil) - - newConf := <-watcher.Config() - test.That(t, newConf, test.ShouldResemble, &confToExpect) - - confToReturn = config.Config{ - Cloud: newCloudConf(), - Components: []resource.Config{ - { - API: arm.API, - Name: "world", - Model: resource.DefaultModelFamily.WithModel("world"), - Attributes: rutils.AttributeMap{ - "hello": 1.0, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "2", - Name: "bar", - }, - }, - Network: config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }}, - } - - // update the config with the newer config - storeConfigInServer(confToReturn) - - confToExpect = confToReturn - confToExpect.Cloud.TLSCertificate = certsToReturn.TLSCertificate - confToExpect.Cloud.TLSPrivateKey = certsToReturn.TLSPrivateKey - test.That(t, confToExpect.Ensure(true, logger), test.ShouldBeNil) - - newConf = <-watcher.Config() - test.That(t, newConf, test.ShouldResemble, &confToExpect) - - // fake server will start returning 5xx on requests. - // no new configs should be emitted to channel until the fake server starts returning again - fakeServer.FailOnConfigAndCerts(true) - timer := time.NewTimer(5 * time.Second) - defer timer.Stop() - select { - case c := <-watcher.Config(): - test.That(t, c, test.ShouldBeNil) - case <-timer.C: - } - fakeServer.FailOnConfigAndCerts(false) - - newConf = <-watcher.Config() - test.That(t, newConf, test.ShouldResemble, &confToExpect) - - confToReturn = config.Config{ - Cloud: newCloudConf(), - Components: []resource.Config{ - { - API: arm.API, - Name: "woo", - Model: resource.DefaultModelFamily.WithModel("woo"), - Attributes: rutils.AttributeMap{ - "wah": 1.0, - }, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "wee", - Name: "mah", - }, - }, - Network: config.NetworkConfig{NetworkConfigData: config.NetworkConfigData{ - BindAddress: "localhost:8080", - Sessions: config.SessionsConfig{ - HeartbeatWindow: config.DefaultSessionHeartbeatWindow, - }, - }}, - } - - storeConfigInServer(confToReturn) - - confToExpect = confToReturn - confToExpect.Cloud.TLSCertificate = certsToReturn.TLSCertificate - confToExpect.Cloud.TLSPrivateKey = certsToReturn.TLSPrivateKey - test.That(t, confToExpect.Ensure(true, logger), test.ShouldBeNil) - - newConf = <-watcher.Config() - test.That(t, newConf, test.ShouldResemble, &confToExpect) - - test.That(t, watcher.Close(), test.ShouldBeNil) -} diff --git a/control/constant_test.go b/control/constant_test.go deleted file mode 100644 index 35edba5db16..00000000000 --- a/control/constant_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package control - -import ( - "context" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -func TestConstantConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - for _, c := range []struct { - conf BlockConfig - err string - }{ - { - BlockConfig{ - Name: "constant1", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_val": 1.89345, - }, - DependsOn: []string{}, - }, - "", - }, - { - BlockConfig{ - Name: "constant1", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_S": 1.89345, - }, - DependsOn: []string{}, - }, - "constant block constant1 doesn't have a constant_val field", - }, - { - BlockConfig{ - Name: "constant1", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_val": 1.89345, - }, - DependsOn: []string{"A", "B"}, - }, - "invalid number of inputs for constant block constant1 expected 0 got 2", - }, - } { - b, err := newConstant(c.conf, logger) - if c.err == "" { - s := b.(*constant) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(s.y), test.ShouldEqual, 1) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, c.err) - } - } -} - -func TestConstantNext(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - c := BlockConfig{ - Name: "constant1", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_val": 1.89345, - }, - DependsOn: []string{}, - } - s, err := newConstant(c, logger) - test.That(t, err, test.ShouldBeNil) - out, ok := s.Next(ctx, []*Signal{}, (time.Millisecond * 1)) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 1.89345) -} diff --git a/control/control_loop_test.go b/control/control_loop_test.go deleted file mode 100644 index 810071c4325..00000000000 --- a/control/control_loop_test.go +++ /dev/null @@ -1,385 +0,0 @@ -package control - -import ( - "context" - "fmt" - "math" - "math/rand" - "strings" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -type wrapBlocks struct { - c BlockConfig - x int - y int -} - -func generateNInputs(n int, baseName string) []wrapBlocks { - out := make([]wrapBlocks, n) - - out[0].c = BlockConfig{ - Name: "", - Type: "endpoint", - Attribute: utils.AttributeMap{ - "motor_name": "MotorFake", - }, - DependsOn: []string{}, - } - out[0].x = 0 - out[0].y = 0 - out[0].c.Name = fmt.Sprintf("%s%d", baseName, 0) - for i := 1; i < n; i++ { - out[i].c = BlockConfig{ - Name: "S1", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_val": 3.0, - }, - DependsOn: []string{}, - } - out[i].x = 0 - out[i].y = i - out[i].c.Name = fmt.Sprintf("%s%d", baseName, i) - } - return out -} - -func generateNSums(n, xMax, yMax int, baseName string, ins []wrapBlocks) []wrapBlocks { - b := wrapBlocks{ - c: BlockConfig{ - Name: "", - Type: "sum", - Attribute: utils.AttributeMap{ - "sum_string": "", - }, - DependsOn: []string{}, - }, - x: xMax - 1, - y: (yMax - 1) / 2, - } - b.c.Name = fmt.Sprintf("%s%d", baseName, 0) - ins = append(ins, b) - for i := 1; i < n; i++ { - var xR int - var yR int - for { - xR = rand.Intn(xMax-2) + 2 - yR = rand.Intn(yMax) - j := 0 - for ; j < len(ins); j++ { - if ins[j].x == xR && ins[j].y == yR { - j = 0 - break - } - } - if j == len(ins) { - break - } - } - b = wrapBlocks{ - c: BlockConfig{ - Name: "", - Type: "sum", - Attribute: utils.AttributeMap{ - "sum_string": "", - }, - DependsOn: []string{}, - }, - x: xR, - y: yR, - } - b.c.Name = fmt.Sprintf("%s%d", baseName, i) - ins = append(ins, b) - } - return ins -} - -func generateNBlocks(n, xMax, yMax int, baseName string, ins []wrapBlocks) []wrapBlocks { - for i := 0; i < n; i++ { - var xR int - var yR int - for { - xR = rand.Intn(xMax-1) + 1 - yR = rand.Intn(yMax) - j := 0 - for ; j < len(ins); j++ { - if ins[j].x == xR && ins[j].y == yR { - j = 0 - break - } - } - if j == len(ins) { - break - } - } - b := wrapBlocks{ - c: BlockConfig{ - Name: "C", - Type: "gain", - Attribute: utils.AttributeMap{ - "gain": -2.0, - }, - DependsOn: []string{}, - }, - x: xR, - y: yR, - } - b.c.Name = fmt.Sprintf("%s%d", baseName, i) - ins = append(ins, b) - } - return ins -} - -func findVerticalBlock(xStart, xMax, yStart int, grid [][]*wrapBlocks) *wrapBlocks { - for i := xStart + 1; i < xMax; i++ { - if grid[i][yStart] != nil { - return grid[i][yStart] - } - } - return nil -} - -func findSumHalfSquare(xMax, yMax, xStart, yStart int, grid [][]*wrapBlocks) *wrapBlocks { - for i := xStart + 1; i < int(math.Max(float64(xMax), float64(xStart+1))); i++ { - for j := yStart - 1; j < yStart+1; j++ { - if i > xMax-1 || j > yMax-1 || i < 0 || j < 0 { - continue - } - if grid[i][j] != nil && (grid[i][j].c.Type == "sum") { - return grid[i][j] - } - } - } - return nil -} - -func mergedAll(xMax, yMax int, grid [][]*wrapBlocks, def *wrapBlocks) { - for i, l := range grid { - for j, b := range l { - if b == nil { - continue - } - n := findVerticalBlock(i, xMax, j, grid) - if n == nil { - n = findSumHalfSquare(xMax, yMax, i, j, grid) - if n == nil { - if b != def { - def.c.DependsOn = append(def.c.DependsOn, b.c.Name) - } - continue - } - } - n.c.DependsOn = append(n.c.DependsOn, b.c.Name) - if n.c.Type != "sum" { - n = findSumHalfSquare(xMax, yMax, i, j, grid) - if n != nil { - n.c.DependsOn = append(n.c.DependsOn, b.c.Name) - } - } - } - } -} - -func benchNBlocks(b *testing.B, n int, freq float64) { - b.Helper() - rand.New(rand.NewSource(time.Now().UnixNano())) - if n < 10 { - return - } - nObjs := n - nI := 1 + int(float64(n)*0.2) - nObjs -= nI - nS := 1 + int(float64(n)*0.2) - nObjs -= nS - nB := nObjs - yMax := nI - xMax := n/yMax + 2 - out := generateNInputs(nI, "Inputs") - out = generateNSums(nS, xMax, yMax, "Sums", out) - out = generateNBlocks(nB, xMax, yMax, "Blocks", out) - lastSum := &out[nI] - grid := make([][]*wrapBlocks, xMax) - for i := range grid { - grid[i] = make([]*wrapBlocks, yMax) - } - for i, b := range out { - grid[b.x][b.y] = &out[i] - } - mergedAll(xMax, yMax, grid, lastSum) - - cfg := Config{ - Frequency: freq, - Blocks: []BlockConfig{}, - } - for i := range out { - if out[i].c.Type == "sum" { - out[i].c.Attribute["sum_string"] = strings.Repeat("+", len(out[i].c.DependsOn)) - } - cfg.Blocks = append(cfg.Blocks, out[i].c) - } - logger := logging.NewTestLogger(b) - cloop, err := createLoop(logger, cfg, nil) - if err == nil { - b.ResetTimer() - cloop.startBenchmark(b.N) - cloop.activeBackgroundWorkers.Wait() - } -} - -func BenchmarkLoop10(b *testing.B) { - benchNBlocks(b, 10, 100.0) -} - -func BenchmarkLoop30(b *testing.B) { - benchNBlocks(b, 30, 100.0) -} - -func BenchmarkLoop100(b *testing.B) { - benchNBlocks(b, 100, 100.0) -} - -func TestControlLoop(t *testing.T) { - // flaky test, will see behavior after RSDK-6164 - t.Skip() - logger := logging.NewTestLogger(t) - ctx := context.Background() - cfg := Config{ - Blocks: []BlockConfig{ - { - Name: "A", - Type: "endpoint", - Attribute: utils.AttributeMap{ - "motor_name": "MotorFake", - }, - DependsOn: []string{"E"}, - }, - { - Name: "B", - Type: "sum", - Attribute: utils.AttributeMap{ - "sum_string": "+-", - }, - DependsOn: []string{"A", "S1"}, - }, - { - Name: "S1", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_val": 3.0, - }, - DependsOn: []string{}, - }, - { - Name: "C", - Type: "gain", - Attribute: utils.AttributeMap{ - "gain": -2.0, - }, - DependsOn: []string{"B"}, - }, - { - Name: "D", - Type: "sum", - Attribute: utils.AttributeMap{ - "sum_string": "+-", - }, - DependsOn: []string{"C", "S2"}, - }, - { - Name: "S2", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_val": 10.0, - }, - DependsOn: []string{}, - }, - { - Name: "E", - Type: "gain", - Attribute: utils.AttributeMap{ - "gain": -2.0, - }, - DependsOn: []string{"D"}, - }, - }, - Frequency: 20.0, - } - cLoop, err := createLoop(logger, cfg, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, cLoop, test.ShouldNotBeNil) - cLoop.Start() - time.Sleep(500 * time.Millisecond) - b, err := cLoop.OutputAt(ctx, "E") - test.That(t, b[0].GetSignalValueAt(0), test.ShouldEqual, 8.0) - test.That(t, err, test.ShouldBeNil) - b, err = cLoop.OutputAt(ctx, "B") - test.That(t, b[0].GetSignalValueAt(0), test.ShouldEqual, -3.0) - test.That(t, err, test.ShouldBeNil) - - cLoop.Stop() -} - -func TestMultiSignalLoop(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg := Config{ - Blocks: []BlockConfig{ - { - Name: "sensor-base", - Type: "endpoint", - Attribute: utils.AttributeMap{ - "base_name": "base", // How to input this - }, - DependsOn: []string{"pid_block"}, - }, - { - Name: "pid_block", - Type: "PID", - Attribute: utils.AttributeMap{ - "kP": 10.0, // random for now - "kD": 0.5, - "kI": 0.2, - }, - DependsOn: []string{"gain_block"}, - }, - { - Name: "gain_block", - Type: "gain", - Attribute: utils.AttributeMap{ - "gain": 1.0, // need to update dynamically? Or should I just use the trapezoidal velocity profile - }, - DependsOn: []string{"sum_block"}, - }, - { - Name: "sum_block", - Type: "sum", - Attribute: utils.AttributeMap{ - "sum_string": "+-", // should this be +- or does it follow dependency order? - }, - DependsOn: []string{"sensor-base", "constant"}, - }, - { - Name: "constant", - Type: "constant", - Attribute: utils.AttributeMap{ - "constant_val": 10.0, - }, - DependsOn: []string{}, - }, - }, - Frequency: 20.0, - } - cLoop, err := createLoop(logger, cfg, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, cLoop, test.ShouldNotBeNil) - cLoop.Start() - test.That(t, err, test.ShouldBeNil) - - cLoop.Stop() -} diff --git a/control/derivative_test.go b/control/derivative_test.go deleted file mode 100644 index 75306d7cbe0..00000000000 --- a/control/derivative_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package control - -import ( - "context" - "math" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -func TestDerivativeConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - for _, c := range []struct { - conf BlockConfig - err string - }{ - { - BlockConfig{ - Name: "Derive1", - Type: "derivative", - Attribute: utils.AttributeMap{ - "derive_type": "backward1st1", - }, - DependsOn: []string{"A"}, - }, - "", - }, - { - BlockConfig{ - Name: "Derive1", - Type: "derivative", - Attribute: utils.AttributeMap{ - "derive_type": "backward5st1", - }, - DependsOn: []string{"A"}, - }, - "unsupported derive_type backward5st1 for block Derive1", - }, - { - BlockConfig{ - Name: "Derive1", - Type: "derivative", - Attribute: utils.AttributeMap{ - "derive_type": "backward2nd1", - }, - DependsOn: []string{"A", "B"}, - }, - "derive block Derive1 only supports one input got 2", - }, - { - BlockConfig{ - Name: "Derive1", - Type: "derivative", - Attribute: utils.AttributeMap{ - "derive_type2": "backward2nd1", - }, - DependsOn: []string{"A"}, - }, - "derive block Derive1 doesn't have a derive_type field", - }, - } { - b, err := newDerivative(c.conf, logger) - if c.err == "" { - d := b.(*derivative) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(d.y), test.ShouldEqual, 1) - test.That(t, len(d.y[0].signal), test.ShouldEqual, 1) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, c.err) - } - } -} - -func TestDerivativeNext(t *testing.T) { - const iter int64 = 3000 - const tenMs = 10 * int64(time.Millisecond) - logger := logging.NewTestLogger(t) - ctx := context.Background() - cfg := BlockConfig{ - Name: "Derive1", - Type: "derivative", - Attribute: utils.AttributeMap{ - "derive_type": "backward2nd2", - }, - DependsOn: []string{"A"}, - } - b, err := newDerivative(cfg, logger) - d := b.(*derivative) - test.That(t, err, test.ShouldBeNil) - var sin []float64 - for i := int64(0); i < iter; i++ { - sin = append(sin, math.Sin(time.Duration(i*tenMs).Seconds())) - } - sig := &Signal{ - name: "A", - signal: make([]float64, 1), - time: make([]int, 1), - dimension: 1, - } - for i := int64(0); i < iter; i++ { - sig.SetSignalValueAt(0, sin[i]) - out, ok := d.Next(ctx, []*Signal{sig}, (10 * time.Millisecond)) - test.That(t, ok, test.ShouldBeTrue) - if i > 5 { - test.That(t, out[0].GetSignalValueAt(0), test.ShouldAlmostEqual, - -math.Sin((time.Duration(i * tenMs).Seconds())), 0.01) - } - } - cfg = BlockConfig{ - Name: "Derive1", - Type: "derivative", - Attribute: utils.AttributeMap{ - "derive_type": "backward1st2", - }, - DependsOn: []string{"A"}, - } - err = d.UpdateConfig(ctx, cfg) - test.That(t, err, test.ShouldBeNil) - for i := int64(0); i < iter; i++ { - sig.SetSignalValueAt(0, sin[i]) - out, ok := d.Next(ctx, []*Signal{sig}, (10 * time.Millisecond)) - test.That(t, ok, test.ShouldBeTrue) - if i > 5 { - test.That(t, out[0].GetSignalValueAt(0), test.ShouldAlmostEqual, - math.Cos((time.Duration(i * tenMs).Seconds())), 0.01) - } - } - cfg = BlockConfig{ - Name: "Derive1", - Type: "derivative", - Attribute: utils.AttributeMap{ - "derive_type": "backward1st3", - }, - DependsOn: []string{"A"}, - } - err = d.UpdateConfig(ctx, cfg) - test.That(t, err, test.ShouldBeNil) - sin = nil - for i := int64(0); i < iter; i++ { - sin = append(sin, math.Sin(2*math.Pi*(time.Duration(i*tenMs).Seconds()))) - } - for i := int64(0); i < iter; i++ { - sig.SetSignalValueAt(0, sin[i]) - out, ok := d.Next(ctx, []*Signal{sig}, 10*time.Millisecond) - test.That(t, ok, test.ShouldBeTrue) - if i > 5 { - test.That(t, out[0].GetSignalValueAt(0), test.ShouldAlmostEqual, - 2*math.Pi*math.Cos(2*math.Pi*(time.Duration(i*tenMs).Seconds())), 0.01) - } - } -} diff --git a/control/fir_filters_test.go b/control/fir_filters_test.go deleted file mode 100644 index daf60ae4838..00000000000 --- a/control/fir_filters_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package control - -import ( - "testing" - - "go.viam.com/test" -) - -func TestFIRFilterMovingAverage(t *testing.T) { - firFlt := movingAverageFilter{filterSize: 20} - firFlt.Reset() - test.That(t, len(firFlt.x), test.ShouldEqual, firFlt.filterSize) -} - -func TestFIRFilterSinc(t *testing.T) { - firFlt := firSinc{smpFreq: 1000, cutOffFreq: 6, order: 10} - firFlt.Reset() - test.That(t, len(firFlt.coeffs), test.ShouldEqual, firFlt.order) - test.That(t, len(firFlt.x), test.ShouldEqual, firFlt.order) - test.That(t, firFlt.coeffs[0], test.ShouldAlmostEqual, 0.01194252323789503) - test.That(t, firFlt.coeffs[1], test.ShouldAlmostEqual, 0.011965210333859375) - test.That(t, firFlt.coeffs[2], test.ShouldAlmostEqual, 0.011982242600545921) - test.That(t, firFlt.coeffs[3], test.ShouldAlmostEqual, 0.011993605518831918) - test.That(t, firFlt.coeffs[4], test.ShouldAlmostEqual, 0.011999289401107232) - test.That(t, firFlt.coeffs[5], test.ShouldAlmostEqual, 0.011999289401107232) - test.That(t, firFlt.coeffs[6], test.ShouldAlmostEqual, 0.011993605518831918) - test.That(t, firFlt.coeffs[7], test.ShouldAlmostEqual, 0.011982242600545921) - test.That(t, firFlt.coeffs[8], test.ShouldAlmostEqual, 0.011965210333859375) - test.That(t, firFlt.coeffs[9], test.ShouldAlmostEqual, 0.01194252323789503) -} - -func TestFIRFilterWindowedSinc(t *testing.T) { - firFlt := firWindowedSinc{smpFreq: 100, cutOffFreq: 10, kernelSize: 6} - firFlt.Reset() - test.That(t, len(firFlt.kernel), test.ShouldEqual, firFlt.kernelSize) - test.That(t, firFlt.kernel[0], test.ShouldAlmostEqual, 0.10319743280864632) - test.That(t, firFlt.kernel[1], test.ShouldAlmostEqual, 0.1547961492129695) - test.That(t, firFlt.kernel[2], test.ShouldAlmostEqual, 0.19133856308243086) - test.That(t, firFlt.kernel[3], test.ShouldAlmostEqual, 0.20453314260055294) - test.That(t, firFlt.kernel[4], test.ShouldAlmostEqual, 0.19133856308243086) - test.That(t, firFlt.kernel[5], test.ShouldAlmostEqual, 0.1547961492129695) -} diff --git a/control/gain_test.go b/control/gain_test.go deleted file mode 100644 index 515bd4bf05f..00000000000 --- a/control/gain_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package control - -import ( - "context" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -func TestGainConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - for _, c := range []struct { - conf BlockConfig - err string - }{ - { - BlockConfig{ - Name: "Gain1", - Type: "gain", - Attribute: utils.AttributeMap{ - "gain": 1.89345, - }, - DependsOn: []string{"A"}, - }, - "", - }, - { - BlockConfig{ - Name: "Gain1", - Type: "gain", - Attribute: utils.AttributeMap{ - "gainS": 1.89345, - }, - DependsOn: []string{"A"}, - }, - "gain block Gain1 doesn't have a gain field", - }, - { - BlockConfig{ - Name: "Gain1", - Type: "gain", - Attribute: utils.AttributeMap{ - "gain": 1.89345, - }, - DependsOn: []string{"A", "B"}, - }, - "invalid number of inputs for gain block Gain1 expected 1 got 2", - }, - } { - b, err := newGain(c.conf, logger) - if c.err == "" { - s := b.(*gain) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(s.y), test.ShouldEqual, 1) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, c.err) - } - } -} - -func TestGainNext(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - c := BlockConfig{ - Name: "Gain1", - Type: "gain", - Attribute: utils.AttributeMap{ - "gain": 1.89345, - }, - DependsOn: []string{"A"}, - } - s, err := newGain(c, logger) - - test.That(t, err, test.ShouldBeNil) - - signals := []*Signal{ - { - name: "A", - signal: []float64{1.0}, - time: []int{1}, - dimension: 1, - }, - } - out, ok := s.Next(ctx, signals, (time.Millisecond * 1)) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 1.89345) -} diff --git a/control/iir_filters_test.go b/control/iir_filters_test.go deleted file mode 100644 index 315cd85a256..00000000000 --- a/control/iir_filters_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package control - -import ( - "testing" - - "go.viam.com/test" -) - -func TestIIRFilterButter(t *testing.T) { - iirFlt := iirFilter{smpFreq: 10000, cutOffFreq: 1000, n: 4, ripple: 0.0, fltType: "lowpass"} - - test.That(t, iirFlt.n, test.ShouldEqual, 4) - iirFlt.calculateABCoeffs() - - test.That(t, len(iirFlt.aCoeffs), test.ShouldEqual, 5) - test.That(t, len(iirFlt.bCoeffs), test.ShouldEqual, 5) - test.That(t, iirFlt.aCoeffs[0], test.ShouldAlmostEqual, 0.004824343357716) - test.That(t, iirFlt.aCoeffs[1], test.ShouldAlmostEqual, 0.019297373430865) - test.That(t, iirFlt.aCoeffs[2], test.ShouldAlmostEqual, 0.028946060146297) - test.That(t, iirFlt.aCoeffs[3], test.ShouldAlmostEqual, 0.019297373430865) - test.That(t, iirFlt.aCoeffs[4], test.ShouldAlmostEqual, 0.004824343357716) - - test.That(t, iirFlt.bCoeffs[1], test.ShouldAlmostEqual, -2.369513007182) - test.That(t, iirFlt.bCoeffs[2], test.ShouldAlmostEqual, 2.313988414416) - test.That(t, iirFlt.bCoeffs[3], test.ShouldAlmostEqual, -1.054665405879) - test.That(t, iirFlt.bCoeffs[4], test.ShouldAlmostEqual, 0.187379492368) - - iirFlt = iirFilter{smpFreq: 10000, cutOffFreq: 2000, n: 2, ripple: 0.0, fltType: "lowpass"} - iirFlt.calculateABCoeffs() - test.That(t, len(iirFlt.aCoeffs), test.ShouldEqual, 3) - test.That(t, len(iirFlt.bCoeffs), test.ShouldEqual, 3) - test.That(t, iirFlt.aCoeffs[0], test.ShouldAlmostEqual, 0.206572083826148) - test.That(t, iirFlt.aCoeffs[1], test.ShouldAlmostEqual, 0.413144167652296) - test.That(t, iirFlt.aCoeffs[2], test.ShouldAlmostEqual, 0.206572083826148) - test.That(t, iirFlt.bCoeffs[1], test.ShouldAlmostEqual, -0.369527377351) - test.That(t, iirFlt.bCoeffs[2], test.ShouldAlmostEqual, 0.195815712656) - - iirFlt = iirFilter{smpFreq: 10000, cutOffFreq: 3000, n: 6, ripple: 0.0, fltType: "lowpass"} - iirFlt.calculateABCoeffs() - test.That(t, len(iirFlt.aCoeffs), test.ShouldEqual, 7) - test.That(t, len(iirFlt.bCoeffs), test.ShouldEqual, 7) - test.That(t, iirFlt.aCoeffs[0], test.ShouldAlmostEqual, 0.070115413492454) - test.That(t, iirFlt.aCoeffs[1], test.ShouldAlmostEqual, 0.420692480954722) - test.That(t, iirFlt.aCoeffs[2], test.ShouldAlmostEqual, 1.051731202386805) - test.That(t, iirFlt.aCoeffs[3], test.ShouldAlmostEqual, 1.402308269849073) - test.That(t, iirFlt.aCoeffs[4], test.ShouldAlmostEqual, 1.051731202386805) - test.That(t, iirFlt.aCoeffs[5], test.ShouldAlmostEqual, 0.420692480954722) - test.That(t, iirFlt.aCoeffs[6], test.ShouldAlmostEqual, 0.070115413492454) - - test.That(t, iirFlt.bCoeffs[1], test.ShouldAlmostEqual, 1.187600680176) - test.That(t, iirFlt.bCoeffs[2], test.ShouldAlmostEqual, 1.305213349289) - test.That(t, iirFlt.bCoeffs[3], test.ShouldAlmostEqual, 0.674327525298) - test.That(t, iirFlt.bCoeffs[4], test.ShouldAlmostEqual, 0.263469348280) - test.That(t, iirFlt.bCoeffs[5], test.ShouldAlmostEqual, 0.051753033880) - test.That(t, iirFlt.bCoeffs[6], test.ShouldAlmostEqual, 0.005022526595) - - iirFlt = iirFilter{smpFreq: 10000, cutOffFreq: 3000, n: 6, ripple: 0.0, fltType: "highpass"} - iirFlt.calculateABCoeffs() - test.That(t, len(iirFlt.aCoeffs), test.ShouldEqual, 7) - test.That(t, len(iirFlt.bCoeffs), test.ShouldEqual, 7) - - test.That(t, iirFlt.aCoeffs[0], test.ShouldAlmostEqual, 0.010312874762664) - test.That(t, iirFlt.aCoeffs[1], test.ShouldAlmostEqual, -0.061877248575986) - test.That(t, iirFlt.aCoeffs[2], test.ShouldAlmostEqual, 0.154693121439966) - test.That(t, iirFlt.aCoeffs[3], test.ShouldAlmostEqual, -0.206257495253288) - test.That(t, iirFlt.aCoeffs[4], test.ShouldAlmostEqual, 0.154693121439966) - test.That(t, iirFlt.aCoeffs[5], test.ShouldAlmostEqual, -0.061877248575986) - test.That(t, iirFlt.aCoeffs[6], test.ShouldAlmostEqual, 0.010312874762664) - - test.That(t, iirFlt.bCoeffs[1], test.ShouldAlmostEqual, 1.187600680176) - test.That(t, iirFlt.bCoeffs[2], test.ShouldAlmostEqual, 1.305213349289) - test.That(t, iirFlt.bCoeffs[3], test.ShouldAlmostEqual, 0.674327525298) - test.That(t, iirFlt.bCoeffs[4], test.ShouldAlmostEqual, 0.263469348280) - test.That(t, iirFlt.bCoeffs[5], test.ShouldAlmostEqual, 0.051753033880) - test.That(t, iirFlt.bCoeffs[6], test.ShouldAlmostEqual, 0.005022526595) -} - -func TestIIRFilterChebyshevI(t *testing.T) { - iirFlt := iirFilter{smpFreq: 10000, cutOffFreq: 1000, n: 4, ripple: 0.5, fltType: "lowpass"} - - test.That(t, iirFlt.n, test.ShouldEqual, 4) - iirFlt.calculateABCoeffs() - - test.That(t, len(iirFlt.aCoeffs), test.ShouldEqual, 5) - test.That(t, len(iirFlt.bCoeffs), test.ShouldEqual, 5) - - test.That(t, iirFlt.bCoeffs[1], test.ShouldAlmostEqual, -2.7640305047044187) - test.That(t, iirFlt.bCoeffs[2], test.ShouldAlmostEqual, 3.1228526783585413) - test.That(t, iirFlt.bCoeffs[3], test.ShouldAlmostEqual, -1.6645530241054278) - test.That(t, iirFlt.bCoeffs[4], test.ShouldAlmostEqual, 0.3502229603332013) - - test.That(t, iirFlt.aCoeffs[0], test.ShouldAlmostEqual, 0.00620090579054177) - test.That(t, iirFlt.aCoeffs[1], test.ShouldAlmostEqual, 0.024803623162167082) - test.That(t, iirFlt.aCoeffs[2], test.ShouldAlmostEqual, 0.03720543474325062) - test.That(t, iirFlt.aCoeffs[3], test.ShouldAlmostEqual, 0.024803623162167082) - test.That(t, iirFlt.aCoeffs[4], test.ShouldAlmostEqual, 0.00620090579054177) -} - -func TestIIRFilterDesign(t *testing.T) { - iirFlt, err := design(2000, 4250, 3.0, 30.0, 10000) - test.That(t, err, test.ShouldBeNil) - iirFlt.Reset() - test.That(t, iirFlt.n, test.ShouldEqual, 2) - test.That(t, iirFlt.cutOffFreq, test.ShouldAlmostEqual, 2001.7973929699663) - - test.That(t, len(iirFlt.aCoeffs), test.ShouldEqual, 3) - test.That(t, len(iirFlt.bCoeffs), test.ShouldEqual, 3) - test.That(t, iirFlt.bCoeffs[1], test.ShouldAlmostEqual, -0.3681885321516074) - test.That(t, iirFlt.bCoeffs[2], test.ShouldAlmostEqual, 0.1956396086108966) - - test.That(t, iirFlt.aCoeffs[0], test.ShouldAlmostEqual, 0.2068627691148223) - test.That(t, iirFlt.aCoeffs[1], test.ShouldAlmostEqual, 0.4137255382296446) - test.That(t, iirFlt.aCoeffs[2], test.ShouldAlmostEqual, 0.2068627691148223) - - iirFlt, err = design(10, 40, 3.0, 30.0, 100) - test.That(t, err, test.ShouldBeNil) - iirFlt.Reset() - test.That(t, iirFlt.n, test.ShouldEqual, 2) - test.That(t, len(iirFlt.aCoeffs), test.ShouldEqual, 3) - test.That(t, len(iirFlt.bCoeffs), test.ShouldEqual, 3) - test.That(t, iirFlt.bCoeffs[1], test.ShouldAlmostEqual, -1.1420783035180786) - test.That(t, iirFlt.bCoeffs[2], test.ShouldAlmostEqual, 0.4124032098038791) - - test.That(t, iirFlt.aCoeffs[0], test.ShouldAlmostEqual, 0.0675812265714501) - test.That(t, iirFlt.aCoeffs[1], test.ShouldAlmostEqual, 0.1351624531429003) - test.That(t, iirFlt.aCoeffs[2], test.ShouldAlmostEqual, 0.0675812265714501) -} diff --git a/control/pid_test.go b/control/pid_test.go deleted file mode 100644 index c24d1dd2ebe..00000000000 --- a/control/pid_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package control - -import ( - "context" - "fmt" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -var loop = Loop{} - -func TestPIDConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - for i, tc := range []struct { - conf BlockConfig - err string - }{ - { - BlockConfig{ - Name: "PID1", - Attribute: utils.AttributeMap{"kD": 0.11, "kP": 0.12, "kI": 0.22}, - Type: "PID", - DependsOn: []string{"A", "B"}, - }, - "pid block PID1 should have 1 input got 2", - }, - { - BlockConfig{ - Name: "PID1", - Attribute: utils.AttributeMap{"kD": 0.11, "kP": 0.12, "kI": 0.22}, - Type: "PID", - DependsOn: []string{"A"}, - }, - "", - }, - { - BlockConfig{ - Name: "PID1", - Attribute: utils.AttributeMap{"Kdd": 0.11}, - Type: "PID", - DependsOn: []string{"A"}, - }, - "pid block PID1 should have at least one kI, kP or kD field", - }, - } { - t.Run(fmt.Sprintf("Test %d", i), func(t *testing.T) { - _, err := loop.newPID(tc.conf, logger) - if tc.err == "" { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, tc.err) - } - }) - } -} - -func TestPIDBasicIntegralWindup(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - cfg := BlockConfig{ - Name: "PID1", - Attribute: utils.AttributeMap{ - "kD": 0.11, - "kP": 0.12, - "kI": 0.22, - "limit_up": 100.0, - "limit_lo": 0.0, - "int_sat_lim_up": 100.0, - "int_sat_lim_lo": 0.0, - }, - Type: "PID", - DependsOn: []string{"A"}, - } - b, err := loop.newPID(cfg, logger) - pid := b.(*basicPID) - test.That(t, err, test.ShouldBeNil) - s := []*Signal{ - { - name: "A", - signal: make([]float64, 1), - time: make([]int, 1), - }, - } - for i := 0; i < 50; i++ { - dt := time.Duration(1000000 * 10) - s[0].SetSignalValueAt(0, 1000.0) - out, ok := pid.Next(ctx, s, dt) - if i < 46 { - test.That(t, ok, test.ShouldBeTrue) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 100.0) - } else { - test.That(t, pid.int, test.ShouldBeGreaterThanOrEqualTo, 100) - s[0].SetSignalValueAt(0, 0.0) - out, ok := pid.Next(ctx, s, dt) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, pid.int, test.ShouldBeGreaterThanOrEqualTo, 100) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 0.0) - s[0].SetSignalValueAt(0, -1.0) - out, ok = pid.Next(ctx, s, dt) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, pid.int, test.ShouldBeLessThanOrEqualTo, 100) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldAlmostEqual, 88.8778) - break - } - } - err = pid.Reset(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, pid.int, test.ShouldEqual, 0) - test.That(t, pid.error, test.ShouldEqual, 0) -} - -func TestPIDTuner(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - cfg := BlockConfig{ - Name: "PID1", - Attribute: utils.AttributeMap{ - "kD": 0.0, - "kP": 0.0, - "kI": 0.0, - "limit_up": 255.0, - "limit_lo": 0.0, - "int_sat_lim_up": 255.0, - "int_sat_lim_lo": 0.0, - "tune_ssr_value": 2.0, - "tune_step_pct": 0.45, - }, - Type: "PID", - DependsOn: []string{"A"}, - } - b, err := loop.newPID(cfg, logger) - pid := b.(*basicPID) - test.That(t, err, test.ShouldBeNil) - test.That(t, pid.tuning, test.ShouldBeTrue) - test.That(t, pid.tuner.currentPhase, test.ShouldEqual, begin) - s := []*Signal{ - { - name: "A", - signal: make([]float64, 1), - time: make([]int, 1), - }, - } - dt := time.Millisecond * 10 - for i := 0; i < 22; i++ { - s[0].SetSignalValueAt(0, s[0].GetSignalValueAt(0)+2) - out, hold := pid.Next(ctx, s, dt) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 255.0*0.45) - test.That(t, hold, test.ShouldBeTrue) - } - for i := 0; i < 15; i++ { - s[0].SetSignalValueAt(0, 100.0) - out, hold := pid.Next(ctx, s, dt) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 255.0*0.45) - test.That(t, hold, test.ShouldBeTrue) - } - out, hold := pid.Next(ctx, s, dt) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 255.0*0.45+0.5*255.0*0.45) - test.That(t, hold, test.ShouldBeTrue) -} diff --git a/control/sum_test.go b/control/sum_test.go deleted file mode 100644 index dfbdee24ab3..00000000000 --- a/control/sum_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package control - -import ( - "context" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -func TestSumConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - for _, c := range []struct { - conf BlockConfig - err string - }{ - { - BlockConfig{ - Name: "Sum1", - Type: "Sum", - Attribute: utils.AttributeMap{ - "sum_string": "--++", - }, - DependsOn: []string{"A", "B", "C", "D"}, - }, - "", - }, - { - BlockConfig{ - Name: "Sum1", - Type: "Sum", - Attribute: utils.AttributeMap{ - "sum_stringS": "--++", - }, - DependsOn: []string{"A", "B", "C", "D"}, - }, - "sum block Sum1 doesn't have a sum_string", - }, - { - BlockConfig{ - Name: "Sum1", - Type: "Sum", - Attribute: utils.AttributeMap{ - "sum_string": "--++", - }, - DependsOn: []string{"B", "C", "D"}, - }, - "invalid number of inputs for sum block Sum1 expected 4 got 3", - }, - { - BlockConfig{ - Name: "Sum1", - Type: "Sum", - Attribute: utils.AttributeMap{ - "sum_string": "--+\\", - }, - DependsOn: []string{"A", "B", "C", "D"}, - }, - "expected +/- for sum block Sum1 got \\", - }, - } { - b, err := newSum(c.conf, logger) - if c.err == "" { - s := b.(*sum) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(s.y), test.ShouldEqual, 4) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, c.err) - } - } -} - -func TestSumNext(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - c := BlockConfig{ - Name: "Sum1", - Type: "Sum", - Attribute: utils.AttributeMap{ - "sum_string": "--++", - }, - DependsOn: []string{"A", "B", "C", "D"}, - } - s, err := newSum(c, logger) - - test.That(t, err, test.ShouldBeNil) - - signals := []*Signal{ - { - name: "A", - signal: []float64{1.0}, - time: []int{1}, - dimension: 1, - }, - { - name: "B", - signal: []float64{2.0}, - time: []int{2}, - dimension: 1, - }, - { - name: "C", - signal: []float64{1.0}, - time: []int{2}, - dimension: 1, - }, - { - name: "D", - signal: []float64{1.0}, - time: []int{1}, - dimension: 1, - }, - } - out, ok := s.Next(ctx, signals, time.Millisecond*1) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, out[0].GetSignalValueAt(0), test.ShouldEqual, 0.0) - test.That(t, out[1].GetSignalValueAt(0), test.ShouldEqual, -1.0) -} diff --git a/control/trapezoid_velocity_profile_test.go b/control/trapezoid_velocity_profile_test.go deleted file mode 100644 index 36b17070a96..00000000000 --- a/control/trapezoid_velocity_profile_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package control - -import ( - "context" - "math" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -func TestTrapezoidVelocityProfileConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - - for _, c := range []struct { - conf BlockConfig - err string - }{ - { - BlockConfig{ - Name: "Trap1", - Type: "trapezoidalVelocityProfile", - DependsOn: []string{}, - Attribute: utils.AttributeMap{ - "max_acc": 1000.0, - "max_vel": 100.0, - }, - }, - "", - }, - { - BlockConfig{ - Name: "Trap1", - Type: "trapezoidalVelocityProfile", - DependsOn: []string{}, - Attribute: utils.AttributeMap{ - "max_acc": 1000.0, - }, - }, - "trapezoidale velocity profile block Trap1 needs max_vel field", - }, - { - BlockConfig{ - Name: "Trap1", - Type: "trapezoidalVelocityProfile", - DependsOn: []string{}, - Attribute: utils.AttributeMap{ - "max_vel": 1000.0, - }, - }, - "trapezoidale velocity profile block Trap1 needs max_acc field", - }, - } { - _, err := newTrapezoidVelocityProfile(c.conf, logger) - if c.err == "" { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, c.err) - } - } -} - -func TestTrapezoidVelocityProfileGenerator(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - targetPos := 100.0 - posWindow := 10.0 - cfg := BlockConfig{ - Name: "Trap1", - Type: "trapezoidalVelocityProfile", - DependsOn: []string{}, - Attribute: utils.AttributeMap{ - "max_acc": 1000.0, - "max_vel": 100.0, - "pos_window": posWindow, - }, - } - b, err := newTrapezoidVelocityProfile(cfg, logger) - s := b.(*trapezoidVelocityGenerator) - test.That(t, err, test.ShouldBeNil) - - ins := []*Signal{ - { - name: "set_point", - blockType: blockConstant, - time: []int{}, - signal: []float64{targetPos}, - }, - { - name: "endpoint", - blockType: blockEndpoint, - time: []int{}, - signal: []float64{0.0}, - }, - } - - y, ok := s.Next(ctx, ins, (10 * time.Millisecond)) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, s.currentPhase, test.ShouldEqual, active) - test.That(t, y[0].GetSignalValueAt(0), test.ShouldNotBeZeroValue) - for { - y, _ := s.Next(ctx, ins, (10 * time.Millisecond)) - if math.Abs(ins[1].GetSignalValueAt(0)-targetPos) > posWindow { - test.That(t, s.currentPhase, test.ShouldEqual, active) - test.That(t, y[0].GetSignalValueAt(0), test.ShouldNotBeZeroValue) - } else { - test.That(t, s.currentPhase, test.ShouldEqual, rest) - test.That(t, y[0].GetSignalValueAt(0), test.ShouldBeZeroValue) - break - } - ins[1].SetSignalValueAt(0, ins[1].GetSignalValueAt(0)+(10*time.Millisecond).Seconds()*y[0].GetSignalValueAt(0)) - } - ins[0].SetSignalValueAt(0, targetPos-4) - y, ok = s.Next(ctx, ins, (10 * time.Millisecond)) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, s.currentPhase, test.ShouldEqual, rest) - test.That(t, y[0].GetSignalValueAt(0), test.ShouldBeZeroValue) - ins[1].SetSignalValueAt(0, targetPos*2) - for { - y, _ := s.Next(ctx, ins, (10 * time.Millisecond)) - if math.Abs(ins[1].GetSignalValueAt(0)-targetPos+4) > posWindow { - test.That(t, s.currentPhase, test.ShouldEqual, active) - test.That(t, y[0].GetSignalValueAt(0), test.ShouldNotBeZeroValue) - } else { - test.That(t, s.currentPhase, test.ShouldEqual, rest) - test.That(t, y[0].GetSignalValueAt(0), test.ShouldBeZeroValue) - break - } - ins[1].SetSignalValueAt(0, ins[1].GetSignalValueAt(0)+(10*time.Millisecond).Seconds()*y[0].GetSignalValueAt(0)) - } -} diff --git a/data/collector_test.go b/data/collector_test.go deleted file mode 100644 index 6a908235ca2..00000000000 --- a/data/collector_test.go +++ /dev/null @@ -1,373 +0,0 @@ -package data - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sync" - "testing" - "time" - - "github.com/benbjohnson/clock" - "github.com/pkg/errors" - "go.uber.org/zap/zapcore" - v1 "go.viam.com/api/app/datasync/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/services/datamanager/datacapture" -) - -type structReading struct { - Field1 bool -} - -func (r *structReading) toProto() *structpb.Struct { - msg, err := protoutils.StructToStructPb(r) - if err != nil { - return nil - } - return msg -} - -var ( - structCapturer = CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - return dummyStructReading, nil - }) - binaryCapturer = CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - return dummyBytesReading, nil - }) - dummyStructReading = structReading{} - dummyStructReadingProto = dummyStructReading.toProto() - dummyBytesReading = []byte("I sure am bytes") - queueSize = 250 - bufferSize = 4096 - fakeVal = &anypb.Any{} -) - -func TestNewCollector(t *testing.T) { - // If missing parameters should return an error. - c1, err1 := NewCollector(nil, CollectorParams{}) - - test.That(t, c1, test.ShouldBeNil) - test.That(t, err1, test.ShouldNotBeNil) - - // If not missing parameters, should not return an error. - c2, err2 := NewCollector(nil, CollectorParams{ - ComponentName: "name", - Logger: logging.NewTestLogger(t), - Target: datacapture.NewBuffer("dir", nil), - }) - - test.That(t, c2, test.ShouldNotBeNil) - test.That(t, err2, test.ShouldBeNil) -} - -// Test that the Collector correctly writes the SensorData on an interval. -func TestSuccessfulWrite(t *testing.T) { - l := logging.NewTestLogger(t) - tickerInterval := sleepCaptureCutoff + 1 - sleepInterval := sleepCaptureCutoff - 1 - - params := CollectorParams{ - ComponentName: "testComponent", - MethodParams: map[string]*anypb.Any{"name": fakeVal}, - QueueSize: queueSize, - BufferSize: bufferSize, - Logger: l, - } - - tests := []struct { - name string - captureFunc CaptureFunc - interval time.Duration - expectReadings int - expFiles int - }{ - { - name: "Ticker based struct writer.", - captureFunc: structCapturer, - interval: tickerInterval, - expectReadings: 2, - expFiles: 1, - }, - { - name: "Sleep based struct writer.", - captureFunc: structCapturer, - interval: sleepInterval, - expectReadings: 2, - expFiles: 1, - }, - { - name: "Ticker based binary writer.", - captureFunc: binaryCapturer, - interval: tickerInterval, - expectReadings: 2, - expFiles: 2, - }, - { - name: "Sleep based binary writer.", - captureFunc: binaryCapturer, - interval: sleepInterval, - expectReadings: 2, - expFiles: 2, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) - defer cancel() - tmpDir := t.TempDir() - md := v1.DataCaptureMetadata{} - tgt := datacapture.NewBuffer(tmpDir, &md) - test.That(t, tgt, test.ShouldNotBeNil) - wrote := make(chan struct{}) - target := &signalingBuffer{ - bw: tgt, - wrote: wrote, - } - - mockClock := clock.NewMock() - params.Interval = tc.interval - params.Target = target - params.Clock = mockClock - c, err := NewCollector(tc.captureFunc, params) - test.That(t, err, test.ShouldBeNil) - c.Collect() - // We need to avoid adding time until after the underlying goroutine has started sleeping. - // If we add time before that point, data will never be captured, because time will never be greater than - // the initially calculated time. - // Sleeping for 10ms is a hacky way to ensure that we don't encounter this situation. It gives 10ms - // for those few sequential lines in collector.go to execute, so that that occurs before we add time below. - time.Sleep(10 * time.Millisecond) - for i := 0; i < tc.expectReadings; i++ { - mockClock.Add(params.Interval) - select { - case <-ctx.Done(): - t.Fatalf("timed out waiting for data to be written") - case <-wrote: - } - } - close(wrote) - - // If it's a sleep based collector, we need to move the clock forward one more time after calling Close. - // Otherwise, it will stay asleep indefinitely and Close will block forever. - // This loop guarantees that the clock is moved forward at least once after Close is called. After Close - // returns and the closed channel is closed, this loop will terminate. - closed := make(chan struct{}) - sleepCollector := tc.interval < sleepCaptureCutoff - var wg sync.WaitGroup - if sleepCollector { - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 1000; i++ { - select { - case <-closed: - return - default: - time.Sleep(time.Millisecond * 1) - mockClock.Add(tc.interval) - } - } - }() - } - c.Close() - close(closed) - wg.Wait() - - var actReadings []*v1.SensorData - files := getAllFiles(tmpDir) - for _, file := range files { - fileReadings, err := datacapture.SensorDataFromFilePath(filepath.Join(tmpDir, file.Name())) - test.That(t, err, test.ShouldBeNil) - actReadings = append(actReadings, fileReadings...) - } - test.That(t, len(actReadings), test.ShouldEqual, tc.expectReadings) - test.That(t, err, test.ShouldBeNil) - validateReadings(t, actReadings, tc.expectReadings) - }) - } -} - -func TestClose(t *testing.T) { - // Set up a collector. - l := logging.NewTestLogger(t) - tmpDir := t.TempDir() - md := v1.DataCaptureMetadata{} - buf := datacapture.NewBuffer(tmpDir, &md) - wrote := make(chan struct{}) - target := &signalingBuffer{ - bw: buf, - wrote: wrote, - } - mockClock := clock.NewMock() - interval := time.Millisecond * 5 - - params := CollectorParams{ - ComponentName: "testComponent", - Interval: interval, - MethodParams: map[string]*anypb.Any{"name": fakeVal}, - Target: target, - QueueSize: queueSize, - BufferSize: bufferSize, - Logger: l, - Clock: mockClock, - } - c, _ := NewCollector(structCapturer, params) - - // Start collecting, and validate it is writing. - c.Collect() - mockClock.Add(interval) - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*10) - defer cancel() - select { - case <-ctx.Done(): - t.Fatalf("timed out waiting for data to be written") - case <-wrote: - } - - // Close and validate no additional writes occur even after an additional interval. - c.Close() - mockClock.Add(interval) - ctx, cancel = context.WithTimeout(context.Background(), time.Millisecond*10) - defer cancel() - select { - case <-ctx.Done(): - case <-wrote: - t.Fatalf("unexpected write after close") - } -} - -// TestCtxCancelledNotLoggedInTickerBasedCaptureAfterClose verifies that context cancelled errors are not logged if they -// occur after Close has been called. The collector context is cancelled as part of Close, so we expect to see context -// cancelled errors for any running capture routines. -func TestCtxCancelledNotLoggedInTickerBasedCaptureAfterClose(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - tmpDir := t.TempDir() - target := datacapture.NewBuffer(tmpDir, &v1.DataCaptureMetadata{}) - captured := make(chan struct{}) - errorCapturer := CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - select { - case <-ctx.Done(): - return nil, fmt.Errorf("arbitrary wrapping message: %w", ctx.Err()) - case captured <- struct{}{}: - } - return dummyStructReading, nil - }) - - params := CollectorParams{ - ComponentName: "testComponent", - // Ensure that we use ticker-based capture since capturing at 1000+ Hz - // uses a sleep-based capture that may let a context cancelation error - // occasionally be logged. - Interval: sleepCaptureCutoff + time.Microsecond, - MethodParams: map[string]*anypb.Any{"name": fakeVal}, - Target: target, - QueueSize: queueSize, - BufferSize: bufferSize, - Logger: logger, - } - c, _ := NewCollector(errorCapturer, params) - c.Collect() - <-captured - c.Close() - close(captured) - - test.That(t, logs.FilterLevelExact(zapcore.ErrorLevel).Len(), test.ShouldEqual, 0) -} - -func TestLogErrorsOnlyOnce(t *testing.T) { - // Set up a collector. - logger, logs := logging.NewObservedTestLogger(t) - tmpDir := t.TempDir() - md := v1.DataCaptureMetadata{} - buf := datacapture.NewBuffer(tmpDir, &md) - wrote := make(chan struct{}) - errorCapturer := CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - return nil, errors.New("I am an error") - }) - target := &signalingBuffer{ - bw: buf, - wrote: wrote, - } - mockClock := clock.NewMock() - interval := time.Millisecond * 5 - - params := CollectorParams{ - ComponentName: "testComponent", - Interval: interval, - MethodParams: map[string]*anypb.Any{"name": fakeVal}, - Target: target, - QueueSize: queueSize, - BufferSize: bufferSize, - Logger: logger, - Clock: mockClock, - } - c, _ := NewCollector(errorCapturer, params) - - // Start collecting, and validate it is writing. - c.Collect() - mockClock.Add(interval * 5) - - close(wrote) - test.That(t, logs.FilterLevelExact(zapcore.ErrorLevel).Len(), test.ShouldEqual, 1) - mockClock.Add(3 * time.Second) - test.That(t, logs.FilterLevelExact(zapcore.ErrorLevel).Len(), test.ShouldEqual, 2) - mockClock.Add(3 * time.Second) - test.That(t, logs.FilterLevelExact(zapcore.ErrorLevel).Len(), test.ShouldEqual, 3) - c.Close() -} - -func validateReadings(t *testing.T, act []*v1.SensorData, n int) { - t.Helper() - for i := 0; i < n; i++ { - read := act[i] - if read.GetStruct() != nil { - test.That(t, proto.Equal(dummyStructReadingProto, read.GetStruct()), test.ShouldBeTrue) - } else { - test.That(t, read.GetBinary(), test.ShouldResemble, dummyBytesReading) - } - } -} - -//nolint -func getAllFiles(dir string) []os.FileInfo { - var files []os.FileInfo - _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if info.IsDir() { - return nil - } - files = append(files, info) - return nil - }) - return files -} - -type signalingBuffer struct { - bw datacapture.BufferedWriter - wrote chan struct{} -} - -func (b *signalingBuffer) Write(data *v1.SensorData) error { - ret := b.bw.Write(data) - b.wrote <- struct{}{} - return ret -} - -func (b *signalingBuffer) Flush() error { - return b.bw.Flush() -} - -func (b *signalingBuffer) Path() string { - return b.bw.Path() -} diff --git a/data/registry_test.go b/data/registry_test.go deleted file mode 100644 index 44cc2c89639..00000000000 --- a/data/registry_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package data - -import ( - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/resource" -) - -var dummyCollectorConstructor = func(i interface{}, params CollectorParams) (Collector, error) { - return &collector{}, nil -} - -func TestRegister(t *testing.T) { - defer func() { - for k := range collectorRegistry { - delete(collectorRegistry, k) - } - }() - md := MethodMetadata{ - API: resource.APINamespaceRDK.WithComponentType("type"), - MethodName: "method", - } - dummyCollectorConstructor = func(i interface{}, params CollectorParams) (Collector, error) { - return &collector{}, nil - } - - // Return registered collector if one exists. - RegisterCollector(md, dummyCollectorConstructor) - ret := *CollectorLookup(md) - test.That(t, ret, test.ShouldEqual, dummyCollectorConstructor) - - // Return nothing if exact match has not been registered. - wrongType := MethodMetadata{ - API: resource.APINamespaceRDK.WithComponentType("wrongType"), - MethodName: "method", - } - wrongMethod := MethodMetadata{ - API: resource.APINamespaceRDK.WithComponentType("type"), - MethodName: "WrongMethod", - } - test.That(t, CollectorLookup(wrongType), test.ShouldBeNil) - test.That(t, CollectorLookup(wrongMethod), test.ShouldBeNil) - - // Panic if try to register same thing twice. - test.That(t, func() { RegisterCollector(md, dummyCollectorConstructor) }, test.ShouldPanic) -} diff --git a/data/verify_main_test.go b/data/verify_main_test.go deleted file mode 100644 index 31e4da43730..00000000000 --- a/data/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package data - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/etc/subsystem_manifest/main_test.go b/etc/subsystem_manifest/main_test.go deleted file mode 100644 index 3a396209ae7..00000000000 --- a/etc/subsystem_manifest/main_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "testing" - - "go.viam.com/test" -) - -func TestSha256(t *testing.T) { - tempDir := t.TempDir() - filePath := filepath.Join(tempDir, "a.txt") - os.WriteFile(filePath, []byte("hello"), 0o700) - sum, err := sha256sum(filePath) - test.That(t, err, test.ShouldBeNil) - // derived with bash -c "printf "hello" > test.txt ; sha256sum test.txt" - test.That(t, sum, test.ShouldEqual, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824") -} - -func TestArchConversion(t *testing.T) { - arch, err := osArchToViamPlatform("x86_64") - test.That(t, err, test.ShouldBeNil) - test.That(t, arch, test.ShouldEqual, "linux/amd64") - - arch, err = osArchToViamPlatform("aarch64") - test.That(t, err, test.ShouldBeNil) - test.That(t, arch, test.ShouldEqual, "linux/arm64") - - _, err = osArchToViamPlatform("invalid") - test.That(t, err, test.ShouldNotBeNil) -} diff --git a/examples/apis.json b/examples/apis.json deleted file mode 100644 index 9799627b5e2..00000000000 --- a/examples/apis.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "base": { - "func": "IsMoving", - "args": ["context.Background()"] - }, - "board": { - "func": "GPIOPinByName", - "args": ["\"16\""], - "comment": "Note that the pin supplied is a placeholder. Please change this to a valid pin." - }, - "camera": { - "func": "Properties", - "args": ["context.Background()"] - }, - "encoder": { - "func": "Properties", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "motor": { - "func": "IsMoving", - "args": ["context.Background()"] - }, - "sensor": { - "func": "Readings", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "servo": { - "func": "Position", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "arm": { - "func": "EndPosition", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "audio": { - "func": "MediaProperties", - "args": ["context.Background()"] - }, - "gantry": { - "func": "Lengths", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "gripper": { - "func": "IsMoving", - "args": ["context.Background()"] - }, - "input_controller": { - "func": "Controls", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "movement_sensor": { - "func": "LinearAcceleration", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "power_sensor": { - "func": "Power", - "args": ["context.Background()", "map[string]interface{}{}"] - }, - "pose_tracker": { - "func": "Poses", - "args": [] - }, - "motion": { - "func": "GetPose", - "args": [] - }, - "vision": { - "func": "ClassificationsFromCamera", - "args": ["context.Background()", "YOURCAMERANAME", "1", "map[string]interface{}{}"], - "comment": "Note that the Camera supplied is a placeholder. Please change this to a valid Camera." - } -} diff --git a/examples/customresources/README.md b/examples/customresources/README.md deleted file mode 100644 index daf6eea2e1d..00000000000 --- a/examples/customresources/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# CustomResources -This example demonstrates several ways RDK can be extended with custom resources. It contains several sections. Note that `make` is used throughout to help script commands. The actual commands being run should be printed as they are used. You can also look in the various "Makefile" named files throughout, to see the exact targets and what they do. - -For a fully fleshed-out example of a Golang module that uses Github CI to upload to the Viam Registry, take a look at [wifi-sensor](https://github.com/viam-labs/wifi-sensor). For a list of example modules in different Viam SDKs, take a look [here](https://github.com/viamrobotics/upload-module/#example-repos). - -## APIs -APIs represent new types of components or services, with a new interface definition. They consist of protobuf descriptions for the wire level protocol, matching Go interfaces, and concrete Go implementations of a gRPC client and server. - -### gizmoapi -Custom (component) api called "Gizmo" (acme:component:gizmo). -Note that this is split into two files. The content of wrapper.go is only needed to support reconfiguration during standalone (non-modular) use. - -### summationapi -Custom (service) api called "Summation" (acme:service:summation). - -### proto -This folder contains the protobuf for the above two APIs. Only the .proto files are human modified. The rest is generated automatically by running "make" from within this directory. Note that the generation is performed using the "buf" command line tool, which itself is installed automatically as part of the make scripting. To generate protocols for other languages, other tooling or commands may be used. The key takeaway is that just the files with the .proto suffix are needed to generate the basic protobuf libraries for any given language. - -## Models -Models are concrete implementations of a specific type (API) of component or service. - -### mygizmo -A specific model (acme:demo:mygizmo) that implements the custom Gizmo API. - -### mygizmosummer -A specific model (acme:demo:mygizmosummer) that implements the custom Gizmo API and depends on another custom API. - -### mysum -A specific model (acme:demo:mysum) that implements the custom Summation API. Simply adds or subtracts numbers. - -### mybase -Custom component (acme:demo:mybase) that implements Viam's built-in Base API (rdk:component:base) and in turn depends on two secondary "real" motors from the parent robot (such parental dependencies only work in modules, not as remote servers.) - -### mynavigation -Custom service (acme:demo:mynavigation) that implements Viam's built-in Nativation API (rdk:service:navigation) and only reports a static location from its config, and allows waypoints to be added/removed. Defaults to Point Nemo. - -## Demos -Each demo showcases an implementation of custom resources. They fall into two categories. - -* One is a module, which is the newer, preferred method of implementing a custom resource. It involves building a small binary that will (after configuration) be automatically started by a parent-viam server process, and communicate with it via gRPC over a Unix (file-like) socket on the local system. All configuration is done via the single parent robot, with relevant bits being passed to the module as necessary. - -* The other demo is the older, deprecated method, which is creating a standalone robot server (very similar to viam-server itself) with the new/custom component, and other non-needed parts stripped out, and then using that as a "remote" from a parent viam-server (even though it would technically run on the same local machine.) This requires two separate configs, one for the parent, and one for the custom server. - -### complexmodule -This demo is centered around a custom module that supports all four of the custom models above, including both custom APIs. A client demo is also included in a sub folder. - -#### Running -* Start the server `make run-module` - * This uses module.json - * This automatically compiles the module itself first, which can be done manually with `make module`. -* In a separate terminal, run the client with `make run-client` (or move into the client directory and simply run `make`.) - -#### Notes -In the module.json config, the module is defined near the top of the file. The executable_path there is the filesystem path to the executable module file. This path can be either relative (to the working directory where the server is started) or absolute. The provided example is relative to the demo itself, and on real installations, absolute paths may be more reliable. Ex: "/usr/local/bin/custommodule" - -Reconfiguration should work live. Simply edit the module.json file while the server is running and save. The server should detect the changes and update accordingly. You can try adjusting the coordinates for the mynavigation service, flip the "subtract" value of the mysum service, or change the name of "arg1" in mygizmo, then re-run the client to see that it's changed things. - -Additionally, you can comment out the "Reconfigure()" method in either mygizmo, mynavigation, or mysum to see how reconfiguration becomes replacement. If a resource doesn't support direct reconfiguration, it will automatically be recreated with the new config and replaced instead. - -### multiplemodules -This demo is centered around two custom modules that each supports a custom API. A client demo is also included in a sub folder. - -#### Running -* Start the server `make run-module` - * This uses module.json - * This automatically compiles the module itself first, which can be done manually with `make module`. -* In a separate terminal, run the client with `make run-client` (or move into the client directory and simply run `make`.) - -### simplemodule -This is a minimal version of a custom resource module, using the built-in Generic API, and where everything for the module is in one file. It has a simple "counter" component model included, which uses the rdk:component:generic interface. This component simply takes numbers and adds them to a running total, which can also be fetched. This also contains a client demo similar to the complex example. - -#### Running -* Same steps as the complex demo above. -* Start the server `make run-module` - * This uses module.json - * This automatically compiles the module itself first, which can be done manually with `make module`. -* In a separate terminal, run the client with `make run-client` (or move into the client directory and simply run `make`.) - -### remoteserver -This demo provides a standalone server that supports the "mygizmo" component only, intended for use as a "remote" from a parent. The custom server is started and then a parent process can be run which will use the custom server as a "remote" and its custom resource(s) will be mapped through the parent as part of the larger robot. There is also a client demo. - -#### Running -* From within the demo's directory -* Run the server implementing custom resources `make run-remote`. - * This uses remote.json -* From a second terminal, run a standard server connecting to the custom resource server as a remote `make run-parent`. - * This uses parent.json -* From a third terminal, run the client that has loaded the custom gizmo api, and talks to it via the parent `make run-client`. - -#### Notes -The remote server method of implementing custom resources is deprecated, and the modular methods should be used instead. This demo is maintained for testing purposes. Remotes themselves are still used for connecting to viam-server instances on other physical systems however, which was their original intent. diff --git a/examples/customresources/apis/gizmoapi/gizmoapi.go b/examples/customresources/apis/gizmoapi/gizmoapi.go deleted file mode 100644 index e7a75293f39..00000000000 --- a/examples/customresources/apis/gizmoapi/gizmoapi.go +++ /dev/null @@ -1,334 +0,0 @@ -// Package gizmoapi implements the acme:component:gizmo API, a demonstraction API showcasing the available GRPC method types. -package gizmoapi - -import ( - "context" - "io" - - "github.com/pkg/errors" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - pb "go.viam.com/rdk/examples/customresources/apis/proto/api/component/gizmo/v1" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -// API is the full API definition. -var API = resource.APINamespace("acme").WithComponentType("gizmo") - -// Named is a helper for getting the named Gizmo's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named Gizmo from the given Robot. -func FromRobot(r robot.Robot, name string) (Gizmo, error) { - return robot.ResourceFromRobot[Gizmo](r, Named(name)) -} - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Gizmo]{ - // Reconfigurable, and contents of reconfwrapper.go are only needed for standalone (non-module) uses. - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterGizmoServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.GizmoService_ServiceDesc, - RPCClient: func( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, - ) (Gizmo, error) { - return NewClientFromConn(conn, remoteName, name, logger), nil - }, - }) -} - -// Gizmo defines the Go interface for the component (should match the protobuf methods.) -type Gizmo interface { - resource.Resource - DoOne(ctx context.Context, arg1 string) (bool, error) - DoOneClientStream(ctx context.Context, arg1 []string) (bool, error) - DoOneServerStream(ctx context.Context, arg1 string) ([]bool, error) - DoOneBiDiStream(ctx context.Context, arg1 []string) ([]bool, error) - DoTwo(ctx context.Context, arg1 bool) (string, error) -} - -// serviceServer implements the Gizmo RPC service from gripper.proto. -type serviceServer struct { - pb.UnimplementedGizmoServiceServer - coll resource.APIResourceCollection[Gizmo] -} - -// NewRPCServiceServer returns a new RPC server for the gizmo API. -func NewRPCServiceServer(coll resource.APIResourceCollection[Gizmo]) interface{} { - return &serviceServer{coll: coll} -} - -func (s *serviceServer) DoOne(ctx context.Context, req *pb.DoOneRequest) (*pb.DoOneResponse, error) { - g, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - resp, err := g.DoOne(ctx, req.Arg1) - if err != nil { - return nil, err - } - return &pb.DoOneResponse{Ret1: resp}, nil -} - -func (s *serviceServer) DoOneClientStream(server pb.GizmoService_DoOneClientStreamServer) error { - var name string - var args []string - for { - msg, err := server.Recv() - if errors.Is(err, io.EOF) { - break - } - - args = append(args, msg.Arg1) - if name == "" { - name = msg.Name - continue - } - if name != msg.Name { - return errors.New("unexpected") - } - } - g, err := s.coll.Resource(name) - if err != nil { - return err - } - resp, err := g.DoOneClientStream(server.Context(), args) - if err != nil { - return err - } - return server.SendAndClose(&pb.DoOneClientStreamResponse{Ret1: resp}) -} - -func (s *serviceServer) DoOneServerStream(req *pb.DoOneServerStreamRequest, stream pb.GizmoService_DoOneServerStreamServer) error { - g, err := s.coll.Resource(req.Name) - if err != nil { - return err - } - resp, err := g.DoOneServerStream(stream.Context(), req.Arg1) - if err != nil { - return err - } - for _, ret := range resp { - if err := stream.Send(&pb.DoOneServerStreamResponse{ - Ret1: ret, - }); err != nil { - return err - } - } - return nil -} - -func (s *serviceServer) DoOneBiDiStream(server pb.GizmoService_DoOneBiDiStreamServer) error { - var name string - var args []string - for { - msg, err := server.Recv() - if errors.Is(err, io.EOF) { - break - } - - args = append(args, msg.Arg1) - if name == "" { - name = msg.Name - continue - } - if name != msg.Name { - return errors.New("unexpected") - } - } - g, err := s.coll.Resource(name) - if err != nil { - return err - } - resp, err := g.DoOneBiDiStream(server.Context(), args) - if err != nil { - return err - } - for _, respRet := range resp { - if err := server.Send(&pb.DoOneBiDiStreamResponse{Ret1: respRet}); err != nil { - return err - } - } - return nil -} - -func (s *serviceServer) DoTwo(ctx context.Context, req *pb.DoTwoRequest) (*pb.DoTwoResponse, error) { - g, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - resp, err := g.DoTwo(ctx, req.Arg1) - if err != nil { - return nil, err - } - return &pb.DoTwoResponse{Ret1: resp}, nil -} - -func (s *serviceServer) DoCommand(ctx context.Context, req *pb.DoCommandRequest) (*pb.DoCommandResponse, error) { - g, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - resp, err := g.DoCommand(ctx, req.Command.AsMap()) - if err != nil { - return nil, err - } - pbResp, err := protoutils.StructToStructPb(resp) - if err != nil { - return nil, err - } - return &pb.DoCommandResponse{Result: pbResp}, nil -} - -// NewClientFromConn creates a new gizmo RPC client from an existing connection. -func NewClientFromConn(conn rpc.ClientConn, remoteName string, name resource.Name, logger logging.Logger) Gizmo { - sc := newSvcClientFromConn(conn, remoteName, name, logger) - return clientFromSvcClient(sc, name.ShortName()) -} - -func newSvcClientFromConn(conn rpc.ClientConn, remoteName string, name resource.Name, logger logging.Logger) *serviceClient { - client := pb.NewGizmoServiceClient(conn) - sc := &serviceClient{ - Named: name.PrependRemote(remoteName).AsNamed(), - client: client, - logger: logger, - } - return sc -} - -type serviceClient struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - client pb.GizmoServiceClient - logger logging.Logger -} - -// client is an gripper client. -type client struct { - *serviceClient - name string -} - -func clientFromSvcClient(sc *serviceClient, name string) Gizmo { - return &client{sc, name} -} - -func (c *client) DoOne(ctx context.Context, arg1 string) (bool, error) { - resp, err := c.client.DoOne(ctx, &pb.DoOneRequest{ - Name: c.name, - Arg1: arg1, - }) - if err != nil { - return false, err - } - return resp.Ret1, nil -} - -func (c *client) DoOneClientStream(ctx context.Context, arg1 []string) (bool, error) { - client, err := c.client.DoOneClientStream(ctx) - if err != nil { - return false, err - } - for _, arg := range arg1 { - if err := client.Send(&pb.DoOneClientStreamRequest{ - Name: c.name, - Arg1: arg, - }); err != nil { - return false, err - } - } - resp, err := client.CloseAndRecv() - if err != nil { - return false, err - } - return resp.Ret1, nil -} - -func (c *client) DoOneServerStream(ctx context.Context, arg1 string) ([]bool, error) { - resp, err := c.client.DoOneServerStream(ctx, &pb.DoOneServerStreamRequest{ - Name: c.name, - Arg1: arg1, - }) - if err != nil { - return nil, err - } - var rets []bool - for { - resp, err := resp.Recv() - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, err - } - rets = append(rets, resp.Ret1) - } - return rets, nil -} - -func (c *client) DoOneBiDiStream(ctx context.Context, arg1 []string) ([]bool, error) { - client, err := c.client.DoOneBiDiStream(ctx) - if err != nil { - return nil, err - } - for _, arg := range arg1 { - if err := client.Send(&pb.DoOneBiDiStreamRequest{ - Name: c.name, - Arg1: arg, - }); err != nil { - return nil, err - } - } - if err := client.CloseSend(); err != nil { - return nil, err - } - - var rets []bool - for { - resp, err := client.Recv() - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, err - } - rets = append(rets, resp.Ret1) - } - return rets, nil -} - -func (c *client) DoTwo(ctx context.Context, arg1 bool) (string, error) { - resp, err := c.client.DoTwo(ctx, &pb.DoTwoRequest{ - Name: c.name, - Arg1: arg1, - }) - if err != nil { - return "", err - } - return resp.Ret1, nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - command, err := protoutils.StructToStructPb(cmd) - if err != nil { - return nil, err - } - resp, err := c.client.DoCommand(ctx, &pb.DoCommandRequest{ - Name: c.name, - Command: command, - }) - if err != nil { - return nil, err - } - return resp.Result.AsMap(), nil -} diff --git a/examples/customresources/apis/proto/.gitignore b/examples/customresources/apis/proto/.gitignore deleted file mode 100644 index e660fd93d31..00000000000 --- a/examples/customresources/apis/proto/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bin/ diff --git a/examples/customresources/apis/proto/Makefile b/examples/customresources/apis/proto/Makefile deleted file mode 100644 index 7e821b65526..00000000000 --- a/examples/customresources/apis/proto/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -.PHONY: protobuf - -default: protobuf - -bin/buf bin/protoc-gen-go bin/protoc-gen-grpc-gateway bin/protoc-gen-go-grpc: - GOBIN=$(shell pwd)/bin go install \ - github.com/bufbuild/buf/cmd/buf \ - google.golang.org/protobuf/cmd/protoc-gen-go \ - github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \ - google.golang.org/grpc/cmd/protoc-gen-go-grpc - -protobuf: api/component/gizmo/v1/gizmo.proto api/service/summation/v1/summation.proto bin/buf bin/protoc-gen-go bin/protoc-gen-grpc-gateway bin/protoc-gen-go-grpc - PATH="$(shell pwd)/bin" buf generate diff --git a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.pb.go b/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.pb.go deleted file mode 100644 index 5cf6dd334c9..00000000000 --- a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.pb.go +++ /dev/null @@ -1,969 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.30.0 -// protoc (unknown) -// source: api/component/gizmo/v1/gizmo.proto - -package v1 - -import ( - reflect "reflect" - sync "sync" - - _ "google.golang.org/genproto/googleapis/api/annotations" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - structpb "google.golang.org/protobuf/types/known/structpb" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type DoOneRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Arg1 string `protobuf:"bytes,2,opt,name=arg1,proto3" json:"arg1,omitempty"` -} - -func (x *DoOneRequest) Reset() { - *x = DoOneRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneRequest) ProtoMessage() {} - -func (x *DoOneRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneRequest.ProtoReflect.Descriptor instead. -func (*DoOneRequest) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{0} -} - -func (x *DoOneRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *DoOneRequest) GetArg1() string { - if x != nil { - return x.Arg1 - } - return "" -} - -type DoOneResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Ret1 bool `protobuf:"varint,1,opt,name=ret1,proto3" json:"ret1,omitempty"` -} - -func (x *DoOneResponse) Reset() { - *x = DoOneResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneResponse) ProtoMessage() {} - -func (x *DoOneResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneResponse.ProtoReflect.Descriptor instead. -func (*DoOneResponse) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{1} -} - -func (x *DoOneResponse) GetRet1() bool { - if x != nil { - return x.Ret1 - } - return false -} - -type DoOneServerStreamRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Arg1 string `protobuf:"bytes,2,opt,name=arg1,proto3" json:"arg1,omitempty"` -} - -func (x *DoOneServerStreamRequest) Reset() { - *x = DoOneServerStreamRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneServerStreamRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneServerStreamRequest) ProtoMessage() {} - -func (x *DoOneServerStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneServerStreamRequest.ProtoReflect.Descriptor instead. -func (*DoOneServerStreamRequest) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{2} -} - -func (x *DoOneServerStreamRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *DoOneServerStreamRequest) GetArg1() string { - if x != nil { - return x.Arg1 - } - return "" -} - -type DoOneServerStreamResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Ret1 bool `protobuf:"varint,1,opt,name=ret1,proto3" json:"ret1,omitempty"` -} - -func (x *DoOneServerStreamResponse) Reset() { - *x = DoOneServerStreamResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneServerStreamResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneServerStreamResponse) ProtoMessage() {} - -func (x *DoOneServerStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneServerStreamResponse.ProtoReflect.Descriptor instead. -func (*DoOneServerStreamResponse) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{3} -} - -func (x *DoOneServerStreamResponse) GetRet1() bool { - if x != nil { - return x.Ret1 - } - return false -} - -type DoOneClientStreamRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Arg1 string `protobuf:"bytes,2,opt,name=arg1,proto3" json:"arg1,omitempty"` -} - -func (x *DoOneClientStreamRequest) Reset() { - *x = DoOneClientStreamRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneClientStreamRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneClientStreamRequest) ProtoMessage() {} - -func (x *DoOneClientStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneClientStreamRequest.ProtoReflect.Descriptor instead. -func (*DoOneClientStreamRequest) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{4} -} - -func (x *DoOneClientStreamRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *DoOneClientStreamRequest) GetArg1() string { - if x != nil { - return x.Arg1 - } - return "" -} - -type DoOneClientStreamResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Ret1 bool `protobuf:"varint,1,opt,name=ret1,proto3" json:"ret1,omitempty"` -} - -func (x *DoOneClientStreamResponse) Reset() { - *x = DoOneClientStreamResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneClientStreamResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneClientStreamResponse) ProtoMessage() {} - -func (x *DoOneClientStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneClientStreamResponse.ProtoReflect.Descriptor instead. -func (*DoOneClientStreamResponse) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{5} -} - -func (x *DoOneClientStreamResponse) GetRet1() bool { - if x != nil { - return x.Ret1 - } - return false -} - -type DoOneBiDiStreamRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Arg1 string `protobuf:"bytes,2,opt,name=arg1,proto3" json:"arg1,omitempty"` -} - -func (x *DoOneBiDiStreamRequest) Reset() { - *x = DoOneBiDiStreamRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneBiDiStreamRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneBiDiStreamRequest) ProtoMessage() {} - -func (x *DoOneBiDiStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneBiDiStreamRequest.ProtoReflect.Descriptor instead. -func (*DoOneBiDiStreamRequest) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{6} -} - -func (x *DoOneBiDiStreamRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *DoOneBiDiStreamRequest) GetArg1() string { - if x != nil { - return x.Arg1 - } - return "" -} - -type DoOneBiDiStreamResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Ret1 bool `protobuf:"varint,1,opt,name=ret1,proto3" json:"ret1,omitempty"` -} - -func (x *DoOneBiDiStreamResponse) Reset() { - *x = DoOneBiDiStreamResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoOneBiDiStreamResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoOneBiDiStreamResponse) ProtoMessage() {} - -func (x *DoOneBiDiStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoOneBiDiStreamResponse.ProtoReflect.Descriptor instead. -func (*DoOneBiDiStreamResponse) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{7} -} - -func (x *DoOneBiDiStreamResponse) GetRet1() bool { - if x != nil { - return x.Ret1 - } - return false -} - -type DoTwoRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Arg1 bool `protobuf:"varint,2,opt,name=arg1,proto3" json:"arg1,omitempty"` -} - -func (x *DoTwoRequest) Reset() { - *x = DoTwoRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoTwoRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoTwoRequest) ProtoMessage() {} - -func (x *DoTwoRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoTwoRequest.ProtoReflect.Descriptor instead. -func (*DoTwoRequest) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{8} -} - -func (x *DoTwoRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *DoTwoRequest) GetArg1() bool { - if x != nil { - return x.Arg1 - } - return false -} - -type DoTwoResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Ret1 string `protobuf:"bytes,1,opt,name=ret1,proto3" json:"ret1,omitempty"` -} - -func (x *DoTwoResponse) Reset() { - *x = DoTwoResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoTwoResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoTwoResponse) ProtoMessage() {} - -func (x *DoTwoResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoTwoResponse.ProtoReflect.Descriptor instead. -func (*DoTwoResponse) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{9} -} - -func (x *DoTwoResponse) GetRet1() string { - if x != nil { - return x.Ret1 - } - return "" -} - -type DoCommandRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Command *structpb.Struct `protobuf:"bytes,2,opt,name=command,proto3" json:"command,omitempty"` -} - -func (x *DoCommandRequest) Reset() { - *x = DoCommandRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoCommandRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoCommandRequest) ProtoMessage() {} - -func (x *DoCommandRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoCommandRequest.ProtoReflect.Descriptor instead. -func (*DoCommandRequest) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{10} -} - -func (x *DoCommandRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *DoCommandRequest) GetCommand() *structpb.Struct { - if x != nil { - return x.Command - } - return nil -} - -type DoCommandResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Result *structpb.Struct `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` -} - -func (x *DoCommandResponse) Reset() { - *x = DoCommandResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DoCommandResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DoCommandResponse) ProtoMessage() {} - -func (x *DoCommandResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_component_gizmo_v1_gizmo_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DoCommandResponse.ProtoReflect.Descriptor instead. -func (*DoCommandResponse) Descriptor() ([]byte, []int) { - return file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP(), []int{11} -} - -func (x *DoCommandResponse) GetResult() *structpb.Struct { - if x != nil { - return x.Result - } - return nil -} - -var File_api_component_gizmo_v1_gizmo_proto protoreflect.FileDescriptor - -var file_api_component_gizmo_v1_gizmo_proto_rawDesc = []byte{ - 0x0a, 0x22, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, - 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, - 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, - 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x36, 0x0a, 0x0c, 0x44, 0x6f, 0x4f, - 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x61, 0x72, 0x67, 0x31, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, - 0x31, 0x22, 0x23, 0x0a, 0x0d, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x74, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x04, 0x72, 0x65, 0x74, 0x31, 0x22, 0x42, 0x0a, 0x18, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x31, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x31, 0x22, 0x2f, 0x0a, 0x19, 0x44, 0x6f, - 0x4f, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, 0x74, 0x31, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x72, 0x65, 0x74, 0x31, 0x22, 0x42, 0x0a, 0x18, 0x44, - 0x6f, 0x4f, 0x6e, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, - 0x72, 0x67, 0x31, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x31, 0x22, - 0x2f, 0x0a, 0x19, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, - 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x72, 0x65, 0x74, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x72, 0x65, 0x74, 0x31, - 0x22, 0x40, 0x0a, 0x16, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x42, 0x69, 0x44, 0x69, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x61, 0x72, 0x67, 0x31, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, - 0x67, 0x31, 0x22, 0x2d, 0x0a, 0x17, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x42, 0x69, 0x44, 0x69, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x72, 0x65, 0x74, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x72, 0x65, 0x74, - 0x31, 0x22, 0x36, 0x0a, 0x0c, 0x44, 0x6f, 0x54, 0x77, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x31, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x04, 0x61, 0x72, 0x67, 0x31, 0x22, 0x23, 0x0a, 0x0d, 0x44, 0x6f, 0x54, - 0x77, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x65, - 0x74, 0x31, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x65, 0x74, 0x31, 0x22, 0x59, - 0x0a, 0x10, 0x44, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x44, 0x0a, 0x11, 0x44, 0x6f, 0x43, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, - 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, - 0xbb, 0x06, 0x0a, 0x0c, 0x47, 0x69, 0x7a, 0x6d, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x8a, 0x01, 0x0a, 0x05, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x12, 0x25, 0x2e, 0x61, 0x63, 0x6d, - 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, - 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, - 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x4f, 0x6e, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x32, 0x82, 0xd3, 0xe4, 0x93, 0x02, - 0x2c, 0x22, 0x2a, 0x2f, 0x61, 0x63, 0x6d, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, - 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2f, - 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x5f, 0x6f, 0x6e, 0x65, 0x12, 0x7c, 0x0a, - 0x11, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, - 0x61, 0x6d, 0x12, 0x31, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, - 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x4f, - 0x6e, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, - 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x7c, 0x0a, 0x11, 0x44, - 0x6f, 0x4f, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x12, 0x31, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, - 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x4f, 0x6e, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, - 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, - 0x4f, 0x6e, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x78, 0x0a, 0x0f, 0x44, 0x6f, 0x4f, - 0x6e, 0x65, 0x42, 0x69, 0x44, 0x69, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x2f, 0x2e, 0x61, - 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, - 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x42, 0x69, 0x44, 0x69, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, - 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, - 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x4f, 0x6e, 0x65, 0x42, 0x69, 0x44, - 0x69, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, - 0x01, 0x30, 0x01, 0x12, 0x8a, 0x01, 0x0a, 0x05, 0x44, 0x6f, 0x54, 0x77, 0x6f, 0x12, 0x25, 0x2e, - 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, - 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x54, 0x77, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, - 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x6f, 0x54, 0x77, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x32, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x2c, 0x22, 0x2a, 0x2f, 0x61, 0x63, 0x6d, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x69, 0x7a, - 0x6d, 0x6f, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x5f, 0x74, 0x77, 0x6f, - 0x12, 0x9a, 0x01, 0x0a, 0x09, 0x44, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x29, - 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, - 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x61, 0x63, 0x6d, 0x65, - 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2e, 0x67, 0x69, 0x7a, 0x6d, 0x6f, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x6f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x30, 0x22, 0x2e, 0x2f, - 0x61, 0x63, 0x6d, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6d, 0x70, - 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x2f, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, - 0x65, 0x7d, 0x2f, 0x64, 0x6f, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x2a, 0x5a, - 0x28, 0x67, 0x6f, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, - 0x2f, 0x67, 0x69, 0x7a, 0x6d, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, -} - -var ( - file_api_component_gizmo_v1_gizmo_proto_rawDescOnce sync.Once - file_api_component_gizmo_v1_gizmo_proto_rawDescData = file_api_component_gizmo_v1_gizmo_proto_rawDesc -) - -func file_api_component_gizmo_v1_gizmo_proto_rawDescGZIP() []byte { - file_api_component_gizmo_v1_gizmo_proto_rawDescOnce.Do(func() { - file_api_component_gizmo_v1_gizmo_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_component_gizmo_v1_gizmo_proto_rawDescData) - }) - return file_api_component_gizmo_v1_gizmo_proto_rawDescData -} - -var file_api_component_gizmo_v1_gizmo_proto_msgTypes = make([]protoimpl.MessageInfo, 12) -var file_api_component_gizmo_v1_gizmo_proto_goTypes = []interface{}{ - (*DoOneRequest)(nil), // 0: acme.component.gizmo.v1.DoOneRequest - (*DoOneResponse)(nil), // 1: acme.component.gizmo.v1.DoOneResponse - (*DoOneServerStreamRequest)(nil), // 2: acme.component.gizmo.v1.DoOneServerStreamRequest - (*DoOneServerStreamResponse)(nil), // 3: acme.component.gizmo.v1.DoOneServerStreamResponse - (*DoOneClientStreamRequest)(nil), // 4: acme.component.gizmo.v1.DoOneClientStreamRequest - (*DoOneClientStreamResponse)(nil), // 5: acme.component.gizmo.v1.DoOneClientStreamResponse - (*DoOneBiDiStreamRequest)(nil), // 6: acme.component.gizmo.v1.DoOneBiDiStreamRequest - (*DoOneBiDiStreamResponse)(nil), // 7: acme.component.gizmo.v1.DoOneBiDiStreamResponse - (*DoTwoRequest)(nil), // 8: acme.component.gizmo.v1.DoTwoRequest - (*DoTwoResponse)(nil), // 9: acme.component.gizmo.v1.DoTwoResponse - (*DoCommandRequest)(nil), // 10: acme.component.gizmo.v1.DoCommandRequest - (*DoCommandResponse)(nil), // 11: acme.component.gizmo.v1.DoCommandResponse - (*structpb.Struct)(nil), // 12: google.protobuf.Struct -} -var file_api_component_gizmo_v1_gizmo_proto_depIdxs = []int32{ - 12, // 0: acme.component.gizmo.v1.DoCommandRequest.command:type_name -> google.protobuf.Struct - 12, // 1: acme.component.gizmo.v1.DoCommandResponse.result:type_name -> google.protobuf.Struct - 0, // 2: acme.component.gizmo.v1.GizmoService.DoOne:input_type -> acme.component.gizmo.v1.DoOneRequest - 4, // 3: acme.component.gizmo.v1.GizmoService.DoOneClientStream:input_type -> acme.component.gizmo.v1.DoOneClientStreamRequest - 2, // 4: acme.component.gizmo.v1.GizmoService.DoOneServerStream:input_type -> acme.component.gizmo.v1.DoOneServerStreamRequest - 6, // 5: acme.component.gizmo.v1.GizmoService.DoOneBiDiStream:input_type -> acme.component.gizmo.v1.DoOneBiDiStreamRequest - 8, // 6: acme.component.gizmo.v1.GizmoService.DoTwo:input_type -> acme.component.gizmo.v1.DoTwoRequest - 10, // 7: acme.component.gizmo.v1.GizmoService.DoCommand:input_type -> acme.component.gizmo.v1.DoCommandRequest - 1, // 8: acme.component.gizmo.v1.GizmoService.DoOne:output_type -> acme.component.gizmo.v1.DoOneResponse - 5, // 9: acme.component.gizmo.v1.GizmoService.DoOneClientStream:output_type -> acme.component.gizmo.v1.DoOneClientStreamResponse - 3, // 10: acme.component.gizmo.v1.GizmoService.DoOneServerStream:output_type -> acme.component.gizmo.v1.DoOneServerStreamResponse - 7, // 11: acme.component.gizmo.v1.GizmoService.DoOneBiDiStream:output_type -> acme.component.gizmo.v1.DoOneBiDiStreamResponse - 9, // 12: acme.component.gizmo.v1.GizmoService.DoTwo:output_type -> acme.component.gizmo.v1.DoTwoResponse - 11, // 13: acme.component.gizmo.v1.GizmoService.DoCommand:output_type -> acme.component.gizmo.v1.DoCommandResponse - 8, // [8:14] is the sub-list for method output_type - 2, // [2:8] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name -} - -func init() { file_api_component_gizmo_v1_gizmo_proto_init() } -func file_api_component_gizmo_v1_gizmo_proto_init() { - if File_api_component_gizmo_v1_gizmo_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_api_component_gizmo_v1_gizmo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneServerStreamRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneServerStreamResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneClientStreamRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneClientStreamResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneBiDiStreamRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoOneBiDiStreamResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoTwoRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoTwoResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoCommandRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_component_gizmo_v1_gizmo_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DoCommandResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_api_component_gizmo_v1_gizmo_proto_rawDesc, - NumEnums: 0, - NumMessages: 12, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_api_component_gizmo_v1_gizmo_proto_goTypes, - DependencyIndexes: file_api_component_gizmo_v1_gizmo_proto_depIdxs, - MessageInfos: file_api_component_gizmo_v1_gizmo_proto_msgTypes, - }.Build() - File_api_component_gizmo_v1_gizmo_proto = out.File - file_api_component_gizmo_v1_gizmo_proto_rawDesc = nil - file_api_component_gizmo_v1_gizmo_proto_goTypes = nil - file_api_component_gizmo_v1_gizmo_proto_depIdxs = nil -} diff --git a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.pb.gw.go b/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.pb.gw.go deleted file mode 100644 index a146983c1f0..00000000000 --- a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.pb.gw.go +++ /dev/null @@ -1,660 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: api/component/gizmo/v1/gizmo.proto - -/* -Package v1 is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package v1 - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -var ( - filter_GizmoService_DoOne_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 2, 0, 0}, Check: []int{0, 1, 2, 2}} -) - -func request_GizmoService_DoOne_0(ctx context.Context, marshaler runtime.Marshaler, client GizmoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq DoOneRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GizmoService_DoOne_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.DoOne(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_GizmoService_DoOne_0(ctx context.Context, marshaler runtime.Marshaler, server GizmoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq DoOneRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GizmoService_DoOne_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.DoOne(ctx, &protoReq) - return msg, metadata, err - -} - -func request_GizmoService_DoOneClientStream_0(ctx context.Context, marshaler runtime.Marshaler, client GizmoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var metadata runtime.ServerMetadata - stream, err := client.DoOneClientStream(ctx) - if err != nil { - grpclog.Infof("Failed to start streaming: %v", err) - return nil, metadata, err - } - dec := marshaler.NewDecoder(req.Body) - for { - var protoReq DoOneClientStreamRequest - err = dec.Decode(&protoReq) - if err == io.EOF { - break - } - if err != nil { - grpclog.Infof("Failed to decode request: %v", err) - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err = stream.Send(&protoReq); err != nil { - if err == io.EOF { - break - } - grpclog.Infof("Failed to send request: %v", err) - return nil, metadata, err - } - } - - if err := stream.CloseSend(); err != nil { - grpclog.Infof("Failed to terminate client stream: %v", err) - return nil, metadata, err - } - header, err := stream.Header() - if err != nil { - grpclog.Infof("Failed to get header from client: %v", err) - return nil, metadata, err - } - metadata.HeaderMD = header - - msg, err := stream.CloseAndRecv() - metadata.TrailerMD = stream.Trailer() - return msg, metadata, err - -} - -func request_GizmoService_DoOneServerStream_0(ctx context.Context, marshaler runtime.Marshaler, client GizmoServiceClient, req *http.Request, pathParams map[string]string) (GizmoService_DoOneServerStreamClient, runtime.ServerMetadata, error) { - var protoReq DoOneServerStreamRequest - var metadata runtime.ServerMetadata - - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - stream, err := client.DoOneServerStream(ctx, &protoReq) - if err != nil { - return nil, metadata, err - } - header, err := stream.Header() - if err != nil { - return nil, metadata, err - } - metadata.HeaderMD = header - return stream, metadata, nil - -} - -func request_GizmoService_DoOneBiDiStream_0(ctx context.Context, marshaler runtime.Marshaler, client GizmoServiceClient, req *http.Request, pathParams map[string]string) (GizmoService_DoOneBiDiStreamClient, runtime.ServerMetadata, error) { - var metadata runtime.ServerMetadata - stream, err := client.DoOneBiDiStream(ctx) - if err != nil { - grpclog.Infof("Failed to start streaming: %v", err) - return nil, metadata, err - } - dec := marshaler.NewDecoder(req.Body) - handleSend := func() error { - var protoReq DoOneBiDiStreamRequest - err := dec.Decode(&protoReq) - if err == io.EOF { - return err - } - if err != nil { - grpclog.Infof("Failed to decode request: %v", err) - return err - } - if err := stream.Send(&protoReq); err != nil { - grpclog.Infof("Failed to send request: %v", err) - return err - } - return nil - } - go func() { - for { - if err := handleSend(); err != nil { - break - } - } - if err := stream.CloseSend(); err != nil { - grpclog.Infof("Failed to terminate client stream: %v", err) - } - }() - header, err := stream.Header() - if err != nil { - grpclog.Infof("Failed to get header from client: %v", err) - return nil, metadata, err - } - metadata.HeaderMD = header - return stream, metadata, nil -} - -var ( - filter_GizmoService_DoTwo_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 2, 0, 0}, Check: []int{0, 1, 2, 2}} -) - -func request_GizmoService_DoTwo_0(ctx context.Context, marshaler runtime.Marshaler, client GizmoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq DoTwoRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GizmoService_DoTwo_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.DoTwo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_GizmoService_DoTwo_0(ctx context.Context, marshaler runtime.Marshaler, server GizmoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq DoTwoRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GizmoService_DoTwo_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.DoTwo(ctx, &protoReq) - return msg, metadata, err - -} - -var ( - filter_GizmoService_DoCommand_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 2, 0, 0}, Check: []int{0, 1, 2, 2}} -) - -func request_GizmoService_DoCommand_0(ctx context.Context, marshaler runtime.Marshaler, client GizmoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq DoCommandRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GizmoService_DoCommand_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.DoCommand(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_GizmoService_DoCommand_0(ctx context.Context, marshaler runtime.Marshaler, server GizmoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq DoCommandRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_GizmoService_DoCommand_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.DoCommand(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterGizmoServiceHandlerServer registers the http handlers for service GizmoService to "mux". -// UnaryRPC :call GizmoServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterGizmoServiceHandlerFromEndpoint instead. -func RegisterGizmoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server GizmoServiceServer) error { - - mux.Handle("POST", pattern_GizmoService_DoOne_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoOne", runtime.WithHTTPPathPattern("/acme/api/v1/component/gizmo/{name}/do_one")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_GizmoService_DoOne_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoOne_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_GizmoService_DoOneClientStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") - _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - }) - - mux.Handle("POST", pattern_GizmoService_DoOneServerStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") - _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - }) - - mux.Handle("POST", pattern_GizmoService_DoOneBiDiStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") - _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - }) - - mux.Handle("POST", pattern_GizmoService_DoTwo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoTwo", runtime.WithHTTPPathPattern("/acme/api/v1/component/gizmo/{name}/do_two")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_GizmoService_DoTwo_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoTwo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_GizmoService_DoCommand_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoCommand", runtime.WithHTTPPathPattern("/acme/api/v1/component/gizmo/{name}/do_command")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_GizmoService_DoCommand_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoCommand_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterGizmoServiceHandlerFromEndpoint is same as RegisterGizmoServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterGizmoServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterGizmoServiceHandler(ctx, mux, conn) -} - -// RegisterGizmoServiceHandler registers the http handlers for service GizmoService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterGizmoServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterGizmoServiceHandlerClient(ctx, mux, NewGizmoServiceClient(conn)) -} - -// RegisterGizmoServiceHandlerClient registers the http handlers for service GizmoService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "GizmoServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "GizmoServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "GizmoServiceClient" to call the correct interceptors. -func RegisterGizmoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client GizmoServiceClient) error { - - mux.Handle("POST", pattern_GizmoService_DoOne_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoOne", runtime.WithHTTPPathPattern("/acme/api/v1/component/gizmo/{name}/do_one")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_GizmoService_DoOne_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoOne_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_GizmoService_DoOneClientStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoOneClientStream", runtime.WithHTTPPathPattern("/acme.component.gizmo.v1.GizmoService/DoOneClientStream")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_GizmoService_DoOneClientStream_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoOneClientStream_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_GizmoService_DoOneServerStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoOneServerStream", runtime.WithHTTPPathPattern("/acme.component.gizmo.v1.GizmoService/DoOneServerStream")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_GizmoService_DoOneServerStream_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoOneServerStream_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_GizmoService_DoOneBiDiStream_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoOneBiDiStream", runtime.WithHTTPPathPattern("/acme.component.gizmo.v1.GizmoService/DoOneBiDiStream")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_GizmoService_DoOneBiDiStream_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoOneBiDiStream_0(annotatedContext, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_GizmoService_DoTwo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoTwo", runtime.WithHTTPPathPattern("/acme/api/v1/component/gizmo/{name}/do_two")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_GizmoService_DoTwo_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoTwo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - mux.Handle("POST", pattern_GizmoService_DoCommand_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/acme.component.gizmo.v1.GizmoService/DoCommand", runtime.WithHTTPPathPattern("/acme/api/v1/component/gizmo/{name}/do_command")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_GizmoService_DoCommand_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_GizmoService_DoCommand_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_GizmoService_DoOne_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"acme", "api", "v1", "component", "gizmo", "name", "do_one"}, "")) - - pattern_GizmoService_DoOneClientStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"acme.component.gizmo.v1.GizmoService", "DoOneClientStream"}, "")) - - pattern_GizmoService_DoOneServerStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"acme.component.gizmo.v1.GizmoService", "DoOneServerStream"}, "")) - - pattern_GizmoService_DoOneBiDiStream_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"acme.component.gizmo.v1.GizmoService", "DoOneBiDiStream"}, "")) - - pattern_GizmoService_DoTwo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"acme", "api", "v1", "component", "gizmo", "name", "do_two"}, "")) - - pattern_GizmoService_DoCommand_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"acme", "api", "v1", "component", "gizmo", "name", "do_command"}, "")) -) - -var ( - forward_GizmoService_DoOne_0 = runtime.ForwardResponseMessage - - forward_GizmoService_DoOneClientStream_0 = runtime.ForwardResponseMessage - - forward_GizmoService_DoOneServerStream_0 = runtime.ForwardResponseStream - - forward_GizmoService_DoOneBiDiStream_0 = runtime.ForwardResponseStream - - forward_GizmoService_DoTwo_0 = runtime.ForwardResponseMessage - - forward_GizmoService_DoCommand_0 = runtime.ForwardResponseMessage -) diff --git a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.proto b/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.proto deleted file mode 100644 index 35a68c05d17..00000000000 --- a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo.proto +++ /dev/null @@ -1,88 +0,0 @@ -syntax = "proto3"; - -package acme.component.gizmo.v1; - -import "google/api/annotations.proto"; -import "google/protobuf/struct.proto"; - -option go_package = "go.acme.com/proto/api/component/gizmo/v1"; - -service GizmoService { - rpc DoOne(DoOneRequest) returns (DoOneResponse) { - option (google.api.http) = { - post: "/acme/api/v1/component/gizmo/{name}/do_one" - }; - } - - rpc DoOneClientStream(stream DoOneClientStreamRequest) returns (DoOneClientStreamResponse); - - rpc DoOneServerStream(DoOneServerStreamRequest) returns (stream DoOneServerStreamResponse); - - rpc DoOneBiDiStream(stream DoOneBiDiStreamRequest) returns (stream DoOneBiDiStreamResponse); - - rpc DoTwo(DoTwoRequest) returns (DoTwoResponse) { - option (google.api.http) = { - post: "/acme/api/v1/component/gizmo/{name}/do_two" - }; - } - - rpc DoCommand(DoCommandRequest) returns (DoCommandResponse) { - option (google.api.http) = { - post: "/acme/api/v1/component/gizmo/{name}/do_command" - }; - } -} - -message DoOneRequest { - string name = 1; - string arg1 = 2; -} - -message DoOneResponse { - bool ret1 = 1; -} - -message DoOneServerStreamRequest { - string name = 1; - string arg1 = 2; -} - -message DoOneServerStreamResponse { - bool ret1 = 1; -} - -message DoOneClientStreamRequest { - string name = 1; - string arg1 = 2; -} - -message DoOneClientStreamResponse { - bool ret1 = 1; -} - -message DoOneBiDiStreamRequest { - string name = 1; - string arg1 = 2; -} - -message DoOneBiDiStreamResponse { - bool ret1 = 1; -} - -message DoTwoRequest { - string name = 1; - bool arg1 = 2; -} - -message DoTwoResponse { - string ret1 = 1; -} - -message DoCommandRequest { - string name = 1; - google.protobuf.Struct command = 2; -} - -message DoCommandResponse { - google.protobuf.Struct result = 1; -} diff --git a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo_grpc.pb.go b/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo_grpc.pb.go deleted file mode 100644 index bae63bb74d5..00000000000 --- a/examples/customresources/apis/proto/api/component/gizmo/v1/gizmo_grpc.pb.go +++ /dev/null @@ -1,380 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.2.0 -// - protoc (unknown) -// source: api/component/gizmo/v1/gizmo.proto - -package v1 - -import ( - context "context" - - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 - -// GizmoServiceClient is the client API for GizmoService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type GizmoServiceClient interface { - DoOne(ctx context.Context, in *DoOneRequest, opts ...grpc.CallOption) (*DoOneResponse, error) - DoOneClientStream(ctx context.Context, opts ...grpc.CallOption) (GizmoService_DoOneClientStreamClient, error) - DoOneServerStream(ctx context.Context, in *DoOneServerStreamRequest, opts ...grpc.CallOption) (GizmoService_DoOneServerStreamClient, error) - DoOneBiDiStream(ctx context.Context, opts ...grpc.CallOption) (GizmoService_DoOneBiDiStreamClient, error) - DoTwo(ctx context.Context, in *DoTwoRequest, opts ...grpc.CallOption) (*DoTwoResponse, error) - DoCommand(ctx context.Context, in *DoCommandRequest, opts ...grpc.CallOption) (*DoCommandResponse, error) -} - -type gizmoServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewGizmoServiceClient(cc grpc.ClientConnInterface) GizmoServiceClient { - return &gizmoServiceClient{cc} -} - -func (c *gizmoServiceClient) DoOne(ctx context.Context, in *DoOneRequest, opts ...grpc.CallOption) (*DoOneResponse, error) { - out := new(DoOneResponse) - err := c.cc.Invoke(ctx, "/acme.component.gizmo.v1.GizmoService/DoOne", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *gizmoServiceClient) DoOneClientStream(ctx context.Context, opts ...grpc.CallOption) (GizmoService_DoOneClientStreamClient, error) { - stream, err := c.cc.NewStream(ctx, &GizmoService_ServiceDesc.Streams[0], "/acme.component.gizmo.v1.GizmoService/DoOneClientStream", opts...) - if err != nil { - return nil, err - } - x := &gizmoServiceDoOneClientStreamClient{stream} - return x, nil -} - -type GizmoService_DoOneClientStreamClient interface { - Send(*DoOneClientStreamRequest) error - CloseAndRecv() (*DoOneClientStreamResponse, error) - grpc.ClientStream -} - -type gizmoServiceDoOneClientStreamClient struct { - grpc.ClientStream -} - -func (x *gizmoServiceDoOneClientStreamClient) Send(m *DoOneClientStreamRequest) error { - return x.ClientStream.SendMsg(m) -} - -func (x *gizmoServiceDoOneClientStreamClient) CloseAndRecv() (*DoOneClientStreamResponse, error) { - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - m := new(DoOneClientStreamResponse) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *gizmoServiceClient) DoOneServerStream(ctx context.Context, in *DoOneServerStreamRequest, opts ...grpc.CallOption) (GizmoService_DoOneServerStreamClient, error) { - stream, err := c.cc.NewStream(ctx, &GizmoService_ServiceDesc.Streams[1], "/acme.component.gizmo.v1.GizmoService/DoOneServerStream", opts...) - if err != nil { - return nil, err - } - x := &gizmoServiceDoOneServerStreamClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type GizmoService_DoOneServerStreamClient interface { - Recv() (*DoOneServerStreamResponse, error) - grpc.ClientStream -} - -type gizmoServiceDoOneServerStreamClient struct { - grpc.ClientStream -} - -func (x *gizmoServiceDoOneServerStreamClient) Recv() (*DoOneServerStreamResponse, error) { - m := new(DoOneServerStreamResponse) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *gizmoServiceClient) DoOneBiDiStream(ctx context.Context, opts ...grpc.CallOption) (GizmoService_DoOneBiDiStreamClient, error) { - stream, err := c.cc.NewStream(ctx, &GizmoService_ServiceDesc.Streams[2], "/acme.component.gizmo.v1.GizmoService/DoOneBiDiStream", opts...) - if err != nil { - return nil, err - } - x := &gizmoServiceDoOneBiDiStreamClient{stream} - return x, nil -} - -type GizmoService_DoOneBiDiStreamClient interface { - Send(*DoOneBiDiStreamRequest) error - Recv() (*DoOneBiDiStreamResponse, error) - grpc.ClientStream -} - -type gizmoServiceDoOneBiDiStreamClient struct { - grpc.ClientStream -} - -func (x *gizmoServiceDoOneBiDiStreamClient) Send(m *DoOneBiDiStreamRequest) error { - return x.ClientStream.SendMsg(m) -} - -func (x *gizmoServiceDoOneBiDiStreamClient) Recv() (*DoOneBiDiStreamResponse, error) { - m := new(DoOneBiDiStreamResponse) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *gizmoServiceClient) DoTwo(ctx context.Context, in *DoTwoRequest, opts ...grpc.CallOption) (*DoTwoResponse, error) { - out := new(DoTwoResponse) - err := c.cc.Invoke(ctx, "/acme.component.gizmo.v1.GizmoService/DoTwo", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *gizmoServiceClient) DoCommand(ctx context.Context, in *DoCommandRequest, opts ...grpc.CallOption) (*DoCommandResponse, error) { - out := new(DoCommandResponse) - err := c.cc.Invoke(ctx, "/acme.component.gizmo.v1.GizmoService/DoCommand", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// GizmoServiceServer is the server API for GizmoService service. -// All implementations must embed UnimplementedGizmoServiceServer -// for forward compatibility -type GizmoServiceServer interface { - DoOne(context.Context, *DoOneRequest) (*DoOneResponse, error) - DoOneClientStream(GizmoService_DoOneClientStreamServer) error - DoOneServerStream(*DoOneServerStreamRequest, GizmoService_DoOneServerStreamServer) error - DoOneBiDiStream(GizmoService_DoOneBiDiStreamServer) error - DoTwo(context.Context, *DoTwoRequest) (*DoTwoResponse, error) - DoCommand(context.Context, *DoCommandRequest) (*DoCommandResponse, error) - mustEmbedUnimplementedGizmoServiceServer() -} - -// UnimplementedGizmoServiceServer must be embedded to have forward compatible implementations. -type UnimplementedGizmoServiceServer struct { -} - -func (UnimplementedGizmoServiceServer) DoOne(context.Context, *DoOneRequest) (*DoOneResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DoOne not implemented") -} -func (UnimplementedGizmoServiceServer) DoOneClientStream(GizmoService_DoOneClientStreamServer) error { - return status.Errorf(codes.Unimplemented, "method DoOneClientStream not implemented") -} -func (UnimplementedGizmoServiceServer) DoOneServerStream(*DoOneServerStreamRequest, GizmoService_DoOneServerStreamServer) error { - return status.Errorf(codes.Unimplemented, "method DoOneServerStream not implemented") -} -func (UnimplementedGizmoServiceServer) DoOneBiDiStream(GizmoService_DoOneBiDiStreamServer) error { - return status.Errorf(codes.Unimplemented, "method DoOneBiDiStream not implemented") -} -func (UnimplementedGizmoServiceServer) DoTwo(context.Context, *DoTwoRequest) (*DoTwoResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DoTwo not implemented") -} -func (UnimplementedGizmoServiceServer) DoCommand(context.Context, *DoCommandRequest) (*DoCommandResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method DoCommand not implemented") -} -func (UnimplementedGizmoServiceServer) mustEmbedUnimplementedGizmoServiceServer() {} - -// UnsafeGizmoServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to GizmoServiceServer will -// result in compilation errors. -type UnsafeGizmoServiceServer interface { - mustEmbedUnimplementedGizmoServiceServer() -} - -func RegisterGizmoServiceServer(s grpc.ServiceRegistrar, srv GizmoServiceServer) { - s.RegisterService(&GizmoService_ServiceDesc, srv) -} - -func _GizmoService_DoOne_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DoOneRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GizmoServiceServer).DoOne(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/acme.component.gizmo.v1.GizmoService/DoOne", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GizmoServiceServer).DoOne(ctx, req.(*DoOneRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _GizmoService_DoOneClientStream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(GizmoServiceServer).DoOneClientStream(&gizmoServiceDoOneClientStreamServer{stream}) -} - -type GizmoService_DoOneClientStreamServer interface { - SendAndClose(*DoOneClientStreamResponse) error - Recv() (*DoOneClientStreamRequest, error) - grpc.ServerStream -} - -type gizmoServiceDoOneClientStreamServer struct { - grpc.ServerStream -} - -func (x *gizmoServiceDoOneClientStreamServer) SendAndClose(m *DoOneClientStreamResponse) error { - return x.ServerStream.SendMsg(m) -} - -func (x *gizmoServiceDoOneClientStreamServer) Recv() (*DoOneClientStreamRequest, error) { - m := new(DoOneClientStreamRequest) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func _GizmoService_DoOneServerStream_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(DoOneServerStreamRequest) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(GizmoServiceServer).DoOneServerStream(m, &gizmoServiceDoOneServerStreamServer{stream}) -} - -type GizmoService_DoOneServerStreamServer interface { - Send(*DoOneServerStreamResponse) error - grpc.ServerStream -} - -type gizmoServiceDoOneServerStreamServer struct { - grpc.ServerStream -} - -func (x *gizmoServiceDoOneServerStreamServer) Send(m *DoOneServerStreamResponse) error { - return x.ServerStream.SendMsg(m) -} - -func _GizmoService_DoOneBiDiStream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(GizmoServiceServer).DoOneBiDiStream(&gizmoServiceDoOneBiDiStreamServer{stream}) -} - -type GizmoService_DoOneBiDiStreamServer interface { - Send(*DoOneBiDiStreamResponse) error - Recv() (*DoOneBiDiStreamRequest, error) - grpc.ServerStream -} - -type gizmoServiceDoOneBiDiStreamServer struct { - grpc.ServerStream -} - -func (x *gizmoServiceDoOneBiDiStreamServer) Send(m *DoOneBiDiStreamResponse) error { - return x.ServerStream.SendMsg(m) -} - -func (x *gizmoServiceDoOneBiDiStreamServer) Recv() (*DoOneBiDiStreamRequest, error) { - m := new(DoOneBiDiStreamRequest) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func _GizmoService_DoTwo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DoTwoRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GizmoServiceServer).DoTwo(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/acme.component.gizmo.v1.GizmoService/DoTwo", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GizmoServiceServer).DoTwo(ctx, req.(*DoTwoRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _GizmoService_DoCommand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DoCommandRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GizmoServiceServer).DoCommand(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/acme.component.gizmo.v1.GizmoService/DoCommand", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GizmoServiceServer).DoCommand(ctx, req.(*DoCommandRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// GizmoService_ServiceDesc is the grpc.ServiceDesc for GizmoService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var GizmoService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "acme.component.gizmo.v1.GizmoService", - HandlerType: (*GizmoServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "DoOne", - Handler: _GizmoService_DoOne_Handler, - }, - { - MethodName: "DoTwo", - Handler: _GizmoService_DoTwo_Handler, - }, - { - MethodName: "DoCommand", - Handler: _GizmoService_DoCommand_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "DoOneClientStream", - Handler: _GizmoService_DoOneClientStream_Handler, - ClientStreams: true, - }, - { - StreamName: "DoOneServerStream", - Handler: _GizmoService_DoOneServerStream_Handler, - ServerStreams: true, - }, - { - StreamName: "DoOneBiDiStream", - Handler: _GizmoService_DoOneBiDiStream_Handler, - ServerStreams: true, - ClientStreams: true, - }, - }, - Metadata: "api/component/gizmo/v1/gizmo.proto", -} diff --git a/examples/customresources/apis/proto/api/service/summation/v1/summation.pb.go b/examples/customresources/apis/proto/api/service/summation/v1/summation.pb.go deleted file mode 100644 index d1a67552993..00000000000 --- a/examples/customresources/apis/proto/api/service/summation/v1/summation.pb.go +++ /dev/null @@ -1,234 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.30.0 -// protoc (unknown) -// source: api/service/summation/v1/summation.proto - -package v1 - -import ( - reflect "reflect" - sync "sync" - - _ "google.golang.org/genproto/googleapis/api/annotations" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type SumRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Numbers []float64 `protobuf:"fixed64,2,rep,packed,name=numbers,proto3" json:"numbers,omitempty"` -} - -func (x *SumRequest) Reset() { - *x = SumRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_api_service_summation_v1_summation_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *SumRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SumRequest) ProtoMessage() {} - -func (x *SumRequest) ProtoReflect() protoreflect.Message { - mi := &file_api_service_summation_v1_summation_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SumRequest.ProtoReflect.Descriptor instead. -func (*SumRequest) Descriptor() ([]byte, []int) { - return file_api_service_summation_v1_summation_proto_rawDescGZIP(), []int{0} -} - -func (x *SumRequest) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *SumRequest) GetNumbers() []float64 { - if x != nil { - return x.Numbers - } - return nil -} - -type SumResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Sum float64 `protobuf:"fixed64,1,opt,name=sum,proto3" json:"sum,omitempty"` -} - -func (x *SumResponse) Reset() { - *x = SumResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_api_service_summation_v1_summation_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *SumResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*SumResponse) ProtoMessage() {} - -func (x *SumResponse) ProtoReflect() protoreflect.Message { - mi := &file_api_service_summation_v1_summation_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use SumResponse.ProtoReflect.Descriptor instead. -func (*SumResponse) Descriptor() ([]byte, []int) { - return file_api_service_summation_v1_summation_proto_rawDescGZIP(), []int{1} -} - -func (x *SumResponse) GetSum() float64 { - if x != nil { - return x.Sum - } - return 0 -} - -var File_api_service_summation_v1_summation_proto protoreflect.FileDescriptor - -var file_api_service_summation_v1_summation_proto_rawDesc = []byte{ - 0x0a, 0x28, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x73, 0x75, - 0x6d, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x75, 0x6d, 0x6d, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x19, 0x61, 0x63, 0x6d, 0x65, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x3a, 0x0a, 0x0a, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x01, 0x52, 0x07, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x22, - 0x1f, 0x0a, 0x0b, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, - 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x03, 0x73, 0x75, 0x6d, - 0x32, 0x9c, 0x01, 0x0a, 0x10, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x87, 0x01, 0x0a, 0x03, 0x53, 0x75, 0x6d, 0x12, 0x25, 0x2e, - 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x73, 0x75, 0x6d, - 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x2e, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x75, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x31, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x2b, 0x22, 0x29, 0x2f, 0x61, 0x63, 0x6d, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x76, 0x31, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x73, 0x75, 0x6d, 0x6d, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x2f, 0x73, 0x75, 0x6d, 0x42, - 0x2c, 0x5a, 0x2a, 0x67, 0x6f, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x2f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_api_service_summation_v1_summation_proto_rawDescOnce sync.Once - file_api_service_summation_v1_summation_proto_rawDescData = file_api_service_summation_v1_summation_proto_rawDesc -) - -func file_api_service_summation_v1_summation_proto_rawDescGZIP() []byte { - file_api_service_summation_v1_summation_proto_rawDescOnce.Do(func() { - file_api_service_summation_v1_summation_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_service_summation_v1_summation_proto_rawDescData) - }) - return file_api_service_summation_v1_summation_proto_rawDescData -} - -var file_api_service_summation_v1_summation_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_api_service_summation_v1_summation_proto_goTypes = []interface{}{ - (*SumRequest)(nil), // 0: acme.service.summation.v1.SumRequest - (*SumResponse)(nil), // 1: acme.service.summation.v1.SumResponse -} -var file_api_service_summation_v1_summation_proto_depIdxs = []int32{ - 0, // 0: acme.service.summation.v1.SummationService.Sum:input_type -> acme.service.summation.v1.SumRequest - 1, // 1: acme.service.summation.v1.SummationService.Sum:output_type -> acme.service.summation.v1.SumResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_api_service_summation_v1_summation_proto_init() } -func file_api_service_summation_v1_summation_proto_init() { - if File_api_service_summation_v1_summation_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_api_service_summation_v1_summation_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SumRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_api_service_summation_v1_summation_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SumResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_api_service_summation_v1_summation_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_api_service_summation_v1_summation_proto_goTypes, - DependencyIndexes: file_api_service_summation_v1_summation_proto_depIdxs, - MessageInfos: file_api_service_summation_v1_summation_proto_msgTypes, - }.Build() - File_api_service_summation_v1_summation_proto = out.File - file_api_service_summation_v1_summation_proto_rawDesc = nil - file_api_service_summation_v1_summation_proto_goTypes = nil - file_api_service_summation_v1_summation_proto_depIdxs = nil -} diff --git a/examples/customresources/apis/proto/api/service/summation/v1/summation.pb.gw.go b/examples/customresources/apis/proto/api/service/summation/v1/summation.pb.gw.go deleted file mode 100644 index 9eb45eb1173..00000000000 --- a/examples/customresources/apis/proto/api/service/summation/v1/summation.pb.gw.go +++ /dev/null @@ -1,207 +0,0 @@ -// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. -// source: api/service/summation/v1/summation.proto - -/* -Package v1 is a reverse proxy. - -It translates gRPC into RESTful JSON APIs. -*/ -package v1 - -import ( - "context" - "io" - "net/http" - - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" -) - -// Suppress "imported and not used" errors -var _ codes.Code -var _ io.Reader -var _ status.Status -var _ = runtime.String -var _ = utilities.NewDoubleArray -var _ = metadata.Join - -var ( - filter_SummationService_Sum_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 2, 0, 0}, Check: []int{0, 1, 2, 2}} -) - -func request_SummationService_Sum_0(ctx context.Context, marshaler runtime.Marshaler, client SummationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq SumRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SummationService_Sum_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := client.Sum(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) - return msg, metadata, err - -} - -func local_request_SummationService_Sum_0(ctx context.Context, marshaler runtime.Marshaler, server SummationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { - var protoReq SumRequest - var metadata runtime.ServerMetadata - - var ( - val string - ok bool - err error - _ = err - ) - - val, ok = pathParams["name"] - if !ok { - return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") - } - - protoReq.Name, err = runtime.String(val) - if err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) - } - - if err := req.ParseForm(); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_SummationService_Sum_0); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) - } - - msg, err := server.Sum(ctx, &protoReq) - return msg, metadata, err - -} - -// RegisterSummationServiceHandlerServer registers the http handlers for service SummationService to "mux". -// UnaryRPC :call SummationServiceServer directly. -// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. -// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterSummationServiceHandlerFromEndpoint instead. -func RegisterSummationServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server SummationServiceServer) error { - - mux.Handle("POST", pattern_SummationService_Sum_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - var stream runtime.ServerTransportStream - ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/acme.service.summation.v1.SummationService/Sum", runtime.WithHTTPPathPattern("/acme/api/v1/service/summation/{name}/sum")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := local_request_SummationService_Sum_0(annotatedContext, inboundMarshaler, server, req, pathParams) - md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_SummationService_Sum_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -// RegisterSummationServiceHandlerFromEndpoint is same as RegisterSummationServiceHandler but -// automatically dials to "endpoint" and closes the connection when "ctx" gets done. -func RegisterSummationServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.DialContext(ctx, endpoint, opts...) - if err != nil { - return err - } - defer func() { - if err != nil { - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - return - } - go func() { - <-ctx.Done() - if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) - } - }() - }() - - return RegisterSummationServiceHandler(ctx, mux, conn) -} - -// RegisterSummationServiceHandler registers the http handlers for service SummationService to "mux". -// The handlers forward requests to the grpc endpoint over "conn". -func RegisterSummationServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - return RegisterSummationServiceHandlerClient(ctx, mux, NewSummationServiceClient(conn)) -} - -// RegisterSummationServiceHandlerClient registers the http handlers for service SummationService -// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "SummationServiceClient". -// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "SummationServiceClient" -// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "SummationServiceClient" to call the correct interceptors. -func RegisterSummationServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SummationServiceClient) error { - - mux.Handle("POST", pattern_SummationService_Sum_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(req.Context()) - defer cancel() - inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) - var err error - var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/acme.service.summation.v1.SummationService/Sum", runtime.WithHTTPPathPattern("/acme/api/v1/service/summation/{name}/sum")) - if err != nil { - runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) - return - } - resp, md, err := request_SummationService_Sum_0(annotatedContext, inboundMarshaler, client, req, pathParams) - annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) - if err != nil { - runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) - return - } - - forward_SummationService_Sum_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) - - }) - - return nil -} - -var ( - pattern_SummationService_Sum_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5, 2, 6}, []string{"acme", "api", "v1", "service", "summation", "name", "sum"}, "")) -) - -var ( - forward_SummationService_Sum_0 = runtime.ForwardResponseMessage -) diff --git a/examples/customresources/apis/proto/api/service/summation/v1/summation.proto b/examples/customresources/apis/proto/api/service/summation/v1/summation.proto deleted file mode 100644 index ed88eb629ae..00000000000 --- a/examples/customresources/apis/proto/api/service/summation/v1/summation.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "proto3"; - -package acme.service.summation.v1; - -import "google/api/annotations.proto"; - -option go_package = "go.acme.com/proto/api/service/summation/v1"; - -service SummationService { - rpc Sum(SumRequest) returns (SumResponse) { - option (google.api.http) = { - post: "/acme/api/v1/service/summation/{name}/sum" - }; - } -} - -message SumRequest { - string name = 1; - repeated double numbers = 2; -} - -message SumResponse { - double sum = 1; -} diff --git a/examples/customresources/apis/proto/api/service/summation/v1/summation_grpc.pb.go b/examples/customresources/apis/proto/api/service/summation/v1/summation_grpc.pb.go deleted file mode 100644 index 1b465cf647d..00000000000 --- a/examples/customresources/apis/proto/api/service/summation/v1/summation_grpc.pb.go +++ /dev/null @@ -1,106 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.2.0 -// - protoc (unknown) -// source: api/service/summation/v1/summation.proto - -package v1 - -import ( - context "context" - - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 - -// SummationServiceClient is the client API for SummationService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type SummationServiceClient interface { - Sum(ctx context.Context, in *SumRequest, opts ...grpc.CallOption) (*SumResponse, error) -} - -type summationServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewSummationServiceClient(cc grpc.ClientConnInterface) SummationServiceClient { - return &summationServiceClient{cc} -} - -func (c *summationServiceClient) Sum(ctx context.Context, in *SumRequest, opts ...grpc.CallOption) (*SumResponse, error) { - out := new(SumResponse) - err := c.cc.Invoke(ctx, "/acme.service.summation.v1.SummationService/Sum", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// SummationServiceServer is the server API for SummationService service. -// All implementations must embed UnimplementedSummationServiceServer -// for forward compatibility -type SummationServiceServer interface { - Sum(context.Context, *SumRequest) (*SumResponse, error) - mustEmbedUnimplementedSummationServiceServer() -} - -// UnimplementedSummationServiceServer must be embedded to have forward compatible implementations. -type UnimplementedSummationServiceServer struct { -} - -func (UnimplementedSummationServiceServer) Sum(context.Context, *SumRequest) (*SumResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Sum not implemented") -} -func (UnimplementedSummationServiceServer) mustEmbedUnimplementedSummationServiceServer() {} - -// UnsafeSummationServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to SummationServiceServer will -// result in compilation errors. -type UnsafeSummationServiceServer interface { - mustEmbedUnimplementedSummationServiceServer() -} - -func RegisterSummationServiceServer(s grpc.ServiceRegistrar, srv SummationServiceServer) { - s.RegisterService(&SummationService_ServiceDesc, srv) -} - -func _SummationService_Sum_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(SumRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(SummationServiceServer).Sum(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/acme.service.summation.v1.SummationService/Sum", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(SummationServiceServer).Sum(ctx, req.(*SumRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// SummationService_ServiceDesc is the grpc.ServiceDesc for SummationService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var SummationService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "acme.service.summation.v1.SummationService", - HandlerType: (*SummationServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Sum", - Handler: _SummationService_Sum_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "api/service/summation/v1/summation.proto", -} diff --git a/examples/customresources/apis/proto/buf.gen.yaml b/examples/customresources/apis/proto/buf.gen.yaml deleted file mode 100644 index 8ec45b8c8b2..00000000000 --- a/examples/customresources/apis/proto/buf.gen.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: v1 -plugins: - - name: go - out: . - opt: - - paths=source_relative - - name: go-grpc - out: . - opt: - - paths=source_relative - - name: grpc-gateway - out: . - opt: - - paths=source_relative - - generate_unbound_methods=true diff --git a/examples/customresources/apis/proto/buf.lock b/examples/customresources/apis/proto/buf.lock deleted file mode 100644 index 2e0cd71c1f2..00000000000 --- a/examples/customresources/apis/proto/buf.lock +++ /dev/null @@ -1,10 +0,0 @@ -# Generated by buf. DO NOT EDIT. -version: v1 -deps: - - remote: buf.build - owner: googleapis - repository: googleapis - branch: main - commit: 62f35d8aed1149c291d606d958a7ce32 - digest: b1-2K8F7EHYBrgGfmCQmWc1zBNyiyLoIABNm3BQVj7q8DY= - create_time: 2021-10-27T15:08:18.775093Z diff --git a/examples/customresources/apis/proto/buf.yaml b/examples/customresources/apis/proto/buf.yaml deleted file mode 100644 index d521bc00cac..00000000000 --- a/examples/customresources/apis/proto/buf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: v1 -deps: - - buf.build/googleapis/googleapis:c33c435046f2f4db5e8d6db52bd6c662f50f60d8 -breaking: - use: - - FILE -lint: - use: - - DEFAULT diff --git a/examples/customresources/apis/summationapi/summationapi.go b/examples/customresources/apis/summationapi/summationapi.go deleted file mode 100644 index c384070f7eb..00000000000 --- a/examples/customresources/apis/summationapi/summationapi.go +++ /dev/null @@ -1,115 +0,0 @@ -// Package summationapi defines a simple number summing service API for demonstration purposes. -package summationapi - -import ( - "context" - - "go.viam.com/utils/rpc" - - pb "go.viam.com/rdk/examples/customresources/apis/proto/api/service/summation/v1" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -// API is the full API definition. -var API = resource.APINamespace("acme").WithServiceType("summation") - -// Named is a helper for getting the named Summation's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named Summation from the given Robot. -func FromRobot(r robot.Robot, name string) (Summation, error) { - return robot.ResourceFromRobot[Summation](r, Named(name)) -} - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Summation]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterSummationServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.SummationService_ServiceDesc, - RPCClient: func( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, - ) (Summation, error) { - return newClientFromConn(conn, remoteName, name, logger), nil - }, - }) -} - -// Summation defines the Go interface for the service (should match the protobuf methods.) -type Summation interface { - resource.Resource - Sum(ctx context.Context, nums []float64) (float64, error) -} - -// serviceServer implements the Summation RPC service from summation.proto. -type serviceServer struct { - pb.UnimplementedSummationServiceServer - coll resource.APIResourceCollection[Summation] -} - -// NewRPCServiceServer returns a new RPC server for the summation API. -func NewRPCServiceServer(coll resource.APIResourceCollection[Summation]) interface{} { - return &serviceServer{coll: coll} -} - -func (s *serviceServer) Sum(ctx context.Context, req *pb.SumRequest) (*pb.SumResponse, error) { - g, err := s.coll.Resource(req.Name) - if err != nil { - return nil, err - } - resp, err := g.Sum(ctx, req.Numbers) - if err != nil { - return nil, err - } - return &pb.SumResponse{Sum: resp}, nil -} - -func newClientFromConn(conn rpc.ClientConn, remoteName string, name resource.Name, logger logging.Logger) Summation { - sc := newSvcClientFromConn(conn, remoteName, name, logger) - return clientFromSvcClient(sc, name.ShortName()) -} - -func newSvcClientFromConn(conn rpc.ClientConn, remoteName string, name resource.Name, logger logging.Logger) *serviceClient { - client := pb.NewSummationServiceClient(conn) - sc := &serviceClient{ - Named: name.PrependRemote(remoteName).AsNamed(), - client: client, - logger: logger, - } - return sc -} - -type serviceClient struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - client pb.SummationServiceClient - logger logging.Logger -} - -type client struct { - *serviceClient - name string -} - -func clientFromSvcClient(sc *serviceClient, name string) Summation { - return &client{sc, name} -} - -func (c *client) Sum(ctx context.Context, nums []float64) (float64, error) { - resp, err := c.client.Sum(ctx, &pb.SumRequest{ - Name: c.name, - Numbers: nums, - }) - if err != nil { - return 0, err - } - return resp.Sum, nil -} diff --git a/examples/customresources/demos/complexmodule/.gitignore b/examples/customresources/demos/complexmodule/.gitignore deleted file mode 100644 index d0b04ca96fb..00000000000 --- a/examples/customresources/demos/complexmodule/.gitignore +++ /dev/null @@ -1 +0,0 @@ -complexmodule diff --git a/examples/customresources/demos/complexmodule/Makefile b/examples/customresources/demos/complexmodule/Makefile deleted file mode 100644 index 664ca6364a4..00000000000 --- a/examples/customresources/demos/complexmodule/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -.PHONY: module - -default: run-module - -module: - go build ./ - -run-module: - go run ../../../../web/cmd/server/main.go -config module.json - -run-client: - cd client && make diff --git a/examples/customresources/demos/complexmodule/client/Makefile b/examples/customresources/demos/complexmodule/client/Makefile deleted file mode 100644 index a6975c81e7f..00000000000 --- a/examples/customresources/demos/complexmodule/client/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -default: run-client - -run-client: - go run ./ diff --git a/examples/customresources/demos/complexmodule/client/client.go b/examples/customresources/demos/complexmodule/client/client.go deleted file mode 100644 index e61879b267c..00000000000 --- a/examples/customresources/demos/complexmodule/client/client.go +++ /dev/null @@ -1,182 +0,0 @@ -// Package main tests out all four custom models in the complexmodule. -package main - -import ( - "context" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot/client" - "go.viam.com/rdk/services/navigation" -) - -func main() { - logger := logging.NewLogger("client") - robot, err := client.New( - context.Background(), - "localhost:8080", - logger, - ) - if err != nil { - logger.Fatal(err) - } - - defer func() { - if err := robot.Close(context.Background()); err != nil { - logger.Fatal(err) - } - }() - - logger.Info("---- Testing gizmo1 (gizmoapi) -----") - comp1, err := gizmoapi.FromRobot(robot, "gizmo1") - if err != nil { - logger.Fatal(err) - } - ret1, err := comp1.DoOne(context.Background(), "hello") - if err != nil { - logger.Fatal(err) - } - logger.Info(ret1) - - ret2, err := comp1.DoOneClientStream(context.Background(), []string{"hello", "arg1", "foo"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret2) - - ret2, err = comp1.DoOneClientStream(context.Background(), []string{"arg1", "arg1", "arg1"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret2) - - ret3, err := comp1.DoOneServerStream(context.Background(), "hello") - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) - - ret3, err = comp1.DoOneBiDiStream(context.Background(), []string{"hello", "arg1", "foo"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) - - ret3, err = comp1.DoOneBiDiStream(context.Background(), []string{"arg1", "arg1", "arg1"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) - - logger.Info("---- Testing adder (summationapi) -----") - add, err := summationapi.FromRobot(robot, "adder") - if err != nil { - logger.Fatal(err) - } - - nums := []float64{10, 0.5, 12} - retAdd, err := add.Sum(context.Background(), nums) - if err != nil { - logger.Fatal(err) - } - logger.Info(nums, " sum to ", retAdd) - - logger.Info("---- Testing subtractor (summationapi) -----") - sub, err := summationapi.FromRobot(robot, "subtractor") - if err != nil { - logger.Fatal(err) - } - retSub, err := sub.Sum(context.Background(), nums) - if err != nil { - logger.Fatal(err) - } - logger.Info(nums, " subtract to ", retSub) - - logger.Info("---- Testing denali (navigation) -----") - nav, err := navigation.FromRobot(robot, "denali") - if err != nil { - logger.Fatal(err) - } - geoPose, err := nav.Location(context.Background(), nil) - if err != nil { - logger.Fatal(err) - } - - logger.Infof("denali service reports its location as %0.8f, %0.8f", geoPose.Location().Lat(), geoPose.Location().Lng()) - - err = nav.AddWaypoint(context.Background(), geo.NewPoint(55.1, 22.2), nil) - if err != nil { - logger.Fatal(err) - } - err = nav.AddWaypoint(context.Background(), geo.NewPoint(10.77, 17.88), nil) - if err != nil { - logger.Fatal(err) - } - err = nav.AddWaypoint(context.Background(), geo.NewPoint(42.0, 42.0), nil) - if err != nil { - logger.Fatal(err) - } - waypoints, err := nav.Waypoints(context.Background(), nil) - if err != nil { - logger.Fatal(err) - } - - logger.Info("denali waypoints stored") - for _, w := range waypoints { - logger.Infof("%.8f %.8f", w.Lat, w.Long) - } - - logger.Info("---- Testing base1 (base) -----") - mybase, err := base.FromRobot(robot, "base1") - if err != nil { - logger.Fatal(err) - } - - logger.Info("generic echo") - testCmd := map[string]interface{}{"foo": "bar"} - ret, err := mybase.DoCommand(context.Background(), testCmd) - if err != nil { - logger.Fatal(err) - } - logger.Infof("sent: %v received: %v", testCmd, ret) - - logger.Info("move forward") - err = mybase.SetPower(context.Background(), r3.Vector{Y: 1}, r3.Vector{}, nil) - if err != nil { - logger.Fatal(err) - } - time.Sleep(time.Second) - - logger.Info("move backward") - err = mybase.SetPower(context.Background(), r3.Vector{Y: -1}, r3.Vector{}, nil) - if err != nil { - logger.Fatal(err) - } - time.Sleep(time.Second) - - logger.Info("spin left") - err = mybase.SetPower(context.Background(), r3.Vector{}, r3.Vector{Z: 1}, nil) - if err != nil { - logger.Fatal(err) - } - time.Sleep(time.Second) - - logger.Info("spin right") - err = mybase.SetPower(context.Background(), r3.Vector{}, r3.Vector{Z: -1}, nil) - if err != nil { - logger.Fatal(err) - } - time.Sleep(time.Second * 1) - - logger.Info("stop") - err = mybase.Stop(context.Background(), nil) - if err != nil { - logger.Fatal(err) - } -} diff --git a/examples/customresources/demos/complexmodule/module.go b/examples/customresources/demos/complexmodule/module.go deleted file mode 100644 index 9ad307d0d1e..00000000000 --- a/examples/customresources/demos/complexmodule/module.go +++ /dev/null @@ -1,60 +0,0 @@ -// Package main is a module, which serves all four custom model types in the customresources examples, including both custom APIs. -package main - -import ( - "context" - - "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/examples/customresources/models/mybase" - "go.viam.com/rdk/examples/customresources/models/mygizmo" - "go.viam.com/rdk/examples/customresources/models/mynavigation" - "go.viam.com/rdk/examples/customresources/models/mysum" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" - "go.viam.com/rdk/services/navigation" -) - -func main() { - // NewLoggerFromArgs will create a logging.Logger at "DebugLevel" if - // "--log-level=debug" is the third argument in os.Args and at "InfoLevel" - // otherwise. - utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("complexmodule")) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (err error) { - myMod, err := module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - - // Models and APIs add helpers to the registry during their init(). - // They can then be added to the module here. - err = myMod.AddModelFromRegistry(ctx, gizmoapi.API, mygizmo.Model) - if err != nil { - return err - } - err = myMod.AddModelFromRegistry(ctx, summationapi.API, mysum.Model) - if err != nil { - return err - } - err = myMod.AddModelFromRegistry(ctx, base.API, mybase.Model) - if err != nil { - return err - } - err = myMod.AddModelFromRegistry(ctx, navigation.API, mynavigation.Model) - if err != nil { - return err - } - - err = myMod.Start(ctx) - defer myMod.Close(ctx) - if err != nil { - return err - } - <-ctx.Done() - return nil -} diff --git a/examples/customresources/demos/complexmodule/module.json b/examples/customresources/demos/complexmodule/module.json deleted file mode 100644 index c7abbe7cf5a..00000000000 --- a/examples/customresources/demos/complexmodule/module.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "modules": [ - { - "name": "AcmeModule", - "executable_path": "./run.sh", - "log_level": "debug" - } - ], - "services": [ - { - "namespace": "rdk", - "type": "navigation", - "name": "denali", - "model": "acme:demo:mynavigation", - "attributes": { - "lat": 63.0691739667009, - "long": -151.00698515692034 - } - }, - { - "namespace": "acme", - "type": "summation", - "name": "adder", - "model": "acme:demo:mysum", - "attributes" : {} - }, - { - "namespace": "acme", - "type": "summation", - "name": "subtractor", - "model": "acme:demo:mysum", - "attributes": { - "subtract": true - } - } - ], - "components": [ - { - "namespace": "rdk", - "type": "motor", - "name": "motor1", - "model": "rdk:builtin:fake", - "attributes": { - "max_rpm": 500, - "encoder": "encoder1", - "max_power_pct": 0.5, - "ticks_per_rotation": 10000 - }, - "depends_on": ["encoder1"] - }, - { - "name": "encoder1", - "type": "encoder", - "model": "fake" - }, - { - "namespace": "rdk", - "type": "motor", - "name": "motor2", - "model": "rdk:builtin:fake", - "attributes": { - "max_rpm": 500, - "encoder": "encoder2", - "max_power_pct": 0.5, - "ticks_per_rotation": 10000 - }, - "depends_on": ["encoder2"] - }, - { - "name": "encoder2", - "type": "encoder", - "model": "fake" - }, - { - "model": "acme:demo:mygizmo", - "name": "gizmo1", - "namespace": "acme", - "type": "gizmo", - "attributes": { - "arg1": "arg1" - } - }, - { - "namespace": "rdk", - "type": "base", - "name": "base1", - "model": "acme:demo:mybase", - "attributes": { - "motorL": "motor1", - "motorR": "motor2" - } - } - ], - "network": { - "bind_address": ":8080" - } -} diff --git a/examples/customresources/demos/complexmodule/moduletest/bad_modular_validation.json b/examples/customresources/demos/complexmodule/moduletest/bad_modular_validation.json deleted file mode 100644 index 484d4c2011b..00000000000 --- a/examples/customresources/demos/complexmodule/moduletest/bad_modular_validation.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "modules": [ - { - "name": "AcmeModule", - "executable_path": "./complexmodule" - } - ], - "components": [ - { - "namespace": "rdk", - "type": "motor", - "name": "motor1", - "model": "rdk:builtin:fake" - }, - { - "namespace": "rdk", - "type": "motor", - "name": "motor2", - "model": "rdk:builtin:fake" - }, - { - "namespace": "rdk", - "type": "base", - "name": "base1", - "model": "acme:demo:mybase", - "attributes": { - "motorR": "motor2" - } - } - ], - "network": { - "bind_address": ":8080" - } -} diff --git a/examples/customresources/demos/complexmodule/moduletest/module_test.go b/examples/customresources/demos/complexmodule/moduletest/module_test.go deleted file mode 100644 index 6bd902f0ddc..00000000000 --- a/examples/customresources/demos/complexmodule/moduletest/module_test.go +++ /dev/null @@ -1,394 +0,0 @@ -//go:build !no_tflite - -// Package main tests out all four custom models in the complexmodule. -package main_test - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "testing" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - goutils "go.viam.com/utils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/config" - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/client" - "go.viam.com/rdk/services/navigation" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/robottestutils" - "go.viam.com/rdk/utils" -) - -// This test ends up being a great validation of the logical clock on resource graph node -// modifications since the base depends on something it needs during initialization that -// needs to be added to the web service before it normally would be avalilable after completing -// a config cycle. -func TestComplexModule(t *testing.T) { - logger, observer := logging.NewObservedTestLogger(t) - - var port int - success := false - for portTryNum := 0; portTryNum < 10; portTryNum++ { - // Modify the example config to run directly, without compiling the module first. - cfgFilename, portLocal, err := modifyCfg(t, utils.ResolveFile("examples/customresources/demos/complexmodule/module.json"), logger) - port = portLocal - test.That(t, err, test.ShouldBeNil) - - server := robottestutils.ServerAsSeparateProcess(t, cfgFilename, logger) - - err = server.Start(context.Background()) - test.That(t, err, test.ShouldBeNil) - - if robottestutils.WaitForServing(observer, port) { - success = true - defer func() { - test.That(t, server.Stop(), test.ShouldBeNil) - }() - break - } - server.Stop() - } - test.That(t, success, test.ShouldBeTrue) - - rc, err := connect(port, logger) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, rc.Close(context.Background()), test.ShouldBeNil) - }() - - // Gizmo is a custom component model and API. - t.Run("Test Gizmo", func(t *testing.T) { - res, err := rc.ResourceByName(gizmoapi.Named("gizmo1")) - test.That(t, err, test.ShouldBeNil) - - giz := res.(gizmoapi.Gizmo) - ret1, err := giz.DoOne(context.Background(), "hello") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret1, test.ShouldBeFalse) - - // also tests that the ForeignServiceHandler does not drop the first message - ret2, err := giz.DoOneClientStream(context.Background(), []string{"hello", "arg1", "arg1"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret2, test.ShouldBeFalse) - - ret2, err = giz.DoOneClientStream(context.Background(), []string{"arg1", "arg1", "arg1"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret2, test.ShouldBeTrue) - - ret3, err := giz.DoOneServerStream(context.Background(), "hello") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret3, test.ShouldResemble, []bool{false, false, true, false}) - - ret3, err = giz.DoOneBiDiStream(context.Background(), []string{"hello", "arg1", "foo"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret3, test.ShouldResemble, []bool{false, true, false}) - - ret3, err = giz.DoOneBiDiStream(context.Background(), []string{"arg1", "arg1", "arg1"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret3, test.ShouldResemble, []bool{true, true, true}) - }) - - // Summation is a custom service model and API. - t.Run("Test Summation", func(t *testing.T) { - res, err := rc.ResourceByName(summationapi.Named("adder")) - test.That(t, err, test.ShouldBeNil) - add := res.(summationapi.Summation) - nums := []float64{10, 0.5, 12} - retAdd, err := add.Sum(context.Background(), nums) - test.That(t, err, test.ShouldBeNil) - test.That(t, retAdd, test.ShouldEqual, 22.5) - - res, err = rc.ResourceByName(summationapi.Named("subtractor")) - test.That(t, err, test.ShouldBeNil) - - sub := res.(summationapi.Summation) - retSub, err := sub.Sum(context.Background(), nums) - test.That(t, err, test.ShouldBeNil) - test.That(t, retSub, test.ShouldEqual, -22.5) - }) - - // Base is a custom component, but built-in API. It also depends on built-in motors, so tests dependencies. - t.Run("Test Base", func(t *testing.T) { - res, err := rc.ResourceByName(motor.Named("motor1")) - test.That(t, err, test.ShouldBeNil) - motorL := res.(motor.Motor) - - res, err = rc.ResourceByName(motor.Named("motor2")) - test.That(t, err, test.ShouldBeNil) - motorR := res.(motor.Motor) - - res, err = rc.ResourceByName(base.Named("base1")) - test.That(t, err, test.ShouldBeNil) - mybase := res.(base.Base) - - // Test generic echo - testCmd := map[string]interface{}{"foo": "bar"} - ret, err := mybase.DoCommand(context.Background(), testCmd) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret, test.ShouldResemble, testCmd) - - // Stopped to begin with - moving, speed, err := motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - // Forward - err = mybase.SetPower(context.Background(), r3.Vector{Y: 1}, r3.Vector{}, nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, 1.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, 1.0) - - // Stop - err = mybase.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - // Backward - err = mybase.SetPower(context.Background(), r3.Vector{Y: -1}, r3.Vector{}, nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, -1.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, -1.0) - - // Stop - err = mybase.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - // Spin Left - err = mybase.SetPower(context.Background(), r3.Vector{}, r3.Vector{Z: 1}, nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, -1.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, 1.0) - - // Stop - err = mybase.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - // Spin Right - err = mybase.SetPower(context.Background(), r3.Vector{}, r3.Vector{Z: -1}, nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, 1.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeTrue) - test.That(t, speed, test.ShouldEqual, -1.0) - - // Stop - err = mybase.Stop(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - moving, speed, err = motorL.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - - moving, speed, err = motorR.IsPowered(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, moving, test.ShouldBeFalse) - test.That(t, speed, test.ShouldEqual, 0.0) - }) - - // Navigation is a custom model, but built-in API. - t.Run("Test Navigation", func(t *testing.T) { - res, err := rc.ResourceByName(navigation.Named("denali")) - test.That(t, err, test.ShouldBeNil) - - nav := res.(navigation.Service) - geoPose, err := nav.Location(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, geoPose.Location().Lat(), test.ShouldAlmostEqual, 63.0691739667009) - test.That(t, geoPose.Location().Lng(), test.ShouldAlmostEqual, -151.00698515692034) - - err = nav.AddWaypoint(context.Background(), geo.NewPoint(55.1, 22.2), nil) - test.That(t, err, test.ShouldBeNil) - - err = nav.AddWaypoint(context.Background(), geo.NewPoint(10.77, 17.88), nil) - test.That(t, err, test.ShouldBeNil) - - err = nav.AddWaypoint(context.Background(), geo.NewPoint(42.0, 42.0), nil) - test.That(t, err, test.ShouldBeNil) - - waypoints, err := nav.Waypoints(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - expected := []navigation.Waypoint{ - {Lat: 55.1, Long: 22.2}, - {Lat: 10.77, Long: 17.88}, - {Lat: 42.0, Long: 42.0}, - } - - test.That(t, waypoints, test.ShouldResemble, expected) - }) -} - -func connect(port int, logger logging.Logger) (robot.Robot, error) { - connectCtx, cancelConn := context.WithTimeout(context.Background(), time.Second*30) - defer cancelConn() - for { - dialCtx, dialCancel := context.WithTimeout(context.Background(), time.Millisecond*500) - rc, err := client.New(dialCtx, fmt.Sprintf("localhost:%d", port), logger, - client.WithDialOptions(rpc.WithForceDirectGRPC()), - client.WithDisableSessions(), // TODO(PRODUCT-343): add session support to modules - ) - dialCancel() - if !errors.Is(err, context.DeadlineExceeded) { - return rc, err - } - select { - case <-connectCtx.Done(): - return nil, connectCtx.Err() - default: - } - } -} - -func modifyCfg(t *testing.T, cfgIn string, logger logging.Logger) (string, int, error) { - modPath := testutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - - port, err := goutils.TryReserveRandomPort() - if err != nil { - return "", 0, err - } - - cfg, err := config.Read(context.Background(), cfgIn, logger) - if err != nil { - return "", 0, err - } - cfg.Network.BindAddress = fmt.Sprintf("localhost:%d", port) - cfg.Modules[0].ExePath = modPath - output, err := json.Marshal(cfg) - if err != nil { - return "", 0, err - } - file, err := os.CreateTemp(t.TempDir(), "viam-test-config-*") - if err != nil { - return "", 0, err - } - cfgFilename := file.Name() - _, err = file.Write(output) - if err != nil { - return "", 0, err - } - return cfgFilename, port, file.Close() -} - -func TestValidationFailure(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - - var port int - success := false - for portTryNum := 0; portTryNum < 10; portTryNum++ { - // bad_modular_validation.json contains a "mybase" modular component that will - // fail modular Validation due to a missing "motorL" attribute. - cfgFilename, localPort, err := modifyCfg(t, - utils.ResolveFile("examples/customresources/demos/complexmodule/moduletest/bad_modular_validation.json"), logger) - test.That(t, err, test.ShouldBeNil) - port = localPort - - server := robottestutils.ServerAsSeparateProcess(t, cfgFilename, logger) - - err = server.Start(context.Background()) - test.That(t, err, test.ShouldBeNil) - - if robottestutils.WaitForServing(logs, port) { - success = true - defer func() { - test.That(t, server.Stop(), test.ShouldBeNil) - }() - break - } - server.Stop() - } - test.That(t, success, test.ShouldBeTrue) - - rc, err := connect(port, logger) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, rc.Close(context.Background()), test.ShouldBeNil) - }() - - // Assert that motors were added but base was not. - _, err = rc.ResourceByName(motor.Named("motor1")) - test.That(t, err, test.ShouldBeNil) - _, err = rc.ResourceByName(motor.Named("motor2")) - test.That(t, err, test.ShouldBeNil) - _, err = rc.ResourceByName(base.Named("base1")) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, `resource "rdk:component:base/base1" not found`) -} diff --git a/examples/customresources/demos/complexmodule/run.sh b/examples/customresources/demos/complexmodule/run.sh deleted file mode 100755 index c93d96b6fe3..00000000000 --- a/examples/customresources/demos/complexmodule/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build ./ -exec ./complexmodule $@ diff --git a/examples/customresources/demos/multiplemodules/.gitignore b/examples/customresources/demos/multiplemodules/.gitignore deleted file mode 100644 index c8ab46cec6a..00000000000 --- a/examples/customresources/demos/multiplemodules/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -gizmomodule/gizmomodule -summationmodule/summationmodule diff --git a/examples/customresources/demos/multiplemodules/Makefile b/examples/customresources/demos/multiplemodules/Makefile deleted file mode 100644 index 0090d168036..00000000000 --- a/examples/customresources/demos/multiplemodules/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -.PHONY: module - -default: run-module - -module: - go build -o summationmodule/summationmodule ./summationmodule - go build -o gizmomodule/gizmomodule ./gizmomodule - -run-module: - go run ../../../../web/cmd/server/main.go -config module.json - -run-client: - cd client && make diff --git a/examples/customresources/demos/multiplemodules/client/Makefile b/examples/customresources/demos/multiplemodules/client/Makefile deleted file mode 100644 index a6975c81e7f..00000000000 --- a/examples/customresources/demos/multiplemodules/client/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -default: run-client - -run-client: - go run ./ diff --git a/examples/customresources/demos/multiplemodules/client/client.go b/examples/customresources/demos/multiplemodules/client/client.go deleted file mode 100644 index 8b357972cff..00000000000 --- a/examples/customresources/demos/multiplemodules/client/client.go +++ /dev/null @@ -1,82 +0,0 @@ -// Package main tests out all 2 custom models in the multiplemodules. -package main - -import ( - "context" - - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot/client" -) - -func main() { - logger := logging.NewLogger("client") - robot, err := client.New( - context.Background(), - "localhost:8080", - logger, - ) - if err != nil { - logger.Fatal(err) - } - defer func() { - if err := robot.Close(context.Background()); err != nil { - logger.Fatal(err) - } - }() - - logger.Info("---- Testing gizmo1 (gizmoapi) -----") - comp1, err := gizmoapi.FromRobot(robot, "gizmo1") - if err != nil { - logger.Fatal(err) - } - ret1, err := comp1.DoOne(context.Background(), "1.0") - if err != nil { - logger.Fatal(err) - } - logger.Info(ret1) - - ret2, err := comp1.DoOneClientStream(context.Background(), []string{"1.0", "2.0", "3.0"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret2) - - ret3, err := comp1.DoOneServerStream(context.Background(), "1.0") - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) - - ret3, err = comp1.DoOneBiDiStream(context.Background(), []string{"1.0", "2.0", "3.0"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) - - ret4, err := comp1.DoTwo(context.Background(), true) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret4) - - ret4, err = comp1.DoTwo(context.Background(), false) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret4) - - logger.Info("---- Testing adder (summationapi) -----") - add, err := summationapi.FromRobot(robot, "adder") - if err != nil { - logger.Fatal(err) - } - - nums := []float64{10, 0.5, 12} - retAdd, err := add.Sum(context.Background(), nums) - if err != nil { - logger.Fatal(err) - } - logger.Info(nums, " sum to ", retAdd) -} diff --git a/examples/customresources/demos/multiplemodules/gizmomodule/gizmomodule.go b/examples/customresources/demos/multiplemodules/gizmomodule/gizmomodule.go deleted file mode 100644 index 2320bd0e445..00000000000 --- a/examples/customresources/demos/multiplemodules/gizmomodule/gizmomodule.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package main is a module, which serves the mygizmosummer custom model type in the customresources examples. -package main - -import ( - "context" - - "go.viam.com/utils" - - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/models/mygizmosummer" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" -) - -func main() { - // NewLoggerFromArgs will create a logging.Logger at "DebugLevel" if - // "--log-level=debug" is the third argument in os.Args and at "InfoLevel" - // otherwise. - utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("gizmomodule")) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (err error) { - myMod, err := module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - - // Models and APIs add helpers to the registry during their init(). - // They can then be added to the module here. - err = myMod.AddModelFromRegistry(ctx, gizmoapi.API, mygizmosummer.Model) - if err != nil { - return err - } - - err = myMod.Start(ctx) - defer myMod.Close(ctx) - if err != nil { - return err - } - <-ctx.Done() - return nil -} diff --git a/examples/customresources/demos/multiplemodules/gizmomodule/run.sh b/examples/customresources/demos/multiplemodules/gizmomodule/run.sh deleted file mode 100755 index 85764afe748..00000000000 --- a/examples/customresources/demos/multiplemodules/gizmomodule/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build ./ -exec ./gizmomodule $@ diff --git a/examples/customresources/demos/multiplemodules/module.json b/examples/customresources/demos/multiplemodules/module.json deleted file mode 100644 index f7af0c133cd..00000000000 --- a/examples/customresources/demos/multiplemodules/module.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "modules": [ - { - "name": "GizmoModule", - "executable_path": "gizmomodule/run.sh", - "log_level": "debug" - }, - { - "name": "SummationModule", - "executable_path": "summationmodule/run.sh", - "log_level": "debug" - } - ], - "services": [ - { - "namespace": "acme", - "type": "summation", - "name": "adder", - "model": "acme:demo:mysum", - "attributes" : {} - } - ], - "components": [ - { - "model": "acme:demo:mygizmosummer", - "name": "gizmo1", - "namespace": "acme", - "type": "gizmo", - "attributes": { - "summer": "adder" - } - } - ], - "network": { - "bind_address": ":8080" - } -} diff --git a/examples/customresources/demos/multiplemodules/moduletest/module_test.go b/examples/customresources/demos/multiplemodules/moduletest/module_test.go deleted file mode 100644 index e859174f4bb..00000000000 --- a/examples/customresources/demos/multiplemodules/moduletest/module_test.go +++ /dev/null @@ -1,162 +0,0 @@ -//go:build !no_tflite - -// Package main tests out all the custom models in the multiplemodules. -package main_test - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "testing" - "time" - - "go.viam.com/test" - goutils "go.viam.com/utils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/client" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/robottestutils" - "go.viam.com/rdk/utils" -) - -func TestMultipleModules(t *testing.T) { - logger, observer := logging.NewObservedTestLogger(t) - - var port int - success := false - for portTryNum := 0; portTryNum < 10; portTryNum++ { - // Modify the example config to run directly, without compiling the module first. - cfgFilename, portLocal, err := modifyCfg(t, utils.ResolveFile("examples/customresources/demos/multiplemodules/module.json"), logger) - port = portLocal - test.That(t, err, test.ShouldBeNil) - - server := robottestutils.ServerAsSeparateProcess(t, cfgFilename, logger) - - err = server.Start(context.Background()) - test.That(t, err, test.ShouldBeNil) - - if robottestutils.WaitForServing(observer, port) { - success = true - defer func() { - test.That(t, server.Stop(), test.ShouldBeNil) - }() - break - } - server.Stop() - } - test.That(t, success, test.ShouldBeTrue) - - rc, err := connect(port, logger) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, rc.Close(context.Background()), test.ShouldBeNil) - }() - - // Gizmo is a custom component model and API. - t.Run("Test Gizmo", func(t *testing.T) { - res, err := rc.ResourceByName(gizmoapi.Named("gizmo1")) - test.That(t, err, test.ShouldBeNil) - - giz := res.(gizmoapi.Gizmo) - ret1, err := giz.DoOne(context.Background(), "1.0") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret1, test.ShouldBeTrue) - - // also tests that the ForeignServiceHandler does not drop the first message - ret2, err := giz.DoOneClientStream(context.Background(), []string{"1.0", "2.0", "3.0"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret2, test.ShouldBeFalse) - - ret2, err = giz.DoOneClientStream(context.Background(), []string{"0", "2.0", "3.0"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret2, test.ShouldBeTrue) - - ret3, err := giz.DoOneServerStream(context.Background(), "1.0") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret3, test.ShouldResemble, []bool{true, false, true, false}) - - ret3, err = giz.DoOneBiDiStream(context.Background(), []string{"1.0", "2.0", "3.0"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret3, test.ShouldResemble, []bool{true, true, true}) - - ret4, err := giz.DoTwo(context.Background(), true) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret4, test.ShouldEqual, "sum=4") - - ret4, err = giz.DoTwo(context.Background(), false) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret4, test.ShouldEqual, "sum=5") - }) - - // Summation is a custom service model and API. - t.Run("Test Summation", func(t *testing.T) { - res, err := rc.ResourceByName(summationapi.Named("adder")) - test.That(t, err, test.ShouldBeNil) - add := res.(summationapi.Summation) - nums := []float64{10, 0.5, 12} - retAdd, err := add.Sum(context.Background(), nums) - test.That(t, err, test.ShouldBeNil) - test.That(t, retAdd, test.ShouldEqual, 22.5) - }) -} - -func connect(port int, logger logging.Logger) (robot.Robot, error) { - connectCtx, cancelConn := context.WithTimeout(context.Background(), time.Second*30) - defer cancelConn() - for { - dialCtx, dialCancel := context.WithTimeout(context.Background(), time.Millisecond*500) - rc, err := client.New(dialCtx, fmt.Sprintf("localhost:%d", port), logger, - client.WithDialOptions(rpc.WithForceDirectGRPC()), - client.WithDisableSessions(), // TODO(PRODUCT-343): add session support to modules - ) - dialCancel() - if !errors.Is(err, context.DeadlineExceeded) { - return rc, err - } - select { - case <-connectCtx.Done(): - return nil, connectCtx.Err() - default: - } - } -} - -func modifyCfg(t *testing.T, cfgIn string, logger logging.Logger) (string, int, error) { - gizmoModPath := testutils.BuildTempModule(t, "examples/customresources/demos/multiplemodules/gizmomodule") - summationModPath := testutils.BuildTempModule(t, "examples/customresources/demos/multiplemodules/summationmodule") - - port, err := goutils.TryReserveRandomPort() - if err != nil { - return "", 0, err - } - - cfg, err := config.Read(context.Background(), cfgIn, logger) - if err != nil { - return "", 0, err - } - cfg.Network.BindAddress = fmt.Sprintf("localhost:%d", port) - cfg.Modules[0].ExePath = gizmoModPath - cfg.Modules[1].ExePath = summationModPath - output, err := json.Marshal(cfg) - if err != nil { - return "", 0, err - } - file, err := os.CreateTemp(t.TempDir(), "viam-test-config-*") - if err != nil { - return "", 0, err - } - cfgFilename := file.Name() - _, err = file.Write(output) - if err != nil { - return "", 0, err - } - return cfgFilename, port, file.Close() -} diff --git a/examples/customresources/demos/multiplemodules/summationmodule/run.sh b/examples/customresources/demos/multiplemodules/summationmodule/run.sh deleted file mode 100755 index 0759c86e5ed..00000000000 --- a/examples/customresources/demos/multiplemodules/summationmodule/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build ./ -exec ./summationmodule $@ diff --git a/examples/customresources/demos/multiplemodules/summationmodule/summationmodule.go b/examples/customresources/demos/multiplemodules/summationmodule/summationmodule.go deleted file mode 100644 index bee14a7b20e..00000000000 --- a/examples/customresources/demos/multiplemodules/summationmodule/summationmodule.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package main is a module, which serves the mysum custom model type in the customresources examples. -package main - -import ( - "context" - - "go.viam.com/utils" - - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/examples/customresources/models/mysum" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" -) - -func main() { - // NewLoggerFromArgs will create a logging.Logger at "DebugLevel" if - // "--log-level=debug" is the third argument in os.Args and at "InfoLevel" - // otherwise. - utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("summationmodule")) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (err error) { - myMod, err := module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - - // Models and APIs add helpers to the registry during their init(). - // They can then be added to the module here. - err = myMod.AddModelFromRegistry(ctx, summationapi.API, mysum.Model) - if err != nil { - return err - } - - err = myMod.Start(ctx) - defer myMod.Close(ctx) - if err != nil { - return err - } - <-ctx.Done() - return nil -} diff --git a/examples/customresources/demos/remoteserver/Makefile b/examples/customresources/demos/remoteserver/Makefile deleted file mode 100644 index 6904e24b1d7..00000000000 --- a/examples/customresources/demos/remoteserver/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -run-remote: - go run ./ -config remote.json - -run-parent: - go run ../../../../web/cmd/server/main.go -config parent.json - -run-client: - cd client && make diff --git a/examples/customresources/demos/remoteserver/client/Makefile b/examples/customresources/demos/remoteserver/client/Makefile deleted file mode 100644 index a6975c81e7f..00000000000 --- a/examples/customresources/demos/remoteserver/client/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -default: run-client - -run-client: - go run ./ diff --git a/examples/customresources/demos/remoteserver/client/client.go b/examples/customresources/demos/remoteserver/client/client.go deleted file mode 100644 index 31817a65fde..00000000000 --- a/examples/customresources/demos/remoteserver/client/client.go +++ /dev/null @@ -1,65 +0,0 @@ -// Package main tests out a Gizmo client. -package main - -import ( - "context" - - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot/client" -) - -func main() { - logger := logging.NewDebugLogger("client") - robot, err := client.New( - context.Background(), - "localhost:8080", - logger, - ) - if err != nil { - logger.Fatal(err) - } - //nolint:errcheck - defer robot.Close(context.Background()) - - res, err := robot.ResourceByName(gizmoapi.Named("gizmo1")) - if err != nil { - logger.Fatal(err) - } - comp1 := res.(gizmoapi.Gizmo) - ret1, err := comp1.DoOne(context.Background(), "hello") - if err != nil { - logger.Fatal(err) - } - logger.Info(ret1) - - ret2, err := comp1.DoOneClientStream(context.Background(), []string{"hello", "arg1", "foo"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret2) - - ret2, err = comp1.DoOneClientStream(context.Background(), []string{"arg1", "arg1", "arg1"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret2) - - ret3, err := comp1.DoOneServerStream(context.Background(), "hello") - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) - - ret3, err = comp1.DoOneBiDiStream(context.Background(), []string{"hello", "arg1", "foo"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) - - ret3, err = comp1.DoOneBiDiStream(context.Background(), []string{"arg1", "arg1", "arg1"}) - if err != nil { - logger.Fatal(err) - } - logger.Info(ret3) -} diff --git a/examples/customresources/demos/remoteserver/parent.json b/examples/customresources/demos/remoteserver/parent.json deleted file mode 100644 index f130233d9f1..00000000000 --- a/examples/customresources/demos/remoteserver/parent.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "network": { - "bind_address": "localhost:8080" - }, - "remotes": [ - { - "name": "robot1", - "address": "localhost:8081" - } - ] -} diff --git a/examples/customresources/demos/remoteserver/remote.json b/examples/customresources/demos/remoteserver/remote.json deleted file mode 100644 index 0c56a43727e..00000000000 --- a/examples/customresources/demos/remoteserver/remote.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "network": { - "bind_address": "localhost:8081" - }, - "components": [ - { - "model": "acme:demo:mygizmo", - "name": "gizmo1", - "namespace": "acme", - "type": "gizmo", - "attributes": { - "arg1": "arg1" - } - } - ] -} diff --git a/examples/customresources/demos/remoteserver/server.go b/examples/customresources/demos/remoteserver/server.go deleted file mode 100644 index 9e1f4e526ad..00000000000 --- a/examples/customresources/demos/remoteserver/server.go +++ /dev/null @@ -1,51 +0,0 @@ -// Package main is a standalone server (for use as a remote) serving a demo Gizmo component. -package main - -import ( - "context" - "errors" - - goutils "go.viam.com/utils" - - "go.viam.com/rdk/config" - _ "go.viam.com/rdk/examples/customresources/models/mygizmo" - "go.viam.com/rdk/logging" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/robot/web" -) - -var logger = logging.NewDebugLogger("gizmoserver") - -// Arguments for the command. -type Arguments struct { - ConfigFile string `flag:"config,usage=robot config file"` -} - -func main() { - goutils.ContextualMain(mainWithArgs, logger) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (err error) { - var argsParsed Arguments - if err := goutils.ParseFlags(args, &argsParsed); err != nil { - return err - } - - if argsParsed.ConfigFile == "" { - return errors.New("please specify a config file through the -config parameter") - } - - cfg, err := config.Read(ctx, argsParsed.ConfigFile, logger) - if err != nil { - return err - } - - myRobot, err := robotimpl.RobotFromConfig(ctx, cfg, logger) - if err != nil { - return err - } - //nolint:errcheck - defer myRobot.Close(ctx) - - return web.RunWebWithConfig(ctx, myRobot, cfg, logger) -} diff --git a/examples/customresources/demos/remoteserver/server_test.go b/examples/customresources/demos/remoteserver/server_test.go deleted file mode 100644 index 835edc4a9a0..00000000000 --- a/examples/customresources/demos/remoteserver/server_test.go +++ /dev/null @@ -1,152 +0,0 @@ -//go:build !no_tflite - -package main_test - -import ( - "context" - "fmt" - "os" - "strings" - "testing" - "time" - - "go.viam.com/test" - goutils "go.viam.com/utils" - "go.viam.com/utils/pexec" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - _ "go.viam.com/rdk/examples/customresources/models/mygizmo" - "go.viam.com/rdk/logging" - robotimpl "go.viam.com/rdk/robot/impl" - weboptions "go.viam.com/rdk/robot/web/options" - "go.viam.com/rdk/testutils/robottestutils" - "go.viam.com/rdk/utils" -) - -func TestGizmo(t *testing.T) { - // This test sets up three robots as a chain of remotes: MainPart -> A -> B. For setup, the test - // brings up the robots in reverse order. Remote "B" constructs a component using a custom - // "Gizmo" API + model. The test then asserts a connection to the `MainPart` can get a handle on - // a remote resource provided by "B". Additionally, remote "A" is not aware of the gizmo API nor - // the gizmo model. - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Create remote B. Loop to ensure we find an available port. - var remoteAddrB string - for portTryNum := 0; portTryNum < 10; portTryNum++ { - port, err := goutils.TryReserveRandomPort() - test.That(t, err, test.ShouldBeNil) - remoteAddrB = fmt.Sprintf("localhost:%d", port) - test.That(t, err, test.ShouldBeNil) - - cfgServer, err := config.Read(ctx, utils.ResolveFile("./examples/customresources/demos/remoteserver/remote.json"), logger) - test.That(t, err, test.ShouldBeNil) - remoteB, err := robotimpl.New(ctx, cfgServer, logger.Sublogger("remoteB")) - test.That(t, err, test.ShouldBeNil) - options := weboptions.New() - options.Network.BindAddress = remoteAddrB - - err = remoteB.StartWeb(ctx, options) - if err != nil && strings.Contains(err.Error(), "address already in use") { - logger.Infow("Port in use. Restarting on new port.", "port", port, "err", err) - test.That(t, remoteB.Close(context.Background()), test.ShouldBeNil) - continue - } - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, remoteB.Close(context.Background()), test.ShouldBeNil) - }() - break - } - - // Create remote A. Loop to ensure we find an available port. - var remoteAddrA string - success := false - for portTryNum := 0; portTryNum < 10; portTryNum++ { - // The process executing this test has loaded the "gizmo" API + model into a global registry - // object. We want this intermediate remote to be unaware of the custom gizmo resource. We - // start up a separate viam-server process to achieve this. - port, err := goutils.TryReserveRandomPort() - test.That(t, err, test.ShouldBeNil) - remoteAddrA = fmt.Sprintf("localhost:%d", port) - - tmpConf, err := os.CreateTemp(t.TempDir(), "*.json") - test.That(t, err, test.ShouldBeNil) - _, err = tmpConf.WriteString(fmt.Sprintf( - `{"network":{"bind_address":"%s"},"remotes":[{"address":"%s","name":"robot1"}]}`, - remoteAddrA, remoteAddrB)) - test.That(t, err, test.ShouldBeNil) - err = tmpConf.Sync() - test.That(t, err, test.ShouldBeNil) - - processLogger, logObserver := logging.NewObservedTestLogger(t) - pmgr := pexec.NewProcessManager(processLogger.Sublogger("remoteA").AsZap()) - pCfg := pexec.ProcessConfig{ - ID: "Intermediate", - Name: "go", - Args: []string{"run", utils.ResolveFile("./web/cmd/server/main.go"), "-config", tmpConf.Name()}, - CWD: "", - OneShot: false, - Log: true, - } - _, err = pmgr.AddProcessFromConfig(context.Background(), pCfg) - test.That(t, err, test.ShouldBeNil) - err = pmgr.Start(context.Background()) - test.That(t, err, test.ShouldBeNil) - if success = robottestutils.WaitForServing(logObserver, port); success { - defer func() { - test.That(t, pmgr.Stop(), test.ShouldBeNil) - }() - break - } - logger.Infow("Port in use. Restarting on new port.", "port", port, "err", err) - pmgr.Stop() - continue - } - test.That(t, success, test.ShouldBeTrue) - - // Create the MainPart. Note we will access this directly and therefore it does not need to - // start a gRPC server. - mainPartConfig := &config.Config{ - Remotes: []config.Remote{ - { - Name: "remoteA", - Address: remoteAddrA, - }, - }, - } - mainPart, err := robotimpl.New(ctx, mainPartConfig, logger.Sublogger("mainPart.client")) - defer func() { - test.That(t, mainPart.Close(context.Background()), test.ShouldBeNil) - }() - test.That(t, err, test.ShouldBeNil) - - // remotes can take a few seconds to show up, so we wait for the resource - var res interface{} - testutils.WaitForAssertionWithSleep(t, time.Second, 120, func(tb testing.TB) { - res, err = mainPart.ResourceByName(gizmoapi.Named("gizmo1")) - test.That(tb, err, test.ShouldBeNil) - }) - - gizmo1, ok := res.(gizmoapi.Gizmo) - test.That(t, ok, test.ShouldBeTrue) - _, err = gizmo1.DoOne(context.Background(), "hello") - test.That(t, err, test.ShouldBeNil) - _, err = gizmo1.DoOneClientStream(context.Background(), []string{"hello", "arg1", "foo"}) - test.That(t, err, test.ShouldBeNil) - _, err = gizmo1.DoOneServerStream(context.Background(), "hello") - test.That(t, err, test.ShouldBeNil) - _, err = gizmo1.DoOneBiDiStream(context.Background(), []string{"hello", "arg1", "foo"}) - test.That(t, err, test.ShouldBeNil) - _, err = gizmo1.DoOneBiDiStream(context.Background(), []string{"arg1", "arg1", "arg1"}) - test.That(t, err, test.ShouldBeNil) - - _, err = mainPart.ResourceByName(gizmoapi.Named("remoteA:robot1:gizmo1")) - test.That(t, err, test.ShouldBeNil) - - _, err = mainPart.ResourceByName(gizmoapi.Named("remoteA:gizmo1")) - test.That(t, err, test.ShouldNotBeNil) -} diff --git a/examples/customresources/demos/rtppassthrough/myrtppassthrough.go b/examples/customresources/demos/rtppassthrough/myrtppassthrough.go deleted file mode 100644 index 478baefa953..00000000000 --- a/examples/customresources/demos/rtppassthrough/myrtppassthrough.go +++ /dev/null @@ -1,53 +0,0 @@ -// Package main implements a fake passthrough camera module -package main - -import ( - "context" - - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" - "go.viam.com/rdk/resource" -) - -var model = resource.NewModel("acme", "camera", "fake") - -func main() { - goutils.ContextualMain(mainWithArgs, logging.NewDebugLogger("rtp-passthrough-camera")) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (err error) { - resource.RegisterComponent( - camera.API, - model, - resource.Registration[camera.Camera, *fake.Config]{Constructor: newFakeCamera}) - - module, err := module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - if err := module.AddModelFromRegistry(ctx, camera.API, model); err != nil { - return err - } - - err = module.Start(ctx) - defer module.Close(ctx) - if err != nil { - return err - } - - <-ctx.Done() - return nil -} - -func newFakeCamera( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (camera.Camera, error) { - return fake.NewCamera(ctx, deps, conf, logger) -} diff --git a/examples/customresources/demos/simplemodule/.gitignore b/examples/customresources/demos/simplemodule/.gitignore deleted file mode 100644 index 716cb7ae2c2..00000000000 --- a/examples/customresources/demos/simplemodule/.gitignore +++ /dev/null @@ -1 +0,0 @@ -simplemodule diff --git a/examples/customresources/demos/simplemodule/Makefile b/examples/customresources/demos/simplemodule/Makefile deleted file mode 100644 index 4f946b6989d..00000000000 --- a/examples/customresources/demos/simplemodule/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -.PHONY: module - -default: run-module - -module: - go build ./ - -run-module: module - go run ../../../../web/cmd/server/main.go -config module.json - -run-client: - cd client && make diff --git a/examples/customresources/demos/simplemodule/client/Makefile b/examples/customresources/demos/simplemodule/client/Makefile deleted file mode 100644 index a6975c81e7f..00000000000 --- a/examples/customresources/demos/simplemodule/client/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -default: run-client - -run-client: - go run ./ diff --git a/examples/customresources/demos/simplemodule/client/client.go b/examples/customresources/demos/simplemodule/client/client.go deleted file mode 100644 index 095f95de5b3..00000000000 --- a/examples/customresources/demos/simplemodule/client/client.go +++ /dev/null @@ -1,62 +0,0 @@ -// Package main tests out all four custom models in the complexmodule. -package main - -import ( - "context" - "math/rand" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/client" -) - -func main() { - logger := logging.NewLogger("client") - - // Connect to the default localhost port for viam-server. - robot, err := client.New( - context.Background(), - "localhost:8080", - logger, - ) - if err != nil { - logger.Fatal(err) - } - //nolint:errcheck - defer robot.Close(context.Background()) - - // Get the two counter components. - logger.Info("---- Getting counter1 (generic api) -----") - counter1, err := robot.ResourceByName(generic.Named("counter1")) - if err != nil { - logger.Fatal(err) - } - - logger.Info("---- Getting counter2 (generic api) -----") - counter2, err := robot.ResourceByName(generic.Named("counter2")) - if err != nil { - logger.Fatal(err) - } - - // For each counter, we'll look at the total, then add 20 random numbers to it. - // Only on restart of the server will they get reset. - for name, c := range []resource.Resource{counter1, counter2} { - // Get the starting value of the given counter. - ret, err := counter1.DoCommand(context.Background(), map[string]interface{}{"command": "get"}) - if err != nil { - logger.Fatal(err) - } - logger.Infof("---- Adding random values to counter%d -----", name+1) - logger.Infof("start\t=%.0f", ret["total"]) // numeric values are floats by default - for n := 0; n < 20; n++ { - //nolint:gosec - val := rand.Intn(100) - ret, err := c.DoCommand(context.Background(), map[string]interface{}{"command": "add", "value": val}) - if err != nil { - logger.Fatal(err) - } - logger.Infof("+%d\t=%.0f", val, ret["total"]) - } - } -} diff --git a/examples/customresources/demos/simplemodule/module.go b/examples/customresources/demos/simplemodule/module.go deleted file mode 100644 index a0f243d322a..00000000000 --- a/examples/customresources/demos/simplemodule/module.go +++ /dev/null @@ -1,110 +0,0 @@ -// Package main is a module with a built-in "counter" component model, that will simply track numbers. -// It uses the rdk:component:generic interface for simplicity. -package main - -import ( - "context" - "fmt" - "sync/atomic" - - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" - "go.viam.com/rdk/resource" -) - -var myModel = resource.NewModel("acme", "demo", "mycounter") - -func main() { - utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("simple-module")) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error { - // Instantiate the module itself - myMod, err := module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - - // We first put our component's constructor in the registry, then tell the module to load it - // Note that all resources must be added before the module is started. - resource.RegisterComponent(generic.API, myModel, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: newCounter, - }) - err = myMod.AddModelFromRegistry(ctx, generic.API, myModel) - if err != nil { - return err - } - - // The module is started. - err = myMod.Start(ctx) - // Close is deferred and will run automatically when this function returns. - defer myMod.Close(ctx) - if err != nil { - return err - } - - // This will block (leaving the module running) until the context is cancelled. - // The utils.ContextualMain catches OS signals and will cancel our context for us when one is sent for shutdown/termination. - <-ctx.Done() - // The deferred myMod.Close() will now run. - return nil -} - -// newCounter is used to create a new instance of our specific model. It is called for each component in the robot's config with this model. -func newCounter(ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (resource.Resource, error) { - return &counter{ - Named: conf.ResourceName().AsNamed(), - }, nil -} - -// counter is the representation of this model. It holds only a "total" count. -type counter struct { - resource.Named - resource.TriviallyCloseable - total int64 -} - -func (c *counter) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - atomic.StoreInt64(&c.total, 0) - return nil -} - -// DoCommand is the only method of this component. It looks up the "real" command from the map it's passed. -// Because of this, any arbitrary commands can be received, and any data returned. -func (c *counter) DoCommand(ctx context.Context, req map[string]interface{}) (map[string]interface{}, error) { - // We look for a map key called "command" - cmd, ok := req["command"] - if !ok { - return nil, errors.New("missing 'command' string") - } - - // If it's "get" we return the current total. - if cmd == "get" { - return map[string]interface{}{"total": atomic.LoadInt64(&c.total)}, nil - } - - // If it's "add" we atomically add a second key "value" to the total. - if cmd == "add" { - _, ok := req["value"] - if !ok { - return nil, errors.New("value must exist") - } - val, ok := req["value"].(float64) - if !ok { - return nil, errors.New("value must be a number") - } - atomic.AddInt64(&c.total, int64(val)) - // We return the new total after the addition. - return map[string]interface{}{"total": atomic.LoadInt64(&c.total)}, nil - } - // The command must've been something else. - return nil, fmt.Errorf("unknown command string %s", cmd) -} diff --git a/examples/customresources/demos/simplemodule/module.json b/examples/customresources/demos/simplemodule/module.json deleted file mode 100644 index 49871782980..00000000000 --- a/examples/customresources/demos/simplemodule/module.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "modules": [ - { - "name": "SimpleModule", - "executable_path": "./simplemodule" - } - ], - "components": [ - { - "namespace": "rdk", - "type": "generic", - "name": "counter1", - "model": "acme:demo:mycounter" - }, - { - "namespace": "rdk", - "type": "generic", - "name": "counter2", - "model": "acme:demo:mycounter" - } - ], - "network": { - "bind_address": ":8080" - } -} diff --git a/examples/customresources/demos/simplemodule/run.sh b/examples/customresources/demos/simplemodule/run.sh deleted file mode 100755 index c7c364cf75f..00000000000 --- a/examples/customresources/demos/simplemodule/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build ./ -exec ./simplemodule $@ diff --git a/examples/customresources/models/mybase/mybase.go b/examples/customresources/models/mybase/mybase.go deleted file mode 100644 index 3b3668a6f7a..00000000000 --- a/examples/customresources/models/mybase/mybase.go +++ /dev/null @@ -1,182 +0,0 @@ -// Package mybase implements a base that only supports SetPower (basic forward/back/turn controls.) -package mybase - -import ( - "context" - "fmt" - "math" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.uber.org/multierr" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -var ( - // Model is the full model definition. - Model = resource.NewModel("acme", "demo", "mybase") - errUnimplemented = errors.New("unimplemented") -) - -const ( - myBaseWidthMm = 500.0 // our dummy base has a wheel tread of 500 millimeters - myBaseTurningRadiusM = 0.3 // our dummy base turns around a circle of radius .3 meters -) - -func init() { - resource.RegisterComponent(base.API, Model, resource.Registration[base.Base, *Config]{ - Constructor: newBase, - }) -} - -func newBase(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (base.Base, error) { - b := &myBase{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - if err := b.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return b, nil -} - -// Reconfigure reconfigures with new settings. -func (b *myBase) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - b.left = nil - b.right = nil - - // This takes the generic resource.Config passed down from the parent and converts it to the - // model-specific (aka "native") Config structure defined above making it easier to directly access attributes. - baseConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - b.left, err = motor.FromDependencies(deps, baseConfig.LeftMotor) - if err != nil { - return errors.Wrapf(err, "unable to get motor %v for mybase", baseConfig.LeftMotor) - } - - b.right, err = motor.FromDependencies(deps, baseConfig.RightMotor) - if err != nil { - return errors.Wrapf(err, "unable to get motor %v for mybase", baseConfig.RightMotor) - } - - if conf.Frame != nil && conf.Frame.Geometry != nil { - geometry, err := conf.Frame.Geometry.ParseConfig() - if err != nil { - return err - } - b.geometries = []spatialmath.Geometry{geometry} - } - - // Good practice to stop motors, but also this effectively tests https://viam.atlassian.net/browse/RSDK-2496 - return multierr.Combine(b.left.Stop(context.Background(), nil), b.right.Stop(context.Background(), nil)) -} - -// DoCommand simply echos whatever was sent. -func (b *myBase) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return cmd, nil -} - -// Config contains two component (motor) names. -type Config struct { - LeftMotor string `json:"motorL"` - RightMotor string `json:"motorR"` -} - -// Validate validates the config and returns implicit dependencies, -// this Validate checks if the left and right motors exist for the module's base model. -func (cfg *Config) Validate(path string) ([]string, error) { - // check if the attribute fields for the right and left motors are non-empty - // this makes them reuqired for the model to successfully build - if cfg.LeftMotor == "" { - return nil, fmt.Errorf(`expected "motorL" attribute for mybase %q`, path) - } - if cfg.RightMotor == "" { - return nil, fmt.Errorf(`expected "motorR" attribute for mybase %q`, path) - } - - // Return the left and right motor names so that `newBase` can access them as dependencies. - return []string{cfg.LeftMotor, cfg.RightMotor}, nil -} - -type myBase struct { - resource.Named - left motor.Motor - right motor.Motor - logger logging.Logger - geometries []spatialmath.Geometry -} - -// MoveStraight does nothing. -func (b *myBase) MoveStraight(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - return errUnimplemented -} - -// Spin does nothing. -func (b *myBase) Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - return errUnimplemented -} - -// SetVelocity does nothing. -func (b *myBase) SetVelocity(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return errUnimplemented -} - -// SetPower computes relative power between the wheels and sets power for both motors. -func (b *myBase) SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - b.logger.CDebugf(ctx, "SetPower Linear: %.2f Angular: %.2f", linear.Y, angular.Z) - if math.Abs(linear.Y) < 0.01 && math.Abs(angular.Z) < 0.01 { - return b.Stop(ctx, extra) - } - sum := math.Abs(linear.Y) + math.Abs(angular.Z) - err1 := b.left.SetPower(ctx, (linear.Y-angular.Z)/sum, extra) - err2 := b.right.SetPower(ctx, (linear.Y+angular.Z)/sum, extra) - return multierr.Combine(err1, err2) -} - -// Stop halts motion. -func (b *myBase) Stop(ctx context.Context, extra map[string]interface{}) error { - b.logger.CDebug(ctx, "Stop") - err1 := b.left.Stop(ctx, extra) - err2 := b.right.Stop(ctx, extra) - return multierr.Combine(err1, err2) -} - -// IsMoving returns true if either motor is active. -func (b *myBase) IsMoving(ctx context.Context) (bool, error) { - for _, m := range []motor.Motor{b.left, b.right} { - isMoving, _, err := m.IsPowered(ctx, nil) - if err != nil { - return false, err - } - if isMoving { - return true, err - } - } - return false, nil -} - -// Properties returns details about the physics of the base. -func (b *myBase) Properties(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{ - TurningRadiusMeters: myBaseTurningRadiusM, - WidthMeters: myBaseWidthMm * 0.001, // converting millimeters to meters - }, nil -} - -// Geometries returns physical dimensions. -func (b *myBase) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - return b.geometries, nil -} - -// Close stops motion during shutdown. -func (b *myBase) Close(ctx context.Context) error { - return b.Stop(ctx, nil) -} diff --git a/examples/customresources/models/mygizmo/mygizmo.go b/examples/customresources/models/mygizmo/mygizmo.go deleted file mode 100644 index c4c69b59a53..00000000000 --- a/examples/customresources/models/mygizmo/mygizmo.go +++ /dev/null @@ -1,124 +0,0 @@ -// Package mygizmo implements an acme:component:gizmo, a demonstration component that simply shows the various methods available in grpc. -package mygizmo - -import ( - "context" - "fmt" - "sync" - - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// Model is the full model definition. -var Model = resource.NewModel("acme", "demo", "mygizmo") - -// Config is the gizmo model's config. -type Config struct { - Arg1 string `json:"arg1"` -} - -// Validate ensures that `Arg1` is a non-empty string. -// Validation error will stop the associated resource from building. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.Arg1 == "" { - return nil, fmt.Errorf(`expected "arg1" attribute for myGizmo %q`, path) - } - - // there are no dependencies for this model, so we return an empty list of strings - return []string{}, nil -} - -func init() { - resource.RegisterComponent(gizmoapi.API, Model, resource.Registration[gizmoapi.Gizmo, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (gizmoapi.Gizmo, error) { - return NewMyGizmo(deps, conf, logger) - }, - }) -} - -type myActualGizmo struct { - resource.Named - resource.TriviallyCloseable - - myArgMu sync.Mutex - myArg string -} - -// NewMyGizmo returns a new mygizmo. -func NewMyGizmo( - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (gizmoapi.Gizmo, error) { - g := &myActualGizmo{ - Named: conf.ResourceName().AsNamed(), - } - if err := g.Reconfigure(context.Background(), deps, conf); err != nil { - return nil, err - } - return g, nil -} - -func (g *myActualGizmo) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - // This takes the generic resource.Config passed down from the parent and converts it to the - // model-specific (aka "native") Config structure defined above making it easier to directly access attributes. - gizmoConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - g.myArgMu.Lock() - g.myArg = gizmoConfig.Arg1 - g.myArgMu.Unlock() - return nil -} - -func (g *myActualGizmo) DoOne(ctx context.Context, arg1 string) (bool, error) { - g.myArgMu.Lock() - defer g.myArgMu.Unlock() - return arg1 == g.myArg, nil -} - -func (g *myActualGizmo) DoOneClientStream(ctx context.Context, arg1 []string) (bool, error) { - g.myArgMu.Lock() - defer g.myArgMu.Unlock() - if len(arg1) == 0 { - return false, nil - } - ret := true - for _, arg := range arg1 { - ret = ret && arg == g.myArg - } - return ret, nil -} - -func (g *myActualGizmo) DoOneServerStream(ctx context.Context, arg1 string) ([]bool, error) { - g.myArgMu.Lock() - defer g.myArgMu.Unlock() - return []bool{arg1 == g.myArg, false, true, false}, nil -} - -func (g *myActualGizmo) DoOneBiDiStream(ctx context.Context, arg1 []string) ([]bool, error) { - g.myArgMu.Lock() - defer g.myArgMu.Unlock() - var rets []bool - for _, arg := range arg1 { - rets = append(rets, arg == g.myArg) - } - return rets, nil -} - -func (g *myActualGizmo) DoTwo(ctx context.Context, arg1 bool) (string, error) { - return fmt.Sprintf("arg1=%t", arg1), nil -} - -func (g *myActualGizmo) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return cmd, nil -} diff --git a/examples/customresources/models/mygizmosummer/mygizmosummer.go b/examples/customresources/models/mygizmosummer/mygizmosummer.go deleted file mode 100644 index 6927ccee291..00000000000 --- a/examples/customresources/models/mygizmosummer/mygizmosummer.go +++ /dev/null @@ -1,178 +0,0 @@ -// Package mygizmosummer implements an acme:component:gizmo and depends on another custom API. -package mygizmosummer - -import ( - "context" - "fmt" - "strconv" - "sync" - - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// Model is the full model definition. -var Model = resource.NewModel("acme", "demo", "mygizmosummer") - -// Config is the gizmo model's config. -type Config struct { - Summer string `json:"Summer"` -} - -// Validate ensures that `Summer` is a non-empty string. -// Validation error will stop the associated resource from building. -func (cfg *Config) Validate(path string) ([]string, error) { - if cfg.Summer == "" { - return nil, fmt.Errorf(`expected "summer" attribute for myGizmo %q`, path) - } - - // there are no dependencies for this model, so we return an empty list of strings - return []string{cfg.Summer}, nil -} - -func init() { - resource.RegisterComponent(gizmoapi.API, Model, resource.Registration[gizmoapi.Gizmo, *Config]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (gizmoapi.Gizmo, error) { - return NewMyGizmoSummer(deps, conf, logger) - }, - }) -} - -type myActualGizmo struct { - resource.Named - resource.TriviallyCloseable - - mySummerMu sync.Mutex - mySummer summationapi.Summation - logger logging.Logger -} - -// NewMyGizmoSummer returns a new mygizmosummer. -func NewMyGizmoSummer( - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (gizmoapi.Gizmo, error) { - g := &myActualGizmo{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - if err := g.Reconfigure(context.Background(), deps, conf); err != nil { - return nil, err - } - return g, nil -} - -func (g *myActualGizmo) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - // This takes the generic resource.Config passed down from the parent and converts it to the - // model-specific (aka "native") Config structure defined above making it easier to directly access attributes. - gizmoConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - summer, err := resource.FromDependencies[summationapi.Summation](deps, summationapi.Named(gizmoConfig.Summer)) - if err != nil { - return err - } - - g.mySummerMu.Lock() - g.mySummer = summer - g.mySummerMu.Unlock() - return nil -} - -func (g *myActualGizmo) DoOne(ctx context.Context, arg1 string) (bool, error) { - g.mySummerMu.Lock() - defer g.mySummerMu.Unlock() - - n, err := strconv.ParseFloat(arg1, 64) - if err != nil { - return false, err - } - sum, err := g.mySummer.Sum(ctx, []float64{n}) - if err != nil { - return false, err - } - return sum == n, nil -} - -func (g *myActualGizmo) DoOneClientStream(ctx context.Context, arg1 []string) (bool, error) { - g.mySummerMu.Lock() - defer g.mySummerMu.Unlock() - if len(arg1) == 0 { - return false, nil - } - ns := []float64{} - for _, arg := range arg1 { - n, err := strconv.ParseFloat(arg, 64) - if err != nil { - return false, err - } - ns = append(ns, n) - } - sum, err := g.mySummer.Sum(ctx, ns) - if err != nil { - return false, err - } - return sum == 5, nil -} - -func (g *myActualGizmo) DoOneServerStream(ctx context.Context, arg1 string) ([]bool, error) { - g.mySummerMu.Lock() - defer g.mySummerMu.Unlock() - n, err := strconv.ParseFloat(arg1, 64) - if err != nil { - return nil, err - } - sum, err := g.mySummer.Sum(ctx, []float64{n}) - if err != nil { - return nil, err - } - return []bool{sum == n, false, true, false}, nil -} - -func (g *myActualGizmo) DoOneBiDiStream(ctx context.Context, arg1 []string) ([]bool, error) { - g.mySummerMu.Lock() - defer g.mySummerMu.Unlock() - var rets []bool - g.logger.Info(arg1) - for _, arg := range arg1 { - g.logger.Info(arg) - n, err := strconv.ParseFloat(arg, 64) - if err != nil { - return nil, err - } - sum, err := g.mySummer.Sum(ctx, []float64{n}) - if err != nil { - return nil, err - } - rets = append(rets, sum == n) - } - g.logger.Info(rets) - return rets, nil -} - -func (g *myActualGizmo) DoTwo(ctx context.Context, arg1 bool) (string, error) { - g.mySummerMu.Lock() - defer g.mySummerMu.Unlock() - n := 1.0 - if !arg1 { - n = 2.0 - } - sum, err := g.mySummer.Sum(ctx, []float64{n, 3.0}) - if err != nil { - return "", err - } - return fmt.Sprintf("sum=%v", sum), nil -} - -func (g *myActualGizmo) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return cmd, nil -} diff --git a/examples/customresources/models/mynavigation/mynavigation.go b/examples/customresources/models/mynavigation/mynavigation.go deleted file mode 100644 index 4446ee565da..00000000000 --- a/examples/customresources/models/mynavigation/mynavigation.go +++ /dev/null @@ -1,138 +0,0 @@ -// Package mynavigation contains an example navigation service that only stores waypoints, and returns a fixed, configurable location. -package mynavigation - -import ( - "context" - "errors" - "sync" - - geo "github.com/kellydunn/golang-geo" - "go.mongodb.org/mongo-driver/bson/primitive" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/navigation" - "go.viam.com/rdk/spatialmath" -) - -var errUnimplemented = errors.New("unimplemented") - -// Model is the full model definition. -var Model = resource.NewModel("acme", "demo", "mynavigation") - -func init() { - resource.RegisterService(navigation.API, Model, resource.Registration[navigation.Service, *Config]{ - Constructor: newNav, - }) -} - -// Config is the navigation model's config. -type Config struct { - Lat *float64 `json:"lat,omitempty"` // omitempty for a pointer to a float64 defaults to nil in golang - Long *float64 `json:"long,omitempty"` - - // Embed TriviallyValidateConfig to make config validation a no-op. We will not check if any attributes exist - // or are set to anything in particular, and there will be no implicit dependencies. - // Config structs used in resource registration must implement Validate. - resource.TriviallyValidateConfig -} - -func newNav( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (navigation.Service, error) { - // This takes the generic resource.Config passed down from the parent and converts it to the - // model-specific (aka "native") Config structure defined above making it easier to directly access attributes. - navConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return nil, err - } - - // here we set a default latitude, if the config latitude field is not omitted (omitempty) - // we use the value it is set to and return it in the nav service struct. - lat := -48.876667 - if navConfig.Lat != nil { - lat = *navConfig.Lat - } - - // here we set a default longitude, if the config latitude field is not omitted (omitempty) - // we use the value it is set to and return it in the nav service struct. - lng := -48.876667 - if navConfig.Lat != nil { - lng = *navConfig.Long - } - - navSvc := &navSvc{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - loc: geo.NewPoint(lat, lng), - } - return navSvc, nil -} - -type navSvc struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - - loc *geo.Point - logger logging.Logger - - waypointsMu sync.RWMutex - waypoints []navigation.Waypoint -} - -func (svc *navSvc) Mode(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - return 0, nil -} - -func (svc *navSvc) SetMode(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - return nil -} - -func (svc *navSvc) Location(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - svc.waypointsMu.RLock() - defer svc.waypointsMu.RUnlock() - geoPose := spatialmath.NewGeoPose(svc.loc, 0) - return geoPose, nil -} - -func (svc *navSvc) Waypoints(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) { - svc.waypointsMu.RLock() - defer svc.waypointsMu.RUnlock() - wpsCopy := make([]navigation.Waypoint, len(svc.waypoints)) - copy(wpsCopy, svc.waypoints) - return wpsCopy, nil -} - -func (svc *navSvc) AddWaypoint(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - svc.waypointsMu.Lock() - defer svc.waypointsMu.Unlock() - svc.waypoints = append(svc.waypoints, navigation.Waypoint{Lat: point.Lat(), Long: point.Lng()}) - return nil -} - -func (svc *navSvc) RemoveWaypoint(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - svc.waypointsMu.Lock() - defer svc.waypointsMu.Unlock() - newWps := make([]navigation.Waypoint, 0, len(svc.waypoints)-1) - for _, wp := range svc.waypoints { - if wp.ID == id { - continue - } - newWps = append(newWps, wp) - } - svc.waypoints = newWps - return nil -} - -func (svc *navSvc) Obstacles(ctx context.Context, extra map[string]interface{}) ([]*spatialmath.GeoObstacle, error) { - return []*spatialmath.GeoObstacle{}, errUnimplemented -} - -func (svc *navSvc) Paths(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) { - return []*navigation.Path{}, errUnimplemented -} - -func (svc *navSvc) Properties(ctx context.Context) (navigation.Properties, error) { - return navigation.Properties{}, errUnimplemented -} diff --git a/examples/customresources/models/mysum/mysum.go b/examples/customresources/models/mysum/mysum.go deleted file mode 100644 index f026ecc6426..00000000000 --- a/examples/customresources/models/mysum/mysum.go +++ /dev/null @@ -1,82 +0,0 @@ -// Package mysum implements an acme:service:summation, a demo service which sums (or subtracts) a given list of numbers. -package mysum - -import ( - "context" - "errors" - "sync" - - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" -) - -// Model is the full model definition. -var Model = resource.NewModel("acme", "demo", "mysum") - -// Config is the sum model's config. -type Config struct { - Subtract bool `json:"subtract,omitempty"` // the omitempty defaults the bool to golang's default of false - - // Embed TriviallyValidateConfig to make config validation a no-op. We will not check if any attributes exist - // or are set to anything in particular, and there will be no implicit dependencies. - // Config structs used in resource registration must implement Validate. - resource.TriviallyValidateConfig -} - -func init() { - resource.RegisterService(summationapi.API, Model, resource.Registration[summationapi.Summation, *Config]{ - Constructor: newMySum, - }) -} - -type mySum struct { - resource.Named - resource.TriviallyCloseable - - mu sync.Mutex - subtract bool -} - -func newMySum(ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (summationapi.Summation, error) { - summer := &mySum{ - Named: conf.ResourceName().AsNamed(), - } - if err := summer.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return summer, nil -} - -func (m *mySum) Sum(ctx context.Context, nums []float64) (float64, error) { - if len(nums) == 0 { - return 0, errors.New("must provide at least one number to sum") - } - var ret float64 - for _, n := range nums { - if m.subtract { - ret -= n - } else { - ret += n - } - } - return ret, nil -} - -func (m *mySum) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - // This takes the generic resource.Config passed down from the parent and converts it to the - // model-specific (aka "native") Config structure defined above making it easier to directly access attributes. - sumConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - m.mu.Lock() - defer m.mu.Unlock() - m.subtract = sumConfig.Subtract - return nil -} diff --git a/examples/mysensor/README.md b/examples/mysensor/README.md deleted file mode 100644 index 69cbbbcfe12..00000000000 --- a/examples/mysensor/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# MySensor - -This example demonstrates a user defining a new sensor model. - - -## How to add custom models - -Models can be added to the viam-server by creating a struct that implements the interface of the API we want to register. -For example, if we want to create a sensor, we would want to make sure that we implement the Sensor interface, which includes the method `Readings` and also `DoCommand` from the `resource.Generic` interface. -The `resource.Generic` interface allows you to add arbitrary commands with arbitrary input arguments and return messages, which is useful if you want to extend the functionality of a model implementation beyond the API interface. - -``` - type Sensor interface { - resource.Resource - // Readings return data specific to the type of sensor and can be of any type. - Readings(ctx context.Context) (map[string]interface{}, error) - } -``` - - -The model then has to be registered through an init function, which should live in the package implementing the new model. -Init functions are run on import, so we have to make sure we are importing it somewhere in our code! - -``` - // registering the component model on init is how we make sure the new model is picked up and usable. - func init() { - resource.RegisterComponent( - sensor.API, - resource.DefaultModelFamily.WithModel("mySensor"), - resource.Registration[sensor.Sensor, *Config]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (sensor.Sensor, error) { - return newSensor(config.Name), nil - }}) - } -``` - -In this case, since the model is implemented in the same file as the main package, we don't have to import the package anywhere. -But in other cases, we would have to import the package implementing the new model somewhere in the main package. -``` - // import the custom sensor package to register it properly. A blank import would run the init() function. - _ "go.viam.com/rdk/examples/mysensor/mysensor" -``` - -The `server` package can now be built then run (`go build -o main server/server.go` then `./main`) or run directly (`go run server/server.go`) - -Check the custom sensor and server code out [here](https://github.com/viamrobotics/rdk/blob/main/examples/mysensor/server/server.go), and a simple client [here](https://github.com/viamrobotics/rdk/blob/main/examples/mysensor/client/client.go). - -## Running the example - -* Run the server implementing a new sensor `go run server/server.go`. Alternatively, you can build it by `go build -o main server/server.go`. -* Run the client `go run client/client.go`. - -## Using the custom server as a remote - -To use this custom server as part of a larger robot, you’ll want to add it as a remote in the config for your main part. - -``` - "remotes": [ - { - "name": "my-custom-sensor", // The name of the remote, can be anything - "insecure": true, // Whether this connection should use SSL - "address": "localhost:8081" // The location of the remote robot - } - ] -``` - -And to ensure that the custom server starts up with the rest of the robot, you can run the custom server binary as a process as part of the robot config. - -``` - "processes": [ - { - "id": "0", - "log": true, - "name": "/home/pi/mysensor/main" - } - ] -``` - -NB: The viam-server starts as a root process, so you may need to switch users to run the custom server binary. - -``` - "processes": [ - { - "id": "0", - "log": true, - "name": "sudo", - "args": [ - "-u", - "pi", - "/home/pi/mysensor/main" - ] - } - ] -``` diff --git a/examples/mysensor/client/client.go b/examples/mysensor/client/client.go deleted file mode 100644 index 81dd46dd2bc..00000000000 --- a/examples/mysensor/client/client.go +++ /dev/null @@ -1,33 +0,0 @@ -// Package main tests out the mySensor component type. -package main - -import ( - "context" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot/client" -) - -func main() { - logger := logging.NewDebugLogger("client") - robot, err := client.New( - context.Background(), - "localhost:8081", - logger, - ) - if err != nil { - logger.Fatal(err) - } - - // we can get the custom sensor here by name and use it like any other sensor. - sensor, err := sensor.FromRobot(robot, "sensor1") - if err != nil { - logger.Error(err) - } - reading, err := sensor.Readings(context.Background(), make(map[string]interface{})) - if err != nil { - logger.Error(err) - } - logger.Info(reading) -} diff --git a/examples/mysensor/package.go b/examples/mysensor/package.go deleted file mode 100644 index 286a5ef7ac9..00000000000 --- a/examples/mysensor/package.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package mysensor contains an example on creating a custom model. -package mysensor diff --git a/examples/mysensor/server/server.go b/examples/mysensor/server/server.go deleted file mode 100644 index e4b796e1d0c..00000000000 --- a/examples/mysensor/server/server.go +++ /dev/null @@ -1,72 +0,0 @@ -// Package main is an example of a custom viam server. -package main - -import ( - "context" - - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/robot/web" - weboptions "go.viam.com/rdk/robot/web/options" -) - -var logger = logging.NewDebugLogger("mysensor") - -// registering the component model on init is how we make sure the new model is picked up and usable. -func init() { - resource.RegisterComponent( - sensor.API, - resource.DefaultModelFamily.WithModel("mySensor"), - resource.Registration[sensor.Sensor, resource.NoNativeConfig]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (sensor.Sensor, error) { - return newSensor(conf.ResourceName()), nil - }}) -} - -func newSensor(name resource.Name) sensor.Sensor { - return &mySensor{ - Named: name.AsNamed(), - } -} - -// mySensor is a sensor device that always returns "hello world". -type mySensor struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable -} - -// Readings always returns "hello world". -func (s *mySensor) Readings(ctx context.Context, _ map[string]interface{}) (map[string]interface{}, error) { - return map[string]interface{}{"hello": "world"}, nil -} - -func main() { - goutils.ContextualMain(mainWithArgs, logger) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (err error) { - name := sensor.Named("sensor1") - s := newSensor(name) - - myRobot, err := robotimpl.RobotFromResources(ctx, map[resource.Name]resource.Resource{name: s}, logger) - if err != nil { - return err - } - //nolint:errcheck - defer myRobot.Close(ctx) - o := weboptions.New() - // the default bind address is localhost:8080, specifying a different bind address to avoid collisions. - o.Network.BindAddress = "localhost:8081" - - // runs the web server on the robot and blocks until the program is stopped. - return web.RunWeb(ctx, myRobot, o, logger) -} diff --git a/examples/package.go b/examples/package.go deleted file mode 100644 index aa5184706e7..00000000000 --- a/examples/package.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package examples contain a few examples of using the RDK. -package examples diff --git a/examples/simpleserver/cmd.go b/examples/simpleserver/cmd.go deleted file mode 100644 index a979770abdc..00000000000 --- a/examples/simpleserver/cmd.go +++ /dev/null @@ -1,53 +0,0 @@ -// Package main shows a simple server with a fake arm. -package main - -import ( - "context" - - "go.viam.com/utils" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/arm/fake" - "go.viam.com/rdk/gostream/codec/x264" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/robot/web" -) - -var logger = logging.NewDebugLogger("simpleserver") - -func main() { - utils.ContextualMain(mainWithArgs, logger) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error { - arm1Name := arm.Named("arm1") - cfg := resource.Config{ - Name: arm1Name.Name, - Model: resource.DefaultModelFamily.WithModel("ur5e"), - ConvertedAttributes: &fake.Config{ - ArmModel: "ur5e", - }, - } - arm1, err := fake.NewArm(context.Background(), nil, cfg, logger) - if err != nil { - return err - } - myRobot, err := robotimpl.RobotFromResources( - ctx, - map[resource.Name]resource.Resource{ - arm1Name: arm1, - }, - logger, - robotimpl.WithWebOptions(web.WithStreamConfig(x264.DefaultStreamConfig)), - ) - if err != nil { - return err - } - //nolint:errcheck - defer myRobot.Close(ctx) - - <-ctx.Done() - return nil -} diff --git a/go.mod b/go.mod index 62ed069606d..7ddd5670970 100644 --- a/go.mod +++ b/go.mod @@ -4,38 +4,27 @@ go 1.21 require ( github.com/AlekSi/gocov-xml v1.0.0 - github.com/CPRT/roboclaw v0.0.0-20190825181223-76871438befc github.com/Masterminds/semver/v3 v3.2.1 github.com/Masterminds/sprig v2.22.0+incompatible github.com/NYTimes/gziphandler v1.1.1 github.com/a8m/envsubst v1.4.2 - github.com/adrianmo/go-nmea v1.7.0 github.com/axw/gocov v1.1.0 github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e github.com/benbjohnson/clock v1.3.3 github.com/bep/debounce v1.2.1 - github.com/bluenviron/gortsplib/v4 v4.8.0 - github.com/bluenviron/mediacommon v1.9.2 github.com/bufbuild/buf v1.6.0 github.com/creack/pty v1.1.19-0.20220421211855-0d412c9fbeb1 github.com/d2r2/go-i2c v0.0.0-20191123181816-73a8a799d6bc github.com/d2r2/go-logger v0.0.0-20210606094344-60e9d1233e22 - github.com/de-bkg/gognss v0.0.0-20220601150219-24ccfdcdbb5d github.com/disintegration/imaging v1.6.2 github.com/docker/go-units v0.5.0 - github.com/edaniels/gobag v1.0.7-0.20220607183102-4242cd9e2848 github.com/edaniels/golinters v0.0.5-0.20220906153528-641155550742 - github.com/edaniels/golog v0.0.0-20230215213219-28954395e8d0 github.com/edaniels/lidario v0.0.0-20220607182921-5879aa7b96dd github.com/fatih/color v1.15.0 github.com/fogleman/gg v1.3.0 github.com/fsnotify/fsnotify v1.6.0 github.com/fullstorydev/grpcurl v1.8.6 - github.com/go-audio/audio v1.0.0 - github.com/go-audio/transforms v0.0.0-20180121090939-51830ccc35a5 - github.com/go-audio/wav v1.1.0 github.com/go-gl/mathgl v1.0.0 - github.com/go-gnss/rtcm v0.0.3 github.com/go-nlopt/nlopt v0.0.0-20230219125344-443d3362dcb5 github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 github.com/goccy/go-graphviz v0.1.3-0.20240305010347-606fdf55b06d @@ -45,41 +34,29 @@ require ( github.com/golang/protobuf v1.5.3 github.com/golangci/golangci-lint v1.54.0 github.com/google/flatbuffers v2.0.6+incompatible - github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.6.0 - github.com/gotesttools/gotestfmt/v2 v2.4.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 github.com/invopop/jsonschema v0.6.0 - github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/jhump/protoreflect v1.15.1 github.com/kellydunn/golang-geo v0.7.0 - github.com/lestrrat-go/jwx v1.2.29 github.com/lmittmann/ppm v1.0.2 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-tflite v1.0.4 github.com/matttproud/golang_protobuf_extensions v1.0.4 github.com/mitchellh/copystructure v1.2.0 - github.com/mkch/gpio v0.0.0-20190919032813-8327cd97d95e - github.com/montanaflynn/stats v0.7.0 github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 github.com/muesli/kmeans v0.3.1 github.com/nathan-fiscaletti/consolesize-go v0.0.0-20220204101620-317176b6684d - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/pion/mediadevices v0.6.4 - github.com/pion/rtp v1.8.5 github.com/pion/webrtc/v3 v3.2.36 github.com/rhysd/actionlint v1.6.24 github.com/rs/cors v1.9.0 github.com/sergi/go-diff v1.3.1 - github.com/u2takey/ffmpeg-go v0.4.1 github.com/urfave/cli/v2 v2.10.3 github.com/viam-labs/go-libjpeg v0.3.1 - github.com/viamrobotics/evdev v0.1.3 github.com/xfmoulet/qoi v0.2.0 go-hep.org/x/hep v0.32.1 - go.einride.tech/vlp16 v0.7.0 go.mongodb.org/mongo-driver v1.11.6 go.opencensus.io v0.24.0 go.uber.org/atomic v1.10.0 @@ -93,20 +70,15 @@ require ( golang.org/x/mobile v0.0.0-20240112133503-c713f31d574b golang.org/x/sys v0.20.0 golang.org/x/term v0.20.0 - golang.org/x/time v0.3.0 golang.org/x/tools v0.17.0 gonum.org/v1/gonum v0.12.0 gonum.org/v1/plot v0.12.0 - google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 google.golang.org/grpc v1.58.3 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 google.golang.org/protobuf v1.34.1 - gopkg.in/src-d/go-billy.v4 v4.3.2 gorgonia.org/tensor v0.9.24 gotest.tools/gotestsum v1.10.0 - periph.io/x/conn/v3 v3.7.0 - periph.io/x/host/v3 v3.8.1-0.20230331112814-9f0d9f7d76db ) require ( @@ -140,10 +112,8 @@ require ( github.com/ashanbrown/forbidigo v1.6.0 // indirect github.com/ashanbrown/makezero v1.1.1 // indirect github.com/aws/aws-sdk-go v1.38.20 // indirect - github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.1 // indirect - github.com/blackjack/webcam v0.6.1 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v3 v3.4.0 // indirect github.com/breml/bidichk v0.2.4 // indirect @@ -171,6 +141,7 @@ require ( github.com/denis-tingaikin/go-header v0.4.3 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dnephin/pflag v1.0.7 // indirect + github.com/edaniels/golog v0.0.0-20230215213219-28954395e8d0 // indirect github.com/edaniels/zeroconf v1.0.10 // indirect github.com/envoyproxy/go-control-plane v0.11.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect @@ -180,15 +151,12 @@ require ( github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/gen2brain/malgo v0.11.21 // indirect github.com/gin-gonic/gin v1.9.1 // indirect - github.com/go-audio/riff v1.0.0 // indirect github.com/go-chi/chi/v5 v5.0.7 // indirect github.com/go-critic/go-critic v0.8.2 // indirect github.com/go-fonts/liberation v0.3.0 // indirect github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 // indirect github.com/go-pdf/fpdf v0.6.0 // indirect - github.com/go-restruct/restruct v1.2.0-alpha.0.20210525045353-983b86fa188e // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.1.0 // indirect @@ -216,6 +184,7 @@ require ( github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect github.com/gonuts/binary v0.2.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect @@ -258,6 +227,7 @@ require ( github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx v1.2.29 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lib/pq v1.10.9 // indirect github.com/lufeee/execinquery v1.2.1 // indirect @@ -275,16 +245,17 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/montanaflynn/stats v0.7.0 // indirect github.com/moricho/tparallel v0.3.1 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nishanths/exhaustive v0.11.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.13.3 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pierrec/lz4 v2.0.5+incompatible // indirect github.com/pion/datachannel v1.5.5 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/ice/v2 v2.3.13 // indirect @@ -293,6 +264,7 @@ require ( github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.14 // indirect + github.com/pion/rtp v1.8.5 // indirect github.com/pion/sctp v1.8.14 // indirect github.com/pion/sdp/v3 v3.0.9 // indirect github.com/pion/srtp/v2 v2.0.18 // indirect @@ -341,14 +313,12 @@ require ( github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect - github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 // indirect github.com/tdakkota/asciicheck v0.2.0 // indirect github.com/tetafro/godot v1.4.11 // indirect github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect github.com/timonwong/loggercheck v0.9.4 // indirect github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect - github.com/u2takey/go-utils v0.3.1 // indirect github.com/ultraware/funlen v0.1.0 // indirect github.com/ultraware/whitespace v0.0.5 // indirect github.com/uudashr/gocognit v1.0.7 // indirect @@ -378,6 +348,7 @@ require ( google.golang.org/api v0.126.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -385,7 +356,6 @@ require ( gorgonia.org/vecf32 v0.9.0 // indirect gorgonia.org/vecf64 v0.9.0 // indirect honnef.co/go/tools v0.4.3 // indirect - howett.net/plist v1.0.0 // indirect mvdan.cc/gofumpt v0.5.0 // indirect mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect diff --git a/go.sum b/go.sum index f43e53d0127..e63a627f103 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CPRT/roboclaw v0.0.0-20190825181223-76871438befc h1:X6Ptoxdi1w0lofv51zchUBBvULiL97ruSjtTRa/SL5Q= -github.com/CPRT/roboclaw v0.0.0-20190825181223-76871438befc/go.mod h1:1nLByU34wsr4qNJ4yrfxuGFmIl/0/eAuB8VAiGK0GHs= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0 h1:3ZBs7LAezy8gh0uECsA6CGU43FF3zsx5f4eah5FxTMA= @@ -110,8 +108,6 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrU github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= -github.com/adrianmo/go-nmea v1.7.0 h1:ji8IeiuUG+LTpVoUxmLPHr/WuxFvvD2S6lYDfDze5yw= -github.com/adrianmo/go-nmea v1.7.0/go.mod h1:u8bPnpKt/D/5rll/5l9f6iDfeq5WZW0+/SXdkwix6Tg= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= @@ -161,8 +157,6 @@ github.com/axw/gocov v1.1.0 h1:y5U1krExoJDlb/kNtzxyZQmNRprFOFCutWbNjcQvmVM= github.com/axw/gocov v1.1.0/go.mod h1:H9G4tivgdN3pYSSVrTFBr6kGDCmAkgbJhtxFzAvgcdw= github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e h1:dSeuFcs4WAJJnswS8vXy7YY1+fdlbVPuEVmDAfqvFOQ= github.com/aybabtme/uniplot v0.0.0-20151203143629-039c559e5e7e/go.mod h1:uh71c5Vc3VNIplXOFXsnDy21T1BepgT32c5X/YPrOyc= -github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883 h1:XNtOMwxmV2PI/vuTHDZnFzGIFNUh8MK73q7+Kna7AXs= -github.com/bamiaux/iobit v0.0.0-20170418073505-498159a04883/go.mod h1:9IjZnSQGh45J46HHS45pxuMJ6WFTtSXbaX0FoHDvxh8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.3 h1:g+rSsSaAzhHJYcIQE78hJ3AhyjjtQvleKDjlhdBnIhc= github.com/benbjohnson/clock v1.3.3/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -177,14 +171,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/bkielbasa/cyclop v1.2.0/go.mod h1:qOI0yy6A7dYC4Zgsa72Ppm9kONl0RoIlPbzot9mhmeI= github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= -github.com/blackjack/webcam v0.6.1 h1:K0T6Q0zto23U99gNAa5q/hFoye6uGcKr2aE6hFoxVoE= -github.com/blackjack/webcam v0.6.1/go.mod h1:zs+RkUZzqpFPHPiwBZ6U5B34ZXXe9i+SiHLKnnukJuI= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= -github.com/bluenviron/gortsplib/v4 v4.8.0 h1:nvFp6rHALcSep3G9uBFI0uogS9stVZLNq/92TzGZdQg= -github.com/bluenviron/gortsplib/v4 v4.8.0/go.mod h1:+d+veuyvhvikUNp0GRQkk6fEbd/DtcXNidMRm7FQRaA= -github.com/bluenviron/mediacommon v1.9.2 h1:EHcvoC5YMXRcFE010bTNf07ZiSlB/e/AdZyG7GsEYN0= -github.com/bluenviron/mediacommon v1.9.2/go.mod h1:lt8V+wMyPw8C69HAqDWV5tsAwzN9u2Z+ca8B6C//+n0= github.com/bombsimon/wsl/v3 v3.2.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc= github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo= @@ -287,8 +275,6 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/de-bkg/gognss v0.0.0-20220601150219-24ccfdcdbb5d h1:AHDio/bKqSNW6c8d9vP0KsVsBBOdvuN5FyDHpnGY7t0= -github.com/de-bkg/gognss v0.0.0-20220601150219-24ccfdcdbb5d/go.mod h1:9ExAdt7E0RgTFoLc2GFji9LmfnMA11NVRz3eJ6yogvE= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= @@ -310,8 +296,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edaniels/gobag v1.0.7-0.20220607183102-4242cd9e2848 h1:JVz0wMVFlh5ziW4aZcGnet1IxRfrQjf9IaLRh/2rAhA= -github.com/edaniels/gobag v1.0.7-0.20220607183102-4242cd9e2848/go.mod h1:FXvLMxXtMPU+U9Kp8kDOrEW258kzh6PKlRkHEW5h9CY= github.com/edaniels/golinters v0.0.4/go.mod h1:KzjC7OrCrRlFxufhH+kQ1Sdyzuj2eanHHzPaWxD3lgk= github.com/edaniels/golinters v0.0.5-0.20220906153528-641155550742 h1:ZoFlzcasiB47esGHs8W0GzTKUDUfIa94ZUWzBqEI1fw= github.com/edaniels/golinters v0.0.5-0.20220906153528-641155550742/go.mod h1:i/zcokIKs893VZA41BS8jTOHW23PlqiiZEa7Y+4Nujk= @@ -376,22 +360,12 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gen2brain/malgo v0.11.21 h1:qsS4Dh6zhZgmvAW5CtKRxDjQzHbc2NJlBG9eE0tgS8w= -github.com/gen2brain/malgo v0.11.21/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= -github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= -github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= -github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= -github.com/go-audio/transforms v0.0.0-20180121090939-51830ccc35a5 h1:acgZxkn6oSJCh/snMQdZYuOeroSbZHdOinIa1n251Wk= -github.com/go-audio/transforms v0.0.0-20180121090939-51830ccc35a5/go.mod h1:z9ahC4nc9/kxKfl1BnTZ/D2Cm5TbhjR2LeuUpepL9zI= -github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= -github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-critic/go-critic v0.5.4/go.mod h1:cjB4YGw+n/+X8gREApej7150Uyy1Tg8If6F2XOAUXNE= @@ -409,9 +383,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68= github.com/go-gl/mathgl v1.0.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= -github.com/go-gnss/ntrip v0.0.8/go.mod h1:TBB2ymCoYtH2RfE0Do+mtzmVuZB0gob5b8arEonOXys= -github.com/go-gnss/rtcm v0.0.3 h1:JdMhDoP6wmF1FUyqvGGayICk5V6LXUIHXF4O99vzzq0= -github.com/go-gnss/rtcm v0.0.3/go.mod h1:uqfG5wB+gG7VG4NuCfFkTRpNMtia/D5xpgmSeYoq8Fc= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -423,7 +394,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-nlopt/nlopt v0.0.0-20230219125344-443d3362dcb5 h1:JlR5qQ/dy4NPpeKld/CJR6cIcL0ll4OQ7ieylY5kJ20= @@ -442,8 +412,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-restruct/restruct v1.2.0-alpha.0.20210525045353-983b86fa188e h1:PIFVUcdZ9OADg9XAsN0I8OzUzmYXHU+2msP2X7ST/fo= -github.com/go-restruct/restruct v1.2.0-alpha.0.20210525045353-983b86fa188e/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -481,7 +449,6 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDs github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/gobuffalo/httptest v1.5.0/go.mod h1:F541Rwcu9Ypo1NBAsYxrCV0J6ibAGGEvhM3653G+u5I= github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro= github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg= github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= @@ -661,7 +628,6 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -684,8 +650,6 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= -github.com/gotesttools/gotestfmt/v2 v2.4.1 h1:Ml+KPqPocp/KckpizL+tgsy/dlddI4/z2w6lgS7YIFE= -github.com/gotesttools/gotestfmt/v2 v2.4.1/go.mod h1:oQJg2KZ2aGoqEbMC2PDaAeBYm0tOkocgixK9FzsCdp4= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -755,13 +719,10 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/invopop/jsonschema v0.6.0 h1:8e+xY8ZEn8gDHUYylSlLHy22P+SLeIRIHv3nM3hCbmY= github.com/invopop/jsonschema v0.6.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= -github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4 h1:G2ztCwXov8mRvP0ZfjE6nAlaCX2XbykaeHdbT6KwDz0= -github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs= github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a h1:d4+I1YEKVmWZrgkt6jpXBnLgV2ZjO0YxEtLDdfIZfH4= github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jgautheron/goconst v1.4.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= github.com/jgautheron/goconst v1.5.1 h1:HxVbL1MhydKs8R8n/HE5NPvzfaYmQJA3o879lE4+WcM= github.com/jgautheron/goconst v1.5.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= @@ -785,8 +746,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= -github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -820,7 +779,6 @@ github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= -github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -828,8 +786,6 @@ github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e h1:+lIPJOWl+jSiJOc70QXJ07+2eg2Jy2EC7Mi11BWujeM= -github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= @@ -843,7 +799,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -981,10 +936,6 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mkch/asserting v0.0.0-20190916092325-c18221b2f2b2 h1:ZuTZrURK+9dqhtVw11exJ65tjbh3DVXj4L5R1KnFAU8= -github.com/mkch/asserting v0.0.0-20190916092325-c18221b2f2b2/go.mod h1:7QF1TGIJmEibF74aU9YWLLfDOnl4R94hWzpD/zWowbM= -github.com/mkch/gpio v0.0.0-20190919032813-8327cd97d95e h1:vSAYdBvTvlYVdoDYYQapVnlPd8Klrk19uHPDy29agsg= -github.com/mkch/gpio v0.0.0-20190919032813-8327cd97d95e/go.mod h1:4uOFgu7xPZTSz7NSamkmHD67Y6CdXgK9Lx8Dm0qm1vQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1084,7 +1035,6 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6 github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -1098,11 +1048,8 @@ github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7 github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4 v1.0.2-0.20171218195038-2fcda4cb7018/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/xxHash v0.1.1/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= @@ -1115,8 +1062,6 @@ github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= -github.com/pion/mediadevices v0.6.4 h1:BDR6iEcc80URpJ+wkSUfW/X+pan98nEfoX8ab6mkeAE= -github.com/pion/mediadevices v0.6.4/go.mod h1:Qo9oecByIaLcwH1+S/76FcaGurRqrkHls07y5VjWW1Q= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= @@ -1313,7 +1258,6 @@ github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCp github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= @@ -1378,8 +1322,6 @@ github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM= github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= @@ -1413,10 +1355,6 @@ github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+ github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/u2takey/ffmpeg-go v0.4.1 h1:l5ClIwL3N2LaH1zF3xivb3kP2HW95eyG5xhHE1JdZ9Y= -github.com/u2takey/ffmpeg-go v0.4.1/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc= -github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys= -github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -1442,8 +1380,6 @@ github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/viam-labs/go-libjpeg v0.3.1 h1:J/byavXHFqRI1PFPrnPbP+wFCr1y+Cn1CwKXrORCPD0= github.com/viam-labs/go-libjpeg v0.3.1/go.mod h1:b0ISpf9lJv9MO1h1gXAmSA/osG19cKGYjfYc6aeEjqs= -github.com/viamrobotics/evdev v0.1.3 h1:mR4HFafvbc5Wx4Vp1AUJp6/aITfVx9AKyXWx+rWjpfc= -github.com/viamrobotics/evdev v0.1.3/go.mod h1:N6nuZmPz7HEIpM7esNWwLxbYzqWqLSZkfI/1Sccckqk= github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= @@ -1493,8 +1429,6 @@ go-hep.org/x/hep v0.32.1 h1:O96fOyMP+4ET8X+Uu38VFdegQb7rL0rjmFqXMCSm4VM= go-hep.org/x/hep v0.32.1/go.mod h1:VX3IVUv0Ku5bgWhE+LxRQ1aT7BmWWxSxQu02hfsoeRI= go-simpler.org/assert v0.5.0 h1:+5L/lajuQtzmbtEfh69sr5cRf2/xZzyJhFjoOz/PPqs= go-simpler.org/assert v0.5.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go.einride.tech/vlp16 v0.7.0 h1:mR7jChj9PTxxKhEn2HkHeT3zwKK45qIb6DLZkbyQZuw= -go.einride.tech/vlp16 v0.7.0/go.mod h1:Hk+9Ts3PftwxIeXbabBO2zFPP2vcmnRno3ZgXGMh3Os= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -1533,7 +1467,6 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= @@ -1548,7 +1481,6 @@ go.viam.com/utils v0.1.77 h1:eI2BzUxf2kILSqPT5GbWw605Z26gKAiJ7wGSqx8OfCw= go.viam.com/utils v0.1.77/go.mod h1:3xcBBlLsRX2eWpWuQrFJ7UVxsgXwJg8Fx30Rt/kp52g= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg= go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= @@ -1759,7 +1691,6 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1881,8 +1812,6 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1906,7 +1835,6 @@ golang.org/x/tools v0.0.0-20190529010454-aa71c3f32488/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -2164,11 +2092,8 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -2176,7 +2101,6 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -2193,7 +2117,6 @@ gorgonia.org/vecf64 v0.9.0 h1:bgZDP5x0OzBF64PjMGC3EvTdOoMEcmfAh1VCUnZFm1A= gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA= gotest.tools/gotestsum v1.10.0 h1:lVO4uQJoxdsJb7jgmr1fg8QW7zGQ/tuqvsq5fHKyoHQ= gotest.tools/gotestsum v1.10.0/go.mod h1:6JHCiN6TEjA7Kaz23q1bH0e2Dc3YJjDUZ0DmctFZf+w= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2208,8 +2131,6 @@ honnef.co/go/tools v0.1.2/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= honnef.co/go/tools v0.4.3 h1:o/n5/K5gXqk8Gozvs2cnL0F2S1/g1vcGCAx2vETjITw= honnef.co/go/tools v0.4.3/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA= -howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= -howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= mvdan.cc/gofumpt v0.1.0/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= @@ -2224,15 +2145,10 @@ mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RF nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA= -periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg= -periph.io/x/host/v3 v3.8.1-0.20230331112814-9f0d9f7d76db h1:8+HL7DJFofYRhGoK/UdwhzvQj3I2HrKLQ6dkOC66CZY= -periph.io/x/host/v3 v3.8.1-0.20230331112814-9f0d9f7d76db/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/gostream/.gitignore b/gostream/.gitignore deleted file mode 100644 index 541da6cb485..00000000000 --- a/gostream/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Keep FFmpeg static libraries -!**/ffmpeg/**/*.a diff --git a/gostream/README.md b/gostream/README.md deleted file mode 100644 index 4bf31e2e5ec..00000000000 --- a/gostream/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# gostream - -gostream is a library to simplify the streaming of images as video and audio chunks to audio to a series of WebRTC peers. The impetus for this existing was for doing simple GUI / audio/video streaming to a browser all within go with as little cgo as possible. The package will likely be refactored over time to support some more generalized use cases and as such will be in version 0 for the time being. Many parameters are hard coded and need to be configurable over time. Use at your own risk, and please file issues! - -## TODO - -- Support multiple codecs (e.g. Firefox macos-arm does not support h264 by default yet) -- Verify Windows Logitech StreamCam working -- Reconnect on server restart -- Check closes and frees -- Address code TODOs (including context.TODO) -- Documentation (inner func docs, package docs, example docs) -- Version 0.1.0 -- Support removal of streams -- Synchronize audio with video - -## With NixOS (Experimental) - -`nix-shell --pure` - -## Examples - -* Stream current desktop: `go run gostream/cmd/stream_video/main.go` -* Stream camera: `go run gostream/cmd/stream_video/main.go -camera` -* Stream microphone: `go run gostream/cmd/stream_audio/main.go -playback` -* Stream microphone and camera: `go run gostream/cmd/stream_av/main.go -camera` -* Playback microphone from browser: `go run gostream/cmd/stream_audio/main.go -playback` diff --git a/gostream/audio.go b/gostream/audio.go deleted file mode 100644 index 207a128ec05..00000000000 --- a/gostream/audio.go +++ /dev/null @@ -1,59 +0,0 @@ -package gostream - -import ( - "context" - - "github.com/pion/mediadevices/pkg/driver" - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" -) - -type ( - // An AudioReader is anything that can read and recycle audio data. - AudioReader = MediaReader[wave.Audio] - - // An AudioReaderFunc is a helper to turn a function into an AudioReader. - AudioReaderFunc = MediaReaderFunc[wave.Audio] - - // An AudioSource is responsible for producing audio chunks when requested. A source - // should produce the chunk as quickly as possible and introduce no rate limiting - // of its own as that is handled internally. - AudioSource = MediaSource[wave.Audio] - - // An AudioStream streams audio forever until closed. - AudioStream = MediaStream[wave.Audio] - - // AudioPropertyProvider providers information about an audio source. - AudioPropertyProvider = MediaPropertyProvider[prop.Audio] -) - -// NewAudioSource instantiates a new audio read closer. -func NewAudioSource(r AudioReader, p prop.Audio) AudioSource { - return newMediaSource(nil, r, p) -} - -// NewAudioSourceForDriver instantiates a new audio read closer and references the given -// driver. -func NewAudioSourceForDriver(d driver.Driver, r AudioReader, p prop.Audio) AudioSource { - return newMediaSource(d, r, p) -} - -// ReadAudio gets a single audio wave from an audio source. Using this has less of a guarantee -// than AudioSource.Stream that the Nth wave follows the N-1th wave. -func ReadAudio(ctx context.Context, source AudioSource) (wave.Audio, func(), error) { - return ReadMedia(ctx, source) -} - -// NewEmbeddedAudioStream returns an audio stream from an audio source that is -// intended to be embedded/composed by another source. It defers the creation -// of its stream. -func NewEmbeddedAudioStream(src AudioSource) AudioStream { - return NewEmbeddedMediaStream[wave.Audio, prop.Audio](src) -} - -// NewEmbeddedAudioStreamFromReader returns an audio stream from an audio reader that is -// intended to be embedded/composed by another source. It defers the creation -// of its stream. -func NewEmbeddedAudioStreamFromReader(reader AudioReader) AudioStream { - return NewEmbeddedMediaStreamFromReader(reader, prop.Audio{}) -} diff --git a/gostream/buf.gen.yaml b/gostream/buf.gen.yaml deleted file mode 100644 index c94dd14fd32..00000000000 --- a/gostream/buf.gen.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: v1 -plugins: - - remote: buf.build/protocolbuffers/plugins/go:v1.28.1-1 - out: . - opt: - - paths=source_relative - - remote: buf.build/grpc/plugins/go:v1.2.0-1 - out: . - opt: - - paths=source_relative - - remote: buf.build/grpc-ecosystem/plugins/grpc-gateway:v2.11.2-1 - out: . - opt: - - paths=source_relative - - generate_unbound_methods=true - - remote: buf.build/sawadashota/plugins/protoc-gen-doc:v1.5.1 - out: docs/proto - opt: - - html,index.html diff --git a/gostream/buf.lock b/gostream/buf.lock deleted file mode 100644 index c91b5810c29..00000000000 --- a/gostream/buf.lock +++ /dev/null @@ -1,2 +0,0 @@ -# Generated by buf. DO NOT EDIT. -version: v1 diff --git a/gostream/buf.yaml b/gostream/buf.yaml deleted file mode 100644 index c835c13d83b..00000000000 --- a/gostream/buf.yaml +++ /dev/null @@ -1,13 +0,0 @@ -version: v1 -name: buf.build/erdaniels/gostream -build: - excludes: - - vendor - - frontend/node_modules -lint: - use: - - DEFAULT -breaking: - use: - - FILE - \ No newline at end of file diff --git a/gostream/codec/audio_encoder.go b/gostream/codec/audio_encoder.go deleted file mode 100644 index aeaed087a7e..00000000000 --- a/gostream/codec/audio_encoder.go +++ /dev/null @@ -1,23 +0,0 @@ -package codec - -import ( - "context" - "time" - - "github.com/edaniels/golog" - "github.com/pion/mediadevices/pkg/wave" -) - -// An AudioEncoder is anything that can encode audo chunks into bytes. This means that -// the encoder must follow some type of format dictated by a type (see AudioEncoderFactory.MimeType). -// An encoder that produces bytes of different encoding formats per call is invalid. -type AudioEncoder interface { - Encode(ctx context.Context, chunk wave.Audio) ([]byte, bool, error) - Close() -} - -// An AudioEncoderFactory produces AudioEncoders and provides information about the underlying encoder itself. -type AudioEncoderFactory interface { - New(sampleRate, channelCount int, latency time.Duration, logger golog.Logger) (AudioEncoder, error) - MIMEType() string -} diff --git a/gostream/codec/h264/encoder.go b/gostream/codec/h264/encoder.go deleted file mode 100644 index 91bd2e6488d..00000000000 --- a/gostream/codec/h264/encoder.go +++ /dev/null @@ -1,173 +0,0 @@ -//go:build cgo && linux && !android - -// Package h264 uses a V4L2-compatible h.264 hardware encoder (h264_v4l2m2m) to encode images. -package h264 - -import "C" - -import ( - "context" - "image" - "time" - "unsafe" - - "github.com/edaniels/golog" - "github.com/pion/mediadevices/pkg/io/video" - "github.com/pkg/errors" - - "go.viam.com/rdk/gostream/codec" - "go.viam.com/rdk/gostream/ffmpeg/avcodec" - "go.viam.com/rdk/gostream/ffmpeg/avutil" -) - -const ( - // pixelFormat This format is one of the output formats support by the bcm2835-codec at /dev/video11 - // It is also known as YU12. See https://www.kernel.org/doc/html/v4.10/media/uapi/v4l/pixfmt-yuv420.html - pixelFormat = avcodec.AvPixFmtYuv420p - // V4l2m2m Is a V4L2 memory-to-memory H.264 hardware encoder. - V4l2m2m = "h264_v4l2m2m" - // macroBlock is the encoder boundary block size in bytes. - macroBlock = 64 - // warmupTime is the time to wait for the encoder to warm up in milliseconds. - warmupTime = 1000 // 1 second -) - -type encoder struct { - img image.Image - reader video.Reader - codec *avcodec.Codec - context *avcodec.Context - width int - height int - frame *avutil.Frame - pts int64 - logger golog.Logger -} - -func (h *encoder) Read() (img image.Image, release func(), err error) { - return h.img, nil, nil -} - -// NewEncoder returns an h264 encoder that can encode images of the given width and height. It will -// also ensure that it produces key frames at the given interval. -func NewEncoder(width, height, keyFrameInterval int, logger golog.Logger) (codec.VideoEncoder, error) { - h := &encoder{width: width, height: height, logger: logger} - - if h.codec = avcodec.FindEncoderByName(V4l2m2m); h.codec == nil { - return nil, errors.Errorf("cannot find encoder '%s'", V4l2m2m) - } - - if h.context = h.codec.AllocContext3(); h.context == nil { - return nil, errors.New("cannot allocate video codec context") - } - - h.context.SetEncodeParams(width, height, avcodec.PixelFormat(pixelFormat), false, keyFrameInterval) - h.context.SetFramerate(keyFrameInterval) - - h.reader = video.ToI420((video.ReaderFunc)(h.Read)) - - if h.context.Open2(h.codec, nil) < 0 { - return nil, errors.New("cannot open codec") - } - - if h.frame = avutil.FrameAlloc(); h.frame == nil { - if err := h.Close(); err != nil { - return nil, errors.Wrap(err, "cannot close codec") - } - return nil, errors.New("cannot alloc frame") - } - - // give the encoder some time to warm up - time.Sleep(warmupTime * time.Millisecond) - - return h, nil -} - -func (h *encoder) Encode(ctx context.Context, img image.Image) ([]byte, error) { - if err := avutil.SetFrame(h.frame, h.width, h.height, pixelFormat); err != nil { - return nil, errors.Wrap(err, "cannot set frame properties") - } - - if ret := avutil.FrameMakeWritable(h.frame); ret < 0 { - return nil, errors.Wrap(avutil.ErrorFromCode(ret), "cannot make frame writable") - } - - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - h.img = img - yuvImg, release, err := h.reader.Read() - defer release() - - if err != nil { - return nil, errors.Wrap(err, "cannot read image") - } - - h.frame.SetFrameFromImgMacroAlign(yuvImg.(*image.YCbCr), macroBlock) - h.frame.SetFramePTS(h.pts) - h.pts++ - - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - return h.encodeBytes(ctx) - } -} - -func (h *encoder) encodeBytes(ctx context.Context) ([]byte, error) { - pkt := avcodec.PacketAlloc() - if pkt == nil { - return nil, errors.New("cannot allocate packet") - } - defer pkt.Unref() - defer avutil.FrameUnref(h.frame) - - if ret := h.context.SendFrame((*avcodec.Frame)(unsafe.Pointer(h.frame))); ret < 0 { - return nil, errors.Wrap(avutil.ErrorFromCode(ret), "cannot supply raw video to encoder") - } - - var bytes []byte - var ret int -loop: - // See "send/receive encoding and decoding API overview" from https://ffmpeg.org/doxygen/3.4/group__lavc__encdec.html. - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - ret = h.context.ReceivePacket(pkt) - switch ret { - case avutil.Success: - payload := C.GoBytes(unsafe.Pointer(pkt.Data()), C.int(pkt.Size())) - bytes = append(bytes, payload...) - pkt.Unref() - case avutil.EAGAIN, avutil.EOF: - break loop - default: - return nil, avutil.ErrorFromCode(ret) - } - } - - return bytes, nil -} - -// Close closes the encoder. It is safe to call this method multiple times. -// It is also safe to call this method after a call to Encode. -func (h *encoder) Close() error { - if h.frame != nil { - avutil.FrameUnref(h.frame) - h.frame = nil - } - if h.context != nil { - h.context.FreeContext() - h.context = nil - } - - return nil -} diff --git a/gostream/codec/h264/utils.go b/gostream/codec/h264/utils.go deleted file mode 100644 index 76972d791c0..00000000000 --- a/gostream/codec/h264/utils.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build cgo && linux && !android - -package h264 - -import ( - "github.com/edaniels/golog" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec" -) - -// DefaultStreamConfig configures h264 as the encoder for a stream. -var DefaultStreamConfig gostream.StreamConfig - -func init() { - DefaultStreamConfig.VideoEncoderFactory = NewEncoderFactory() -} - -// NewEncoderFactory returns an h264 encoder factory. -func NewEncoderFactory() codec.VideoEncoderFactory { - return &factory{} -} - -type factory struct{} - -func (f *factory) New(width, height, keyFrameInterval int, logger golog.Logger) (codec.VideoEncoder, error) { - return NewEncoder(width, height, keyFrameInterval, logger) -} - -func (f *factory) MIMEType() string { - return "video/H264" -} diff --git a/gostream/codec/mmal/encoder.go b/gostream/codec/mmal/encoder.go deleted file mode 100644 index 9a8a85e8452..00000000000 --- a/gostream/codec/mmal/encoder.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build mmal - -// Package mmal contains the mmal video codec. -package mmal - -import ( - "context" - "image" - - ourcodec "go.viam.com/rdk/gostream/codec" - - "github.com/edaniels/golog" - "github.com/pion/mediadevices/pkg/codec" - "github.com/pion/mediadevices/pkg/codec/mmal" - "github.com/pion/mediadevices/pkg/prop" -) - -type encoder struct { - codec codec.ReadCloser - img image.Image - logger golog.Logger -} - -// Gives suitable results. Probably want to make this configurable this in the future. -const bitrate = 3_200_000 - -// NewEncoder returns an MMAL encoder that can encode images of the given width and height. It will -// also ensure that it produces key frames at the given interval. -func NewEncoder(width, height, keyFrameInterval int, logger golog.Logger) (ourcodec.VideoEncoder, error) { - enc := &encoder{logger: logger} - - var builder codec.VideoEncoderBuilder - params, err := mmal.NewParams() - if err != nil { - return nil, err - } - builder = ¶ms - params.BitRate = bitrate - params.KeyFrameInterval = keyFrameInterval - - codec, err := builder.BuildVideoEncoder(enc, prop.Media{ - Video: prop.Video{ - Width: width, - Height: height, - }, - }) - if err != nil { - return nil, err - } - enc.codec = codec - - return enc, nil -} - -// Read returns an image for codec to process. -func (v *encoder) Read() (img image.Image, release func(), err error) { - return v.img, nil, nil -} - -// Encode asks the codec to process the given image. -func (v *encoder) Encode(_ context.Context, img image.Image) ([]byte, error) { - v.img = img - data, release, err := v.codec.Read() - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - release() - return dataCopy, err -} diff --git a/gostream/codec/mmal/utils.go b/gostream/codec/mmal/utils.go deleted file mode 100644 index fecf15a82e4..00000000000 --- a/gostream/codec/mmal/utils.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build mmal - -package mmal - -import ( - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec" - - "github.com/edaniels/golog" -) - -// DefaultStreamConfig configures MMAL as the encoder for a stream. -var DefaultStreamConfig gostream.StreamConfig - -func init() { - DefaultStreamConfig.VideoEncoderFactory = NewEncoderFactory() -} - -// NewEncoderFactory returns an MMAL encoder factory. -func NewEncoderFactory() codec.VideoEncoderFactory { - return &factory{} -} - -type factory struct{} - -func (f *factory) New(width, height, keyFrameInterval int, logger golog.Logger) (codec.VideoEncoder, error) { - return NewEncoder(width, height, keyFrameInterval, logger) -} - -func (f *factory) MIMEType() string { - return "video/H264" -} diff --git a/gostream/codec/opus/encoder.go b/gostream/codec/opus/encoder.go deleted file mode 100644 index 41e71e1abe4..00000000000 --- a/gostream/codec/opus/encoder.go +++ /dev/null @@ -1,142 +0,0 @@ -// Package opus contains the opus video codec. -package opus - -import ( - "context" - "io" - "sync" - "time" - - "github.com/edaniels/golog" - "github.com/pion/mediadevices/pkg/codec" - "github.com/pion/mediadevices/pkg/codec/opus" - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "go.viam.com/utils" - - ourcodec "go.viam.com/rdk/gostream/codec" -) - -type encoder struct { - codec codec.ReadCloser - chunkCh chan wave.Audio - encodedCh chan encodedData - logger golog.Logger - cancelCtx context.Context - cancelFunc func() - activeBackgroundWorkers sync.WaitGroup -} - -// Gives suitable results. Probably want to make this configurable this in the future. -const bitrate = 32000 - -type encodedData struct { - data []byte - err error -} - -// NewEncoder returns an Opus encoder that can encode images of the given width and height. It will -// also ensure that it produces key frames at the given interval. -func NewEncoder(sampleRate, channelCount int, latency time.Duration, logger golog.Logger) (ourcodec.AudioEncoder, error) { - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - enc := &encoder{ - chunkCh: make(chan wave.Audio, 1), - encodedCh: make(chan encodedData, 1), - logger: logger, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - } - - var builder codec.AudioEncoderBuilder - params, err := opus.NewParams() - if err != nil { - return nil, err - } - builder = ¶ms - params.BitRate = bitrate - params.Latency = opus.Latency(latency) - - codec, err := builder.BuildAudioEncoder(enc, prop.Media{ - Audio: prop.Audio{ - Latency: latency, - SampleRate: sampleRate, - ChannelCount: channelCount, - }, - }) - if err != nil { - return nil, err - } - enc.codec = codec - - enc.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - for { - if cancelCtx.Err() != nil { - return - } - data, release, err := enc.codec.Read() - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - release() - - select { - case <-cancelCtx.Done(): - return - case enc.encodedCh <- encodedData{dataCopy, err}: - } - } - }, func() { - defer enc.activeBackgroundWorkers.Done() - close(enc.encodedCh) - }) - - return enc, nil -} - -// Read returns an audio chunk for codec to process. -func (a *encoder) Read() (chunk wave.Audio, release func(), err error) { - if err := a.cancelCtx.Err(); err != nil { - return nil, func() {}, err - } - - select { - case <-a.cancelCtx.Done(): - return nil, func() {}, io.EOF - case chunk := <-a.chunkCh: - return chunk, func() {}, nil - } -} - -// Encode asks the codec to process the given audio chunk. -func (a *encoder) Encode(ctx context.Context, chunk wave.Audio) ([]byte, bool, error) { - defer func() { - select { - case <-ctx.Done(): - return - case <-a.cancelCtx.Done(): - return - case a.chunkCh <- chunk: - } - }() - if err := a.cancelCtx.Err(); err != nil { - return nil, false, err - } - select { - case <-ctx.Done(): - return nil, false, ctx.Err() - case <-a.cancelCtx.Done(): - return nil, false, a.cancelCtx.Err() - case encoded := <-a.encodedCh: - if encoded.err != nil { - return nil, false, encoded.err - } - return encoded.data, true, nil - default: - return nil, false, nil - } -} - -func (a *encoder) Close() { - a.cancelFunc() - a.activeBackgroundWorkers.Wait() -} diff --git a/gostream/codec/opus/utils.go b/gostream/codec/opus/utils.go deleted file mode 100644 index e0de5bc1c7d..00000000000 --- a/gostream/codec/opus/utils.go +++ /dev/null @@ -1,32 +0,0 @@ -package opus - -import ( - "time" - - "github.com/edaniels/golog" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec" -) - -// DefaultStreamConfig configures Opus as the audio encoder for a stream. -var DefaultStreamConfig gostream.StreamConfig - -func init() { - DefaultStreamConfig.AudioEncoderFactory = NewEncoderFactory() -} - -// NewEncoderFactory returns an Opus audio encoder factory. -func NewEncoderFactory() codec.AudioEncoderFactory { - return &factory{} -} - -type factory struct{} - -func (f *factory) New(sampleRate, channelCount int, latency time.Duration, logger golog.Logger) (codec.AudioEncoder, error) { - return NewEncoder(sampleRate, channelCount, latency, logger) -} - -func (f *factory) MIMEType() string { - return "audio/opus" -} diff --git a/gostream/codec/video_encoder.go b/gostream/codec/video_encoder.go deleted file mode 100644 index 61471d2603b..00000000000 --- a/gostream/codec/video_encoder.go +++ /dev/null @@ -1,28 +0,0 @@ -// Package codec defines the encoder and factory interfaces for encoding video frames and audio chunks. -package codec - -import ( - "context" - "image" - - "github.com/edaniels/golog" -) - -// DefaultKeyFrameInterval is the default interval chosen -// in order to produce high enough quality results at a low -// latency. -const DefaultKeyFrameInterval = 30 - -// A VideoEncoder is anything that can encode images into bytes. This means that -// the encoder must follow some type of format dictated by a type (see EncoderFactory.MimeType). -// An encoder that produces bytes of different encoding formats per call is invalid. -type VideoEncoder interface { - Encode(ctx context.Context, img image.Image) ([]byte, error) - Close() error -} - -// A VideoEncoderFactory produces VideoEncoders and provides information about the underlying encoder itself. -type VideoEncoderFactory interface { - New(height, width, keyFrameInterval int, logger golog.Logger) (VideoEncoder, error) - MIMEType() string -} diff --git a/gostream/codec/vpx/encoder.go b/gostream/codec/vpx/encoder.go deleted file mode 100644 index 22489fb1751..00000000000 --- a/gostream/codec/vpx/encoder.go +++ /dev/null @@ -1,94 +0,0 @@ -// Package vpx contains the vpx video codec. -package vpx - -import ( - "context" - "fmt" - "image" - - "github.com/edaniels/golog" - "github.com/pion/mediadevices/pkg/codec" - "github.com/pion/mediadevices/pkg/codec/vpx" - "github.com/pion/mediadevices/pkg/prop" - - ourcodec "go.viam.com/rdk/gostream/codec" -) - -type encoder struct { - codec codec.ReadCloser - img image.Image - logger golog.Logger -} - -// Version determines the version of a vpx codec. -type Version string - -// The set of allowed vpx versions. -const ( - Version8 Version = "vp8" - Version9 Version = "vp9" -) - -// Gives suitable results. Probably want to make this configurable this in the future. -const bitrate = 3_200_000 - -// NewEncoder returns a vpx encoder of the given type that can encode images of the given width and height. It will -// also ensure that it produces key frames at the given interval. -func NewEncoder(codecVersion Version, width, height, keyFrameInterval int, logger golog.Logger) (ourcodec.VideoEncoder, error) { - enc := &encoder{logger: logger} - - var builder codec.VideoEncoderBuilder - switch codecVersion { - case Version8: - params, err := vpx.NewVP8Params() - if err != nil { - return nil, err - } - builder = ¶ms - params.BitRate = bitrate - params.KeyFrameInterval = keyFrameInterval - case Version9: - params, err := vpx.NewVP9Params() - if err != nil { - return nil, err - } - builder = ¶ms - params.BitRate = bitrate - params.KeyFrameInterval = keyFrameInterval - default: - return nil, fmt.Errorf("unsupported vpx version: %s", codecVersion) - } - - codec, err := builder.BuildVideoEncoder(enc, prop.Media{ - Video: prop.Video{ - Width: width, - Height: height, - }, - }) - if err != nil { - return nil, err - } - enc.codec = codec - - return enc, nil -} - -// Read returns an image for codec to process. -func (v *encoder) Read() (img image.Image, release func(), err error) { - return v.img, nil, nil -} - -// Encode asks the codec to process the given image. -func (v *encoder) Encode(_ context.Context, img image.Image) ([]byte, error) { - v.img = img - data, release, err := v.codec.Read() - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - release() - return dataCopy, err -} - -// Close closes the encoder. -func (v *encoder) Close() error { - return v.codec.Close() -} diff --git a/gostream/codec/vpx/utils.go b/gostream/codec/vpx/utils.go deleted file mode 100644 index 4d4b80991df..00000000000 --- a/gostream/codec/vpx/utils.go +++ /dev/null @@ -1,41 +0,0 @@ -package vpx - -import ( - "fmt" - - "github.com/edaniels/golog" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec" -) - -// DefaultStreamConfig configures vpx as the encoder for a stream. -var DefaultStreamConfig gostream.StreamConfig - -func init() { - DefaultStreamConfig.VideoEncoderFactory = NewEncoderFactory(Version8) -} - -// NewEncoderFactory returns a vpx factory for the given vpx codec. -func NewEncoderFactory(codecVersion Version) codec.VideoEncoderFactory { - return &factory{codecVersion} -} - -type factory struct { - codecVersion Version -} - -func (f *factory) New(width, height, keyFrameInterval int, logger golog.Logger) (codec.VideoEncoder, error) { - return NewEncoder(f.codecVersion, width, height, keyFrameInterval, logger) -} - -func (f *factory) MIMEType() string { - switch f.codecVersion { - case Version8: - return "video/vp8" - case Version9: - return "video/vp9" - default: - panic(fmt.Errorf("unknown codec version %q", f.codecVersion)) - } -} diff --git a/gostream/codec/x264/encoder.go b/gostream/codec/x264/encoder.go deleted file mode 100644 index be1f33ca89a..00000000000 --- a/gostream/codec/x264/encoder.go +++ /dev/null @@ -1,71 +0,0 @@ -// Package x264 contains the x264 video codec. -package x264 - -import ( - "context" - "image" - - "github.com/edaniels/golog" - "github.com/pion/mediadevices/pkg/codec" - "github.com/pion/mediadevices/pkg/codec/x264" - "github.com/pion/mediadevices/pkg/prop" - - ourcodec "go.viam.com/rdk/gostream/codec" -) - -type encoder struct { - codec codec.ReadCloser - img image.Image - logger golog.Logger -} - -// Gives suitable results. Probably want to make this configurable this in the future. -const bitrate = 3_200_000 - -// NewEncoder returns an x264 encoder that can encode images of the given width and height. It will -// also ensure that it produces key frames at the given interval. -func NewEncoder(width, height, keyFrameInterval int, logger golog.Logger) (ourcodec.VideoEncoder, error) { - enc := &encoder{logger: logger} - - var builder codec.VideoEncoderBuilder - params, err := x264.NewParams() - if err != nil { - return nil, err - } - builder = ¶ms - params.BitRate = bitrate - params.KeyFrameInterval = keyFrameInterval - - codec, err := builder.BuildVideoEncoder(enc, prop.Media{ - Video: prop.Video{ - Width: width, - Height: height, - }, - }) - if err != nil { - return nil, err - } - enc.codec = codec - - return enc, nil -} - -// Read returns an image for codec to process. -func (v *encoder) Read() (img image.Image, release func(), err error) { - return v.img, nil, nil -} - -// Encode asks the codec to process the given image. -func (v *encoder) Encode(_ context.Context, img image.Image) ([]byte, error) { - v.img = img - data, release, err := v.codec.Read() - dataCopy := make([]byte, len(data)) - copy(dataCopy, data) - release() - return dataCopy, err -} - -// Close closes the encoder. -func (v *encoder) Close() error { - return v.codec.Close() -} diff --git a/gostream/codec/x264/encoder_test.go b/gostream/codec/x264/encoder_test.go deleted file mode 100644 index b9285b627d8..00000000000 --- a/gostream/codec/x264/encoder_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package x264 - -import ( - "bytes" - "context" - "image" - "image/color" - "image/jpeg" - "image/png" - "os" - "testing" - - "github.com/edaniels/golog" - "github.com/nfnt/resize" - "go.viam.com/test" -) - -const ( - DefaultKeyFrameInterval = 30 - Width = 640 - Height = 480 -) - -func pngToImage(b *testing.B, loc string) (image.Image, error) { - b.Helper() - openBytes, err := os.ReadFile(loc) - test.That(b, err, test.ShouldBeNil) - return png.Decode(bytes.NewReader(openBytes)) -} - -func resizeImg(b *testing.B, img image.Image, width, height uint) image.Image { - b.Helper() - newImage := resize.Resize(width, height, img, resize.Lanczos3) - return newImage -} - -func convertToYCbCr(b *testing.B, src image.Image) (image.Image, error) { - b.Helper() - bf := new(bytes.Buffer) - err := jpeg.Encode(bf, src, nil) - test.That(b, err, test.ShouldBeNil) - dst, _, err := image.Decode(bf) - test.That(b, err, test.ShouldBeNil) - test.That(b, dst.ColorModel(), test.ShouldResemble, color.YCbCrModel) - return dst, err -} - -func getResizedImageFromFile(b *testing.B, loc string) image.Image { - b.Helper() - img, err := pngToImage(b, loc) - test.That(b, err, test.ShouldBeNil) - return resizeImg(b, img, uint(Width), uint(Height)) -} - -func BenchmarkEncodeRGBA(b *testing.B) { - var w bool - var logger golog.Logger - - imgCyan := getResizedImageFromFile(b, "../../data/cyan.png") - imgFuchsia := getResizedImageFromFile(b, "../../data/fuchsia.png") - ctx := context.Background() - encoder, err := NewEncoder(Width, Height, DefaultKeyFrameInterval, logger) - test.That(b, err, test.ShouldBeNil) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - if w { - _, err = encoder.Encode(ctx, imgCyan) - test.That(b, err, test.ShouldBeNil) - } else { - _, err = encoder.Encode(ctx, imgFuchsia) - test.That(b, err, test.ShouldBeNil) - } - w = !w - } -} - -func BenchmarkEncodeYCbCr(b *testing.B) { - var w bool - var logger golog.Logger - imgCyan := getResizedImageFromFile(b, "../../data/cyan.png") - imgFuchsia := getResizedImageFromFile(b, "../../data/fuchsia.png") - - imgFY, err := convertToYCbCr(b, imgFuchsia) - test.That(b, err, test.ShouldBeNil) - - imgCY, err := convertToYCbCr(b, imgCyan) - test.That(b, err, test.ShouldBeNil) - - encoder, err := NewEncoder(Width, Height, DefaultKeyFrameInterval, logger) - test.That(b, err, test.ShouldBeNil) - - ctx := context.Background() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if w { - _, err = encoder.Encode(ctx, imgFY) - test.That(b, err, test.ShouldBeNil) - } else { - _, err = encoder.Encode(ctx, imgCY) - test.That(b, err, test.ShouldBeNil) - } - w = !w - } -} diff --git a/gostream/codec/x264/utils.go b/gostream/codec/x264/utils.go deleted file mode 100644 index 36421f3a554..00000000000 --- a/gostream/codec/x264/utils.go +++ /dev/null @@ -1,30 +0,0 @@ -package x264 - -import ( - "github.com/edaniels/golog" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec" -) - -// DefaultStreamConfig configures x264 as the encoder for a stream. -var DefaultStreamConfig gostream.StreamConfig - -func init() { - DefaultStreamConfig.VideoEncoderFactory = NewEncoderFactory() -} - -// NewEncoderFactory returns an x264 encoder factory. -func NewEncoderFactory() codec.VideoEncoderFactory { - return &factory{} -} - -type factory struct{} - -func (f *factory) New(width, height, keyFrameInterval int, logger golog.Logger) (codec.VideoEncoder, error) { - return NewEncoder(width, height, keyFrameInterval, logger) -} - -func (f *factory) MIMEType() string { - return "video/H264" -} diff --git a/gostream/driver.go b/gostream/driver.go deleted file mode 100644 index c4d23cc95f5..00000000000 --- a/gostream/driver.go +++ /dev/null @@ -1,27 +0,0 @@ -package gostream - -import ( - "fmt" - "sync" - - "go.viam.com/utils" -) - -// DriverInUseError is returned when closing drivers that still being read from. -type DriverInUseError struct { - label string -} - -func (err *DriverInUseError) Error() string { - return fmt.Sprintf("driver is still in use: %s", err.label) -} - -// driverRefManager is a lockable map of drivers and reference counts of video readers -// that use them. -type driverRefManager struct { - refs map[string]utils.RefCountedValue - mu sync.Mutex -} - -// initialize a global driverRefManager. -var driverRefs = driverRefManager{refs: map[string]utils.RefCountedValue{}} diff --git a/gostream/etc/.golangci.yaml b/gostream/etc/.golangci.yaml deleted file mode 100644 index db44e4cd5b8..00000000000 --- a/gostream/etc/.golangci.yaml +++ /dev/null @@ -1,94 +0,0 @@ -service: - golangci-lint-version: 1.50.x -run: - deadline: 900s - modules-download-mode: readonly - skip-dirs: - - genfiles$ - - gen$ - - vendor$ - - test$ - tests: true -linters: - enable-all: true - disable: - - asasalint - - containedctx - - cyclop - - deadcode - - exhaustivestruct - - exhaustruct - - forcetypeassert - - funlen - - gocognit - - godox - - goerr113 - - gochecknoglobals - - gochecknoinits - - gocyclo - - gofmt - - goimports - - golint - - gomnd - - ifshort - - importas - - interfacebloat - - interfacer - - ireturn - - maintidx - - maligned - - makezero - - nestif - - nlreturn - - nosnakecase - - nonamedreturns - - nosprintfhostport - - paralleltest - - prealloc - - scopelint - - structcheck - - tagliatelle - - testpackage - - unparam # broken with generics - - varcheck - - varnamelen - - wrapcheck - - wsl -linters-settings: - errcheck: - check-blank: true - gci: - sections: - - standard - - default - - prefix(gostream) - gofumpt: - lang-version: "1.18" - extra-rules: true - gosec: - excludes: - - G601 - govet: - enable-all: true - disable: - - fieldalignment - - shadow - lll: - line-length: 140 -issues: - exclude: - - composites - exclude-rules: - - path: _test\.go$|^tests/|^samples/ - linters: - - errcheck - - contextcheck - - exhaustive - - forcetypeassert - - goconst - - gosec - - govet - - noctx - exclude-use-default: false - max-per-linter: 0 - max-same-issues: 0 diff --git a/gostream/etc/buf.web.gen.yaml b/gostream/etc/buf.web.gen.yaml deleted file mode 100644 index 7759d726aed..00000000000 --- a/gostream/etc/buf.web.gen.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: v1 -plugins: - - remote: buf.build/protocolbuffers/plugins/js:v3.20.1-1 - out: dist/js - opt: - - import_style=commonjs - - remote: buf.build/grpc/plugins/web:v1.3.1-2 - out: dist/js - opt: - - import_style=commonjs - - mode=grpcwebtext - - remote: buf.build/euskadi31/plugins/protoc-gen-ts:v0.15.0-1 - out: dist/js - opt: - - service=grpc-web diff --git a/gostream/ffmpeg/Darwin-arm64/lib/libavcodec.a b/gostream/ffmpeg/Darwin-arm64/lib/libavcodec.a deleted file mode 100644 index c8452ba0473..00000000000 Binary files a/gostream/ffmpeg/Darwin-arm64/lib/libavcodec.a and /dev/null differ diff --git a/gostream/ffmpeg/Darwin-arm64/lib/libavdevice.a b/gostream/ffmpeg/Darwin-arm64/lib/libavdevice.a deleted file mode 100644 index f02d0598e47..00000000000 Binary files a/gostream/ffmpeg/Darwin-arm64/lib/libavdevice.a and /dev/null differ diff --git a/gostream/ffmpeg/Darwin-arm64/lib/libavfilter.a b/gostream/ffmpeg/Darwin-arm64/lib/libavfilter.a deleted file mode 100644 index a2324c28009..00000000000 Binary files a/gostream/ffmpeg/Darwin-arm64/lib/libavfilter.a and /dev/null differ diff --git a/gostream/ffmpeg/Darwin-arm64/lib/libavformat.a b/gostream/ffmpeg/Darwin-arm64/lib/libavformat.a deleted file mode 100644 index 6c599766785..00000000000 Binary files a/gostream/ffmpeg/Darwin-arm64/lib/libavformat.a and /dev/null differ diff --git a/gostream/ffmpeg/Darwin-arm64/lib/libavutil.a b/gostream/ffmpeg/Darwin-arm64/lib/libavutil.a deleted file mode 100644 index e7762883233..00000000000 Binary files a/gostream/ffmpeg/Darwin-arm64/lib/libavutil.a and /dev/null differ diff --git a/gostream/ffmpeg/Darwin-arm64/lib/libswresample.a b/gostream/ffmpeg/Darwin-arm64/lib/libswresample.a deleted file mode 100644 index c4558ebe44d..00000000000 Binary files a/gostream/ffmpeg/Darwin-arm64/lib/libswresample.a and /dev/null differ diff --git a/gostream/ffmpeg/Darwin-arm64/lib/libswscale.a b/gostream/ffmpeg/Darwin-arm64/lib/libswscale.a deleted file mode 100644 index 8d1b8d4092e..00000000000 Binary files a/gostream/ffmpeg/Darwin-arm64/lib/libswscale.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-aarch64/lib/libavcodec.a b/gostream/ffmpeg/Linux-aarch64/lib/libavcodec.a deleted file mode 100644 index 421fe93d402..00000000000 Binary files a/gostream/ffmpeg/Linux-aarch64/lib/libavcodec.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-aarch64/lib/libavdevice.a b/gostream/ffmpeg/Linux-aarch64/lib/libavdevice.a deleted file mode 100644 index ec5f567dbab..00000000000 Binary files a/gostream/ffmpeg/Linux-aarch64/lib/libavdevice.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-aarch64/lib/libavfilter.a b/gostream/ffmpeg/Linux-aarch64/lib/libavfilter.a deleted file mode 100644 index 944f17e57de..00000000000 Binary files a/gostream/ffmpeg/Linux-aarch64/lib/libavfilter.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-aarch64/lib/libavformat.a b/gostream/ffmpeg/Linux-aarch64/lib/libavformat.a deleted file mode 100644 index 28509581c5b..00000000000 Binary files a/gostream/ffmpeg/Linux-aarch64/lib/libavformat.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-aarch64/lib/libavutil.a b/gostream/ffmpeg/Linux-aarch64/lib/libavutil.a deleted file mode 100644 index bf9c8be42e2..00000000000 Binary files a/gostream/ffmpeg/Linux-aarch64/lib/libavutil.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-aarch64/lib/libswresample.a b/gostream/ffmpeg/Linux-aarch64/lib/libswresample.a deleted file mode 100644 index cb132593da5..00000000000 Binary files a/gostream/ffmpeg/Linux-aarch64/lib/libswresample.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-aarch64/lib/libswscale.a b/gostream/ffmpeg/Linux-aarch64/lib/libswscale.a deleted file mode 100644 index 348ca1ddcfc..00000000000 Binary files a/gostream/ffmpeg/Linux-aarch64/lib/libswscale.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-armv7l/lib/libavcodec.a b/gostream/ffmpeg/Linux-armv7l/lib/libavcodec.a deleted file mode 100644 index cedffbff8ce..00000000000 Binary files a/gostream/ffmpeg/Linux-armv7l/lib/libavcodec.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-armv7l/lib/libavdevice.a b/gostream/ffmpeg/Linux-armv7l/lib/libavdevice.a deleted file mode 100644 index 16e0636f39b..00000000000 Binary files a/gostream/ffmpeg/Linux-armv7l/lib/libavdevice.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-armv7l/lib/libavfilter.a b/gostream/ffmpeg/Linux-armv7l/lib/libavfilter.a deleted file mode 100644 index 4366beb3ac8..00000000000 Binary files a/gostream/ffmpeg/Linux-armv7l/lib/libavfilter.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-armv7l/lib/libavformat.a b/gostream/ffmpeg/Linux-armv7l/lib/libavformat.a deleted file mode 100644 index 098c109282c..00000000000 Binary files a/gostream/ffmpeg/Linux-armv7l/lib/libavformat.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-armv7l/lib/libavutil.a b/gostream/ffmpeg/Linux-armv7l/lib/libavutil.a deleted file mode 100644 index 29260e7c7f8..00000000000 Binary files a/gostream/ffmpeg/Linux-armv7l/lib/libavutil.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-armv7l/lib/libswresample.a b/gostream/ffmpeg/Linux-armv7l/lib/libswresample.a deleted file mode 100644 index 9de87817254..00000000000 Binary files a/gostream/ffmpeg/Linux-armv7l/lib/libswresample.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-armv7l/lib/libswscale.a b/gostream/ffmpeg/Linux-armv7l/lib/libswscale.a deleted file mode 100644 index 400d9cb85c3..00000000000 Binary files a/gostream/ffmpeg/Linux-armv7l/lib/libswscale.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-x86_64/lib/libavcodec.a b/gostream/ffmpeg/Linux-x86_64/lib/libavcodec.a deleted file mode 100644 index 0cf986bb475..00000000000 Binary files a/gostream/ffmpeg/Linux-x86_64/lib/libavcodec.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-x86_64/lib/libavdevice.a b/gostream/ffmpeg/Linux-x86_64/lib/libavdevice.a deleted file mode 100644 index b8eee4e0aeb..00000000000 Binary files a/gostream/ffmpeg/Linux-x86_64/lib/libavdevice.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-x86_64/lib/libavfilter.a b/gostream/ffmpeg/Linux-x86_64/lib/libavfilter.a deleted file mode 100644 index 5cbdd19f7d1..00000000000 Binary files a/gostream/ffmpeg/Linux-x86_64/lib/libavfilter.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-x86_64/lib/libavformat.a b/gostream/ffmpeg/Linux-x86_64/lib/libavformat.a deleted file mode 100644 index 647d097c24a..00000000000 Binary files a/gostream/ffmpeg/Linux-x86_64/lib/libavformat.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-x86_64/lib/libavutil.a b/gostream/ffmpeg/Linux-x86_64/lib/libavutil.a deleted file mode 100644 index 08d2e925a70..00000000000 Binary files a/gostream/ffmpeg/Linux-x86_64/lib/libavutil.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-x86_64/lib/libswresample.a b/gostream/ffmpeg/Linux-x86_64/lib/libswresample.a deleted file mode 100644 index 616d7cb3ce9..00000000000 Binary files a/gostream/ffmpeg/Linux-x86_64/lib/libswresample.a and /dev/null differ diff --git a/gostream/ffmpeg/Linux-x86_64/lib/libswscale.a b/gostream/ffmpeg/Linux-x86_64/lib/libswscale.a deleted file mode 100644 index 280957263df..00000000000 Binary files a/gostream/ffmpeg/Linux-x86_64/lib/libswscale.a and /dev/null differ diff --git a/gostream/ffmpeg/avcodec/avcodec.go b/gostream/ffmpeg/avcodec/avcodec.go deleted file mode 100644 index 6b7f7bafda1..00000000000 --- a/gostream/ffmpeg/avcodec/avcodec.go +++ /dev/null @@ -1,316 +0,0 @@ -//go:build cgo && ((linux && !android) || (darwin && arm64)) - -// Package avcodec is a wrapper around FFmpeg/release6.1. -// See: https://github.com/FFmpeg/FFmpeg/tree/release/6.1 -package avcodec - -//#cgo CFLAGS: -I${SRCDIR}/../include -//#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/../Linux-aarch64/lib -lavformat -lavcodec -lavutil -lm -//#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../Linux-x86_64/lib -lavformat -lavcodec -lavutil -lm -//#cgo linux,arm LDFLAGS: -L${SRCDIR}/../Linux-armv7l/lib -lavformat -lavcodec -lavutil -lm -//#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/../Darwin-arm64/lib -lavformat -lavcodec -lavutil -lm -//#include -//#include -//#include -//#include -import "C" - -import ( - "unsafe" - - "go.viam.com/rdk/gostream/ffmpeg/avlog" -) - -const ( - // AvPixFmtYuv420p the pixel format AV_PIX_FMT_YUV420P - AvPixFmtYuv420p = C.AV_PIX_FMT_YUV420P - // Target bitrate in bits per second. - bitrate = 1_200_000 - // Target bitrate tolerance factor. - // E.g., 2.0 gives 100% deviation from the target bitrate. - bitrateDeviation = 2 -) - -type ( - // Codec an AVCodec - Codec C.struct_AVCodec - // Context an AVCodecContext - Context C.struct_AVCodecContext - // Dictionary an AVDictionary - Dictionary C.struct_AVDictionary - // Frame an AVFrame - Frame C.struct_AVFrame - // Packet an AVPacket - Packet C.struct_AVPacket - // PixelFormat an AVPixelFormat - PixelFormat C.enum_AVPixelFormat -) - -// FindEncoderByName Find a registered encoder with the specified name. -// -// @param name the name of the requested encoder -// @return An encoder if one was found, NULL otherwise. -func FindEncoderByName(c string) *Codec { - return (*Codec)(C.avcodec_find_encoder_by_name(C.CString(c))) -} - -// PacketAlloc Allocate an AVPacket and set its fields to default values. The resulting -// struct must be freed using av_packet_free(). -// -// @return An AVPacket filled with default values or NULL on failure. -// -// @note this only allocates the AVPacket itself, not the data buffers. Those -// must be allocated through other means such as av_new_packet. -// -// @see av_new_packet -func PacketAlloc() *Packet { - return (*Packet)(C.av_packet_alloc()) -} - -// AllocContext3 Allocate an AVCodecContext and set its fields to default values. The -// resulting struct should be freed with avcodec_free_context(). -// -// @param codec if non-NULL, allocate private data and initialize defaults -// -// for the given codec. It is illegal to then call avcodec_open2() -// with a different codec. -// If NULL, then the codec-specific defaults won't be initialized, -// which may result in suboptimal default settings (this is -// important mainly for encoders, e.g. libx264). -// -// @return An AVCodecContext filled with default values or NULL on failure. -func (c *Codec) AllocContext3() *Context { - return (*Context)(C.avcodec_alloc_context3((*C.struct_AVCodec)(c))) -} - -// FreeContext Free the codec context and everything associated with it and write NULL to -// the provided pointer. -func (ctxt *Context) FreeContext() { - pCtxt := (*C.struct_AVCodecContext)(ctxt) - //nolint:gocritic // suppresses "dupSubExpr: suspicious identical LHS and RHS for `==` operator" - C.avcodec_free_context(&pCtxt) -} - -// SetEncodeParams sets the context's width, height, pixel format (pxlFmt), if it has b-frames and GOP size. -func (ctxt *Context) SetEncodeParams(width, height int, pxlFmt PixelFormat, hasBFrames bool, gopSize int) { - ctxt.width = C.int(width) - ctxt.height = C.int(height) - ctxt.bit_rate = bitrate - ctxt.gop_size = C.int(gopSize) - if hasBFrames { - ctxt.has_b_frames = 1 - } else { - ctxt.has_b_frames = 0 - } - ctxt.pix_fmt = int32(pxlFmt) - ctxt.rc_buffer_size = bitrate * bitrateDeviation -} - -// SetFramerate sets the context's framerate -func (ctxt *Context) SetFramerate(fps int) { - ctxt.framerate.num = C.int(fps) - ctxt.framerate.den = C.int(1) - - // timebase should be 1/framerate - ctxt.time_base.num = C.int(1) - ctxt.time_base.den = C.int(fps) -} - -// Open2 Initialize the AVCodecContext to use the given AVCodec. Prior to using this -// function the context has to be allocated with avcodec_alloc_context3(). -// -// The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(), -// avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for -// retrieving a codec. -// -// Depending on the codec, you might need to set options in the codec context -// also for decoding (e.g. width, height, or the pixel or audio sample format in -// the case the information is not available in the bitstream, as when decoding -// raw audio or video). -// -// Options in the codec context can be set either by setting them in the options -// AVDictionary, or by setting the values in the context itself, directly or by -// using the av_opt_set() API before calling this function. -// -// Example: -// @code -// av_dict_set(&opts, "b", "2.5M", 0); -// codec = avcodec_find_decoder(AV_CODEC_ID_H264); -// if (!codec) -// -// exit(1); -// -// context = avcodec_alloc_context3(codec); -// -// if (avcodec_open2(context, codec, opts) < 0) -// -// exit(1); -// -// @endcode -// -// In the case AVCodecParameters are available (e.g. when demuxing a stream -// using libavformat, and accessing the AVStream contained in the demuxer), the -// codec parameters can be copied to the codec context using -// avcodec_parameters_to_context(), as in the following example: -// -// @code -// AVStream *stream = ...; -// context = avcodec_alloc_context3(codec); -// if (avcodec_parameters_to_context(context, stream->codecpar) < 0) -// -// exit(1); -// -// if (avcodec_open2(context, codec, NULL) < 0) -// -// exit(1); -// -// @endcode -// -// @note Always call this function before using decoding routines (such as -// @ref avcodec_receive_frame()). -// -// @param avctx The context to initialize. -// @param codec The codec to open this context for. If a non-NULL codec has been -// -// previously passed to avcodec_alloc_context3() or -// for this context, then this parameter MUST be either NULL or -// equal to the previously passed codec. -// -// @param options A dictionary filled with AVCodecContext and codec-private -// -// options, which are set on top of the options already set in -// avctx, can be NULL. On return this object will be filled with -// options that were not found in the avctx codec context. -// -// @return zero on success, a negative value on error -// @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(), -// -// av_dict_set(), av_opt_set(), av_opt_find(), avcodec_parameters_to_context() -func (ctxt *Context) Open2(c *Codec, d **Dictionary) int { - return int(C.avcodec_open2((*C.struct_AVCodecContext)(ctxt), (*C.struct_AVCodec)(c), (**C.struct_AVDictionary)(unsafe.Pointer(d)))) -} - -// Close a given AVCodecContext and free all the data associated with it -// (but not the AVCodecContext itself). -// -// Calling this function on an AVCodecContext that hasn't been opened will free -// the codec-specific data allocated in avcodec_alloc_context3() with a non-NULL -// codec. Subsequent calls will do nothing. -// -// @note Do not use this function. Use avcodec_free_context() to destroy a -// codec context (either open or closed). Opening and closing a codec context -// multiple times is not supported anymore -- use multiple codec contexts -// instead. -func (ctxt *Context) Close() int { - return int(C.avcodec_close((*C.struct_AVCodecContext)(ctxt))) -} - -// Unref Wipe the packet. -// -// Unreference the buffer referenced by the packet and reset the -// remaining packet fields to their default values. -// -// @param pkt The packet to be unreferenced. -func (p *Packet) Unref() { - C.av_packet_unref((*C.struct_AVPacket)(p)) -} - -// SendFrame Supply a raw video or audio frame to the encoder. Use avcodec_receive_packet() -// to retrieve buffered output packets. -// -// @param avctx codec context -// @param[in] frame AVFrame containing the raw audio or video frame to be encoded. -// -// Ownership of the frame remains with the caller, and the -// encoder will not write to the frame. The encoder may create -// a reference to the frame data (or copy it if the frame is -// not reference-counted). -// It can be NULL, in which case it is considered a flush -// packet. This signals the end of the stream. If the encoder -// still has packets buffered, it will return them after this -// call. Once flushing mode has been entered, additional flush -// packets are ignored, and sending frames will return -// AVERROR_EOF. -// -// For audio: -// If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame -// can have any number of samples. -// If it is not set, frame->nb_samples must be equal to -// avctx->frame_size for all frames except the last. -// The final frame may be smaller than avctx->frame_size. -// -// @retval 0 success -// @retval AVERROR(EAGAIN) input is not accepted in the current state - user must -// -// read output with avcodec_receive_packet() (once all -// output is read, the packet should be resent, and the -// call will not fail with EAGAIN). -// -// @retval AVERROR_EOF the encoder has been flushed, and no new frames can -// -// be sent to it -// -// @retval AVERROR(EINVAL) codec not opened, it is a decoder, or requires flush -// @retval AVERROR(ENOMEM) failed to add packet to internal queue, or similar -// @retval "another negative error code" legitimate encoding errors -func (ctxt *Context) SendFrame(f *Frame) int { - return int(C.avcodec_send_frame((*C.struct_AVCodecContext)(ctxt), (*C.struct_AVFrame)(f))) -} - -// ReceivePacket Read encoded data from the encoder. -// -// @param avctx codec context -// @param avpkt This will be set to a reference-counted packet allocated by the -// -// encoder. Note that the function will always call -// av_packet_unref(avpkt) before doing anything else. -// -// @retval 0 success -// @retval AVERROR(EAGAIN) output is not available in the current state - user must -// -// try to send input -// -// @retval AVERROR_EOF the encoder has been fully flushed, and there will be no -// -// more output packets -// -// @retval AVERROR(EINVAL) codec not opened, or it is a decoder -// @retval "another negative error code" legitimate encoding errors -func (ctxt *Context) ReceivePacket(a *Packet) int { - return int(C.avcodec_receive_packet((*C.struct_AVCodecContext)(ctxt), (*C.struct_AVPacket)(a))) -} - -// Data returns the packet's data -func (p *Packet) Data() *uint8 { - return (*uint8)(p.data) -} - -// Size returns the packet size -func (p *Packet) Size() int { - return int(p.size) -} - -// EncoderIsAvailable returns true if the given encoder is available, false otherwise. -func EncoderIsAvailable(enc string) bool { - // Quiet logging during function execution, but reset afterward. - lvl := avlog.GetLevel() - defer avlog.SetLevel(lvl) - avlog.SetLevel(avlog.LogQuiet) - - codec := FindEncoderByName(enc) - if codec == nil { - return false - } - - context := codec.AllocContext3() - if context == nil { - return false - } - defer context.FreeContext() - - // Only need positive values - context.SetEncodeParams(1, 1, AvPixFmtYuv420p, false, 1) - context.SetFramerate(1) - - return context.Open2(codec, nil) == 0 -} diff --git a/gostream/ffmpeg/avcodec/avcodec_test.go b/gostream/ffmpeg/avcodec/avcodec_test.go deleted file mode 100644 index 4ba30a91407..00000000000 --- a/gostream/ffmpeg/avcodec/avcodec_test.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build cgo && ((linux && !android) || (darwin && arm64)) - -package avcodec - -import ( - "testing" - - "go.viam.com/test" -) - -func TestEncoderIsAvailable(t *testing.T) { - isAvailable := EncoderIsAvailable("foo") - test.That(t, isAvailable, test.ShouldBeFalse) -} diff --git a/gostream/ffmpeg/avlog/avlog.go b/gostream/ffmpeg/avlog/avlog.go deleted file mode 100644 index f4f9d73cb97..00000000000 --- a/gostream/ffmpeg/avlog/avlog.go +++ /dev/null @@ -1,56 +0,0 @@ -//go:build cgo && ((linux && !android) || (darwin && arm64)) - -// Package avlog is libav's logging facilities -// See https://ffmpeg.org/doxygen/3.0/log_8h.html -package avlog - -//#cgo CFLAGS: -I${SRCDIR}/../include -//#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/../Linux-aarch64/lib -lavformat -lavcodec -lavutil -lm -//#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../Linux-x86_64/lib -lavformat -lavcodec -lavutil -lm -//#cgo linux,arm LDFLAGS: -L${SRCDIR}/../Linux-armv7l/lib -lavformat -lavcodec -lavutil -lm -//#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/../Darwin-arm64/lib -lavformat -lavcodec -lavutil -lm -//#include -import "C" - -const ( - // LogQuiet Print no output. - LogQuiet = (iota * 8) - 8 - - // LogPanic Something went really wrong and we will crash now. - LogPanic - - // LogFatal Something went wrong and recovery is not possible. - // For example, no header was found for a format which depends - // on headers or an illegal combination of parameters is used. - LogFatal - - // LogError Something went wrong and cannot losslessly be recovered. - // However, not all future data is affected. - LogError - - // LogWarning Something somehow does not look correct. This may or may not - // lead to problems. An example would be the use of '-vstrict -2'. - LogWarning - - // LogInfo Standard information. - LogInfo - - // LogVerbose Detailed information. - LogVerbose - - // LogDebug Stuff which is only useful for libav* developers. - LogDebug - - // LogTrace Extremely verbose debugging, useful for libav* development. - LogTrace -) - -// SetLevel sets the log level -func SetLevel(level int) { - C.av_log_set_level(C.int(level)) -} - -// GetLevel returns the current log level -func GetLevel() int { - return int(C.av_log_get_level()) -} diff --git a/gostream/ffmpeg/avutil/avutil.go b/gostream/ffmpeg/avutil/avutil.go deleted file mode 100644 index 8efd33120cb..00000000000 --- a/gostream/ffmpeg/avutil/avutil.go +++ /dev/null @@ -1,145 +0,0 @@ -//go:build cgo && ((linux && !android) || (darwin && arm64)) - -// Package avutil is a wrapper around FFmpeg/release6.1. -// See: https://github.com/FFmpeg/FFmpeg/tree/release/6.1 -package avutil - -//#cgo CFLAGS: -I${SRCDIR}/../include -//#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/../Linux-aarch64/lib -lavformat -lavcodec -lavutil -lm -//#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../Linux-x86_64/lib -lavformat -lavcodec -lavutil -lm -//#cgo linux,arm LDFLAGS: -L${SRCDIR}/../Linux-armv7l/lib -lavformat -lavcodec -lavutil -lm -//#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/../Darwin-arm64/lib -lavformat -lavcodec -lavutil -lm -//#include -//#include -//#include -// static const char *error2string(int code) { return av_err2str(code); } -import "C" - -import ( - "image" - "reflect" - "unsafe" - - "github.com/pkg/errors" -) - -// Frame an AVFrame -type Frame C.struct_AVFrame - -// FrameAlloc Allocate an AVFrame and set its fields to default values. The resulting -// struct must be freed using av_frame_free(). -// -// @return An AVFrame filled with default values or NULL on failure. -// -// @note this only allocates the AVFrame itself, not the data buffers. Those -// must be allocated through other means, e.g. with av_frame_get_buffer() or -// manually. -func FrameAlloc() *Frame { - return (*Frame)(unsafe.Pointer(C.av_frame_alloc())) -} - -// SetFrame sets the given frame from the given width (w), height (h), and pixel format (pixFmt) -func SetFrame(f *Frame, w, h, pixFmt int) error { - f.width = C.int(w) - f.height = C.int(h) - f.format = C.int(pixFmt) - if ret := C.av_frame_get_buffer((*C.struct_AVFrame)(unsafe.Pointer(f)), 0 /*alignment*/); ret < 0 { - return errors.Errorf("error allocating avframe buffer: return value %d", int(ret)) - } - return nil -} - -// FrameMakeWritable Ensure that the frame data is writable, avoiding data copy if possible. -// -// Do nothing if the frame is writable, allocate new buffers and copy the data -// if it is not. Non-refcounted frames behave as non-writable, i.e. a copy -// is always made. -// -// @return 0 on success, a negative AVERROR on error. -// -// @see av_frame_is_writable(), av_buffer_is_writable(), -// av_buffer_make_writable() -func FrameMakeWritable(f *Frame) int { - return int(C.av_frame_make_writable((*C.struct_AVFrame)(unsafe.Pointer(f)))) -} - -// FrameUnref Unreference all the buffers referenced by frame and reset the frame fields. -func FrameUnref(f *Frame) { - C.av_frame_unref((*C.struct_AVFrame)(unsafe.Pointer(f))) -} - -func ptr(buf []byte) *C.uint8_t { - h := (*reflect.SliceHeader)(unsafe.Pointer(&buf)) - return (*C.uint8_t)(unsafe.Pointer(h.Data)) -} - -// SetFrameFromImgMacroAlign sets the frame from the given image.YCbCr adding -// line padding to the image to ensure that the data is aligned to the given boundary. -// For example see alignment requirements for the Raspberry Pi GPU codec: -// https://github.com/raspberrypi/linux/blob/rpi-6.1.y/drivers/staging/vc04_services/bcm2835-codec/bcm2835-v4l2-codec.c#L174 -func (f *Frame) SetFrameFromImgMacroAlign(img *image.YCbCr, boundary int) { - // Calculating padded strides - // Rounding up to next multiple of boundary value - paddedYStride := ((img.YStride + boundary - 1) / boundary) * boundary - // UV half the Y stride for 4:2:0 - paddedCbCrStride := paddedYStride / 2 - - // Allocate new buffers with padding - // These will be freed by the GC - paddedY := make([]byte, paddedYStride*img.Rect.Dy()) - paddedCb := make([]byte, paddedCbCrStride*img.Rect.Dy()/2) - paddedCr := make([]byte, paddedCbCrStride*img.Rect.Dy()/2) - - // Copy data from img to padded buffers line by line - for i := 0; i < img.Rect.Dy(); i++ { - copy(paddedY[i*paddedYStride:(i+1)*paddedYStride], img.Y[i*img.YStride:]) - } - for i := 0; i < img.Rect.Dy()/2; i++ { - copy(paddedCb[i*paddedCbCrStride:(i+1)*paddedCbCrStride], img.Cb[i*img.CStride:]) - copy(paddedCr[i*paddedCbCrStride:(i+1)*paddedCbCrStride], img.Cr[i*img.CStride:]) - } - - // Update AVFrame data pointers and linesize - // Casting from go slice to C array without changing memory - f.data[0] = (*C.uchar)(unsafe.Pointer(&paddedY[0])) - f.data[1] = (*C.uchar)(unsafe.Pointer(&paddedCb[0])) - f.data[2] = (*C.uchar)(unsafe.Pointer(&paddedCr[0])) - f.linesize[0] = C.int(paddedYStride) - f.linesize[1] = C.int(paddedCbCrStride) - f.linesize[2] = C.int(paddedCbCrStride) -} - -// SetFrameFromImg sets the frame from the given image.YCbCr -func (f *Frame) SetFrameFromImg(img *image.YCbCr) { - f.data[0] = ptr(img.Y) - f.data[1] = ptr(img.Cb) - f.data[2] = ptr(img.Cr) - - w := C.int(img.Bounds().Dx()) - f.linesize[0] = w - f.linesize[1] = w / 2 - f.linesize[2] = w / 2 -} - -// SetFramePTS sets the presentation time stamp (PTS) -func (f *Frame) SetFramePTS(pts int64) { - f.pts = C.int64_t(pts) -} - -const ( - // EAGAIN Resource temporarily unavailable - EAGAIN = -11 - // EOF End of file - EOF = int(C.AVERROR_EOF) - // Success no errors - Success = 0 -) - -// ErrorFromCode returns an error from the given code -func ErrorFromCode(code int) error { - if code >= 0 { - return nil - } - - return errors.New(C.GoString(C.error2string(C.int(code)))) -} diff --git a/gostream/ffmpeg/include/libavcodec/ac3_parser.h b/gostream/ffmpeg/include/libavcodec/ac3_parser.h deleted file mode 100644 index ff8cc4cf093..00000000000 --- a/gostream/ffmpeg/include/libavcodec/ac3_parser.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * AC-3 parser prototypes - * Copyright (c) 2003 Fabrice Bellard - * Copyright (c) 2003 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_AC3_PARSER_H -#define AVCODEC_AC3_PARSER_H - -#include -#include - -/** - * Extract the bitstream ID and the frame size from AC-3 data. - */ -int av_ac3_parse_header(const uint8_t *buf, size_t size, - uint8_t *bitstream_id, uint16_t *frame_size); - - -#endif /* AVCODEC_AC3_PARSER_H */ diff --git a/gostream/ffmpeg/include/libavcodec/adts_parser.h b/gostream/ffmpeg/include/libavcodec/adts_parser.h deleted file mode 100644 index f85becd1313..00000000000 --- a/gostream/ffmpeg/include/libavcodec/adts_parser.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_ADTS_PARSER_H -#define AVCODEC_ADTS_PARSER_H - -#include -#include - -#define AV_AAC_ADTS_HEADER_SIZE 7 - -/** - * Extract the number of samples and frames from AAC data. - * @param[in] buf pointer to AAC data buffer - * @param[out] samples Pointer to where number of samples is written - * @param[out] frames Pointer to where number of frames is written - * @return Returns 0 on success, error code on failure. - */ -int av_adts_header_parse(const uint8_t *buf, uint32_t *samples, - uint8_t *frames); - -#endif /* AVCODEC_ADTS_PARSER_H */ diff --git a/gostream/ffmpeg/include/libavcodec/avcodec.h b/gostream/ffmpeg/include/libavcodec/avcodec.h deleted file mode 100644 index 7fb44e28f45..00000000000 --- a/gostream/ffmpeg/include/libavcodec/avcodec.h +++ /dev/null @@ -1,3154 +0,0 @@ -/* - * copyright (c) 2001 Fabrice Bellard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_AVCODEC_H -#define AVCODEC_AVCODEC_H - -/** - * @file - * @ingroup libavc - * Libavcodec external API header - */ - -#include "libavutil/samplefmt.h" -#include "libavutil/attributes.h" -#include "libavutil/avutil.h" -#include "libavutil/buffer.h" -#include "libavutil/channel_layout.h" -#include "libavutil/dict.h" -#include "libavutil/frame.h" -#include "libavutil/log.h" -#include "libavutil/pixfmt.h" -#include "libavutil/rational.h" - -#include "codec.h" -#include "codec_id.h" -#include "defs.h" -#include "packet.h" -#include "version_major.h" -#ifndef HAVE_AV_CONFIG_H -/* When included as part of the ffmpeg build, only include the major version - * to avoid unnecessary rebuilds. When included externally, keep including - * the full version information. */ -#include "version.h" - -#include "codec_desc.h" -#include "codec_par.h" -#endif - -struct AVCodecParameters; - -/** - * @defgroup libavc libavcodec - * Encoding/Decoding Library - * - * @{ - * - * @defgroup lavc_decoding Decoding - * @{ - * @} - * - * @defgroup lavc_encoding Encoding - * @{ - * @} - * - * @defgroup lavc_codec Codecs - * @{ - * @defgroup lavc_codec_native Native Codecs - * @{ - * @} - * @defgroup lavc_codec_wrappers External library wrappers - * @{ - * @} - * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge - * @{ - * @} - * @} - * @defgroup lavc_internal Internal - * @{ - * @} - * @} - */ - -/** - * @ingroup libavc - * @defgroup lavc_encdec send/receive encoding and decoding API overview - * @{ - * - * The avcodec_send_packet()/avcodec_receive_frame()/avcodec_send_frame()/ - * avcodec_receive_packet() functions provide an encode/decode API, which - * decouples input and output. - * - * The API is very similar for encoding/decoding and audio/video, and works as - * follows: - * - Set up and open the AVCodecContext as usual. - * - Send valid input: - * - For decoding, call avcodec_send_packet() to give the decoder raw - * compressed data in an AVPacket. - * - For encoding, call avcodec_send_frame() to give the encoder an AVFrame - * containing uncompressed audio or video. - * - * In both cases, it is recommended that AVPackets and AVFrames are - * refcounted, or libavcodec might have to copy the input data. (libavformat - * always returns refcounted AVPackets, and av_frame_get_buffer() allocates - * refcounted AVFrames.) - * - Receive output in a loop. Periodically call one of the avcodec_receive_*() - * functions and process their output: - * - For decoding, call avcodec_receive_frame(). On success, it will return - * an AVFrame containing uncompressed audio or video data. - * - For encoding, call avcodec_receive_packet(). On success, it will return - * an AVPacket with a compressed frame. - * - * Repeat this call until it returns AVERROR(EAGAIN) or an error. The - * AVERROR(EAGAIN) return value means that new input data is required to - * return new output. In this case, continue with sending input. For each - * input frame/packet, the codec will typically return 1 output frame/packet, - * but it can also be 0 or more than 1. - * - * At the beginning of decoding or encoding, the codec might accept multiple - * input frames/packets without returning a frame, until its internal buffers - * are filled. This situation is handled transparently if you follow the steps - * outlined above. - * - * In theory, sending input can result in EAGAIN - this should happen only if - * not all output was received. You can use this to structure alternative decode - * or encode loops other than the one suggested above. For example, you could - * try sending new input on each iteration, and try to receive output if that - * returns EAGAIN. - * - * End of stream situations. These require "flushing" (aka draining) the codec, - * as the codec might buffer multiple frames or packets internally for - * performance or out of necessity (consider B-frames). - * This is handled as follows: - * - Instead of valid input, send NULL to the avcodec_send_packet() (decoding) - * or avcodec_send_frame() (encoding) functions. This will enter draining - * mode. - * - Call avcodec_receive_frame() (decoding) or avcodec_receive_packet() - * (encoding) in a loop until AVERROR_EOF is returned. The functions will - * not return AVERROR(EAGAIN), unless you forgot to enter draining mode. - * - Before decoding can be resumed again, the codec has to be reset with - * avcodec_flush_buffers(). - * - * Using the API as outlined above is highly recommended. But it is also - * possible to call functions outside of this rigid schema. For example, you can - * call avcodec_send_packet() repeatedly without calling - * avcodec_receive_frame(). In this case, avcodec_send_packet() will succeed - * until the codec's internal buffer has been filled up (which is typically of - * size 1 per output frame, after initial input), and then reject input with - * AVERROR(EAGAIN). Once it starts rejecting input, you have no choice but to - * read at least some output. - * - * Not all codecs will follow a rigid and predictable dataflow; the only - * guarantee is that an AVERROR(EAGAIN) return value on a send/receive call on - * one end implies that a receive/send call on the other end will succeed, or - * at least will not fail with AVERROR(EAGAIN). In general, no codec will - * permit unlimited buffering of input or output. - * - * A codec is not allowed to return AVERROR(EAGAIN) for both sending and receiving. This - * would be an invalid state, which could put the codec user into an endless - * loop. The API has no concept of time either: it cannot happen that trying to - * do avcodec_send_packet() results in AVERROR(EAGAIN), but a repeated call 1 second - * later accepts the packet (with no other receive/flush API calls involved). - * The API is a strict state machine, and the passage of time is not supposed - * to influence it. Some timing-dependent behavior might still be deemed - * acceptable in certain cases. But it must never result in both send/receive - * returning EAGAIN at the same time at any point. It must also absolutely be - * avoided that the current state is "unstable" and can "flip-flop" between - * the send/receive APIs allowing progress. For example, it's not allowed that - * the codec randomly decides that it actually wants to consume a packet now - * instead of returning a frame, after it just returned AVERROR(EAGAIN) on an - * avcodec_send_packet() call. - * @} - */ - -/** - * @defgroup lavc_core Core functions/structures. - * @ingroup libavc - * - * Basic definitions, functions for querying libavcodec capabilities, - * allocating core structures, etc. - * @{ - */ - -/** - * @ingroup lavc_encoding - * minimum encoding buffer size - * Used to avoid some checks during header writing. - */ -#define AV_INPUT_BUFFER_MIN_SIZE 16384 - -/** - * @ingroup lavc_encoding - */ -typedef struct RcOverride{ - int start_frame; - int end_frame; - int qscale; // If this is 0 then quality_factor will be used instead. - float quality_factor; -} RcOverride; - -/* encoding support - These flags can be passed in AVCodecContext.flags before initialization. - Note: Not everything is supported yet. -*/ - -/** - * Allow decoders to produce frames with data planes that are not aligned - * to CPU requirements (e.g. due to cropping). - */ -#define AV_CODEC_FLAG_UNALIGNED (1 << 0) -/** - * Use fixed qscale. - */ -#define AV_CODEC_FLAG_QSCALE (1 << 1) -/** - * 4 MV per MB allowed / advanced prediction for H.263. - */ -#define AV_CODEC_FLAG_4MV (1 << 2) -/** - * Output even those frames that might be corrupted. - */ -#define AV_CODEC_FLAG_OUTPUT_CORRUPT (1 << 3) -/** - * Use qpel MC. - */ -#define AV_CODEC_FLAG_QPEL (1 << 4) -#if FF_API_DROPCHANGED -/** - * Don't output frames whose parameters differ from first - * decoded frame in stream. - * - * @deprecated callers should implement this functionality in their own code - */ -#define AV_CODEC_FLAG_DROPCHANGED (1 << 5) -#endif -/** - * Request the encoder to output reconstructed frames, i.e.\ frames that would - * be produced by decoding the encoded bistream. These frames may be retrieved - * by calling avcodec_receive_frame() immediately after a successful call to - * avcodec_receive_packet(). - * - * Should only be used with encoders flagged with the - * @ref AV_CODEC_CAP_ENCODER_RECON_FRAME capability. - * - * @note - * Each reconstructed frame returned by the encoder corresponds to the last - * encoded packet, i.e. the frames are returned in coded order rather than - * presentation order. - * - * @note - * Frame parameters (like pixel format or dimensions) do not have to match the - * AVCodecContext values. Make sure to use the values from the returned frame. - */ -#define AV_CODEC_FLAG_RECON_FRAME (1 << 6) -/** - * @par decoding - * Request the decoder to propagate each packet's AVPacket.opaque and - * AVPacket.opaque_ref to its corresponding output AVFrame. - * - * @par encoding: - * Request the encoder to propagate each frame's AVFrame.opaque and - * AVFrame.opaque_ref values to its corresponding output AVPacket. - * - * @par - * May only be set on encoders that have the - * @ref AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE capability flag. - * - * @note - * While in typical cases one input frame produces exactly one output packet - * (perhaps after a delay), in general the mapping of frames to packets is - * M-to-N, so - * - Any number of input frames may be associated with any given output packet. - * This includes zero - e.g. some encoders may output packets that carry only - * metadata about the whole stream. - * - A given input frame may be associated with any number of output packets. - * Again this includes zero - e.g. some encoders may drop frames under certain - * conditions. - * . - * This implies that when using this flag, the caller must NOT assume that - * - a given input frame's opaques will necessarily appear on some output packet; - * - every output packet will have some non-NULL opaque value. - * . - * When an output packet contains multiple frames, the opaque values will be - * taken from the first of those. - * - * @note - * The converse holds for decoders, with frames and packets switched. - */ -#define AV_CODEC_FLAG_COPY_OPAQUE (1 << 7) -/** - * Signal to the encoder that the values of AVFrame.duration are valid and - * should be used (typically for transferring them to output packets). - * - * If this flag is not set, frame durations are ignored. - */ -#define AV_CODEC_FLAG_FRAME_DURATION (1 << 8) -/** - * Use internal 2pass ratecontrol in first pass mode. - */ -#define AV_CODEC_FLAG_PASS1 (1 << 9) -/** - * Use internal 2pass ratecontrol in second pass mode. - */ -#define AV_CODEC_FLAG_PASS2 (1 << 10) -/** - * loop filter. - */ -#define AV_CODEC_FLAG_LOOP_FILTER (1 << 11) -/** - * Only decode/encode grayscale. - */ -#define AV_CODEC_FLAG_GRAY (1 << 13) -/** - * error[?] variables will be set during encoding. - */ -#define AV_CODEC_FLAG_PSNR (1 << 15) -/** - * Use interlaced DCT. - */ -#define AV_CODEC_FLAG_INTERLACED_DCT (1 << 18) -/** - * Force low delay. - */ -#define AV_CODEC_FLAG_LOW_DELAY (1 << 19) -/** - * Place global headers in extradata instead of every keyframe. - */ -#define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22) -/** - * Use only bitexact stuff (except (I)DCT). - */ -#define AV_CODEC_FLAG_BITEXACT (1 << 23) -/* Fx : Flag for H.263+ extra options */ -/** - * H.263 advanced intra coding / MPEG-4 AC prediction - */ -#define AV_CODEC_FLAG_AC_PRED (1 << 24) -/** - * interlaced motion estimation - */ -#define AV_CODEC_FLAG_INTERLACED_ME (1 << 29) -#define AV_CODEC_FLAG_CLOSED_GOP (1U << 31) - -/** - * Allow non spec compliant speedup tricks. - */ -#define AV_CODEC_FLAG2_FAST (1 << 0) -/** - * Skip bitstream encoding. - */ -#define AV_CODEC_FLAG2_NO_OUTPUT (1 << 2) -/** - * Place global headers at every keyframe instead of in extradata. - */ -#define AV_CODEC_FLAG2_LOCAL_HEADER (1 << 3) - -/** - * Input bitstream might be truncated at a packet boundaries - * instead of only at frame boundaries. - */ -#define AV_CODEC_FLAG2_CHUNKS (1 << 15) -/** - * Discard cropping information from SPS. - */ -#define AV_CODEC_FLAG2_IGNORE_CROP (1 << 16) - -/** - * Show all frames before the first keyframe - */ -#define AV_CODEC_FLAG2_SHOW_ALL (1 << 22) -/** - * Export motion vectors through frame side data - */ -#define AV_CODEC_FLAG2_EXPORT_MVS (1 << 28) -/** - * Do not skip samples and export skip information as frame side data - */ -#define AV_CODEC_FLAG2_SKIP_MANUAL (1 << 29) -/** - * Do not reset ASS ReadOrder field on flush (subtitles decoding) - */ -#define AV_CODEC_FLAG2_RO_FLUSH_NOOP (1 << 30) -/** - * Generate/parse ICC profiles on encode/decode, as appropriate for the type of - * file. No effect on codecs which cannot contain embedded ICC profiles, or - * when compiled without support for lcms2. - */ -#define AV_CODEC_FLAG2_ICC_PROFILES (1U << 31) - -/* Exported side data. - These flags can be passed in AVCodecContext.export_side_data before initialization. -*/ -/** - * Export motion vectors through frame side data - */ -#define AV_CODEC_EXPORT_DATA_MVS (1 << 0) -/** - * Export encoder Producer Reference Time through packet side data - */ -#define AV_CODEC_EXPORT_DATA_PRFT (1 << 1) -/** - * Decoding only. - * Export the AVVideoEncParams structure through frame side data. - */ -#define AV_CODEC_EXPORT_DATA_VIDEO_ENC_PARAMS (1 << 2) -/** - * Decoding only. - * Do not apply film grain, export it instead. - */ -#define AV_CODEC_EXPORT_DATA_FILM_GRAIN (1 << 3) - -/** - * The decoder will keep a reference to the frame and may reuse it later. - */ -#define AV_GET_BUFFER_FLAG_REF (1 << 0) - -/** - * The encoder will keep a reference to the packet and may reuse it later. - */ -#define AV_GET_ENCODE_BUFFER_FLAG_REF (1 << 0) - -/** - * main external API structure. - * New fields can be added to the end with minor version bumps. - * Removal, reordering and changes to existing fields require a major - * version bump. - * You can use AVOptions (av_opt* / av_set/get*()) to access these fields from user - * applications. - * The name string for AVOptions options matches the associated command line - * parameter name and can be found in libavcodec/options_table.h - * The AVOption/command line parameter names differ in some cases from the C - * structure field names for historic reasons or brevity. - * sizeof(AVCodecContext) must not be used outside libav*. - */ -typedef struct AVCodecContext { - /** - * information on struct for av_log - * - set by avcodec_alloc_context3 - */ - const AVClass *av_class; - int log_level_offset; - - enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */ - const struct AVCodec *codec; - enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */ - - /** - * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A'). - * This is used to work around some encoder bugs. - * A demuxer should set this to what is stored in the field used to identify the codec. - * If there are multiple such fields in a container then the demuxer should choose the one - * which maximizes the information about the used codec. - * If the codec tag field in a container is larger than 32 bits then the demuxer should - * remap the longer ID to 32 bits with a table or other structure. Alternatively a new - * extra_codec_tag + size could be added but for this a clear advantage must be demonstrated - * first. - * - encoding: Set by user, if not then the default based on codec_id will be used. - * - decoding: Set by user, will be converted to uppercase by libavcodec during init. - */ - unsigned int codec_tag; - - void *priv_data; - - /** - * Private context used for internal data. - * - * Unlike priv_data, this is not codec-specific. It is used in general - * libavcodec functions. - */ - struct AVCodecInternal *internal; - - /** - * Private data of the user, can be used to carry app specific stuff. - * - encoding: Set by user. - * - decoding: Set by user. - */ - void *opaque; - - /** - * the average bitrate - * - encoding: Set by user; unused for constant quantizer encoding. - * - decoding: Set by user, may be overwritten by libavcodec - * if this info is available in the stream - */ - int64_t bit_rate; - - /** - * number of bits the bitstream is allowed to diverge from the reference. - * the reference can be CBR (for CBR pass1) or VBR (for pass2) - * - encoding: Set by user; unused for constant quantizer encoding. - * - decoding: unused - */ - int bit_rate_tolerance; - - /** - * Global quality for codecs which cannot change it per frame. - * This should be proportional to MPEG-1/2/4 qscale. - * - encoding: Set by user. - * - decoding: unused - */ - int global_quality; - - /** - * - encoding: Set by user. - * - decoding: unused - */ - int compression_level; -#define FF_COMPRESSION_DEFAULT -1 - - /** - * AV_CODEC_FLAG_*. - * - encoding: Set by user. - * - decoding: Set by user. - */ - int flags; - - /** - * AV_CODEC_FLAG2_* - * - encoding: Set by user. - * - decoding: Set by user. - */ - int flags2; - - /** - * some codecs need / can use extradata like Huffman tables. - * MJPEG: Huffman tables - * rv10: additional flags - * MPEG-4: global headers (they can be in the bitstream or here) - * The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger - * than extradata_size to avoid problems if it is read with the bitstream reader. - * The bytewise contents of extradata must not depend on the architecture or CPU endianness. - * Must be allocated with the av_malloc() family of functions. - * - encoding: Set/allocated/freed by libavcodec. - * - decoding: Set/allocated/freed by user. - */ - uint8_t *extradata; - int extradata_size; - - /** - * This is the fundamental unit of time (in seconds) in terms - * of which frame timestamps are represented. For fixed-fps content, - * timebase should be 1/framerate and timestamp increments should be - * identically 1. - * This often, but not always is the inverse of the frame rate or field rate - * for video. 1/time_base is not the average frame rate if the frame rate is not - * constant. - * - * Like containers, elementary streams also can store timestamps, 1/time_base - * is the unit in which these timestamps are specified. - * As example of such codec time base see ISO/IEC 14496-2:2001(E) - * vop_time_increment_resolution and fixed_vop_rate - * (fixed_vop_rate == 0 implies that it is different from the framerate) - * - * - encoding: MUST be set by user. - * - decoding: unused. - */ - AVRational time_base; - -#if FF_API_TICKS_PER_FRAME - /** - * For some codecs, the time base is closer to the field rate than the frame rate. - * Most notably, H.264 and MPEG-2 specify time_base as half of frame duration - * if no telecine is used ... - * - * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2. - * - * @deprecated - * - decoding: Use AVCodecDescriptor.props & AV_CODEC_PROP_FIELDS - * - encoding: Set AVCodecContext.framerate instead - * - */ - attribute_deprecated - int ticks_per_frame; -#endif - - /** - * Codec delay. - * - * Encoding: Number of frames delay there will be from the encoder input to - * the decoder output. (we assume the decoder matches the spec) - * Decoding: Number of frames delay in addition to what a standard decoder - * as specified in the spec would produce. - * - * Video: - * Number of frames the decoded output will be delayed relative to the - * encoded input. - * - * Audio: - * For encoding, this field is unused (see initial_padding). - * - * For decoding, this is the number of samples the decoder needs to - * output before the decoder's output is valid. When seeking, you should - * start decoding this many samples prior to your desired seek point. - * - * - encoding: Set by libavcodec. - * - decoding: Set by libavcodec. - */ - int delay; - - - /* video only */ - /** - * picture width / height. - * - * @note Those fields may not match the values of the last - * AVFrame output by avcodec_receive_frame() due frame - * reordering. - * - * - encoding: MUST be set by user. - * - decoding: May be set by the user before opening the decoder if known e.g. - * from the container. Some decoders will require the dimensions - * to be set by the caller. During decoding, the decoder may - * overwrite those values as required while parsing the data. - */ - int width, height; - - /** - * Bitstream width / height, may be different from width/height e.g. when - * the decoded frame is cropped before being output or lowres is enabled. - * - * @note Those field may not match the value of the last - * AVFrame output by avcodec_receive_frame() due frame - * reordering. - * - * - encoding: unused - * - decoding: May be set by the user before opening the decoder if known - * e.g. from the container. During decoding, the decoder may - * overwrite those values as required while parsing the data. - */ - int coded_width, coded_height; - - /** - * the number of pictures in a group of pictures, or 0 for intra_only - * - encoding: Set by user. - * - decoding: unused - */ - int gop_size; - - /** - * Pixel format, see AV_PIX_FMT_xxx. - * May be set by the demuxer if known from headers. - * May be overridden by the decoder if it knows better. - * - * @note This field may not match the value of the last - * AVFrame output by avcodec_receive_frame() due frame - * reordering. - * - * - encoding: Set by user. - * - decoding: Set by user if known, overridden by libavcodec while - * parsing the data. - */ - enum AVPixelFormat pix_fmt; - - /** - * If non NULL, 'draw_horiz_band' is called by the libavcodec - * decoder to draw a horizontal band. It improves cache usage. Not - * all codecs can do that. You must check the codec capabilities - * beforehand. - * When multithreading is used, it may be called from multiple threads - * at the same time; threads might draw different parts of the same AVFrame, - * or multiple AVFrames, and there is no guarantee that slices will be drawn - * in order. - * The function is also used by hardware acceleration APIs. - * It is called at least once during frame decoding to pass - * the data needed for hardware render. - * In that mode instead of pixel data, AVFrame points to - * a structure specific to the acceleration API. The application - * reads the structure and can change some fields to indicate progress - * or mark state. - * - encoding: unused - * - decoding: Set by user. - * @param height the height of the slice - * @param y the y position of the slice - * @param type 1->top field, 2->bottom field, 3->frame - * @param offset offset into the AVFrame.data from which the slice should be read - */ - void (*draw_horiz_band)(struct AVCodecContext *s, - const AVFrame *src, int offset[AV_NUM_DATA_POINTERS], - int y, int type, int height); - - /** - * Callback to negotiate the pixel format. Decoding only, may be set by the - * caller before avcodec_open2(). - * - * Called by some decoders to select the pixel format that will be used for - * the output frames. This is mainly used to set up hardware acceleration, - * then the provided format list contains the corresponding hwaccel pixel - * formats alongside the "software" one. The software pixel format may also - * be retrieved from \ref sw_pix_fmt. - * - * This callback will be called when the coded frame properties (such as - * resolution, pixel format, etc.) change and more than one output format is - * supported for those new properties. If a hardware pixel format is chosen - * and initialization for it fails, the callback may be called again - * immediately. - * - * This callback may be called from different threads if the decoder is - * multi-threaded, but not from more than one thread simultaneously. - * - * @param fmt list of formats which may be used in the current - * configuration, terminated by AV_PIX_FMT_NONE. - * @warning Behavior is undefined if the callback returns a value other - * than one of the formats in fmt or AV_PIX_FMT_NONE. - * @return the chosen format or AV_PIX_FMT_NONE - */ - enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt); - - /** - * maximum number of B-frames between non-B-frames - * Note: The output will be delayed by max_b_frames+1 relative to the input. - * - encoding: Set by user. - * - decoding: unused - */ - int max_b_frames; - - /** - * qscale factor between IP and B-frames - * If > 0 then the last P-frame quantizer will be used (q= lastp_q*factor+offset). - * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset). - * - encoding: Set by user. - * - decoding: unused - */ - float b_quant_factor; - - /** - * qscale offset between IP and B-frames - * - encoding: Set by user. - * - decoding: unused - */ - float b_quant_offset; - - /** - * Size of the frame reordering buffer in the decoder. - * For MPEG-2 it is 1 IPB or 0 low delay IP. - * - encoding: Set by libavcodec. - * - decoding: Set by libavcodec. - */ - int has_b_frames; - - /** - * qscale factor between P- and I-frames - * If > 0 then the last P-frame quantizer will be used (q = lastp_q * factor + offset). - * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset). - * - encoding: Set by user. - * - decoding: unused - */ - float i_quant_factor; - - /** - * qscale offset between P and I-frames - * - encoding: Set by user. - * - decoding: unused - */ - float i_quant_offset; - - /** - * luminance masking (0-> disabled) - * - encoding: Set by user. - * - decoding: unused - */ - float lumi_masking; - - /** - * temporary complexity masking (0-> disabled) - * - encoding: Set by user. - * - decoding: unused - */ - float temporal_cplx_masking; - - /** - * spatial complexity masking (0-> disabled) - * - encoding: Set by user. - * - decoding: unused - */ - float spatial_cplx_masking; - - /** - * p block masking (0-> disabled) - * - encoding: Set by user. - * - decoding: unused - */ - float p_masking; - - /** - * darkness masking (0-> disabled) - * - encoding: Set by user. - * - decoding: unused - */ - float dark_masking; - -#if FF_API_SLICE_OFFSET - /** - * slice count - * - encoding: Set by libavcodec. - * - decoding: Set by user (or 0). - */ - attribute_deprecated - int slice_count; - - /** - * slice offsets in the frame in bytes - * - encoding: Set/allocated by libavcodec. - * - decoding: Set/allocated by user (or NULL). - */ - attribute_deprecated - int *slice_offset; -#endif - - /** - * sample aspect ratio (0 if unknown) - * That is the width of a pixel divided by the height of the pixel. - * Numerator and denominator must be relatively prime and smaller than 256 for some video standards. - * - encoding: Set by user. - * - decoding: Set by libavcodec. - */ - AVRational sample_aspect_ratio; - - /** - * motion estimation comparison function - * - encoding: Set by user. - * - decoding: unused - */ - int me_cmp; - /** - * subpixel motion estimation comparison function - * - encoding: Set by user. - * - decoding: unused - */ - int me_sub_cmp; - /** - * macroblock comparison function (not supported yet) - * - encoding: Set by user. - * - decoding: unused - */ - int mb_cmp; - /** - * interlaced DCT comparison function - * - encoding: Set by user. - * - decoding: unused - */ - int ildct_cmp; -#define FF_CMP_SAD 0 -#define FF_CMP_SSE 1 -#define FF_CMP_SATD 2 -#define FF_CMP_DCT 3 -#define FF_CMP_PSNR 4 -#define FF_CMP_BIT 5 -#define FF_CMP_RD 6 -#define FF_CMP_ZERO 7 -#define FF_CMP_VSAD 8 -#define FF_CMP_VSSE 9 -#define FF_CMP_NSSE 10 -#define FF_CMP_W53 11 -#define FF_CMP_W97 12 -#define FF_CMP_DCTMAX 13 -#define FF_CMP_DCT264 14 -#define FF_CMP_MEDIAN_SAD 15 -#define FF_CMP_CHROMA 256 - - /** - * ME diamond size & shape - * - encoding: Set by user. - * - decoding: unused - */ - int dia_size; - - /** - * amount of previous MV predictors (2a+1 x 2a+1 square) - * - encoding: Set by user. - * - decoding: unused - */ - int last_predictor_count; - - /** - * motion estimation prepass comparison function - * - encoding: Set by user. - * - decoding: unused - */ - int me_pre_cmp; - - /** - * ME prepass diamond size & shape - * - encoding: Set by user. - * - decoding: unused - */ - int pre_dia_size; - - /** - * subpel ME quality - * - encoding: Set by user. - * - decoding: unused - */ - int me_subpel_quality; - - /** - * maximum motion estimation search range in subpel units - * If 0 then no limit. - * - * - encoding: Set by user. - * - decoding: unused - */ - int me_range; - - /** - * slice flags - * - encoding: unused - * - decoding: Set by user. - */ - int slice_flags; -#define SLICE_FLAG_CODED_ORDER 0x0001 ///< draw_horiz_band() is called in coded order instead of display -#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG-2 field pics) -#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1) - - /** - * macroblock decision mode - * - encoding: Set by user. - * - decoding: unused - */ - int mb_decision; -#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp -#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits -#define FF_MB_DECISION_RD 2 ///< rate distortion - - /** - * custom intra quantization matrix - * Must be allocated with the av_malloc() family of functions, and will be freed in - * avcodec_free_context(). - * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL. - * - decoding: Set/allocated/freed by libavcodec. - */ - uint16_t *intra_matrix; - - /** - * custom inter quantization matrix - * Must be allocated with the av_malloc() family of functions, and will be freed in - * avcodec_free_context(). - * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL. - * - decoding: Set/allocated/freed by libavcodec. - */ - uint16_t *inter_matrix; - - /** - * precision of the intra DC coefficient - 8 - * - encoding: Set by user. - * - decoding: Set by libavcodec - */ - int intra_dc_precision; - - /** - * Number of macroblock rows at the top which are skipped. - * - encoding: unused - * - decoding: Set by user. - */ - int skip_top; - - /** - * Number of macroblock rows at the bottom which are skipped. - * - encoding: unused - * - decoding: Set by user. - */ - int skip_bottom; - - /** - * minimum MB Lagrange multiplier - * - encoding: Set by user. - * - decoding: unused - */ - int mb_lmin; - - /** - * maximum MB Lagrange multiplier - * - encoding: Set by user. - * - decoding: unused - */ - int mb_lmax; - - /** - * - encoding: Set by user. - * - decoding: unused - */ - int bidir_refine; - - /** - * minimum GOP size - * - encoding: Set by user. - * - decoding: unused - */ - int keyint_min; - - /** - * number of reference frames - * - encoding: Set by user. - * - decoding: Set by lavc. - */ - int refs; - - /** - * Note: Value depends upon the compare function used for fullpel ME. - * - encoding: Set by user. - * - decoding: unused - */ - int mv0_threshold; - - /** - * Chromaticity coordinates of the source primaries. - * - encoding: Set by user - * - decoding: Set by libavcodec - */ - enum AVColorPrimaries color_primaries; - - /** - * Color Transfer Characteristic. - * - encoding: Set by user - * - decoding: Set by libavcodec - */ - enum AVColorTransferCharacteristic color_trc; - - /** - * YUV colorspace type. - * - encoding: Set by user - * - decoding: Set by libavcodec - */ - enum AVColorSpace colorspace; - - /** - * MPEG vs JPEG YUV range. - * - encoding: Set by user to override the default output color range value, - * If not specified, libavcodec sets the color range depending on the - * output format. - * - decoding: Set by libavcodec, can be set by the user to propagate the - * color range to components reading from the decoder context. - */ - enum AVColorRange color_range; - - /** - * This defines the location of chroma samples. - * - encoding: Set by user - * - decoding: Set by libavcodec - */ - enum AVChromaLocation chroma_sample_location; - - /** - * Number of slices. - * Indicates number of picture subdivisions. Used for parallelized - * decoding. - * - encoding: Set by user - * - decoding: unused - */ - int slices; - - /** Field order - * - encoding: set by libavcodec - * - decoding: Set by user. - */ - enum AVFieldOrder field_order; - - /* audio only */ - int sample_rate; ///< samples per second - -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * number of audio channels - * @deprecated use ch_layout.nb_channels - */ - attribute_deprecated - int channels; -#endif - - /** - * audio sample format - * - encoding: Set by user. - * - decoding: Set by libavcodec. - */ - enum AVSampleFormat sample_fmt; ///< sample format - - /* The following data should not be initialized. */ - /** - * Number of samples per channel in an audio frame. - * - * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame - * except the last must contain exactly frame_size samples per channel. - * May be 0 when the codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE set, then the - * frame size is not restricted. - * - decoding: may be set by some decoders to indicate constant frame size - */ - int frame_size; - -#if FF_API_AVCTX_FRAME_NUMBER - /** - * Frame counter, set by libavcodec. - * - * - decoding: total number of frames returned from the decoder so far. - * - encoding: total number of frames passed to the encoder so far. - * - * @note the counter is not incremented if encoding/decoding resulted in - * an error. - * @deprecated use frame_num instead - */ - attribute_deprecated - int frame_number; -#endif - - /** - * number of bytes per packet if constant and known or 0 - * Used by some WAV based audio codecs. - */ - int block_align; - - /** - * Audio cutoff bandwidth (0 means "automatic") - * - encoding: Set by user. - * - decoding: unused - */ - int cutoff; - -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * Audio channel layout. - * - encoding: set by user. - * - decoding: set by user, may be overwritten by libavcodec. - * @deprecated use ch_layout - */ - attribute_deprecated - uint64_t channel_layout; - - /** - * Request decoder to use this channel layout if it can (0 for default) - * - encoding: unused - * - decoding: Set by user. - * @deprecated use "downmix" codec private option - */ - attribute_deprecated - uint64_t request_channel_layout; -#endif - - /** - * Type of service that the audio stream conveys. - * - encoding: Set by user. - * - decoding: Set by libavcodec. - */ - enum AVAudioServiceType audio_service_type; - - /** - * desired sample format - * - encoding: Not used. - * - decoding: Set by user. - * Decoder will decode to this format if it can. - */ - enum AVSampleFormat request_sample_fmt; - - /** - * This callback is called at the beginning of each frame to get data - * buffer(s) for it. There may be one contiguous buffer for all the data or - * there may be a buffer per each data plane or anything in between. What - * this means is, you may set however many entries in buf[] you feel necessary. - * Each buffer must be reference-counted using the AVBuffer API (see description - * of buf[] below). - * - * The following fields will be set in the frame before this callback is - * called: - * - format - * - width, height (video only) - * - sample_rate, channel_layout, nb_samples (audio only) - * Their values may differ from the corresponding values in - * AVCodecContext. This callback must use the frame values, not the codec - * context values, to calculate the required buffer size. - * - * This callback must fill the following fields in the frame: - * - data[] - * - linesize[] - * - extended_data: - * * if the data is planar audio with more than 8 channels, then this - * callback must allocate and fill extended_data to contain all pointers - * to all data planes. data[] must hold as many pointers as it can. - * extended_data must be allocated with av_malloc() and will be freed in - * av_frame_unref(). - * * otherwise extended_data must point to data - * - buf[] must contain one or more pointers to AVBufferRef structures. Each of - * the frame's data and extended_data pointers must be contained in these. That - * is, one AVBufferRef for each allocated chunk of memory, not necessarily one - * AVBufferRef per data[] entry. See: av_buffer_create(), av_buffer_alloc(), - * and av_buffer_ref(). - * - extended_buf and nb_extended_buf must be allocated with av_malloc() by - * this callback and filled with the extra buffers if there are more - * buffers than buf[] can hold. extended_buf will be freed in - * av_frame_unref(). - * - * If AV_CODEC_CAP_DR1 is not set then get_buffer2() must call - * avcodec_default_get_buffer2() instead of providing buffers allocated by - * some other means. - * - * Each data plane must be aligned to the maximum required by the target - * CPU. - * - * @see avcodec_default_get_buffer2() - * - * Video: - * - * If AV_GET_BUFFER_FLAG_REF is set in flags then the frame may be reused - * (read and/or written to if it is writable) later by libavcodec. - * - * avcodec_align_dimensions2() should be used to find the required width and - * height, as they normally need to be rounded up to the next multiple of 16. - * - * Some decoders do not support linesizes changing between frames. - * - * If frame multithreading is used, this callback may be called from a - * different thread, but not from more than one at once. Does not need to be - * reentrant. - * - * @see avcodec_align_dimensions2() - * - * Audio: - * - * Decoders request a buffer of a particular size by setting - * AVFrame.nb_samples prior to calling get_buffer2(). The decoder may, - * however, utilize only part of the buffer by setting AVFrame.nb_samples - * to a smaller value in the output frame. - * - * As a convenience, av_samples_get_buffer_size() and - * av_samples_fill_arrays() in libavutil may be used by custom get_buffer2() - * functions to find the required data size and to fill data pointers and - * linesize. In AVFrame.linesize, only linesize[0] may be set for audio - * since all planes must be the same size. - * - * @see av_samples_get_buffer_size(), av_samples_fill_arrays() - * - * - encoding: unused - * - decoding: Set by libavcodec, user can override. - */ - int (*get_buffer2)(struct AVCodecContext *s, AVFrame *frame, int flags); - - /* - encoding parameters */ - float qcompress; ///< amount of qscale change between easy & hard scenes (0.0-1.0) - float qblur; ///< amount of qscale smoothing over time (0.0-1.0) - - /** - * minimum quantizer - * - encoding: Set by user. - * - decoding: unused - */ - int qmin; - - /** - * maximum quantizer - * - encoding: Set by user. - * - decoding: unused - */ - int qmax; - - /** - * maximum quantizer difference between frames - * - encoding: Set by user. - * - decoding: unused - */ - int max_qdiff; - - /** - * decoder bitstream buffer size - * - encoding: Set by user. - * - decoding: May be set by libavcodec. - */ - int rc_buffer_size; - - /** - * ratecontrol override, see RcOverride - * - encoding: Allocated/set/freed by user. - * - decoding: unused - */ - int rc_override_count; - RcOverride *rc_override; - - /** - * maximum bitrate - * - encoding: Set by user. - * - decoding: Set by user, may be overwritten by libavcodec. - */ - int64_t rc_max_rate; - - /** - * minimum bitrate - * - encoding: Set by user. - * - decoding: unused - */ - int64_t rc_min_rate; - - /** - * Ratecontrol attempt to use, at maximum, of what can be used without an underflow. - * - encoding: Set by user. - * - decoding: unused. - */ - float rc_max_available_vbv_use; - - /** - * Ratecontrol attempt to use, at least, times the amount needed to prevent a vbv overflow. - * - encoding: Set by user. - * - decoding: unused. - */ - float rc_min_vbv_overflow_use; - - /** - * Number of bits which should be loaded into the rc buffer before decoding starts. - * - encoding: Set by user. - * - decoding: unused - */ - int rc_initial_buffer_occupancy; - - /** - * trellis RD quantization - * - encoding: Set by user. - * - decoding: unused - */ - int trellis; - - /** - * pass1 encoding statistics output buffer - * - encoding: Set by libavcodec. - * - decoding: unused - */ - char *stats_out; - - /** - * pass2 encoding statistics input buffer - * Concatenated stuff from stats_out of pass1 should be placed here. - * - encoding: Allocated/set/freed by user. - * - decoding: unused - */ - char *stats_in; - - /** - * Work around bugs in encoders which sometimes cannot be detected automatically. - * - encoding: Set by user - * - decoding: Set by user - */ - int workaround_bugs; -#define FF_BUG_AUTODETECT 1 ///< autodetection -#define FF_BUG_XVID_ILACE 4 -#define FF_BUG_UMP4 8 -#define FF_BUG_NO_PADDING 16 -#define FF_BUG_AMV 32 -#define FF_BUG_QPEL_CHROMA 64 -#define FF_BUG_STD_QPEL 128 -#define FF_BUG_QPEL_CHROMA2 256 -#define FF_BUG_DIRECT_BLOCKSIZE 512 -#define FF_BUG_EDGE 1024 -#define FF_BUG_HPEL_CHROMA 2048 -#define FF_BUG_DC_CLIP 4096 -#define FF_BUG_MS 8192 ///< Work around various bugs in Microsoft's broken decoders. -#define FF_BUG_TRUNCATED 16384 -#define FF_BUG_IEDGE 32768 - - /** - * strictly follow the standard (MPEG-4, ...). - * - encoding: Set by user. - * - decoding: Set by user. - * Setting this to STRICT or higher means the encoder and decoder will - * generally do stupid things, whereas setting it to unofficial or lower - * will mean the encoder might produce output that is not supported by all - * spec-compliant decoders. Decoders don't differentiate between normal, - * unofficial and experimental (that is, they always try to decode things - * when they can) unless they are explicitly asked to behave stupidly - * (=strictly conform to the specs) - * This may only be set to one of the FF_COMPLIANCE_* values in defs.h. - */ - int strict_std_compliance; - - /** - * error concealment flags - * - encoding: unused - * - decoding: Set by user. - */ - int error_concealment; -#define FF_EC_GUESS_MVS 1 -#define FF_EC_DEBLOCK 2 -#define FF_EC_FAVOR_INTER 256 - - /** - * debug - * - encoding: Set by user. - * - decoding: Set by user. - */ - int debug; -#define FF_DEBUG_PICT_INFO 1 -#define FF_DEBUG_RC 2 -#define FF_DEBUG_BITSTREAM 4 -#define FF_DEBUG_MB_TYPE 8 -#define FF_DEBUG_QP 16 -#define FF_DEBUG_DCT_COEFF 0x00000040 -#define FF_DEBUG_SKIP 0x00000080 -#define FF_DEBUG_STARTCODE 0x00000100 -#define FF_DEBUG_ER 0x00000400 -#define FF_DEBUG_MMCO 0x00000800 -#define FF_DEBUG_BUGS 0x00001000 -#define FF_DEBUG_BUFFERS 0x00008000 -#define FF_DEBUG_THREADS 0x00010000 -#define FF_DEBUG_GREEN_MD 0x00800000 -#define FF_DEBUG_NOMC 0x01000000 - - /** - * Error recognition; may misdetect some more or less valid parts as errors. - * This is a bitfield of the AV_EF_* values defined in defs.h. - * - * - encoding: Set by user. - * - decoding: Set by user. - */ - int err_recognition; - -#if FF_API_REORDERED_OPAQUE - /** - * opaque 64-bit number (generally a PTS) that will be reordered and - * output in AVFrame.reordered_opaque - * - encoding: Set by libavcodec to the reordered_opaque of the input - * frame corresponding to the last returned packet. Only - * supported by encoders with the - * AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE capability. - * - decoding: Set by user. - * - * @deprecated Use AV_CODEC_FLAG_COPY_OPAQUE instead - */ - attribute_deprecated - int64_t reordered_opaque; -#endif - - /** - * Hardware accelerator in use - * - encoding: unused. - * - decoding: Set by libavcodec - */ - const struct AVHWAccel *hwaccel; - - /** - * Legacy hardware accelerator context. - * - * For some hardware acceleration methods, the caller may use this field to - * signal hwaccel-specific data to the codec. The struct pointed to by this - * pointer is hwaccel-dependent and defined in the respective header. Please - * refer to the FFmpeg HW accelerator documentation to know how to fill - * this. - * - * In most cases this field is optional - the necessary information may also - * be provided to libavcodec through @ref hw_frames_ctx or @ref - * hw_device_ctx (see avcodec_get_hw_config()). However, in some cases it - * may be the only method of signalling some (optional) information. - * - * The struct and its contents are owned by the caller. - * - * - encoding: May be set by the caller before avcodec_open2(). Must remain - * valid until avcodec_free_context(). - * - decoding: May be set by the caller in the get_format() callback. - * Must remain valid until the next get_format() call, - * or avcodec_free_context() (whichever comes first). - */ - void *hwaccel_context; - - /** - * error - * - encoding: Set by libavcodec if flags & AV_CODEC_FLAG_PSNR. - * - decoding: unused - */ - uint64_t error[AV_NUM_DATA_POINTERS]; - - /** - * DCT algorithm, see FF_DCT_* below - * - encoding: Set by user. - * - decoding: unused - */ - int dct_algo; -#define FF_DCT_AUTO 0 -#define FF_DCT_FASTINT 1 -#define FF_DCT_INT 2 -#define FF_DCT_MMX 3 -#define FF_DCT_ALTIVEC 5 -#define FF_DCT_FAAN 6 - - /** - * IDCT algorithm, see FF_IDCT_* below. - * - encoding: Set by user. - * - decoding: Set by user. - */ - int idct_algo; -#define FF_IDCT_AUTO 0 -#define FF_IDCT_INT 1 -#define FF_IDCT_SIMPLE 2 -#define FF_IDCT_SIMPLEMMX 3 -#define FF_IDCT_ARM 7 -#define FF_IDCT_ALTIVEC 8 -#define FF_IDCT_SIMPLEARM 10 -#define FF_IDCT_XVID 14 -#define FF_IDCT_SIMPLEARMV5TE 16 -#define FF_IDCT_SIMPLEARMV6 17 -#define FF_IDCT_FAAN 20 -#define FF_IDCT_SIMPLENEON 22 -#if FF_API_IDCT_NONE -// formerly used by xvmc -#define FF_IDCT_NONE 24 -#endif -#define FF_IDCT_SIMPLEAUTO 128 - - /** - * bits per sample/pixel from the demuxer (needed for huffyuv). - * - encoding: Set by libavcodec. - * - decoding: Set by user. - */ - int bits_per_coded_sample; - - /** - * Bits per sample/pixel of internal libavcodec pixel/sample format. - * - encoding: set by user. - * - decoding: set by libavcodec. - */ - int bits_per_raw_sample; - - /** - * low resolution decoding, 1-> 1/2 size, 2->1/4 size - * - encoding: unused - * - decoding: Set by user. - */ - int lowres; - - /** - * thread count - * is used to decide how many independent tasks should be passed to execute() - * - encoding: Set by user. - * - decoding: Set by user. - */ - int thread_count; - - /** - * Which multithreading methods to use. - * Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread, - * so clients which cannot provide future frames should not use it. - * - * - encoding: Set by user, otherwise the default is used. - * - decoding: Set by user, otherwise the default is used. - */ - int thread_type; -#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once -#define FF_THREAD_SLICE 2 ///< Decode more than one part of a single frame at once - - /** - * Which multithreading methods are in use by the codec. - * - encoding: Set by libavcodec. - * - decoding: Set by libavcodec. - */ - int active_thread_type; - - /** - * The codec may call this to execute several independent things. - * It will return only after finishing all tasks. - * The user may replace this with some multithreaded implementation, - * the default implementation will execute the parts serially. - * @param count the number of things to execute - * - encoding: Set by libavcodec, user can override. - * - decoding: Set by libavcodec, user can override. - */ - int (*execute)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg), void *arg2, int *ret, int count, int size); - - /** - * The codec may call this to execute several independent things. - * It will return only after finishing all tasks. - * The user may replace this with some multithreaded implementation, - * the default implementation will execute the parts serially. - * @param c context passed also to func - * @param count the number of things to execute - * @param arg2 argument passed unchanged to func - * @param ret return values of executed functions, must have space for "count" values. May be NULL. - * @param func function that will be called count times, with jobnr from 0 to count-1. - * threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS and so that no - * two instances of func executing at the same time will have the same threadnr. - * @return always 0 currently, but code should handle a future improvement where when any call to func - * returns < 0 no further calls to func may be done and < 0 is returned. - * - encoding: Set by libavcodec, user can override. - * - decoding: Set by libavcodec, user can override. - */ - int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count); - - /** - * noise vs. sse weight for the nsse comparison function - * - encoding: Set by user. - * - decoding: unused - */ - int nsse_weight; - - /** - * profile - * - encoding: Set by user. - * - decoding: Set by libavcodec. - * See the AV_PROFILE_* defines in defs.h. - */ - int profile; -#if FF_API_FF_PROFILE_LEVEL - /** @deprecated The following defines are deprecated; use AV_PROFILE_* - * in defs.h instead. */ -#define FF_PROFILE_UNKNOWN -99 -#define FF_PROFILE_RESERVED -100 - -#define FF_PROFILE_AAC_MAIN 0 -#define FF_PROFILE_AAC_LOW 1 -#define FF_PROFILE_AAC_SSR 2 -#define FF_PROFILE_AAC_LTP 3 -#define FF_PROFILE_AAC_HE 4 -#define FF_PROFILE_AAC_HE_V2 28 -#define FF_PROFILE_AAC_LD 22 -#define FF_PROFILE_AAC_ELD 38 -#define FF_PROFILE_MPEG2_AAC_LOW 128 -#define FF_PROFILE_MPEG2_AAC_HE 131 - -#define FF_PROFILE_DNXHD 0 -#define FF_PROFILE_DNXHR_LB 1 -#define FF_PROFILE_DNXHR_SQ 2 -#define FF_PROFILE_DNXHR_HQ 3 -#define FF_PROFILE_DNXHR_HQX 4 -#define FF_PROFILE_DNXHR_444 5 - -#define FF_PROFILE_DTS 20 -#define FF_PROFILE_DTS_ES 30 -#define FF_PROFILE_DTS_96_24 40 -#define FF_PROFILE_DTS_HD_HRA 50 -#define FF_PROFILE_DTS_HD_MA 60 -#define FF_PROFILE_DTS_EXPRESS 70 -#define FF_PROFILE_DTS_HD_MA_X 61 -#define FF_PROFILE_DTS_HD_MA_X_IMAX 62 - - -#define FF_PROFILE_EAC3_DDP_ATMOS 30 - -#define FF_PROFILE_TRUEHD_ATMOS 30 - -#define FF_PROFILE_MPEG2_422 0 -#define FF_PROFILE_MPEG2_HIGH 1 -#define FF_PROFILE_MPEG2_SS 2 -#define FF_PROFILE_MPEG2_SNR_SCALABLE 3 -#define FF_PROFILE_MPEG2_MAIN 4 -#define FF_PROFILE_MPEG2_SIMPLE 5 - -#define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag -#define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag - -#define FF_PROFILE_H264_BASELINE 66 -#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED) -#define FF_PROFILE_H264_MAIN 77 -#define FF_PROFILE_H264_EXTENDED 88 -#define FF_PROFILE_H264_HIGH 100 -#define FF_PROFILE_H264_HIGH_10 110 -#define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA) -#define FF_PROFILE_H264_MULTIVIEW_HIGH 118 -#define FF_PROFILE_H264_HIGH_422 122 -#define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA) -#define FF_PROFILE_H264_STEREO_HIGH 128 -#define FF_PROFILE_H264_HIGH_444 144 -#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244 -#define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA) -#define FF_PROFILE_H264_CAVLC_444 44 - -#define FF_PROFILE_VC1_SIMPLE 0 -#define FF_PROFILE_VC1_MAIN 1 -#define FF_PROFILE_VC1_COMPLEX 2 -#define FF_PROFILE_VC1_ADVANCED 3 - -#define FF_PROFILE_MPEG4_SIMPLE 0 -#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1 -#define FF_PROFILE_MPEG4_CORE 2 -#define FF_PROFILE_MPEG4_MAIN 3 -#define FF_PROFILE_MPEG4_N_BIT 4 -#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5 -#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6 -#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7 -#define FF_PROFILE_MPEG4_HYBRID 8 -#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9 -#define FF_PROFILE_MPEG4_CORE_SCALABLE 10 -#define FF_PROFILE_MPEG4_ADVANCED_CODING 11 -#define FF_PROFILE_MPEG4_ADVANCED_CORE 12 -#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13 -#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14 -#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15 - -#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 1 -#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 2 -#define FF_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 32768 -#define FF_PROFILE_JPEG2000_DCINEMA_2K 3 -#define FF_PROFILE_JPEG2000_DCINEMA_4K 4 - -#define FF_PROFILE_VP9_0 0 -#define FF_PROFILE_VP9_1 1 -#define FF_PROFILE_VP9_2 2 -#define FF_PROFILE_VP9_3 3 - -#define FF_PROFILE_HEVC_MAIN 1 -#define FF_PROFILE_HEVC_MAIN_10 2 -#define FF_PROFILE_HEVC_MAIN_STILL_PICTURE 3 -#define FF_PROFILE_HEVC_REXT 4 -#define FF_PROFILE_HEVC_SCC 9 - -#define FF_PROFILE_VVC_MAIN_10 1 -#define FF_PROFILE_VVC_MAIN_10_444 33 - -#define FF_PROFILE_AV1_MAIN 0 -#define FF_PROFILE_AV1_HIGH 1 -#define FF_PROFILE_AV1_PROFESSIONAL 2 - -#define FF_PROFILE_MJPEG_HUFFMAN_BASELINE_DCT 0xc0 -#define FF_PROFILE_MJPEG_HUFFMAN_EXTENDED_SEQUENTIAL_DCT 0xc1 -#define FF_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT 0xc2 -#define FF_PROFILE_MJPEG_HUFFMAN_LOSSLESS 0xc3 -#define FF_PROFILE_MJPEG_JPEG_LS 0xf7 - -#define FF_PROFILE_SBC_MSBC 1 - -#define FF_PROFILE_PRORES_PROXY 0 -#define FF_PROFILE_PRORES_LT 1 -#define FF_PROFILE_PRORES_STANDARD 2 -#define FF_PROFILE_PRORES_HQ 3 -#define FF_PROFILE_PRORES_4444 4 -#define FF_PROFILE_PRORES_XQ 5 - -#define FF_PROFILE_ARIB_PROFILE_A 0 -#define FF_PROFILE_ARIB_PROFILE_C 1 - -#define FF_PROFILE_KLVA_SYNC 0 -#define FF_PROFILE_KLVA_ASYNC 1 - -#define FF_PROFILE_EVC_BASELINE 0 -#define FF_PROFILE_EVC_MAIN 1 -#endif - - /** - * Encoding level descriptor. - * - encoding: Set by user, corresponds to a specific level defined by the - * codec, usually corresponding to the profile level, if not specified it - * is set to FF_LEVEL_UNKNOWN. - * - decoding: Set by libavcodec. - * See AV_LEVEL_* in defs.h. - */ - int level; -#if FF_API_FF_PROFILE_LEVEL - /** @deprecated The following define is deprecated; use AV_LEVEL_UNKOWN - * in defs.h instead. */ -#define FF_LEVEL_UNKNOWN -99 -#endif - - /** - * Skip loop filtering for selected frames. - * - encoding: unused - * - decoding: Set by user. - */ - enum AVDiscard skip_loop_filter; - - /** - * Skip IDCT/dequantization for selected frames. - * - encoding: unused - * - decoding: Set by user. - */ - enum AVDiscard skip_idct; - - /** - * Skip decoding for selected frames. - * - encoding: unused - * - decoding: Set by user. - */ - enum AVDiscard skip_frame; - - /** - * Header containing style information for text subtitles. - * For SUBTITLE_ASS subtitle type, it should contain the whole ASS - * [Script Info] and [V4+ Styles] section, plus the [Events] line and - * the Format line following. It shouldn't include any Dialogue line. - * - encoding: Set/allocated/freed by user (before avcodec_open2()) - * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2()) - */ - uint8_t *subtitle_header; - int subtitle_header_size; - - /** - * Audio only. The number of "priming" samples (padding) inserted by the - * encoder at the beginning of the audio. I.e. this number of leading - * decoded samples must be discarded by the caller to get the original audio - * without leading padding. - * - * - decoding: unused - * - encoding: Set by libavcodec. The timestamps on the output packets are - * adjusted by the encoder so that they always refer to the - * first sample of the data actually contained in the packet, - * including any added padding. E.g. if the timebase is - * 1/samplerate and the timestamp of the first input sample is - * 0, the timestamp of the first output packet will be - * -initial_padding. - */ - int initial_padding; - - /** - * - decoding: For codecs that store a framerate value in the compressed - * bitstream, the decoder may export it here. { 0, 1} when - * unknown. - * - encoding: May be used to signal the framerate of CFR content to an - * encoder. - */ - AVRational framerate; - - /** - * Nominal unaccelerated pixel format, see AV_PIX_FMT_xxx. - * - encoding: unused. - * - decoding: Set by libavcodec before calling get_format() - */ - enum AVPixelFormat sw_pix_fmt; - - /** - * Timebase in which pkt_dts/pts and AVPacket.dts/pts are expressed. - * - encoding: unused. - * - decoding: set by user. - */ - AVRational pkt_timebase; - - /** - * AVCodecDescriptor - * - encoding: unused. - * - decoding: set by libavcodec. - */ - const struct AVCodecDescriptor *codec_descriptor; - - /** - * Current statistics for PTS correction. - * - decoding: maintained and used by libavcodec, not intended to be used by user apps - * - encoding: unused - */ - int64_t pts_correction_num_faulty_pts; /// Number of incorrect PTS values so far - int64_t pts_correction_num_faulty_dts; /// Number of incorrect DTS values so far - int64_t pts_correction_last_pts; /// PTS of the last frame - int64_t pts_correction_last_dts; /// DTS of the last frame - - /** - * Character encoding of the input subtitles file. - * - decoding: set by user - * - encoding: unused - */ - char *sub_charenc; - - /** - * Subtitles character encoding mode. Formats or codecs might be adjusting - * this setting (if they are doing the conversion themselves for instance). - * - decoding: set by libavcodec - * - encoding: unused - */ - int sub_charenc_mode; -#define FF_SUB_CHARENC_MODE_DO_NOTHING -1 ///< do nothing (demuxer outputs a stream supposed to be already in UTF-8, or the codec is bitmap for instance) -#define FF_SUB_CHARENC_MODE_AUTOMATIC 0 ///< libavcodec will select the mode itself -#define FF_SUB_CHARENC_MODE_PRE_DECODER 1 ///< the AVPacket data needs to be recoded to UTF-8 before being fed to the decoder, requires iconv -#define FF_SUB_CHARENC_MODE_IGNORE 2 ///< neither convert the subtitles, nor check them for valid UTF-8 - - /** - * Skip processing alpha if supported by codec. - * Note that if the format uses pre-multiplied alpha (common with VP6, - * and recommended due to better video quality/compression) - * the image will look as if alpha-blended onto a black background. - * However for formats that do not use pre-multiplied alpha - * there might be serious artefacts (though e.g. libswscale currently - * assumes pre-multiplied alpha anyway). - * - * - decoding: set by user - * - encoding: unused - */ - int skip_alpha; - - /** - * Number of samples to skip after a discontinuity - * - decoding: unused - * - encoding: set by libavcodec - */ - int seek_preroll; - - /** - * custom intra quantization matrix - * - encoding: Set by user, can be NULL. - * - decoding: unused. - */ - uint16_t *chroma_intra_matrix; - - /** - * dump format separator. - * can be ", " or "\n " or anything else - * - encoding: Set by user. - * - decoding: Set by user. - */ - uint8_t *dump_separator; - - /** - * ',' separated list of allowed decoders. - * If NULL then all are allowed - * - encoding: unused - * - decoding: set by user - */ - char *codec_whitelist; - - /** - * Properties of the stream that gets decoded - * - encoding: unused - * - decoding: set by libavcodec - */ - unsigned properties; -#define FF_CODEC_PROPERTY_LOSSLESS 0x00000001 -#define FF_CODEC_PROPERTY_CLOSED_CAPTIONS 0x00000002 -#define FF_CODEC_PROPERTY_FILM_GRAIN 0x00000004 - - /** - * Additional data associated with the entire coded stream. - * - * - decoding: may be set by user before calling avcodec_open2(). - * - encoding: may be set by libavcodec after avcodec_open2(). - */ - AVPacketSideData *coded_side_data; - int nb_coded_side_data; - - /** - * A reference to the AVHWFramesContext describing the input (for encoding) - * or output (decoding) frames. The reference is set by the caller and - * afterwards owned (and freed) by libavcodec - it should never be read by - * the caller after being set. - * - * - decoding: This field should be set by the caller from the get_format() - * callback. The previous reference (if any) will always be - * unreffed by libavcodec before the get_format() call. - * - * If the default get_buffer2() is used with a hwaccel pixel - * format, then this AVHWFramesContext will be used for - * allocating the frame buffers. - * - * - encoding: For hardware encoders configured to use a hwaccel pixel - * format, this field should be set by the caller to a reference - * to the AVHWFramesContext describing input frames. - * AVHWFramesContext.format must be equal to - * AVCodecContext.pix_fmt. - * - * This field should be set before avcodec_open2() is called. - */ - AVBufferRef *hw_frames_ctx; - - /** - * Audio only. The amount of padding (in samples) appended by the encoder to - * the end of the audio. I.e. this number of decoded samples must be - * discarded by the caller from the end of the stream to get the original - * audio without any trailing padding. - * - * - decoding: unused - * - encoding: unused - */ - int trailing_padding; - - /** - * The number of pixels per image to maximally accept. - * - * - decoding: set by user - * - encoding: set by user - */ - int64_t max_pixels; - - /** - * A reference to the AVHWDeviceContext describing the device which will - * be used by a hardware encoder/decoder. The reference is set by the - * caller and afterwards owned (and freed) by libavcodec. - * - * This should be used if either the codec device does not require - * hardware frames or any that are used are to be allocated internally by - * libavcodec. If the user wishes to supply any of the frames used as - * encoder input or decoder output then hw_frames_ctx should be used - * instead. When hw_frames_ctx is set in get_format() for a decoder, this - * field will be ignored while decoding the associated stream segment, but - * may again be used on a following one after another get_format() call. - * - * For both encoders and decoders this field should be set before - * avcodec_open2() is called and must not be written to thereafter. - * - * Note that some decoders may require this field to be set initially in - * order to support hw_frames_ctx at all - in that case, all frames - * contexts used must be created on the same device. - */ - AVBufferRef *hw_device_ctx; - - /** - * Bit set of AV_HWACCEL_FLAG_* flags, which affect hardware accelerated - * decoding (if active). - * - encoding: unused - * - decoding: Set by user (either before avcodec_open2(), or in the - * AVCodecContext.get_format callback) - */ - int hwaccel_flags; - - /** - * Video decoding only. Certain video codecs support cropping, meaning that - * only a sub-rectangle of the decoded frame is intended for display. This - * option controls how cropping is handled by libavcodec. - * - * When set to 1 (the default), libavcodec will apply cropping internally. - * I.e. it will modify the output frame width/height fields and offset the - * data pointers (only by as much as possible while preserving alignment, or - * by the full amount if the AV_CODEC_FLAG_UNALIGNED flag is set) so that - * the frames output by the decoder refer only to the cropped area. The - * crop_* fields of the output frames will be zero. - * - * When set to 0, the width/height fields of the output frames will be set - * to the coded dimensions and the crop_* fields will describe the cropping - * rectangle. Applying the cropping is left to the caller. - * - * @warning When hardware acceleration with opaque output frames is used, - * libavcodec is unable to apply cropping from the top/left border. - * - * @note when this option is set to zero, the width/height fields of the - * AVCodecContext and output AVFrames have different meanings. The codec - * context fields store display dimensions (with the coded dimensions in - * coded_width/height), while the frame fields store the coded dimensions - * (with the display dimensions being determined by the crop_* fields). - */ - int apply_cropping; - - /* - * Video decoding only. Sets the number of extra hardware frames which - * the decoder will allocate for use by the caller. This must be set - * before avcodec_open2() is called. - * - * Some hardware decoders require all frames that they will use for - * output to be defined in advance before decoding starts. For such - * decoders, the hardware frame pool must therefore be of a fixed size. - * The extra frames set here are on top of any number that the decoder - * needs internally in order to operate normally (for example, frames - * used as reference pictures). - */ - int extra_hw_frames; - - /** - * The percentage of damaged samples to discard a frame. - * - * - decoding: set by user - * - encoding: unused - */ - int discard_damaged_percentage; - - /** - * The number of samples per frame to maximally accept. - * - * - decoding: set by user - * - encoding: set by user - */ - int64_t max_samples; - - /** - * Bit set of AV_CODEC_EXPORT_DATA_* flags, which affects the kind of - * metadata exported in frame, packet, or coded stream side data by - * decoders and encoders. - * - * - decoding: set by user - * - encoding: set by user - */ - int export_side_data; - - /** - * This callback is called at the beginning of each packet to get a data - * buffer for it. - * - * The following field will be set in the packet before this callback is - * called: - * - size - * This callback must use the above value to calculate the required buffer size, - * which must padded by at least AV_INPUT_BUFFER_PADDING_SIZE bytes. - * - * In some specific cases, the encoder may not use the entire buffer allocated by this - * callback. This will be reflected in the size value in the packet once returned by - * avcodec_receive_packet(). - * - * This callback must fill the following fields in the packet: - * - data: alignment requirements for AVPacket apply, if any. Some architectures and - * encoders may benefit from having aligned data. - * - buf: must contain a pointer to an AVBufferRef structure. The packet's - * data pointer must be contained in it. See: av_buffer_create(), av_buffer_alloc(), - * and av_buffer_ref(). - * - * If AV_CODEC_CAP_DR1 is not set then get_encode_buffer() must call - * avcodec_default_get_encode_buffer() instead of providing a buffer allocated by - * some other means. - * - * The flags field may contain a combination of AV_GET_ENCODE_BUFFER_FLAG_ flags. - * They may be used for example to hint what use the buffer may get after being - * created. - * Implementations of this callback may ignore flags they don't understand. - * If AV_GET_ENCODE_BUFFER_FLAG_REF is set in flags then the packet may be reused - * (read and/or written to if it is writable) later by libavcodec. - * - * This callback must be thread-safe, as when frame threading is used, it may - * be called from multiple threads simultaneously. - * - * @see avcodec_default_get_encode_buffer() - * - * - encoding: Set by libavcodec, user can override. - * - decoding: unused - */ - int (*get_encode_buffer)(struct AVCodecContext *s, AVPacket *pkt, int flags); - - /** - * Audio channel layout. - * - encoding: must be set by the caller, to one of AVCodec.ch_layouts. - * - decoding: may be set by the caller if known e.g. from the container. - * The decoder can then override during decoding as needed. - */ - AVChannelLayout ch_layout; - - /** - * Frame counter, set by libavcodec. - * - * - decoding: total number of frames returned from the decoder so far. - * - encoding: total number of frames passed to the encoder so far. - * - * @note the counter is not incremented if encoding/decoding resulted in - * an error. - */ - int64_t frame_num; -} AVCodecContext; - -/** - * @defgroup lavc_hwaccel AVHWAccel - * - * @note Nothing in this structure should be accessed by the user. At some - * point in future it will not be externally visible at all. - * - * @{ - */ -typedef struct AVHWAccel { - /** - * Name of the hardware accelerated codec. - * The name is globally unique among encoders and among decoders (but an - * encoder and a decoder can share the same name). - */ - const char *name; - - /** - * Type of codec implemented by the hardware accelerator. - * - * See AVMEDIA_TYPE_xxx - */ - enum AVMediaType type; - - /** - * Codec implemented by the hardware accelerator. - * - * See AV_CODEC_ID_xxx - */ - enum AVCodecID id; - - /** - * Supported pixel format. - * - * Only hardware accelerated formats are supported here. - */ - enum AVPixelFormat pix_fmt; - - /** - * Hardware accelerated codec capabilities. - * see AV_HWACCEL_CODEC_CAP_* - */ - int capabilities; -} AVHWAccel; - -/** - * HWAccel is experimental and is thus avoided in favor of non experimental - * codecs - */ -#define AV_HWACCEL_CODEC_CAP_EXPERIMENTAL 0x0200 - -/** - * Hardware acceleration should be used for decoding even if the codec level - * used is unknown or higher than the maximum supported level reported by the - * hardware driver. - * - * It's generally a good idea to pass this flag unless you have a specific - * reason not to, as hardware tends to under-report supported levels. - */ -#define AV_HWACCEL_FLAG_IGNORE_LEVEL (1 << 0) - -/** - * Hardware acceleration can output YUV pixel formats with a different chroma - * sampling than 4:2:0 and/or other than 8 bits per component. - */ -#define AV_HWACCEL_FLAG_ALLOW_HIGH_DEPTH (1 << 1) - -/** - * Hardware acceleration should still be attempted for decoding when the - * codec profile does not match the reported capabilities of the hardware. - * - * For example, this can be used to try to decode baseline profile H.264 - * streams in hardware - it will often succeed, because many streams marked - * as baseline profile actually conform to constrained baseline profile. - * - * @warning If the stream is actually not supported then the behaviour is - * undefined, and may include returning entirely incorrect output - * while indicating success. - */ -#define AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH (1 << 2) - -/** - * Some hardware decoders (namely nvdec) can either output direct decoder - * surfaces, or make an on-device copy and return said copy. - * There is a hard limit on how many decoder surfaces there can be, and it - * cannot be accurately guessed ahead of time. - * For some processing chains, this can be okay, but others will run into the - * limit and in turn produce very confusing errors that require fine tuning of - * more or less obscure options by the user, or in extreme cases cannot be - * resolved at all without inserting an avfilter that forces a copy. - * - * Thus, the hwaccel will by default make a copy for safety and resilience. - * If a users really wants to minimize the amount of copies, they can set this - * flag and ensure their processing chain does not exhaust the surface pool. - */ -#define AV_HWACCEL_FLAG_UNSAFE_OUTPUT (1 << 3) - -/** - * @} - */ - -enum AVSubtitleType { - SUBTITLE_NONE, - - SUBTITLE_BITMAP, ///< A bitmap, pict will be set - - /** - * Plain text, the text field must be set by the decoder and is - * authoritative. ass and pict fields may contain approximations. - */ - SUBTITLE_TEXT, - - /** - * Formatted text, the ass field must be set by the decoder and is - * authoritative. pict and text fields may contain approximations. - */ - SUBTITLE_ASS, -}; - -#define AV_SUBTITLE_FLAG_FORCED 0x00000001 - -typedef struct AVSubtitleRect { - int x; ///< top left corner of pict, undefined when pict is not set - int y; ///< top left corner of pict, undefined when pict is not set - int w; ///< width of pict, undefined when pict is not set - int h; ///< height of pict, undefined when pict is not set - int nb_colors; ///< number of colors in pict, undefined when pict is not set - - /** - * data+linesize for the bitmap of this subtitle. - * Can be set for text/ass as well once they are rendered. - */ - uint8_t *data[4]; - int linesize[4]; - - enum AVSubtitleType type; - - char *text; ///< 0 terminated plain UTF-8 text - - /** - * 0 terminated ASS/SSA compatible event line. - * The presentation of this is unaffected by the other values in this - * struct. - */ - char *ass; - - int flags; -} AVSubtitleRect; - -typedef struct AVSubtitle { - uint16_t format; /* 0 = graphics */ - uint32_t start_display_time; /* relative to packet pts, in ms */ - uint32_t end_display_time; /* relative to packet pts, in ms */ - unsigned num_rects; - AVSubtitleRect **rects; - int64_t pts; ///< Same as packet pts, in AV_TIME_BASE -} AVSubtitle; - -/** - * Return the LIBAVCODEC_VERSION_INT constant. - */ -unsigned avcodec_version(void); - -/** - * Return the libavcodec build-time configuration. - */ -const char *avcodec_configuration(void); - -/** - * Return the libavcodec license. - */ -const char *avcodec_license(void); - -/** - * Allocate an AVCodecContext and set its fields to default values. The - * resulting struct should be freed with avcodec_free_context(). - * - * @param codec if non-NULL, allocate private data and initialize defaults - * for the given codec. It is illegal to then call avcodec_open2() - * with a different codec. - * If NULL, then the codec-specific defaults won't be initialized, - * which may result in suboptimal default settings (this is - * important mainly for encoders, e.g. libx264). - * - * @return An AVCodecContext filled with default values or NULL on failure. - */ -AVCodecContext *avcodec_alloc_context3(const AVCodec *codec); - -/** - * Free the codec context and everything associated with it and write NULL to - * the provided pointer. - */ -void avcodec_free_context(AVCodecContext **avctx); - -/** - * Get the AVClass for AVCodecContext. It can be used in combination with - * AV_OPT_SEARCH_FAKE_OBJ for examining options. - * - * @see av_opt_find(). - */ -const AVClass *avcodec_get_class(void); - -/** - * Get the AVClass for AVSubtitleRect. It can be used in combination with - * AV_OPT_SEARCH_FAKE_OBJ for examining options. - * - * @see av_opt_find(). - */ -const AVClass *avcodec_get_subtitle_rect_class(void); - -/** - * Fill the parameters struct based on the values from the supplied codec - * context. Any allocated fields in par are freed and replaced with duplicates - * of the corresponding fields in codec. - * - * @return >= 0 on success, a negative AVERROR code on failure - */ -int avcodec_parameters_from_context(struct AVCodecParameters *par, - const AVCodecContext *codec); - -/** - * Fill the codec context based on the values from the supplied codec - * parameters. Any allocated fields in codec that have a corresponding field in - * par are freed and replaced with duplicates of the corresponding field in par. - * Fields in codec that do not have a counterpart in par are not touched. - * - * @return >= 0 on success, a negative AVERROR code on failure. - */ -int avcodec_parameters_to_context(AVCodecContext *codec, - const struct AVCodecParameters *par); - -/** - * Initialize the AVCodecContext to use the given AVCodec. Prior to using this - * function the context has to be allocated with avcodec_alloc_context3(). - * - * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(), - * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for - * retrieving a codec. - * - * Depending on the codec, you might need to set options in the codec context - * also for decoding (e.g. width, height, or the pixel or audio sample format in - * the case the information is not available in the bitstream, as when decoding - * raw audio or video). - * - * Options in the codec context can be set either by setting them in the options - * AVDictionary, or by setting the values in the context itself, directly or by - * using the av_opt_set() API before calling this function. - * - * Example: - * @code - * av_dict_set(&opts, "b", "2.5M", 0); - * codec = avcodec_find_decoder(AV_CODEC_ID_H264); - * if (!codec) - * exit(1); - * - * context = avcodec_alloc_context3(codec); - * - * if (avcodec_open2(context, codec, opts) < 0) - * exit(1); - * @endcode - * - * In the case AVCodecParameters are available (e.g. when demuxing a stream - * using libavformat, and accessing the AVStream contained in the demuxer), the - * codec parameters can be copied to the codec context using - * avcodec_parameters_to_context(), as in the following example: - * - * @code - * AVStream *stream = ...; - * context = avcodec_alloc_context3(codec); - * if (avcodec_parameters_to_context(context, stream->codecpar) < 0) - * exit(1); - * if (avcodec_open2(context, codec, NULL) < 0) - * exit(1); - * @endcode - * - * @note Always call this function before using decoding routines (such as - * @ref avcodec_receive_frame()). - * - * @param avctx The context to initialize. - * @param codec The codec to open this context for. If a non-NULL codec has been - * previously passed to avcodec_alloc_context3() or - * for this context, then this parameter MUST be either NULL or - * equal to the previously passed codec. - * @param options A dictionary filled with AVCodecContext and codec-private - * options, which are set on top of the options already set in - * avctx, can be NULL. On return this object will be filled with - * options that were not found in the avctx codec context. - * - * @return zero on success, a negative value on error - * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(), - * av_dict_set(), av_opt_set(), av_opt_find(), avcodec_parameters_to_context() - */ -int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options); - -/** - * Close a given AVCodecContext and free all the data associated with it - * (but not the AVCodecContext itself). - * - * Calling this function on an AVCodecContext that hasn't been opened will free - * the codec-specific data allocated in avcodec_alloc_context3() with a non-NULL - * codec. Subsequent calls will do nothing. - * - * @note Do not use this function. Use avcodec_free_context() to destroy a - * codec context (either open or closed). Opening and closing a codec context - * multiple times is not supported anymore -- use multiple codec contexts - * instead. - */ -int avcodec_close(AVCodecContext *avctx); - -/** - * Free all allocated data in the given subtitle struct. - * - * @param sub AVSubtitle to free. - */ -void avsubtitle_free(AVSubtitle *sub); - -/** - * @} - */ - -/** - * @addtogroup lavc_decoding - * @{ - */ - -/** - * The default callback for AVCodecContext.get_buffer2(). It is made public so - * it can be called by custom get_buffer2() implementations for decoders without - * AV_CODEC_CAP_DR1 set. - */ -int avcodec_default_get_buffer2(AVCodecContext *s, AVFrame *frame, int flags); - -/** - * The default callback for AVCodecContext.get_encode_buffer(). It is made public so - * it can be called by custom get_encode_buffer() implementations for encoders without - * AV_CODEC_CAP_DR1 set. - */ -int avcodec_default_get_encode_buffer(AVCodecContext *s, AVPacket *pkt, int flags); - -/** - * Modify width and height values so that they will result in a memory - * buffer that is acceptable for the codec if you do not use any horizontal - * padding. - * - * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened. - */ -void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height); - -/** - * Modify width and height values so that they will result in a memory - * buffer that is acceptable for the codec if you also ensure that all - * line sizes are a multiple of the respective linesize_align[i]. - * - * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened. - */ -void avcodec_align_dimensions2(AVCodecContext *s, int *width, int *height, - int linesize_align[AV_NUM_DATA_POINTERS]); - -#ifdef FF_API_AVCODEC_CHROMA_POS -/** - * Converts AVChromaLocation to swscale x/y chroma position. - * - * The positions represent the chroma (0,0) position in a coordinates system - * with luma (0,0) representing the origin and luma(1,1) representing 256,256 - * - * @param xpos horizontal chroma sample position - * @param ypos vertical chroma sample position - * @deprecated Use av_chroma_location_enum_to_pos() instead. - */ - attribute_deprecated -int avcodec_enum_to_chroma_pos(int *xpos, int *ypos, enum AVChromaLocation pos); - -/** - * Converts swscale x/y chroma position to AVChromaLocation. - * - * The positions represent the chroma (0,0) position in a coordinates system - * with luma (0,0) representing the origin and luma(1,1) representing 256,256 - * - * @param xpos horizontal chroma sample position - * @param ypos vertical chroma sample position - * @deprecated Use av_chroma_location_pos_to_enum() instead. - */ - attribute_deprecated -enum AVChromaLocation avcodec_chroma_pos_to_enum(int xpos, int ypos); -#endif - -/** - * Decode a subtitle message. - * Return a negative value on error, otherwise return the number of bytes used. - * If no subtitle could be decompressed, got_sub_ptr is zero. - * Otherwise, the subtitle is stored in *sub. - * Note that AV_CODEC_CAP_DR1 is not available for subtitle codecs. This is for - * simplicity, because the performance difference is expected to be negligible - * and reusing a get_buffer written for video codecs would probably perform badly - * due to a potentially very different allocation pattern. - * - * Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between input - * and output. This means that for some packets they will not immediately - * produce decoded output and need to be flushed at the end of decoding to get - * all the decoded data. Flushing is done by calling this function with packets - * with avpkt->data set to NULL and avpkt->size set to 0 until it stops - * returning subtitles. It is safe to flush even those decoders that are not - * marked with AV_CODEC_CAP_DELAY, then no subtitles will be returned. - * - * @note The AVCodecContext MUST have been opened with @ref avcodec_open2() - * before packets may be fed to the decoder. - * - * @param avctx the codec context - * @param[out] sub The preallocated AVSubtitle in which the decoded subtitle will be stored, - * must be freed with avsubtitle_free if *got_sub_ptr is set. - * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed, otherwise, it is nonzero. - * @param[in] avpkt The input AVPacket containing the input buffer. - */ -int avcodec_decode_subtitle2(AVCodecContext *avctx, AVSubtitle *sub, - int *got_sub_ptr, const AVPacket *avpkt); - -/** - * Supply raw packet data as input to a decoder. - * - * Internally, this call will copy relevant AVCodecContext fields, which can - * influence decoding per-packet, and apply them when the packet is actually - * decoded. (For example AVCodecContext.skip_frame, which might direct the - * decoder to drop the frame contained by the packet sent with this function.) - * - * @warning The input buffer, avpkt->data must be AV_INPUT_BUFFER_PADDING_SIZE - * larger than the actual read bytes because some optimized bitstream - * readers read 32 or 64 bits at once and could read over the end. - * - * @note The AVCodecContext MUST have been opened with @ref avcodec_open2() - * before packets may be fed to the decoder. - * - * @param avctx codec context - * @param[in] avpkt The input AVPacket. Usually, this will be a single video - * frame, or several complete audio frames. - * Ownership of the packet remains with the caller, and the - * decoder will not write to the packet. The decoder may create - * a reference to the packet data (or copy it if the packet is - * not reference-counted). - * Unlike with older APIs, the packet is always fully consumed, - * and if it contains multiple frames (e.g. some audio codecs), - * will require you to call avcodec_receive_frame() multiple - * times afterwards before you can send a new packet. - * It can be NULL (or an AVPacket with data set to NULL and - * size set to 0); in this case, it is considered a flush - * packet, which signals the end of the stream. Sending the - * first flush packet will return success. Subsequent ones are - * unnecessary and will return AVERROR_EOF. If the decoder - * still has frames buffered, it will return them after sending - * a flush packet. - * - * @retval 0 success - * @retval AVERROR(EAGAIN) input is not accepted in the current state - user - * must read output with avcodec_receive_frame() (once - * all output is read, the packet should be resent, - * and the call will not fail with EAGAIN). - * @retval AVERROR_EOF the decoder has been flushed, and no new packets can be - * sent to it (also returned if more than 1 flush - * packet is sent) - * @retval AVERROR(EINVAL) codec not opened, it is an encoder, or requires flush - * @retval AVERROR(ENOMEM) failed to add packet to internal queue, or similar - * @retval "another negative error code" legitimate decoding errors - */ -int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt); - -/** - * Return decoded output data from a decoder or encoder (when the - * @ref AV_CODEC_FLAG_RECON_FRAME flag is used). - * - * @param avctx codec context - * @param frame This will be set to a reference-counted video or audio - * frame (depending on the decoder type) allocated by the - * codec. Note that the function will always call - * av_frame_unref(frame) before doing anything else. - * - * @retval 0 success, a frame was returned - * @retval AVERROR(EAGAIN) output is not available in this state - user must - * try to send new input - * @retval AVERROR_EOF the codec has been fully flushed, and there will be - * no more output frames - * @retval AVERROR(EINVAL) codec not opened, or it is an encoder without the - * @ref AV_CODEC_FLAG_RECON_FRAME flag enabled - * @retval "other negative error code" legitimate decoding errors - */ -int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame); - -/** - * Supply a raw video or audio frame to the encoder. Use avcodec_receive_packet() - * to retrieve buffered output packets. - * - * @param avctx codec context - * @param[in] frame AVFrame containing the raw audio or video frame to be encoded. - * Ownership of the frame remains with the caller, and the - * encoder will not write to the frame. The encoder may create - * a reference to the frame data (or copy it if the frame is - * not reference-counted). - * It can be NULL, in which case it is considered a flush - * packet. This signals the end of the stream. If the encoder - * still has packets buffered, it will return them after this - * call. Once flushing mode has been entered, additional flush - * packets are ignored, and sending frames will return - * AVERROR_EOF. - * - * For audio: - * If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame - * can have any number of samples. - * If it is not set, frame->nb_samples must be equal to - * avctx->frame_size for all frames except the last. - * The final frame may be smaller than avctx->frame_size. - * @retval 0 success - * @retval AVERROR(EAGAIN) input is not accepted in the current state - user must - * read output with avcodec_receive_packet() (once all - * output is read, the packet should be resent, and the - * call will not fail with EAGAIN). - * @retval AVERROR_EOF the encoder has been flushed, and no new frames can - * be sent to it - * @retval AVERROR(EINVAL) codec not opened, it is a decoder, or requires flush - * @retval AVERROR(ENOMEM) failed to add packet to internal queue, or similar - * @retval "another negative error code" legitimate encoding errors - */ -int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame); - -/** - * Read encoded data from the encoder. - * - * @param avctx codec context - * @param avpkt This will be set to a reference-counted packet allocated by the - * encoder. Note that the function will always call - * av_packet_unref(avpkt) before doing anything else. - * @retval 0 success - * @retval AVERROR(EAGAIN) output is not available in the current state - user must - * try to send input - * @retval AVERROR_EOF the encoder has been fully flushed, and there will be no - * more output packets - * @retval AVERROR(EINVAL) codec not opened, or it is a decoder - * @retval "another negative error code" legitimate encoding errors - */ -int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt); - -/** - * Create and return a AVHWFramesContext with values adequate for hardware - * decoding. This is meant to get called from the get_format callback, and is - * a helper for preparing a AVHWFramesContext for AVCodecContext.hw_frames_ctx. - * This API is for decoding with certain hardware acceleration modes/APIs only. - * - * The returned AVHWFramesContext is not initialized. The caller must do this - * with av_hwframe_ctx_init(). - * - * Calling this function is not a requirement, but makes it simpler to avoid - * codec or hardware API specific details when manually allocating frames. - * - * Alternatively to this, an API user can set AVCodecContext.hw_device_ctx, - * which sets up AVCodecContext.hw_frames_ctx fully automatically, and makes - * it unnecessary to call this function or having to care about - * AVHWFramesContext initialization at all. - * - * There are a number of requirements for calling this function: - * - * - It must be called from get_format with the same avctx parameter that was - * passed to get_format. Calling it outside of get_format is not allowed, and - * can trigger undefined behavior. - * - The function is not always supported (see description of return values). - * Even if this function returns successfully, hwaccel initialization could - * fail later. (The degree to which implementations check whether the stream - * is actually supported varies. Some do this check only after the user's - * get_format callback returns.) - * - The hw_pix_fmt must be one of the choices suggested by get_format. If the - * user decides to use a AVHWFramesContext prepared with this API function, - * the user must return the same hw_pix_fmt from get_format. - * - The device_ref passed to this function must support the given hw_pix_fmt. - * - After calling this API function, it is the user's responsibility to - * initialize the AVHWFramesContext (returned by the out_frames_ref parameter), - * and to set AVCodecContext.hw_frames_ctx to it. If done, this must be done - * before returning from get_format (this is implied by the normal - * AVCodecContext.hw_frames_ctx API rules). - * - The AVHWFramesContext parameters may change every time time get_format is - * called. Also, AVCodecContext.hw_frames_ctx is reset before get_format. So - * you are inherently required to go through this process again on every - * get_format call. - * - It is perfectly possible to call this function without actually using - * the resulting AVHWFramesContext. One use-case might be trying to reuse a - * previously initialized AVHWFramesContext, and calling this API function - * only to test whether the required frame parameters have changed. - * - Fields that use dynamically allocated values of any kind must not be set - * by the user unless setting them is explicitly allowed by the documentation. - * If the user sets AVHWFramesContext.free and AVHWFramesContext.user_opaque, - * the new free callback must call the potentially set previous free callback. - * This API call may set any dynamically allocated fields, including the free - * callback. - * - * The function will set at least the following fields on AVHWFramesContext - * (potentially more, depending on hwaccel API): - * - * - All fields set by av_hwframe_ctx_alloc(). - * - Set the format field to hw_pix_fmt. - * - Set the sw_format field to the most suited and most versatile format. (An - * implication is that this will prefer generic formats over opaque formats - * with arbitrary restrictions, if possible.) - * - Set the width/height fields to the coded frame size, rounded up to the - * API-specific minimum alignment. - * - Only _if_ the hwaccel requires a pre-allocated pool: set the initial_pool_size - * field to the number of maximum reference surfaces possible with the codec, - * plus 1 surface for the user to work (meaning the user can safely reference - * at most 1 decoded surface at a time), plus additional buffering introduced - * by frame threading. If the hwaccel does not require pre-allocation, the - * field is left to 0, and the decoder will allocate new surfaces on demand - * during decoding. - * - Possibly AVHWFramesContext.hwctx fields, depending on the underlying - * hardware API. - * - * Essentially, out_frames_ref returns the same as av_hwframe_ctx_alloc(), but - * with basic frame parameters set. - * - * The function is stateless, and does not change the AVCodecContext or the - * device_ref AVHWDeviceContext. - * - * @param avctx The context which is currently calling get_format, and which - * implicitly contains all state needed for filling the returned - * AVHWFramesContext properly. - * @param device_ref A reference to the AVHWDeviceContext describing the device - * which will be used by the hardware decoder. - * @param hw_pix_fmt The hwaccel format you are going to return from get_format. - * @param out_frames_ref On success, set to a reference to an _uninitialized_ - * AVHWFramesContext, created from the given device_ref. - * Fields will be set to values required for decoding. - * Not changed if an error is returned. - * @return zero on success, a negative value on error. The following error codes - * have special semantics: - * AVERROR(ENOENT): the decoder does not support this functionality. Setup - * is always manual, or it is a decoder which does not - * support setting AVCodecContext.hw_frames_ctx at all, - * or it is a software format. - * AVERROR(EINVAL): it is known that hardware decoding is not supported for - * this configuration, or the device_ref is not supported - * for the hwaccel referenced by hw_pix_fmt. - */ -int avcodec_get_hw_frames_parameters(AVCodecContext *avctx, - AVBufferRef *device_ref, - enum AVPixelFormat hw_pix_fmt, - AVBufferRef **out_frames_ref); - - - -/** - * @defgroup lavc_parsing Frame parsing - * @{ - */ - -enum AVPictureStructure { - AV_PICTURE_STRUCTURE_UNKNOWN, ///< unknown - AV_PICTURE_STRUCTURE_TOP_FIELD, ///< coded as top field - AV_PICTURE_STRUCTURE_BOTTOM_FIELD, ///< coded as bottom field - AV_PICTURE_STRUCTURE_FRAME, ///< coded as frame -}; - -typedef struct AVCodecParserContext { - void *priv_data; - const struct AVCodecParser *parser; - int64_t frame_offset; /* offset of the current frame */ - int64_t cur_offset; /* current offset - (incremented by each av_parser_parse()) */ - int64_t next_frame_offset; /* offset of the next frame */ - /* video info */ - int pict_type; /* XXX: Put it back in AVCodecContext. */ - /** - * This field is used for proper frame duration computation in lavf. - * It signals, how much longer the frame duration of the current frame - * is compared to normal frame duration. - * - * frame_duration = (1 + repeat_pict) * time_base - * - * It is used by codecs like H.264 to display telecined material. - */ - int repeat_pict; /* XXX: Put it back in AVCodecContext. */ - int64_t pts; /* pts of the current frame */ - int64_t dts; /* dts of the current frame */ - - /* private data */ - int64_t last_pts; - int64_t last_dts; - int fetch_timestamp; - -#define AV_PARSER_PTS_NB 4 - int cur_frame_start_index; - int64_t cur_frame_offset[AV_PARSER_PTS_NB]; - int64_t cur_frame_pts[AV_PARSER_PTS_NB]; - int64_t cur_frame_dts[AV_PARSER_PTS_NB]; - - int flags; -#define PARSER_FLAG_COMPLETE_FRAMES 0x0001 -#define PARSER_FLAG_ONCE 0x0002 -/// Set if the parser has a valid file offset -#define PARSER_FLAG_FETCHED_OFFSET 0x0004 -#define PARSER_FLAG_USE_CODEC_TS 0x1000 - - int64_t offset; ///< byte offset from starting packet start - int64_t cur_frame_end[AV_PARSER_PTS_NB]; - - /** - * Set by parser to 1 for key frames and 0 for non-key frames. - * It is initialized to -1, so if the parser doesn't set this flag, - * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames - * will be used. - */ - int key_frame; - - // Timestamp generation support: - /** - * Synchronization point for start of timestamp generation. - * - * Set to >0 for sync point, 0 for no sync point and <0 for undefined - * (default). - * - * For example, this corresponds to presence of H.264 buffering period - * SEI message. - */ - int dts_sync_point; - - /** - * Offset of the current timestamp against last timestamp sync point in - * units of AVCodecContext.time_base. - * - * Set to INT_MIN when dts_sync_point unused. Otherwise, it must - * contain a valid timestamp offset. - * - * Note that the timestamp of sync point has usually a nonzero - * dts_ref_dts_delta, which refers to the previous sync point. Offset of - * the next frame after timestamp sync point will be usually 1. - * - * For example, this corresponds to H.264 cpb_removal_delay. - */ - int dts_ref_dts_delta; - - /** - * Presentation delay of current frame in units of AVCodecContext.time_base. - * - * Set to INT_MIN when dts_sync_point unused. Otherwise, it must - * contain valid non-negative timestamp delta (presentation time of a frame - * must not lie in the past). - * - * This delay represents the difference between decoding and presentation - * time of the frame. - * - * For example, this corresponds to H.264 dpb_output_delay. - */ - int pts_dts_delta; - - /** - * Position of the packet in file. - * - * Analogous to cur_frame_pts/dts - */ - int64_t cur_frame_pos[AV_PARSER_PTS_NB]; - - /** - * Byte position of currently parsed frame in stream. - */ - int64_t pos; - - /** - * Previous frame byte position. - */ - int64_t last_pos; - - /** - * Duration of the current frame. - * For audio, this is in units of 1 / AVCodecContext.sample_rate. - * For all other types, this is in units of AVCodecContext.time_base. - */ - int duration; - - enum AVFieldOrder field_order; - - /** - * Indicate whether a picture is coded as a frame, top field or bottom field. - * - * For example, H.264 field_pic_flag equal to 0 corresponds to - * AV_PICTURE_STRUCTURE_FRAME. An H.264 picture with field_pic_flag - * equal to 1 and bottom_field_flag equal to 0 corresponds to - * AV_PICTURE_STRUCTURE_TOP_FIELD. - */ - enum AVPictureStructure picture_structure; - - /** - * Picture number incremented in presentation or output order. - * This field may be reinitialized at the first picture of a new sequence. - * - * For example, this corresponds to H.264 PicOrderCnt. - */ - int output_picture_number; - - /** - * Dimensions of the decoded video intended for presentation. - */ - int width; - int height; - - /** - * Dimensions of the coded video. - */ - int coded_width; - int coded_height; - - /** - * The format of the coded data, corresponds to enum AVPixelFormat for video - * and for enum AVSampleFormat for audio. - * - * Note that a decoder can have considerable freedom in how exactly it - * decodes the data, so the format reported here might be different from the - * one returned by a decoder. - */ - int format; -} AVCodecParserContext; - -typedef struct AVCodecParser { - int codec_ids[7]; /* several codec IDs are permitted */ - int priv_data_size; - int (*parser_init)(AVCodecParserContext *s); - /* This callback never returns an error, a negative value means that - * the frame start was in a previous packet. */ - int (*parser_parse)(AVCodecParserContext *s, - AVCodecContext *avctx, - const uint8_t **poutbuf, int *poutbuf_size, - const uint8_t *buf, int buf_size); - void (*parser_close)(AVCodecParserContext *s); - int (*split)(AVCodecContext *avctx, const uint8_t *buf, int buf_size); -} AVCodecParser; - -/** - * Iterate over all registered codec parsers. - * - * @param opaque a pointer where libavcodec will store the iteration state. Must - * point to NULL to start the iteration. - * - * @return the next registered codec parser or NULL when the iteration is - * finished - */ -const AVCodecParser *av_parser_iterate(void **opaque); - -AVCodecParserContext *av_parser_init(int codec_id); - -/** - * Parse a packet. - * - * @param s parser context. - * @param avctx codec context. - * @param poutbuf set to pointer to parsed buffer or NULL if not yet finished. - * @param poutbuf_size set to size of parsed buffer or zero if not yet finished. - * @param buf input buffer. - * @param buf_size buffer size in bytes without the padding. I.e. the full buffer - size is assumed to be buf_size + AV_INPUT_BUFFER_PADDING_SIZE. - To signal EOF, this should be 0 (so that the last frame - can be output). - * @param pts input presentation timestamp. - * @param dts input decoding timestamp. - * @param pos input byte position in stream. - * @return the number of bytes of the input bitstream used. - * - * Example: - * @code - * while(in_len){ - * len = av_parser_parse2(myparser, AVCodecContext, &data, &size, - * in_data, in_len, - * pts, dts, pos); - * in_data += len; - * in_len -= len; - * - * if(size) - * decode_frame(data, size); - * } - * @endcode - */ -int av_parser_parse2(AVCodecParserContext *s, - AVCodecContext *avctx, - uint8_t **poutbuf, int *poutbuf_size, - const uint8_t *buf, int buf_size, - int64_t pts, int64_t dts, - int64_t pos); - -void av_parser_close(AVCodecParserContext *s); - -/** - * @} - * @} - */ - -/** - * @addtogroup lavc_encoding - * @{ - */ - -int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t *buf, int buf_size, - const AVSubtitle *sub); - - -/** - * @} - */ - -/** - * @defgroup lavc_misc Utility functions - * @ingroup libavc - * - * Miscellaneous utility functions related to both encoding and decoding - * (or neither). - * @{ - */ - -/** - * @defgroup lavc_misc_pixfmt Pixel formats - * - * Functions for working with pixel formats. - * @{ - */ - -/** - * Return a value representing the fourCC code associated to the - * pixel format pix_fmt, or 0 if no associated fourCC code can be - * found. - */ -unsigned int avcodec_pix_fmt_to_codec_tag(enum AVPixelFormat pix_fmt); - -/** - * Find the best pixel format to convert to given a certain source pixel - * format. When converting from one pixel format to another, information loss - * may occur. For example, when converting from RGB24 to GRAY, the color - * information will be lost. Similarly, other losses occur when converting from - * some formats to other formats. avcodec_find_best_pix_fmt_of_2() searches which of - * the given pixel formats should be used to suffer the least amount of loss. - * The pixel formats from which it chooses one, are determined by the - * pix_fmt_list parameter. - * - * - * @param[in] pix_fmt_list AV_PIX_FMT_NONE terminated array of pixel formats to choose from - * @param[in] src_pix_fmt source pixel format - * @param[in] has_alpha Whether the source pixel format alpha channel is used. - * @param[out] loss_ptr Combination of flags informing you what kind of losses will occur. - * @return The best pixel format to convert to or -1 if none was found. - */ -enum AVPixelFormat avcodec_find_best_pix_fmt_of_list(const enum AVPixelFormat *pix_fmt_list, - enum AVPixelFormat src_pix_fmt, - int has_alpha, int *loss_ptr); - -enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext *s, const enum AVPixelFormat * fmt); - -/** - * @} - */ - -void avcodec_string(char *buf, int buf_size, AVCodecContext *enc, int encode); - -int avcodec_default_execute(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2),void *arg, int *ret, int count, int size); -int avcodec_default_execute2(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2, int, int),void *arg, int *ret, int count); -//FIXME func typedef - -/** - * Fill AVFrame audio data and linesize pointers. - * - * The buffer buf must be a preallocated buffer with a size big enough - * to contain the specified samples amount. The filled AVFrame data - * pointers will point to this buffer. - * - * AVFrame extended_data channel pointers are allocated if necessary for - * planar audio. - * - * @param frame the AVFrame - * frame->nb_samples must be set prior to calling the - * function. This function fills in frame->data, - * frame->extended_data, frame->linesize[0]. - * @param nb_channels channel count - * @param sample_fmt sample format - * @param buf buffer to use for frame data - * @param buf_size size of buffer - * @param align plane size sample alignment (0 = default) - * @return >=0 on success, negative error code on failure - * @todo return the size in bytes required to store the samples in - * case of success, at the next libavutil bump - */ -int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels, - enum AVSampleFormat sample_fmt, const uint8_t *buf, - int buf_size, int align); - -/** - * Reset the internal codec state / flush internal buffers. Should be called - * e.g. when seeking or when switching to a different stream. - * - * @note for decoders, this function just releases any references the decoder - * might keep internally, but the caller's references remain valid. - * - * @note for encoders, this function will only do something if the encoder - * declares support for AV_CODEC_CAP_ENCODER_FLUSH. When called, the encoder - * will drain any remaining packets, and can then be re-used for a different - * stream (as opposed to sending a null frame which will leave the encoder - * in a permanent EOF state after draining). This can be desirable if the - * cost of tearing down and replacing the encoder instance is high. - */ -void avcodec_flush_buffers(AVCodecContext *avctx); - -/** - * Return audio frame duration. - * - * @param avctx codec context - * @param frame_bytes size of the frame, or 0 if unknown - * @return frame duration, in samples, if known. 0 if not able to - * determine. - */ -int av_get_audio_frame_duration(AVCodecContext *avctx, int frame_bytes); - -/* memory */ - -/** - * Same behaviour av_fast_malloc but the buffer has additional - * AV_INPUT_BUFFER_PADDING_SIZE at the end which will always be 0. - * - * In addition the whole buffer will initially and after resizes - * be 0-initialized so that no uninitialized data will ever appear. - */ -void av_fast_padded_malloc(void *ptr, unsigned int *size, size_t min_size); - -/** - * Same behaviour av_fast_padded_malloc except that buffer will always - * be 0-initialized after call. - */ -void av_fast_padded_mallocz(void *ptr, unsigned int *size, size_t min_size); - -/** - * @return a positive value if s is open (i.e. avcodec_open2() was called on it - * with no corresponding avcodec_close()), 0 otherwise. - */ -int avcodec_is_open(AVCodecContext *s); - -/** - * @} - */ - -#endif /* AVCODEC_AVCODEC_H */ diff --git a/gostream/ffmpeg/include/libavcodec/avdct.h b/gostream/ffmpeg/include/libavcodec/avdct.h deleted file mode 100644 index 6411fab6f63..00000000000 --- a/gostream/ffmpeg/include/libavcodec/avdct.h +++ /dev/null @@ -1,88 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_AVDCT_H -#define AVCODEC_AVDCT_H - -#include "libavutil/opt.h" - -/** - * AVDCT context. - * @note function pointers can be NULL if the specific features have been - * disabled at build time. - */ -typedef struct AVDCT { - const AVClass *av_class; - - void (*idct)(int16_t *block /* align 16 */); - - /** - * IDCT input permutation. - * Several optimized IDCTs need a permutated input (relative to the - * normal order of the reference IDCT). - * This permutation must be performed before the idct_put/add. - * Note, normally this can be merged with the zigzag/alternate scan
- * An example to avoid confusion: - * - (->decode coeffs -> zigzag reorder -> dequant -> reference IDCT -> ...) - * - (x -> reference DCT -> reference IDCT -> x) - * - (x -> reference DCT -> simple_mmx_perm = idct_permutation - * -> simple_idct_mmx -> x) - * - (-> decode coeffs -> zigzag reorder -> simple_mmx_perm -> dequant - * -> simple_idct_mmx -> ...) - */ - uint8_t idct_permutation[64]; - - void (*fdct)(int16_t *block /* align 16 */); - - - /** - * DCT algorithm. - * must use AVOptions to set this field. - */ - int dct_algo; - - /** - * IDCT algorithm. - * must use AVOptions to set this field. - */ - int idct_algo; - - void (*get_pixels)(int16_t *block /* align 16 */, - const uint8_t *pixels /* align 8 */, - ptrdiff_t line_size); - - int bits_per_sample; - - void (*get_pixels_unaligned)(int16_t *block /* align 16 */, - const uint8_t *pixels, - ptrdiff_t line_size); -} AVDCT; - -/** - * Allocates a AVDCT context. - * This needs to be initialized with avcodec_dct_init() after optionally - * configuring it with AVOptions. - * - * To free it use av_free() - */ -AVDCT *avcodec_dct_alloc(void); -int avcodec_dct_init(AVDCT *); - -const AVClass *avcodec_dct_get_class(void); - -#endif /* AVCODEC_AVDCT_H */ diff --git a/gostream/ffmpeg/include/libavcodec/avfft.h b/gostream/ffmpeg/include/libavcodec/avfft.h deleted file mode 100644 index e3a0da1eb91..00000000000 --- a/gostream/ffmpeg/include/libavcodec/avfft.h +++ /dev/null @@ -1,149 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_AVFFT_H -#define AVCODEC_AVFFT_H - -#include "libavutil/attributes.h" -#include "version_major.h" -#if FF_API_AVFFT - -/** - * @file - * @ingroup lavc_fft - * FFT functions - */ - -/** - * @defgroup lavc_fft FFT functions - * @ingroup lavc_misc - * - * @{ - */ - -typedef float FFTSample; - -typedef struct FFTComplex { - FFTSample re, im; -} FFTComplex; - -typedef struct FFTContext FFTContext; - -/** - * Set up a complex FFT. - * @param nbits log2 of the length of the input array - * @param inverse if 0 perform the forward transform, if 1 perform the inverse - * @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_FFT - */ -attribute_deprecated -FFTContext *av_fft_init(int nbits, int inverse); - -/** - * Do the permutation needed BEFORE calling ff_fft_calc(). - * @deprecated without replacement - */ -attribute_deprecated -void av_fft_permute(FFTContext *s, FFTComplex *z); - -/** - * Do a complex FFT with the parameters defined in av_fft_init(). The - * input data must be permuted before. No 1.0/sqrt(n) normalization is done. - * @deprecated use the av_tx_fn value returned by av_tx_init, which also does permutation - */ -attribute_deprecated -void av_fft_calc(FFTContext *s, FFTComplex *z); - -attribute_deprecated -void av_fft_end(FFTContext *s); - -/** - * @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_MDCT, - * with a flag of AV_TX_FULL_IMDCT for a replacement to av_imdct_calc. - */ -attribute_deprecated -FFTContext *av_mdct_init(int nbits, int inverse, double scale); -attribute_deprecated -void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input); -attribute_deprecated -void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input); -attribute_deprecated -void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input); -attribute_deprecated -void av_mdct_end(FFTContext *s); - -/* Real Discrete Fourier Transform */ - -enum RDFTransformType { - DFT_R2C, - IDFT_C2R, - IDFT_R2C, - DFT_C2R, -}; - -typedef struct RDFTContext RDFTContext; - -/** - * Set up a real FFT. - * @param nbits log2 of the length of the input array - * @param trans the type of transform - * - * @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_RDFT - */ -attribute_deprecated -RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans); -attribute_deprecated -void av_rdft_calc(RDFTContext *s, FFTSample *data); -attribute_deprecated -void av_rdft_end(RDFTContext *s); - -/* Discrete Cosine Transform */ - -typedef struct DCTContext DCTContext; - -enum DCTTransformType { - DCT_II = 0, - DCT_III, - DCT_I, - DST_I, -}; - -/** - * Set up DCT. - * - * @param nbits size of the input array: - * (1 << nbits) for DCT-II, DCT-III and DST-I - * (1 << nbits) + 1 for DCT-I - * @param type the type of transform - * - * @note the first element of the input of DST-I is ignored - * - * @deprecated use av_tx_init from libavutil/tx.h with an appropriate type of AV_TX_FLOAT_DCT - */ -attribute_deprecated -DCTContext *av_dct_init(int nbits, enum DCTTransformType type); -attribute_deprecated -void av_dct_calc(DCTContext *s, FFTSample *data); -attribute_deprecated -void av_dct_end (DCTContext *s); - -/** - * @} - */ - -#endif /* FF_API_AVFFT */ -#endif /* AVCODEC_AVFFT_H */ diff --git a/gostream/ffmpeg/include/libavcodec/bsf.h b/gostream/ffmpeg/include/libavcodec/bsf.h deleted file mode 100644 index a09c69f2428..00000000000 --- a/gostream/ffmpeg/include/libavcodec/bsf.h +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Bitstream filters public API - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_BSF_H -#define AVCODEC_BSF_H - -#include "libavutil/dict.h" -#include "libavutil/log.h" -#include "libavutil/rational.h" - -#include "codec_id.h" -#include "codec_par.h" -#include "packet.h" - -/** - * @defgroup lavc_bsf Bitstream filters - * @ingroup libavc - * - * Bitstream filters transform encoded media data without decoding it. This - * allows e.g. manipulating various header values. Bitstream filters operate on - * @ref AVPacket "AVPackets". - * - * The bitstream filtering API is centered around two structures: - * AVBitStreamFilter and AVBSFContext. The former represents a bitstream filter - * in abstract, the latter a specific filtering process. Obtain an - * AVBitStreamFilter using av_bsf_get_by_name() or av_bsf_iterate(), then pass - * it to av_bsf_alloc() to create an AVBSFContext. Fill in the user-settable - * AVBSFContext fields, as described in its documentation, then call - * av_bsf_init() to prepare the filter context for use. - * - * Submit packets for filtering using av_bsf_send_packet(), obtain filtered - * results with av_bsf_receive_packet(). When no more input packets will be - * sent, submit a NULL AVPacket to signal the end of the stream to the filter. - * av_bsf_receive_packet() will then return trailing packets, if any are - * produced by the filter. - * - * Finally, free the filter context with av_bsf_free(). - * @{ - */ - -/** - * The bitstream filter state. - * - * This struct must be allocated with av_bsf_alloc() and freed with - * av_bsf_free(). - * - * The fields in the struct will only be changed (by the caller or by the - * filter) as described in their documentation, and are to be considered - * immutable otherwise. - */ -typedef struct AVBSFContext { - /** - * A class for logging and AVOptions - */ - const AVClass *av_class; - - /** - * The bitstream filter this context is an instance of. - */ - const struct AVBitStreamFilter *filter; - - /** - * Opaque filter-specific private data. If filter->priv_class is non-NULL, - * this is an AVOptions-enabled struct. - */ - void *priv_data; - - /** - * Parameters of the input stream. This field is allocated in - * av_bsf_alloc(), it needs to be filled by the caller before - * av_bsf_init(). - */ - AVCodecParameters *par_in; - - /** - * Parameters of the output stream. This field is allocated in - * av_bsf_alloc(), it is set by the filter in av_bsf_init(). - */ - AVCodecParameters *par_out; - - /** - * The timebase used for the timestamps of the input packets. Set by the - * caller before av_bsf_init(). - */ - AVRational time_base_in; - - /** - * The timebase used for the timestamps of the output packets. Set by the - * filter in av_bsf_init(). - */ - AVRational time_base_out; -} AVBSFContext; - -typedef struct AVBitStreamFilter { - const char *name; - - /** - * A list of codec ids supported by the filter, terminated by - * AV_CODEC_ID_NONE. - * May be NULL, in that case the bitstream filter works with any codec id. - */ - const enum AVCodecID *codec_ids; - - /** - * A class for the private data, used to declare bitstream filter private - * AVOptions. This field is NULL for bitstream filters that do not declare - * any options. - * - * If this field is non-NULL, the first member of the filter private data - * must be a pointer to AVClass, which will be set by libavcodec generic - * code to this class. - */ - const AVClass *priv_class; -} AVBitStreamFilter; - -/** - * @return a bitstream filter with the specified name or NULL if no such - * bitstream filter exists. - */ -const AVBitStreamFilter *av_bsf_get_by_name(const char *name); - -/** - * Iterate over all registered bitstream filters. - * - * @param opaque a pointer where libavcodec will store the iteration state. Must - * point to NULL to start the iteration. - * - * @return the next registered bitstream filter or NULL when the iteration is - * finished - */ -const AVBitStreamFilter *av_bsf_iterate(void **opaque); - -/** - * Allocate a context for a given bitstream filter. The caller must fill in the - * context parameters as described in the documentation and then call - * av_bsf_init() before sending any data to the filter. - * - * @param filter the filter for which to allocate an instance. - * @param[out] ctx a pointer into which the pointer to the newly-allocated context - * will be written. It must be freed with av_bsf_free() after the - * filtering is done. - * - * @return 0 on success, a negative AVERROR code on failure - */ -int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx); - -/** - * Prepare the filter for use, after all the parameters and options have been - * set. - * - * @param ctx a AVBSFContext previously allocated with av_bsf_alloc() - */ -int av_bsf_init(AVBSFContext *ctx); - -/** - * Submit a packet for filtering. - * - * After sending each packet, the filter must be completely drained by calling - * av_bsf_receive_packet() repeatedly until it returns AVERROR(EAGAIN) or - * AVERROR_EOF. - * - * @param ctx an initialized AVBSFContext - * @param pkt the packet to filter. The bitstream filter will take ownership of - * the packet and reset the contents of pkt. pkt is not touched if an error occurs. - * If pkt is empty (i.e. NULL, or pkt->data is NULL and pkt->side_data_elems zero), - * it signals the end of the stream (i.e. no more non-empty packets will be sent; - * sending more empty packets does nothing) and will cause the filter to output - * any packets it may have buffered internally. - * - * @return - * - 0 on success. - * - AVERROR(EAGAIN) if packets need to be retrieved from the filter (using - * av_bsf_receive_packet()) before new input can be consumed. - * - Another negative AVERROR value if an error occurs. - */ -int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt); - -/** - * Retrieve a filtered packet. - * - * @param ctx an initialized AVBSFContext - * @param[out] pkt this struct will be filled with the contents of the filtered - * packet. It is owned by the caller and must be freed using - * av_packet_unref() when it is no longer needed. - * This parameter should be "clean" (i.e. freshly allocated - * with av_packet_alloc() or unreffed with av_packet_unref()) - * when this function is called. If this function returns - * successfully, the contents of pkt will be completely - * overwritten by the returned data. On failure, pkt is not - * touched. - * - * @return - * - 0 on success. - * - AVERROR(EAGAIN) if more packets need to be sent to the filter (using - * av_bsf_send_packet()) to get more output. - * - AVERROR_EOF if there will be no further output from the filter. - * - Another negative AVERROR value if an error occurs. - * - * @note one input packet may result in several output packets, so after sending - * a packet with av_bsf_send_packet(), this function needs to be called - * repeatedly until it stops returning 0. It is also possible for a filter to - * output fewer packets than were sent to it, so this function may return - * AVERROR(EAGAIN) immediately after a successful av_bsf_send_packet() call. - */ -int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt); - -/** - * Reset the internal bitstream filter state. Should be called e.g. when seeking. - */ -void av_bsf_flush(AVBSFContext *ctx); - -/** - * Free a bitstream filter context and everything associated with it; write NULL - * into the supplied pointer. - */ -void av_bsf_free(AVBSFContext **ctx); - -/** - * Get the AVClass for AVBSFContext. It can be used in combination with - * AV_OPT_SEARCH_FAKE_OBJ for examining options. - * - * @see av_opt_find(). - */ -const AVClass *av_bsf_get_class(void); - -/** - * Structure for chain/list of bitstream filters. - * Empty list can be allocated by av_bsf_list_alloc(). - */ -typedef struct AVBSFList AVBSFList; - -/** - * Allocate empty list of bitstream filters. - * The list must be later freed by av_bsf_list_free() - * or finalized by av_bsf_list_finalize(). - * - * @return Pointer to @ref AVBSFList on success, NULL in case of failure - */ -AVBSFList *av_bsf_list_alloc(void); - -/** - * Free list of bitstream filters. - * - * @param lst Pointer to pointer returned by av_bsf_list_alloc() - */ -void av_bsf_list_free(AVBSFList **lst); - -/** - * Append bitstream filter to the list of bitstream filters. - * - * @param lst List to append to - * @param bsf Filter context to be appended - * - * @return >=0 on success, negative AVERROR in case of failure - */ -int av_bsf_list_append(AVBSFList *lst, AVBSFContext *bsf); - -/** - * Construct new bitstream filter context given it's name and options - * and append it to the list of bitstream filters. - * - * @param lst List to append to - * @param bsf_name Name of the bitstream filter - * @param options Options for the bitstream filter, can be set to NULL - * - * @return >=0 on success, negative AVERROR in case of failure - */ -int av_bsf_list_append2(AVBSFList *lst, const char * bsf_name, AVDictionary **options); -/** - * Finalize list of bitstream filters. - * - * This function will transform @ref AVBSFList to single @ref AVBSFContext, - * so the whole chain of bitstream filters can be treated as single filter - * freshly allocated by av_bsf_alloc(). - * If the call is successful, @ref AVBSFList structure is freed and lst - * will be set to NULL. In case of failure, caller is responsible for - * freeing the structure by av_bsf_list_free() - * - * @param lst Filter list structure to be transformed - * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext structure - * representing the chain of bitstream filters - * - * @return >=0 on success, negative AVERROR in case of failure - */ -int av_bsf_list_finalize(AVBSFList **lst, AVBSFContext **bsf); - -/** - * Parse string describing list of bitstream filters and create single - * @ref AVBSFContext describing the whole chain of bitstream filters. - * Resulting @ref AVBSFContext can be treated as any other @ref AVBSFContext freshly - * allocated by av_bsf_alloc(). - * - * @param str String describing chain of bitstream filters in format - * `bsf1[=opt1=val1:opt2=val2][,bsf2]` - * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext structure - * representing the chain of bitstream filters - * - * @return >=0 on success, negative AVERROR in case of failure - */ -int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf); - -/** - * Get null/pass-through bitstream filter. - * - * @param[out] bsf Pointer to be set to new instance of pass-through bitstream filter - * - * @return - */ -int av_bsf_get_null_filter(AVBSFContext **bsf); - -/** - * @} - */ - -#endif // AVCODEC_BSF_H diff --git a/gostream/ffmpeg/include/libavcodec/codec.h b/gostream/ffmpeg/include/libavcodec/codec.h deleted file mode 100644 index 8034f1a53c9..00000000000 --- a/gostream/ffmpeg/include/libavcodec/codec.h +++ /dev/null @@ -1,378 +0,0 @@ -/* - * AVCodec public API - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_CODEC_H -#define AVCODEC_CODEC_H - -#include - -#include "libavutil/avutil.h" -#include "libavutil/hwcontext.h" -#include "libavutil/log.h" -#include "libavutil/pixfmt.h" -#include "libavutil/rational.h" -#include "libavutil/samplefmt.h" - -#include "libavcodec/codec_id.h" -#include "libavcodec/version_major.h" - -/** - * @addtogroup lavc_core - * @{ - */ - -/** - * Decoder can use draw_horiz_band callback. - */ -#define AV_CODEC_CAP_DRAW_HORIZ_BAND (1 << 0) -/** - * Codec uses get_buffer() or get_encode_buffer() for allocating buffers and - * supports custom allocators. - * If not set, it might not use get_buffer() or get_encode_buffer() at all, or - * use operations that assume the buffer was allocated by - * avcodec_default_get_buffer2 or avcodec_default_get_encode_buffer. - */ -#define AV_CODEC_CAP_DR1 (1 << 1) -/** - * Encoder or decoder requires flushing with NULL input at the end in order to - * give the complete and correct output. - * - * NOTE: If this flag is not set, the codec is guaranteed to never be fed with - * with NULL data. The user can still send NULL data to the public encode - * or decode function, but libavcodec will not pass it along to the codec - * unless this flag is set. - * - * Decoders: - * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL, - * avpkt->size=0 at the end to get the delayed data until the decoder no longer - * returns frames. - * - * Encoders: - * The encoder needs to be fed with NULL data at the end of encoding until the - * encoder no longer returns data. - * - * NOTE: For encoders implementing the AVCodec.encode2() function, setting this - * flag also means that the encoder must set the pts and duration for - * each output packet. If this flag is not set, the pts and duration will - * be determined by libavcodec from the input frame. - */ -#define AV_CODEC_CAP_DELAY (1 << 5) -/** - * Codec can be fed a final frame with a smaller size. - * This can be used to prevent truncation of the last audio samples. - */ -#define AV_CODEC_CAP_SMALL_LAST_FRAME (1 << 6) - -#if FF_API_SUBFRAMES -/** - * Codec can output multiple frames per AVPacket - * Normally demuxers return one frame at a time, demuxers which do not do - * are connected to a parser to split what they return into proper frames. - * This flag is reserved to the very rare category of codecs which have a - * bitstream that cannot be split into frames without timeconsuming - * operations like full decoding. Demuxers carrying such bitstreams thus - * may return multiple frames in a packet. This has many disadvantages like - * prohibiting stream copy in many cases thus it should only be considered - * as a last resort. - */ -#define AV_CODEC_CAP_SUBFRAMES (1 << 8) -#endif - -/** - * Codec is experimental and is thus avoided in favor of non experimental - * encoders - */ -#define AV_CODEC_CAP_EXPERIMENTAL (1 << 9) -/** - * Codec should fill in channel configuration and samplerate instead of container - */ -#define AV_CODEC_CAP_CHANNEL_CONF (1 << 10) -/** - * Codec supports frame-level multithreading. - */ -#define AV_CODEC_CAP_FRAME_THREADS (1 << 12) -/** - * Codec supports slice-based (or partition-based) multithreading. - */ -#define AV_CODEC_CAP_SLICE_THREADS (1 << 13) -/** - * Codec supports changed parameters at any point. - */ -#define AV_CODEC_CAP_PARAM_CHANGE (1 << 14) -/** - * Codec supports multithreading through a method other than slice- or - * frame-level multithreading. Typically this marks wrappers around - * multithreading-capable external libraries. - */ -#define AV_CODEC_CAP_OTHER_THREADS (1 << 15) -/** - * Audio encoder supports receiving a different number of samples in each call. - */ -#define AV_CODEC_CAP_VARIABLE_FRAME_SIZE (1 << 16) -/** - * Decoder is not a preferred choice for probing. - * This indicates that the decoder is not a good choice for probing. - * It could for example be an expensive to spin up hardware decoder, - * or it could simply not provide a lot of useful information about - * the stream. - * A decoder marked with this flag should only be used as last resort - * choice for probing. - */ -#define AV_CODEC_CAP_AVOID_PROBING (1 << 17) - -/** - * Codec is backed by a hardware implementation. Typically used to - * identify a non-hwaccel hardware decoder. For information about hwaccels, use - * avcodec_get_hw_config() instead. - */ -#define AV_CODEC_CAP_HARDWARE (1 << 18) - -/** - * Codec is potentially backed by a hardware implementation, but not - * necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the - * implementation provides some sort of internal fallback. - */ -#define AV_CODEC_CAP_HYBRID (1 << 19) - -/** - * This encoder can reorder user opaque values from input AVFrames and return - * them with corresponding output packets. - * @see AV_CODEC_FLAG_COPY_OPAQUE - */ -#define AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE (1 << 20) - -/** - * This encoder can be flushed using avcodec_flush_buffers(). If this flag is - * not set, the encoder must be closed and reopened to ensure that no frames - * remain pending. - */ -#define AV_CODEC_CAP_ENCODER_FLUSH (1 << 21) - -/** - * The encoder is able to output reconstructed frame data, i.e. raw frames that - * would be produced by decoding the encoded bitstream. - * - * Reconstructed frame output is enabled by the AV_CODEC_FLAG_RECON_FRAME flag. - */ -#define AV_CODEC_CAP_ENCODER_RECON_FRAME (1 << 22) - -/** - * AVProfile. - */ -typedef struct AVProfile { - int profile; - const char *name; ///< short name for the profile -} AVProfile; - -/** - * AVCodec. - */ -typedef struct AVCodec { - /** - * Name of the codec implementation. - * The name is globally unique among encoders and among decoders (but an - * encoder and a decoder can share the same name). - * This is the primary way to find a codec from the user perspective. - */ - const char *name; - /** - * Descriptive name for the codec, meant to be more human readable than name. - * You should use the NULL_IF_CONFIG_SMALL() macro to define it. - */ - const char *long_name; - enum AVMediaType type; - enum AVCodecID id; - /** - * Codec capabilities. - * see AV_CODEC_CAP_* - */ - int capabilities; - uint8_t max_lowres; ///< maximum value for lowres supported by the decoder - const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0} - const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1 - const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0 - const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1 -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * @deprecated use ch_layouts instead - */ - attribute_deprecated - const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0 -#endif - const AVClass *priv_class; ///< AVClass for the private context - const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {AV_PROFILE_UNKNOWN} - - /** - * Group name of the codec implementation. - * This is a short symbolic name of the wrapper backing this codec. A - * wrapper uses some kind of external implementation for the codec, such - * as an external library, or a codec implementation provided by the OS or - * the hardware. - * If this field is NULL, this is a builtin, libavcodec native codec. - * If non-NULL, this will be the suffix in AVCodec.name in most cases - * (usually AVCodec.name will be of the form "_"). - */ - const char *wrapper_name; - - /** - * Array of supported channel layouts, terminated with a zeroed layout. - */ - const AVChannelLayout *ch_layouts; -} AVCodec; - -/** - * Iterate over all registered codecs. - * - * @param opaque a pointer where libavcodec will store the iteration state. Must - * point to NULL to start the iteration. - * - * @return the next registered codec or NULL when the iteration is - * finished - */ -const AVCodec *av_codec_iterate(void **opaque); - -/** - * Find a registered decoder with a matching codec ID. - * - * @param id AVCodecID of the requested decoder - * @return A decoder if one was found, NULL otherwise. - */ -const AVCodec *avcodec_find_decoder(enum AVCodecID id); - -/** - * Find a registered decoder with the specified name. - * - * @param name name of the requested decoder - * @return A decoder if one was found, NULL otherwise. - */ -const AVCodec *avcodec_find_decoder_by_name(const char *name); - -/** - * Find a registered encoder with a matching codec ID. - * - * @param id AVCodecID of the requested encoder - * @return An encoder if one was found, NULL otherwise. - */ -const AVCodec *avcodec_find_encoder(enum AVCodecID id); - -/** - * Find a registered encoder with the specified name. - * - * @param name name of the requested encoder - * @return An encoder if one was found, NULL otherwise. - */ -const AVCodec *avcodec_find_encoder_by_name(const char *name); -/** - * @return a non-zero number if codec is an encoder, zero otherwise - */ -int av_codec_is_encoder(const AVCodec *codec); - -/** - * @return a non-zero number if codec is a decoder, zero otherwise - */ -int av_codec_is_decoder(const AVCodec *codec); - -/** - * Return a name for the specified profile, if available. - * - * @param codec the codec that is searched for the given profile - * @param profile the profile value for which a name is requested - * @return A name for the profile if found, NULL otherwise. - */ -const char *av_get_profile_name(const AVCodec *codec, int profile); - -enum { - /** - * The codec supports this format via the hw_device_ctx interface. - * - * When selecting this format, AVCodecContext.hw_device_ctx should - * have been set to a device of the specified type before calling - * avcodec_open2(). - */ - AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01, - /** - * The codec supports this format via the hw_frames_ctx interface. - * - * When selecting this format for a decoder, - * AVCodecContext.hw_frames_ctx should be set to a suitable frames - * context inside the get_format() callback. The frames context - * must have been created on a device of the specified type. - * - * When selecting this format for an encoder, - * AVCodecContext.hw_frames_ctx should be set to the context which - * will be used for the input frames before calling avcodec_open2(). - */ - AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX = 0x02, - /** - * The codec supports this format by some internal method. - * - * This format can be selected without any additional configuration - - * no device or frames context is required. - */ - AV_CODEC_HW_CONFIG_METHOD_INTERNAL = 0x04, - /** - * The codec supports this format by some ad-hoc method. - * - * Additional settings and/or function calls are required. See the - * codec-specific documentation for details. (Methods requiring - * this sort of configuration are deprecated and others should be - * used in preference.) - */ - AV_CODEC_HW_CONFIG_METHOD_AD_HOC = 0x08, -}; - -typedef struct AVCodecHWConfig { - /** - * For decoders, a hardware pixel format which that decoder may be - * able to decode to if suitable hardware is available. - * - * For encoders, a pixel format which the encoder may be able to - * accept. If set to AV_PIX_FMT_NONE, this applies to all pixel - * formats supported by the codec. - */ - enum AVPixelFormat pix_fmt; - /** - * Bit set of AV_CODEC_HW_CONFIG_METHOD_* flags, describing the possible - * setup methods which can be used with this configuration. - */ - int methods; - /** - * The device type associated with the configuration. - * - * Must be set for AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX and - * AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, otherwise unused. - */ - enum AVHWDeviceType device_type; -} AVCodecHWConfig; - -/** - * Retrieve supported hardware configurations for a codec. - * - * Values of index from zero to some maximum return the indexed configuration - * descriptor; all other values return NULL. If the codec does not support - * any hardware configurations then it will always return NULL. - */ -const AVCodecHWConfig *avcodec_get_hw_config(const AVCodec *codec, int index); - -/** - * @} - */ - -#endif /* AVCODEC_CODEC_H */ diff --git a/gostream/ffmpeg/include/libavcodec/codec_desc.h b/gostream/ffmpeg/include/libavcodec/codec_desc.h deleted file mode 100644 index 96afd20208b..00000000000 --- a/gostream/ffmpeg/include/libavcodec/codec_desc.h +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Codec descriptors public API - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_CODEC_DESC_H -#define AVCODEC_CODEC_DESC_H - -#include "libavutil/avutil.h" - -#include "codec_id.h" - -/** - * @addtogroup lavc_core - * @{ - */ - -/** - * This struct describes the properties of a single codec described by an - * AVCodecID. - * @see avcodec_descriptor_get() - */ -typedef struct AVCodecDescriptor { - enum AVCodecID id; - enum AVMediaType type; - /** - * Name of the codec described by this descriptor. It is non-empty and - * unique for each codec descriptor. It should contain alphanumeric - * characters and '_' only. - */ - const char *name; - /** - * A more descriptive name for this codec. May be NULL. - */ - const char *long_name; - /** - * Codec properties, a combination of AV_CODEC_PROP_* flags. - */ - int props; - /** - * MIME type(s) associated with the codec. - * May be NULL; if not, a NULL-terminated array of MIME types. - * The first item is always non-NULL and is the preferred MIME type. - */ - const char *const *mime_types; - /** - * If non-NULL, an array of profiles recognized for this codec. - * Terminated with AV_PROFILE_UNKNOWN. - */ - const struct AVProfile *profiles; -} AVCodecDescriptor; - -/** - * Codec uses only intra compression. - * Video and audio codecs only. - */ -#define AV_CODEC_PROP_INTRA_ONLY (1 << 0) -/** - * Codec supports lossy compression. Audio and video codecs only. - * @note a codec may support both lossy and lossless - * compression modes - */ -#define AV_CODEC_PROP_LOSSY (1 << 1) -/** - * Codec supports lossless compression. Audio and video codecs only. - */ -#define AV_CODEC_PROP_LOSSLESS (1 << 2) -/** - * Codec supports frame reordering. That is, the coded order (the order in which - * the encoded packets are output by the encoders / stored / input to the - * decoders) may be different from the presentation order of the corresponding - * frames. - * - * For codecs that do not have this property set, PTS and DTS should always be - * equal. - */ -#define AV_CODEC_PROP_REORDER (1 << 3) - -/** - * Video codec supports separate coding of fields in interlaced frames. - */ -#define AV_CODEC_PROP_FIELDS (1 << 4) - -/** - * Subtitle codec is bitmap based - * Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field. - */ -#define AV_CODEC_PROP_BITMAP_SUB (1 << 16) -/** - * Subtitle codec is text based. - * Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field. - */ -#define AV_CODEC_PROP_TEXT_SUB (1 << 17) - -/** - * @return descriptor for given codec ID or NULL if no descriptor exists. - */ -const AVCodecDescriptor *avcodec_descriptor_get(enum AVCodecID id); - -/** - * Iterate over all codec descriptors known to libavcodec. - * - * @param prev previous descriptor. NULL to get the first descriptor. - * - * @return next descriptor or NULL after the last descriptor - */ -const AVCodecDescriptor *avcodec_descriptor_next(const AVCodecDescriptor *prev); - -/** - * @return codec descriptor with the given name or NULL if no such descriptor - * exists. - */ -const AVCodecDescriptor *avcodec_descriptor_get_by_name(const char *name); - -/** - * @} - */ - -#endif // AVCODEC_CODEC_DESC_H diff --git a/gostream/ffmpeg/include/libavcodec/codec_id.h b/gostream/ffmpeg/include/libavcodec/codec_id.h deleted file mode 100644 index 29b410b8d3f..00000000000 --- a/gostream/ffmpeg/include/libavcodec/codec_id.h +++ /dev/null @@ -1,668 +0,0 @@ -/* - * Codec IDs - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_CODEC_ID_H -#define AVCODEC_CODEC_ID_H - -#include "libavutil/avutil.h" -#include "libavutil/samplefmt.h" - -#include "version_major.h" - -/** - * @addtogroup lavc_core - * @{ - */ - -/** - * Identify the syntax and semantics of the bitstream. - * The principle is roughly: - * Two decoders with the same ID can decode the same streams. - * Two encoders with the same ID can encode compatible streams. - * There may be slight deviations from the principle due to implementation - * details. - * - * If you add a codec ID to this list, add it so that - * 1. no value of an existing codec ID changes (that would break ABI), - * 2. it is as close as possible to similar codecs - * - * After adding new codec IDs, do not forget to add an entry to the codec - * descriptor list and bump libavcodec minor version. - */ -enum AVCodecID { - AV_CODEC_ID_NONE, - - /* video codecs */ - AV_CODEC_ID_MPEG1VIDEO, - AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding - AV_CODEC_ID_H261, - AV_CODEC_ID_H263, - AV_CODEC_ID_RV10, - AV_CODEC_ID_RV20, - AV_CODEC_ID_MJPEG, - AV_CODEC_ID_MJPEGB, - AV_CODEC_ID_LJPEG, - AV_CODEC_ID_SP5X, - AV_CODEC_ID_JPEGLS, - AV_CODEC_ID_MPEG4, - AV_CODEC_ID_RAWVIDEO, - AV_CODEC_ID_MSMPEG4V1, - AV_CODEC_ID_MSMPEG4V2, - AV_CODEC_ID_MSMPEG4V3, - AV_CODEC_ID_WMV1, - AV_CODEC_ID_WMV2, - AV_CODEC_ID_H263P, - AV_CODEC_ID_H263I, - AV_CODEC_ID_FLV1, - AV_CODEC_ID_SVQ1, - AV_CODEC_ID_SVQ3, - AV_CODEC_ID_DVVIDEO, - AV_CODEC_ID_HUFFYUV, - AV_CODEC_ID_CYUV, - AV_CODEC_ID_H264, - AV_CODEC_ID_INDEO3, - AV_CODEC_ID_VP3, - AV_CODEC_ID_THEORA, - AV_CODEC_ID_ASV1, - AV_CODEC_ID_ASV2, - AV_CODEC_ID_FFV1, - AV_CODEC_ID_4XM, - AV_CODEC_ID_VCR1, - AV_CODEC_ID_CLJR, - AV_CODEC_ID_MDEC, - AV_CODEC_ID_ROQ, - AV_CODEC_ID_INTERPLAY_VIDEO, - AV_CODEC_ID_XAN_WC3, - AV_CODEC_ID_XAN_WC4, - AV_CODEC_ID_RPZA, - AV_CODEC_ID_CINEPAK, - AV_CODEC_ID_WS_VQA, - AV_CODEC_ID_MSRLE, - AV_CODEC_ID_MSVIDEO1, - AV_CODEC_ID_IDCIN, - AV_CODEC_ID_8BPS, - AV_CODEC_ID_SMC, - AV_CODEC_ID_FLIC, - AV_CODEC_ID_TRUEMOTION1, - AV_CODEC_ID_VMDVIDEO, - AV_CODEC_ID_MSZH, - AV_CODEC_ID_ZLIB, - AV_CODEC_ID_QTRLE, - AV_CODEC_ID_TSCC, - AV_CODEC_ID_ULTI, - AV_CODEC_ID_QDRAW, - AV_CODEC_ID_VIXL, - AV_CODEC_ID_QPEG, - AV_CODEC_ID_PNG, - AV_CODEC_ID_PPM, - AV_CODEC_ID_PBM, - AV_CODEC_ID_PGM, - AV_CODEC_ID_PGMYUV, - AV_CODEC_ID_PAM, - AV_CODEC_ID_FFVHUFF, - AV_CODEC_ID_RV30, - AV_CODEC_ID_RV40, - AV_CODEC_ID_VC1, - AV_CODEC_ID_WMV3, - AV_CODEC_ID_LOCO, - AV_CODEC_ID_WNV1, - AV_CODEC_ID_AASC, - AV_CODEC_ID_INDEO2, - AV_CODEC_ID_FRAPS, - AV_CODEC_ID_TRUEMOTION2, - AV_CODEC_ID_BMP, - AV_CODEC_ID_CSCD, - AV_CODEC_ID_MMVIDEO, - AV_CODEC_ID_ZMBV, - AV_CODEC_ID_AVS, - AV_CODEC_ID_SMACKVIDEO, - AV_CODEC_ID_NUV, - AV_CODEC_ID_KMVC, - AV_CODEC_ID_FLASHSV, - AV_CODEC_ID_CAVS, - AV_CODEC_ID_JPEG2000, - AV_CODEC_ID_VMNC, - AV_CODEC_ID_VP5, - AV_CODEC_ID_VP6, - AV_CODEC_ID_VP6F, - AV_CODEC_ID_TARGA, - AV_CODEC_ID_DSICINVIDEO, - AV_CODEC_ID_TIERTEXSEQVIDEO, - AV_CODEC_ID_TIFF, - AV_CODEC_ID_GIF, - AV_CODEC_ID_DXA, - AV_CODEC_ID_DNXHD, - AV_CODEC_ID_THP, - AV_CODEC_ID_SGI, - AV_CODEC_ID_C93, - AV_CODEC_ID_BETHSOFTVID, - AV_CODEC_ID_PTX, - AV_CODEC_ID_TXD, - AV_CODEC_ID_VP6A, - AV_CODEC_ID_AMV, - AV_CODEC_ID_VB, - AV_CODEC_ID_PCX, - AV_CODEC_ID_SUNRAST, - AV_CODEC_ID_INDEO4, - AV_CODEC_ID_INDEO5, - AV_CODEC_ID_MIMIC, - AV_CODEC_ID_RL2, - AV_CODEC_ID_ESCAPE124, - AV_CODEC_ID_DIRAC, - AV_CODEC_ID_BFI, - AV_CODEC_ID_CMV, - AV_CODEC_ID_MOTIONPIXELS, - AV_CODEC_ID_TGV, - AV_CODEC_ID_TGQ, - AV_CODEC_ID_TQI, - AV_CODEC_ID_AURA, - AV_CODEC_ID_AURA2, - AV_CODEC_ID_V210X, - AV_CODEC_ID_TMV, - AV_CODEC_ID_V210, - AV_CODEC_ID_DPX, - AV_CODEC_ID_MAD, - AV_CODEC_ID_FRWU, - AV_CODEC_ID_FLASHSV2, - AV_CODEC_ID_CDGRAPHICS, - AV_CODEC_ID_R210, - AV_CODEC_ID_ANM, - AV_CODEC_ID_BINKVIDEO, - AV_CODEC_ID_IFF_ILBM, -#define AV_CODEC_ID_IFF_BYTERUN1 AV_CODEC_ID_IFF_ILBM - AV_CODEC_ID_KGV1, - AV_CODEC_ID_YOP, - AV_CODEC_ID_VP8, - AV_CODEC_ID_PICTOR, - AV_CODEC_ID_ANSI, - AV_CODEC_ID_A64_MULTI, - AV_CODEC_ID_A64_MULTI5, - AV_CODEC_ID_R10K, - AV_CODEC_ID_MXPEG, - AV_CODEC_ID_LAGARITH, - AV_CODEC_ID_PRORES, - AV_CODEC_ID_JV, - AV_CODEC_ID_DFA, - AV_CODEC_ID_WMV3IMAGE, - AV_CODEC_ID_VC1IMAGE, - AV_CODEC_ID_UTVIDEO, - AV_CODEC_ID_BMV_VIDEO, - AV_CODEC_ID_VBLE, - AV_CODEC_ID_DXTORY, - AV_CODEC_ID_V410, - AV_CODEC_ID_XWD, - AV_CODEC_ID_CDXL, - AV_CODEC_ID_XBM, - AV_CODEC_ID_ZEROCODEC, - AV_CODEC_ID_MSS1, - AV_CODEC_ID_MSA1, - AV_CODEC_ID_TSCC2, - AV_CODEC_ID_MTS2, - AV_CODEC_ID_CLLC, - AV_CODEC_ID_MSS2, - AV_CODEC_ID_VP9, - AV_CODEC_ID_AIC, - AV_CODEC_ID_ESCAPE130, - AV_CODEC_ID_G2M, - AV_CODEC_ID_WEBP, - AV_CODEC_ID_HNM4_VIDEO, - AV_CODEC_ID_HEVC, -#define AV_CODEC_ID_H265 AV_CODEC_ID_HEVC - AV_CODEC_ID_FIC, - AV_CODEC_ID_ALIAS_PIX, - AV_CODEC_ID_BRENDER_PIX, - AV_CODEC_ID_PAF_VIDEO, - AV_CODEC_ID_EXR, - AV_CODEC_ID_VP7, - AV_CODEC_ID_SANM, - AV_CODEC_ID_SGIRLE, - AV_CODEC_ID_MVC1, - AV_CODEC_ID_MVC2, - AV_CODEC_ID_HQX, - AV_CODEC_ID_TDSC, - AV_CODEC_ID_HQ_HQA, - AV_CODEC_ID_HAP, - AV_CODEC_ID_DDS, - AV_CODEC_ID_DXV, - AV_CODEC_ID_SCREENPRESSO, - AV_CODEC_ID_RSCC, - AV_CODEC_ID_AVS2, - AV_CODEC_ID_PGX, - AV_CODEC_ID_AVS3, - AV_CODEC_ID_MSP2, - AV_CODEC_ID_VVC, -#define AV_CODEC_ID_H266 AV_CODEC_ID_VVC - AV_CODEC_ID_Y41P, - AV_CODEC_ID_AVRP, - AV_CODEC_ID_012V, - AV_CODEC_ID_AVUI, -#if FF_API_AYUV_CODECID - AV_CODEC_ID_AYUV, -#endif - AV_CODEC_ID_TARGA_Y216, - AV_CODEC_ID_V308, - AV_CODEC_ID_V408, - AV_CODEC_ID_YUV4, - AV_CODEC_ID_AVRN, - AV_CODEC_ID_CPIA, - AV_CODEC_ID_XFACE, - AV_CODEC_ID_SNOW, - AV_CODEC_ID_SMVJPEG, - AV_CODEC_ID_APNG, - AV_CODEC_ID_DAALA, - AV_CODEC_ID_CFHD, - AV_CODEC_ID_TRUEMOTION2RT, - AV_CODEC_ID_M101, - AV_CODEC_ID_MAGICYUV, - AV_CODEC_ID_SHEERVIDEO, - AV_CODEC_ID_YLC, - AV_CODEC_ID_PSD, - AV_CODEC_ID_PIXLET, - AV_CODEC_ID_SPEEDHQ, - AV_CODEC_ID_FMVC, - AV_CODEC_ID_SCPR, - AV_CODEC_ID_CLEARVIDEO, - AV_CODEC_ID_XPM, - AV_CODEC_ID_AV1, - AV_CODEC_ID_BITPACKED, - AV_CODEC_ID_MSCC, - AV_CODEC_ID_SRGC, - AV_CODEC_ID_SVG, - AV_CODEC_ID_GDV, - AV_CODEC_ID_FITS, - AV_CODEC_ID_IMM4, - AV_CODEC_ID_PROSUMER, - AV_CODEC_ID_MWSC, - AV_CODEC_ID_WCMV, - AV_CODEC_ID_RASC, - AV_CODEC_ID_HYMT, - AV_CODEC_ID_ARBC, - AV_CODEC_ID_AGM, - AV_CODEC_ID_LSCR, - AV_CODEC_ID_VP4, - AV_CODEC_ID_IMM5, - AV_CODEC_ID_MVDV, - AV_CODEC_ID_MVHA, - AV_CODEC_ID_CDTOONS, - AV_CODEC_ID_MV30, - AV_CODEC_ID_NOTCHLC, - AV_CODEC_ID_PFM, - AV_CODEC_ID_MOBICLIP, - AV_CODEC_ID_PHOTOCD, - AV_CODEC_ID_IPU, - AV_CODEC_ID_ARGO, - AV_CODEC_ID_CRI, - AV_CODEC_ID_SIMBIOSIS_IMX, - AV_CODEC_ID_SGA_VIDEO, - AV_CODEC_ID_GEM, - AV_CODEC_ID_VBN, - AV_CODEC_ID_JPEGXL, - AV_CODEC_ID_QOI, - AV_CODEC_ID_PHM, - AV_CODEC_ID_RADIANCE_HDR, - AV_CODEC_ID_WBMP, - AV_CODEC_ID_MEDIA100, - AV_CODEC_ID_VQC, - AV_CODEC_ID_PDV, - AV_CODEC_ID_EVC, - AV_CODEC_ID_RTV1, - AV_CODEC_ID_VMIX, - - /* various PCM "codecs" */ - AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs - AV_CODEC_ID_PCM_S16LE = 0x10000, - AV_CODEC_ID_PCM_S16BE, - AV_CODEC_ID_PCM_U16LE, - AV_CODEC_ID_PCM_U16BE, - AV_CODEC_ID_PCM_S8, - AV_CODEC_ID_PCM_U8, - AV_CODEC_ID_PCM_MULAW, - AV_CODEC_ID_PCM_ALAW, - AV_CODEC_ID_PCM_S32LE, - AV_CODEC_ID_PCM_S32BE, - AV_CODEC_ID_PCM_U32LE, - AV_CODEC_ID_PCM_U32BE, - AV_CODEC_ID_PCM_S24LE, - AV_CODEC_ID_PCM_S24BE, - AV_CODEC_ID_PCM_U24LE, - AV_CODEC_ID_PCM_U24BE, - AV_CODEC_ID_PCM_S24DAUD, - AV_CODEC_ID_PCM_ZORK, - AV_CODEC_ID_PCM_S16LE_PLANAR, - AV_CODEC_ID_PCM_DVD, - AV_CODEC_ID_PCM_F32BE, - AV_CODEC_ID_PCM_F32LE, - AV_CODEC_ID_PCM_F64BE, - AV_CODEC_ID_PCM_F64LE, - AV_CODEC_ID_PCM_BLURAY, - AV_CODEC_ID_PCM_LXF, - AV_CODEC_ID_S302M, - AV_CODEC_ID_PCM_S8_PLANAR, - AV_CODEC_ID_PCM_S24LE_PLANAR, - AV_CODEC_ID_PCM_S32LE_PLANAR, - AV_CODEC_ID_PCM_S16BE_PLANAR, - AV_CODEC_ID_PCM_S64LE, - AV_CODEC_ID_PCM_S64BE, - AV_CODEC_ID_PCM_F16LE, - AV_CODEC_ID_PCM_F24LE, - AV_CODEC_ID_PCM_VIDC, - AV_CODEC_ID_PCM_SGA, - - /* various ADPCM codecs */ - AV_CODEC_ID_ADPCM_IMA_QT = 0x11000, - AV_CODEC_ID_ADPCM_IMA_WAV, - AV_CODEC_ID_ADPCM_IMA_DK3, - AV_CODEC_ID_ADPCM_IMA_DK4, - AV_CODEC_ID_ADPCM_IMA_WS, - AV_CODEC_ID_ADPCM_IMA_SMJPEG, - AV_CODEC_ID_ADPCM_MS, - AV_CODEC_ID_ADPCM_4XM, - AV_CODEC_ID_ADPCM_XA, - AV_CODEC_ID_ADPCM_ADX, - AV_CODEC_ID_ADPCM_EA, - AV_CODEC_ID_ADPCM_G726, - AV_CODEC_ID_ADPCM_CT, - AV_CODEC_ID_ADPCM_SWF, - AV_CODEC_ID_ADPCM_YAMAHA, - AV_CODEC_ID_ADPCM_SBPRO_4, - AV_CODEC_ID_ADPCM_SBPRO_3, - AV_CODEC_ID_ADPCM_SBPRO_2, - AV_CODEC_ID_ADPCM_THP, - AV_CODEC_ID_ADPCM_IMA_AMV, - AV_CODEC_ID_ADPCM_EA_R1, - AV_CODEC_ID_ADPCM_EA_R3, - AV_CODEC_ID_ADPCM_EA_R2, - AV_CODEC_ID_ADPCM_IMA_EA_SEAD, - AV_CODEC_ID_ADPCM_IMA_EA_EACS, - AV_CODEC_ID_ADPCM_EA_XAS, - AV_CODEC_ID_ADPCM_EA_MAXIS_XA, - AV_CODEC_ID_ADPCM_IMA_ISS, - AV_CODEC_ID_ADPCM_G722, - AV_CODEC_ID_ADPCM_IMA_APC, - AV_CODEC_ID_ADPCM_VIMA, - AV_CODEC_ID_ADPCM_AFC, - AV_CODEC_ID_ADPCM_IMA_OKI, - AV_CODEC_ID_ADPCM_DTK, - AV_CODEC_ID_ADPCM_IMA_RAD, - AV_CODEC_ID_ADPCM_G726LE, - AV_CODEC_ID_ADPCM_THP_LE, - AV_CODEC_ID_ADPCM_PSX, - AV_CODEC_ID_ADPCM_AICA, - AV_CODEC_ID_ADPCM_IMA_DAT4, - AV_CODEC_ID_ADPCM_MTAF, - AV_CODEC_ID_ADPCM_AGM, - AV_CODEC_ID_ADPCM_ARGO, - AV_CODEC_ID_ADPCM_IMA_SSI, - AV_CODEC_ID_ADPCM_ZORK, - AV_CODEC_ID_ADPCM_IMA_APM, - AV_CODEC_ID_ADPCM_IMA_ALP, - AV_CODEC_ID_ADPCM_IMA_MTF, - AV_CODEC_ID_ADPCM_IMA_CUNNING, - AV_CODEC_ID_ADPCM_IMA_MOFLEX, - AV_CODEC_ID_ADPCM_IMA_ACORN, - AV_CODEC_ID_ADPCM_XMD, - - /* AMR */ - AV_CODEC_ID_AMR_NB = 0x12000, - AV_CODEC_ID_AMR_WB, - - /* RealAudio codecs*/ - AV_CODEC_ID_RA_144 = 0x13000, - AV_CODEC_ID_RA_288, - - /* various DPCM codecs */ - AV_CODEC_ID_ROQ_DPCM = 0x14000, - AV_CODEC_ID_INTERPLAY_DPCM, - AV_CODEC_ID_XAN_DPCM, - AV_CODEC_ID_SOL_DPCM, - AV_CODEC_ID_SDX2_DPCM, - AV_CODEC_ID_GREMLIN_DPCM, - AV_CODEC_ID_DERF_DPCM, - AV_CODEC_ID_WADY_DPCM, - AV_CODEC_ID_CBD2_DPCM, - - /* audio codecs */ - AV_CODEC_ID_MP2 = 0x15000, - AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3 - AV_CODEC_ID_AAC, - AV_CODEC_ID_AC3, - AV_CODEC_ID_DTS, - AV_CODEC_ID_VORBIS, - AV_CODEC_ID_DVAUDIO, - AV_CODEC_ID_WMAV1, - AV_CODEC_ID_WMAV2, - AV_CODEC_ID_MACE3, - AV_CODEC_ID_MACE6, - AV_CODEC_ID_VMDAUDIO, - AV_CODEC_ID_FLAC, - AV_CODEC_ID_MP3ADU, - AV_CODEC_ID_MP3ON4, - AV_CODEC_ID_SHORTEN, - AV_CODEC_ID_ALAC, - AV_CODEC_ID_WESTWOOD_SND1, - AV_CODEC_ID_GSM, ///< as in Berlin toast format - AV_CODEC_ID_QDM2, - AV_CODEC_ID_COOK, - AV_CODEC_ID_TRUESPEECH, - AV_CODEC_ID_TTA, - AV_CODEC_ID_SMACKAUDIO, - AV_CODEC_ID_QCELP, - AV_CODEC_ID_WAVPACK, - AV_CODEC_ID_DSICINAUDIO, - AV_CODEC_ID_IMC, - AV_CODEC_ID_MUSEPACK7, - AV_CODEC_ID_MLP, - AV_CODEC_ID_GSM_MS, /* as found in WAV */ - AV_CODEC_ID_ATRAC3, - AV_CODEC_ID_APE, - AV_CODEC_ID_NELLYMOSER, - AV_CODEC_ID_MUSEPACK8, - AV_CODEC_ID_SPEEX, - AV_CODEC_ID_WMAVOICE, - AV_CODEC_ID_WMAPRO, - AV_CODEC_ID_WMALOSSLESS, - AV_CODEC_ID_ATRAC3P, - AV_CODEC_ID_EAC3, - AV_CODEC_ID_SIPR, - AV_CODEC_ID_MP1, - AV_CODEC_ID_TWINVQ, - AV_CODEC_ID_TRUEHD, - AV_CODEC_ID_MP4ALS, - AV_CODEC_ID_ATRAC1, - AV_CODEC_ID_BINKAUDIO_RDFT, - AV_CODEC_ID_BINKAUDIO_DCT, - AV_CODEC_ID_AAC_LATM, - AV_CODEC_ID_QDMC, - AV_CODEC_ID_CELT, - AV_CODEC_ID_G723_1, - AV_CODEC_ID_G729, - AV_CODEC_ID_8SVX_EXP, - AV_CODEC_ID_8SVX_FIB, - AV_CODEC_ID_BMV_AUDIO, - AV_CODEC_ID_RALF, - AV_CODEC_ID_IAC, - AV_CODEC_ID_ILBC, - AV_CODEC_ID_OPUS, - AV_CODEC_ID_COMFORT_NOISE, - AV_CODEC_ID_TAK, - AV_CODEC_ID_METASOUND, - AV_CODEC_ID_PAF_AUDIO, - AV_CODEC_ID_ON2AVC, - AV_CODEC_ID_DSS_SP, - AV_CODEC_ID_CODEC2, - AV_CODEC_ID_FFWAVESYNTH, - AV_CODEC_ID_SONIC, - AV_CODEC_ID_SONIC_LS, - AV_CODEC_ID_EVRC, - AV_CODEC_ID_SMV, - AV_CODEC_ID_DSD_LSBF, - AV_CODEC_ID_DSD_MSBF, - AV_CODEC_ID_DSD_LSBF_PLANAR, - AV_CODEC_ID_DSD_MSBF_PLANAR, - AV_CODEC_ID_4GV, - AV_CODEC_ID_INTERPLAY_ACM, - AV_CODEC_ID_XMA1, - AV_CODEC_ID_XMA2, - AV_CODEC_ID_DST, - AV_CODEC_ID_ATRAC3AL, - AV_CODEC_ID_ATRAC3PAL, - AV_CODEC_ID_DOLBY_E, - AV_CODEC_ID_APTX, - AV_CODEC_ID_APTX_HD, - AV_CODEC_ID_SBC, - AV_CODEC_ID_ATRAC9, - AV_CODEC_ID_HCOM, - AV_CODEC_ID_ACELP_KELVIN, - AV_CODEC_ID_MPEGH_3D_AUDIO, - AV_CODEC_ID_SIREN, - AV_CODEC_ID_HCA, - AV_CODEC_ID_FASTAUDIO, - AV_CODEC_ID_MSNSIREN, - AV_CODEC_ID_DFPWM, - AV_CODEC_ID_BONK, - AV_CODEC_ID_MISC4, - AV_CODEC_ID_APAC, - AV_CODEC_ID_FTR, - AV_CODEC_ID_WAVARC, - AV_CODEC_ID_RKA, - AV_CODEC_ID_AC4, - AV_CODEC_ID_OSQ, - - /* subtitle codecs */ - AV_CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs. - AV_CODEC_ID_DVD_SUBTITLE = 0x17000, - AV_CODEC_ID_DVB_SUBTITLE, - AV_CODEC_ID_TEXT, ///< raw UTF-8 text - AV_CODEC_ID_XSUB, - AV_CODEC_ID_SSA, - AV_CODEC_ID_MOV_TEXT, - AV_CODEC_ID_HDMV_PGS_SUBTITLE, - AV_CODEC_ID_DVB_TELETEXT, - AV_CODEC_ID_SRT, - AV_CODEC_ID_MICRODVD, - AV_CODEC_ID_EIA_608, - AV_CODEC_ID_JACOSUB, - AV_CODEC_ID_SAMI, - AV_CODEC_ID_REALTEXT, - AV_CODEC_ID_STL, - AV_CODEC_ID_SUBVIEWER1, - AV_CODEC_ID_SUBVIEWER, - AV_CODEC_ID_SUBRIP, - AV_CODEC_ID_WEBVTT, - AV_CODEC_ID_MPL2, - AV_CODEC_ID_VPLAYER, - AV_CODEC_ID_PJS, - AV_CODEC_ID_ASS, - AV_CODEC_ID_HDMV_TEXT_SUBTITLE, - AV_CODEC_ID_TTML, - AV_CODEC_ID_ARIB_CAPTION, - - /* other specific kind of codecs (generally used for attachments) */ - AV_CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs. - AV_CODEC_ID_TTF = 0x18000, - - AV_CODEC_ID_SCTE_35, ///< Contain timestamp estimated through PCR of program stream. - AV_CODEC_ID_EPG, - AV_CODEC_ID_BINTEXT, - AV_CODEC_ID_XBIN, - AV_CODEC_ID_IDF, - AV_CODEC_ID_OTF, - AV_CODEC_ID_SMPTE_KLV, - AV_CODEC_ID_DVD_NAV, - AV_CODEC_ID_TIMED_ID3, - AV_CODEC_ID_BIN_DATA, - AV_CODEC_ID_SMPTE_2038, - - - AV_CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf should attempt to identify it - - AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS - * stream (only used by libavformat) */ - AV_CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems - * stream (only used by libavformat) */ - AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information. - AV_CODEC_ID_WRAPPED_AVFRAME = 0x21001, ///< Passthrough codec, AVFrames wrapped in AVPacket - /** - * Dummy null video codec, useful mainly for development and debugging. - * Null encoder/decoder discard all input and never return any output. - */ - AV_CODEC_ID_VNULL, - /** - * Dummy null audio codec, useful mainly for development and debugging. - * Null encoder/decoder discard all input and never return any output. - */ - AV_CODEC_ID_ANULL, -}; - -/** - * Get the type of the given codec. - */ -enum AVMediaType avcodec_get_type(enum AVCodecID codec_id); - -/** - * Get the name of a codec. - * @return a static string identifying the codec; never NULL - */ -const char *avcodec_get_name(enum AVCodecID id); - -/** - * Return codec bits per sample. - * - * @param[in] codec_id the codec - * @return Number of bits per sample or zero if unknown for the given codec. - */ -int av_get_bits_per_sample(enum AVCodecID codec_id); - -/** - * Return codec bits per sample. - * Only return non-zero if the bits per sample is exactly correct, not an - * approximation. - * - * @param[in] codec_id the codec - * @return Number of bits per sample or zero if unknown for the given codec. - */ -int av_get_exact_bits_per_sample(enum AVCodecID codec_id); - -/** - * Return a name for the specified profile, if available. - * - * @param codec_id the ID of the codec to which the requested profile belongs - * @param profile the profile value for which a name is requested - * @return A name for the profile if found, NULL otherwise. - * - * @note unlike av_get_profile_name(), which searches a list of profiles - * supported by a specific decoder or encoder implementation, this - * function searches the list of profiles from the AVCodecDescriptor - */ -const char *avcodec_profile_name(enum AVCodecID codec_id, int profile); - -/** - * Return the PCM codec associated with a sample format. - * @param be endianness, 0 for little, 1 for big, - * -1 (or anything else) for native - * @return AV_CODEC_ID_PCM_* or AV_CODEC_ID_NONE - */ -enum AVCodecID av_get_pcm_codec(enum AVSampleFormat fmt, int be); - -/** - * @} - */ - -#endif // AVCODEC_CODEC_ID_H diff --git a/gostream/ffmpeg/include/libavcodec/codec_par.h b/gostream/ffmpeg/include/libavcodec/codec_par.h deleted file mode 100644 index 64882a97264..00000000000 --- a/gostream/ffmpeg/include/libavcodec/codec_par.h +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Codec parameters public API - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_CODEC_PAR_H -#define AVCODEC_CODEC_PAR_H - -#include - -#include "libavutil/avutil.h" -#include "libavutil/channel_layout.h" -#include "libavutil/rational.h" -#include "libavutil/pixfmt.h" - -#include "codec_id.h" -#include "defs.h" -#include "packet.h" - -/** - * @addtogroup lavc_core - * @{ - */ - -/** - * This struct describes the properties of an encoded stream. - * - * sizeof(AVCodecParameters) is not a part of the public ABI, this struct must - * be allocated with avcodec_parameters_alloc() and freed with - * avcodec_parameters_free(). - */ -typedef struct AVCodecParameters { - /** - * General type of the encoded data. - */ - enum AVMediaType codec_type; - /** - * Specific type of the encoded data (the codec used). - */ - enum AVCodecID codec_id; - /** - * Additional information about the codec (corresponds to the AVI FOURCC). - */ - uint32_t codec_tag; - - /** - * Extra binary data needed for initializing the decoder, codec-dependent. - * - * Must be allocated with av_malloc() and will be freed by - * avcodec_parameters_free(). The allocated size of extradata must be at - * least extradata_size + AV_INPUT_BUFFER_PADDING_SIZE, with the padding - * bytes zeroed. - */ - uint8_t *extradata; - /** - * Size of the extradata content in bytes. - */ - int extradata_size; - - /** - * - video: the pixel format, the value corresponds to enum AVPixelFormat. - * - audio: the sample format, the value corresponds to enum AVSampleFormat. - */ - int format; - - /** - * The average bitrate of the encoded data (in bits per second). - */ - int64_t bit_rate; - - /** - * The number of bits per sample in the codedwords. - * - * This is basically the bitrate per sample. It is mandatory for a bunch of - * formats to actually decode them. It's the number of bits for one sample in - * the actual coded bitstream. - * - * This could be for example 4 for ADPCM - * For PCM formats this matches bits_per_raw_sample - * Can be 0 - */ - int bits_per_coded_sample; - - /** - * This is the number of valid bits in each output sample. If the - * sample format has more bits, the least significant bits are additional - * padding bits, which are always 0. Use right shifts to reduce the sample - * to its actual size. For example, audio formats with 24 bit samples will - * have bits_per_raw_sample set to 24, and format set to AV_SAMPLE_FMT_S32. - * To get the original sample use "(int32_t)sample >> 8"." - * - * For ADPCM this might be 12 or 16 or similar - * Can be 0 - */ - int bits_per_raw_sample; - - /** - * Codec-specific bitstream restrictions that the stream conforms to. - */ - int profile; - int level; - - /** - * Video only. The dimensions of the video frame in pixels. - */ - int width; - int height; - - /** - * Video only. The aspect ratio (width / height) which a single pixel - * should have when displayed. - * - * When the aspect ratio is unknown / undefined, the numerator should be - * set to 0 (the denominator may have any value). - */ - AVRational sample_aspect_ratio; - - /** - * Video only. The order of the fields in interlaced video. - */ - enum AVFieldOrder field_order; - - /** - * Video only. Additional colorspace characteristics. - */ - enum AVColorRange color_range; - enum AVColorPrimaries color_primaries; - enum AVColorTransferCharacteristic color_trc; - enum AVColorSpace color_space; - enum AVChromaLocation chroma_location; - - /** - * Video only. Number of delayed frames. - */ - int video_delay; - -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * Audio only. The channel layout bitmask. May be 0 if the channel layout is - * unknown or unspecified, otherwise the number of bits set must be equal to - * the channels field. - * @deprecated use ch_layout - */ - attribute_deprecated - uint64_t channel_layout; - /** - * Audio only. The number of audio channels. - * @deprecated use ch_layout.nb_channels - */ - attribute_deprecated - int channels; -#endif - /** - * Audio only. The number of audio samples per second. - */ - int sample_rate; - /** - * Audio only. The number of bytes per coded audio frame, required by some - * formats. - * - * Corresponds to nBlockAlign in WAVEFORMATEX. - */ - int block_align; - /** - * Audio only. Audio frame size, if known. Required by some formats to be static. - */ - int frame_size; - - /** - * Audio only. The amount of padding (in samples) inserted by the encoder at - * the beginning of the audio. I.e. this number of leading decoded samples - * must be discarded by the caller to get the original audio without leading - * padding. - */ - int initial_padding; - /** - * Audio only. The amount of padding (in samples) appended by the encoder to - * the end of the audio. I.e. this number of decoded samples must be - * discarded by the caller from the end of the stream to get the original - * audio without any trailing padding. - */ - int trailing_padding; - /** - * Audio only. Number of samples to skip after a discontinuity. - */ - int seek_preroll; - - /** - * Audio only. The channel layout and number of channels. - */ - AVChannelLayout ch_layout; - - /** - * Video only. Number of frames per second, for streams with constant frame - * durations. Should be set to { 0, 1 } when some frames have differing - * durations or if the value is not known. - * - * @note This field correponds to values that are stored in codec-level - * headers and is typically overridden by container/transport-layer - * timestamps, when available. It should thus be used only as a last resort, - * when no higher-level timing information is available. - */ - AVRational framerate; - - /** - * Additional data associated with the entire stream. - */ - AVPacketSideData *coded_side_data; - - /** - * Amount of entries in @ref coded_side_data. - */ - int nb_coded_side_data; -} AVCodecParameters; - -/** - * Allocate a new AVCodecParameters and set its fields to default values - * (unknown/invalid/0). The returned struct must be freed with - * avcodec_parameters_free(). - */ -AVCodecParameters *avcodec_parameters_alloc(void); - -/** - * Free an AVCodecParameters instance and everything associated with it and - * write NULL to the supplied pointer. - */ -void avcodec_parameters_free(AVCodecParameters **par); - -/** - * Copy the contents of src to dst. Any allocated fields in dst are freed and - * replaced with newly allocated duplicates of the corresponding fields in src. - * - * @return >= 0 on success, a negative AVERROR code on failure. - */ -int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src); - -/** - * This function is the same as av_get_audio_frame_duration(), except it works - * with AVCodecParameters instead of an AVCodecContext. - */ -int av_get_audio_frame_duration2(AVCodecParameters *par, int frame_bytes); - -/** - * @} - */ - -#endif // AVCODEC_CODEC_PAR_H diff --git a/gostream/ffmpeg/include/libavcodec/d3d11va.h b/gostream/ffmpeg/include/libavcodec/d3d11va.h deleted file mode 100644 index 6816b6c1e68..00000000000 --- a/gostream/ffmpeg/include/libavcodec/d3d11va.h +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Direct3D11 HW acceleration - * - * copyright (c) 2009 Laurent Aimar - * copyright (c) 2015 Steve Lhomme - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_D3D11VA_H -#define AVCODEC_D3D11VA_H - -/** - * @file - * @ingroup lavc_codec_hwaccel_d3d11va - * Public libavcodec D3D11VA header. - */ - -#if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0602 -#undef _WIN32_WINNT -#define _WIN32_WINNT 0x0602 -#endif - -#include -#include - -/** - * @defgroup lavc_codec_hwaccel_d3d11va Direct3D11 - * @ingroup lavc_codec_hwaccel - * - * @{ - */ - -#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for Direct3D11 and old UVD/UVD+ ATI video cards -#define FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO 2 ///< Work around for Direct3D11 and old Intel GPUs with ClearVideo interface - -/** - * This structure is used to provides the necessary configurations and data - * to the Direct3D11 FFmpeg HWAccel implementation. - * - * The application must make it available as AVCodecContext.hwaccel_context. - * - * Use av_d3d11va_alloc_context() exclusively to allocate an AVD3D11VAContext. - */ -typedef struct AVD3D11VAContext { - /** - * D3D11 decoder object - */ - ID3D11VideoDecoder *decoder; - - /** - * D3D11 VideoContext - */ - ID3D11VideoContext *video_context; - - /** - * D3D11 configuration used to create the decoder - */ - D3D11_VIDEO_DECODER_CONFIG *cfg; - - /** - * The number of surface in the surface array - */ - unsigned surface_count; - - /** - * The array of Direct3D surfaces used to create the decoder - */ - ID3D11VideoDecoderOutputView **surface; - - /** - * A bit field configuring the workarounds needed for using the decoder - */ - uint64_t workaround; - - /** - * Private to the FFmpeg AVHWAccel implementation - */ - unsigned report_id; - - /** - * Mutex to access video_context - */ - HANDLE context_mutex; -} AVD3D11VAContext; - -/** - * Allocate an AVD3D11VAContext. - * - * @return Newly-allocated AVD3D11VAContext or NULL on failure. - */ -AVD3D11VAContext *av_d3d11va_alloc_context(void); - -/** - * @} - */ - -#endif /* AVCODEC_D3D11VA_H */ diff --git a/gostream/ffmpeg/include/libavcodec/defs.h b/gostream/ffmpeg/include/libavcodec/defs.h deleted file mode 100644 index 00d840ec19b..00000000000 --- a/gostream/ffmpeg/include/libavcodec/defs.h +++ /dev/null @@ -1,335 +0,0 @@ -/* - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_DEFS_H -#define AVCODEC_DEFS_H - -/** - * @file - * @ingroup libavc - * Misc types and constants that do not belong anywhere else. - */ - -#include -#include - -/** - * @ingroup lavc_decoding - * Required number of additionally allocated bytes at the end of the input bitstream for decoding. - * This is mainly needed because some optimized bitstream readers read - * 32 or 64 bit at once and could read over the end.
- * Note: If the first 23 bits of the additional bytes are not 0, then damaged - * MPEG bitstreams could cause overread and segfault. - */ -#define AV_INPUT_BUFFER_PADDING_SIZE 64 - -/** - * Verify checksums embedded in the bitstream (could be of either encoded or - * decoded data, depending on the format) and print an error message on mismatch. - * If AV_EF_EXPLODE is also set, a mismatching checksum will result in the - * decoder/demuxer returning an error. - */ -#define AV_EF_CRCCHECK (1<<0) -#define AV_EF_BITSTREAM (1<<1) ///< detect bitstream specification deviations -#define AV_EF_BUFFER (1<<2) ///< detect improper bitstream length -#define AV_EF_EXPLODE (1<<3) ///< abort decoding on minor error detection - -#define AV_EF_IGNORE_ERR (1<<15) ///< ignore errors and continue -#define AV_EF_CAREFUL (1<<16) ///< consider things that violate the spec, are fast to calculate and have not been seen in the wild as errors -#define AV_EF_COMPLIANT (1<<17) ///< consider all spec non compliances as errors -#define AV_EF_AGGRESSIVE (1<<18) ///< consider things that a sane encoder/muxer should not do as an error - -#define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software. -#define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences. -#define FF_COMPLIANCE_NORMAL 0 -#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions -#define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things. - - -#define AV_PROFILE_UNKNOWN -99 -#define AV_PROFILE_RESERVED -100 - -#define AV_PROFILE_AAC_MAIN 0 -#define AV_PROFILE_AAC_LOW 1 -#define AV_PROFILE_AAC_SSR 2 -#define AV_PROFILE_AAC_LTP 3 -#define AV_PROFILE_AAC_HE 4 -#define AV_PROFILE_AAC_HE_V2 28 -#define AV_PROFILE_AAC_LD 22 -#define AV_PROFILE_AAC_ELD 38 -#define AV_PROFILE_MPEG2_AAC_LOW 128 -#define AV_PROFILE_MPEG2_AAC_HE 131 - -#define AV_PROFILE_DNXHD 0 -#define AV_PROFILE_DNXHR_LB 1 -#define AV_PROFILE_DNXHR_SQ 2 -#define AV_PROFILE_DNXHR_HQ 3 -#define AV_PROFILE_DNXHR_HQX 4 -#define AV_PROFILE_DNXHR_444 5 - -#define AV_PROFILE_DTS 20 -#define AV_PROFILE_DTS_ES 30 -#define AV_PROFILE_DTS_96_24 40 -#define AV_PROFILE_DTS_HD_HRA 50 -#define AV_PROFILE_DTS_HD_MA 60 -#define AV_PROFILE_DTS_EXPRESS 70 -#define AV_PROFILE_DTS_HD_MA_X 61 -#define AV_PROFILE_DTS_HD_MA_X_IMAX 62 - -#define AV_PROFILE_EAC3_DDP_ATMOS 30 - -#define AV_PROFILE_TRUEHD_ATMOS 30 - -#define AV_PROFILE_MPEG2_422 0 -#define AV_PROFILE_MPEG2_HIGH 1 -#define AV_PROFILE_MPEG2_SS 2 -#define AV_PROFILE_MPEG2_SNR_SCALABLE 3 -#define AV_PROFILE_MPEG2_MAIN 4 -#define AV_PROFILE_MPEG2_SIMPLE 5 - -#define AV_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag -#define AV_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag - -#define AV_PROFILE_H264_BASELINE 66 -#define AV_PROFILE_H264_CONSTRAINED_BASELINE (66|AV_PROFILE_H264_CONSTRAINED) -#define AV_PROFILE_H264_MAIN 77 -#define AV_PROFILE_H264_EXTENDED 88 -#define AV_PROFILE_H264_HIGH 100 -#define AV_PROFILE_H264_HIGH_10 110 -#define AV_PROFILE_H264_HIGH_10_INTRA (110|AV_PROFILE_H264_INTRA) -#define AV_PROFILE_H264_MULTIVIEW_HIGH 118 -#define AV_PROFILE_H264_HIGH_422 122 -#define AV_PROFILE_H264_HIGH_422_INTRA (122|AV_PROFILE_H264_INTRA) -#define AV_PROFILE_H264_STEREO_HIGH 128 -#define AV_PROFILE_H264_HIGH_444 144 -#define AV_PROFILE_H264_HIGH_444_PREDICTIVE 244 -#define AV_PROFILE_H264_HIGH_444_INTRA (244|AV_PROFILE_H264_INTRA) -#define AV_PROFILE_H264_CAVLC_444 44 - -#define AV_PROFILE_VC1_SIMPLE 0 -#define AV_PROFILE_VC1_MAIN 1 -#define AV_PROFILE_VC1_COMPLEX 2 -#define AV_PROFILE_VC1_ADVANCED 3 - -#define AV_PROFILE_MPEG4_SIMPLE 0 -#define AV_PROFILE_MPEG4_SIMPLE_SCALABLE 1 -#define AV_PROFILE_MPEG4_CORE 2 -#define AV_PROFILE_MPEG4_MAIN 3 -#define AV_PROFILE_MPEG4_N_BIT 4 -#define AV_PROFILE_MPEG4_SCALABLE_TEXTURE 5 -#define AV_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6 -#define AV_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7 -#define AV_PROFILE_MPEG4_HYBRID 8 -#define AV_PROFILE_MPEG4_ADVANCED_REAL_TIME 9 -#define AV_PROFILE_MPEG4_CORE_SCALABLE 10 -#define AV_PROFILE_MPEG4_ADVANCED_CODING 11 -#define AV_PROFILE_MPEG4_ADVANCED_CORE 12 -#define AV_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13 -#define AV_PROFILE_MPEG4_SIMPLE_STUDIO 14 -#define AV_PROFILE_MPEG4_ADVANCED_SIMPLE 15 - -#define AV_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 1 -#define AV_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 2 -#define AV_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 32768 -#define AV_PROFILE_JPEG2000_DCINEMA_2K 3 -#define AV_PROFILE_JPEG2000_DCINEMA_4K 4 - -#define AV_PROFILE_VP9_0 0 -#define AV_PROFILE_VP9_1 1 -#define AV_PROFILE_VP9_2 2 -#define AV_PROFILE_VP9_3 3 - -#define AV_PROFILE_HEVC_MAIN 1 -#define AV_PROFILE_HEVC_MAIN_10 2 -#define AV_PROFILE_HEVC_MAIN_STILL_PICTURE 3 -#define AV_PROFILE_HEVC_REXT 4 -#define AV_PROFILE_HEVC_SCC 9 - -#define AV_PROFILE_VVC_MAIN_10 1 -#define AV_PROFILE_VVC_MAIN_10_444 33 - -#define AV_PROFILE_AV1_MAIN 0 -#define AV_PROFILE_AV1_HIGH 1 -#define AV_PROFILE_AV1_PROFESSIONAL 2 - -#define AV_PROFILE_MJPEG_HUFFMAN_BASELINE_DCT 0xc0 -#define AV_PROFILE_MJPEG_HUFFMAN_EXTENDED_SEQUENTIAL_DCT 0xc1 -#define AV_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT 0xc2 -#define AV_PROFILE_MJPEG_HUFFMAN_LOSSLESS 0xc3 -#define AV_PROFILE_MJPEG_JPEG_LS 0xf7 - -#define AV_PROFILE_SBC_MSBC 1 - -#define AV_PROFILE_PRORES_PROXY 0 -#define AV_PROFILE_PRORES_LT 1 -#define AV_PROFILE_PRORES_STANDARD 2 -#define AV_PROFILE_PRORES_HQ 3 -#define AV_PROFILE_PRORES_4444 4 -#define AV_PROFILE_PRORES_XQ 5 - -#define AV_PROFILE_ARIB_PROFILE_A 0 -#define AV_PROFILE_ARIB_PROFILE_C 1 - -#define AV_PROFILE_KLVA_SYNC 0 -#define AV_PROFILE_KLVA_ASYNC 1 - -#define AV_PROFILE_EVC_BASELINE 0 -#define AV_PROFILE_EVC_MAIN 1 - - -#define AV_LEVEL_UNKNOWN -99 - -enum AVFieldOrder { - AV_FIELD_UNKNOWN, - AV_FIELD_PROGRESSIVE, - AV_FIELD_TT, ///< Top coded_first, top displayed first - AV_FIELD_BB, ///< Bottom coded first, bottom displayed first - AV_FIELD_TB, ///< Top coded first, bottom displayed first - AV_FIELD_BT, ///< Bottom coded first, top displayed first -}; - -/** - * @ingroup lavc_decoding - */ -enum AVDiscard{ - /* We leave some space between them for extensions (drop some - * keyframes for intra-only or drop just some bidir frames). */ - AVDISCARD_NONE =-16, ///< discard nothing - AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi - AVDISCARD_NONREF = 8, ///< discard all non reference - AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames - AVDISCARD_NONINTRA= 24, ///< discard all non intra frames - AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes - AVDISCARD_ALL = 48, ///< discard all -}; - -enum AVAudioServiceType { - AV_AUDIO_SERVICE_TYPE_MAIN = 0, - AV_AUDIO_SERVICE_TYPE_EFFECTS = 1, - AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2, - AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3, - AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4, - AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5, - AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6, - AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7, - AV_AUDIO_SERVICE_TYPE_KARAOKE = 8, - AV_AUDIO_SERVICE_TYPE_NB , ///< Not part of ABI -}; - -/** - * Pan Scan area. - * This specifies the area which should be displayed. - * Note there may be multiple such areas for one frame. - */ -typedef struct AVPanScan { - /** - * id - * - encoding: Set by user. - * - decoding: Set by libavcodec. - */ - int id; - - /** - * width and height in 1/16 pel - * - encoding: Set by user. - * - decoding: Set by libavcodec. - */ - int width; - int height; - - /** - * position of the top left corner in 1/16 pel for up to 3 fields/frames - * - encoding: Set by user. - * - decoding: Set by libavcodec. - */ - int16_t position[3][2]; -} AVPanScan; - -/** - * This structure describes the bitrate properties of an encoded bitstream. It - * roughly corresponds to a subset the VBV parameters for MPEG-2 or HRD - * parameters for H.264/HEVC. - */ -typedef struct AVCPBProperties { - /** - * Maximum bitrate of the stream, in bits per second. - * Zero if unknown or unspecified. - */ - int64_t max_bitrate; - /** - * Minimum bitrate of the stream, in bits per second. - * Zero if unknown or unspecified. - */ - int64_t min_bitrate; - /** - * Average bitrate of the stream, in bits per second. - * Zero if unknown or unspecified. - */ - int64_t avg_bitrate; - - /** - * The size of the buffer to which the ratecontrol is applied, in bits. - * Zero if unknown or unspecified. - */ - int64_t buffer_size; - - /** - * The delay between the time the packet this structure is associated with - * is received and the time when it should be decoded, in periods of a 27MHz - * clock. - * - * UINT64_MAX when unknown or unspecified. - */ - uint64_t vbv_delay; -} AVCPBProperties; - -/** - * Allocate a CPB properties structure and initialize its fields to default - * values. - * - * @param size if non-NULL, the size of the allocated struct will be written - * here. This is useful for embedding it in side data. - * - * @return the newly allocated struct or NULL on failure - */ -AVCPBProperties *av_cpb_properties_alloc(size_t *size); - -/** - * This structure supplies correlation between a packet timestamp and a wall clock - * production time. The definition follows the Producer Reference Time ('prft') - * as defined in ISO/IEC 14496-12 - */ -typedef struct AVProducerReferenceTime { - /** - * A UTC timestamp, in microseconds, since Unix epoch (e.g, av_gettime()). - */ - int64_t wallclock; - int flags; -} AVProducerReferenceTime; - -/** - * Encode extradata length to a buffer. Used by xiph codecs. - * - * @param s buffer to write to; must be at least (v/255+1) bytes long - * @param v size of extradata in bytes - * @return number of bytes written to the buffer. - */ -unsigned int av_xiphlacing(unsigned char *s, unsigned int v); - -#endif // AVCODEC_DEFS_H diff --git a/gostream/ffmpeg/include/libavcodec/dirac.h b/gostream/ffmpeg/include/libavcodec/dirac.h deleted file mode 100644 index 8c348cdc027..00000000000 --- a/gostream/ffmpeg/include/libavcodec/dirac.h +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2007 Marco Gerards - * Copyright (C) 2009 David Conrad - * Copyright (C) 2011 Jordi Ortiz - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_DIRAC_H -#define AVCODEC_DIRAC_H - -/** - * @file - * Interface to Dirac Decoder/Encoder - * @author Marco Gerards - * @author David Conrad - * @author Jordi Ortiz - */ - -#include -#include - -#include "libavutil/pixfmt.h" -#include "libavutil/rational.h" - -/** - * The spec limits the number of wavelet decompositions to 4 for both - * level 1 (VC-2) and 128 (long-gop default). - * 5 decompositions is the maximum before >16-bit buffers are needed. - * Schroedinger allows this for DD 9,7 and 13,7 wavelets only, limiting - * the others to 4 decompositions (or 3 for the fidelity filter). - * - * We use this instead of MAX_DECOMPOSITIONS to save some memory. - */ -#define MAX_DWT_LEVELS 5 - -/** - * Parse code values: - * - * Dirac Specification -> - * 9.6.1 Table 9.1 - * - * VC-2 Specification -> - * 10.4.1 Table 10.1 - */ - -enum DiracParseCodes { - DIRAC_PCODE_SEQ_HEADER = 0x00, - DIRAC_PCODE_END_SEQ = 0x10, - DIRAC_PCODE_AUX = 0x20, - DIRAC_PCODE_PAD = 0x30, - DIRAC_PCODE_PICTURE_CODED = 0x08, - DIRAC_PCODE_PICTURE_RAW = 0x48, - DIRAC_PCODE_PICTURE_LOW_DEL = 0xC8, - DIRAC_PCODE_PICTURE_HQ = 0xE8, - DIRAC_PCODE_INTER_NOREF_CO1 = 0x0A, - DIRAC_PCODE_INTER_NOREF_CO2 = 0x09, - DIRAC_PCODE_INTER_REF_CO1 = 0x0D, - DIRAC_PCODE_INTER_REF_CO2 = 0x0E, - DIRAC_PCODE_INTRA_REF_CO = 0x0C, - DIRAC_PCODE_INTRA_REF_RAW = 0x4C, - DIRAC_PCODE_INTRA_REF_PICT = 0xCC, - DIRAC_PCODE_MAGIC = 0x42424344, -}; - -typedef struct DiracVersionInfo { - int major; - int minor; -} DiracVersionInfo; - -typedef struct AVDiracSeqHeader { - unsigned width; - unsigned height; - uint8_t chroma_format; ///< 0: 444 1: 422 2: 420 - - uint8_t interlaced; - uint8_t top_field_first; - - uint8_t frame_rate_index; ///< index into dirac_frame_rate[] - uint8_t aspect_ratio_index; ///< index into dirac_aspect_ratio[] - - uint16_t clean_width; - uint16_t clean_height; - uint16_t clean_left_offset; - uint16_t clean_right_offset; - - uint8_t pixel_range_index; ///< index into dirac_pixel_range_presets[] - uint8_t color_spec_index; ///< index into dirac_color_spec_presets[] - - int profile; - int level; - - AVRational framerate; - AVRational sample_aspect_ratio; - - enum AVPixelFormat pix_fmt; - enum AVColorRange color_range; - enum AVColorPrimaries color_primaries; - enum AVColorTransferCharacteristic color_trc; - enum AVColorSpace colorspace; - - DiracVersionInfo version; - int bit_depth; -} AVDiracSeqHeader; - -/** - * Parse a Dirac sequence header. - * - * @param dsh this function will allocate and fill an AVDiracSeqHeader struct - * and write it into this pointer. The caller must free it with - * av_free(). - * @param buf the data buffer - * @param buf_size the size of the data buffer in bytes - * @param log_ctx if non-NULL, this function will log errors here - * @return 0 on success, a negative AVERROR code on failure - */ -int av_dirac_parse_sequence_header(AVDiracSeqHeader **dsh, - const uint8_t *buf, size_t buf_size, - void *log_ctx); - -#endif /* AVCODEC_DIRAC_H */ diff --git a/gostream/ffmpeg/include/libavcodec/dv_profile.h b/gostream/ffmpeg/include/libavcodec/dv_profile.h deleted file mode 100644 index 4365f1b4b14..00000000000 --- a/gostream/ffmpeg/include/libavcodec/dv_profile.h +++ /dev/null @@ -1,82 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_DV_PROFILE_H -#define AVCODEC_DV_PROFILE_H - -#include - -#include "libavutil/pixfmt.h" -#include "libavutil/rational.h" - -/* minimum number of bytes to read from a DV stream in order to - * determine the profile */ -#define DV_PROFILE_BYTES (6 * 80) /* 6 DIF blocks */ - - -/* - * AVDVProfile is used to express the differences between various - * DV flavors. For now it's primarily used for differentiating - * 525/60 and 625/50, but the plans are to use it for various - * DV specs as well (e.g. SMPTE314M vs. IEC 61834). - */ -typedef struct AVDVProfile { - int dsf; /* value of the dsf in the DV header */ - int video_stype; /* stype for VAUX source pack */ - int frame_size; /* total size of one frame in bytes */ - int difseg_size; /* number of DIF segments per DIF channel */ - int n_difchan; /* number of DIF channels per frame */ - AVRational time_base; /* 1/framerate */ - int ltc_divisor; /* FPS from the LTS standpoint */ - int height; /* picture height in pixels */ - int width; /* picture width in pixels */ - AVRational sar[2]; /* sample aspect ratios for 4:3 and 16:9 */ - enum AVPixelFormat pix_fmt; /* picture pixel format */ - int bpm; /* blocks per macroblock */ - const uint8_t *block_sizes; /* AC block sizes, in bits */ - int audio_stride; /* size of audio_shuffle table */ - int audio_min_samples[3]; /* min amount of audio samples */ - /* for 48kHz, 44.1kHz and 32kHz */ - int audio_samples_dist[5]; /* how many samples are supposed to be */ - /* in each frame in a 5 frames window */ - const uint8_t (*audio_shuffle)[9]; /* PCM shuffling table */ -} AVDVProfile; - -/** - * Get a DV profile for the provided compressed frame. - * - * @param sys the profile used for the previous frame, may be NULL - * @param frame the compressed data buffer - * @param buf_size size of the buffer in bytes - * @return the DV profile for the supplied data or NULL on failure - */ -const AVDVProfile *av_dv_frame_profile(const AVDVProfile *sys, - const uint8_t *frame, unsigned buf_size); - -/** - * Get a DV profile for the provided stream parameters. - */ -const AVDVProfile *av_dv_codec_profile(int width, int height, enum AVPixelFormat pix_fmt); - -/** - * Get a DV profile for the provided stream parameters. - * The frame rate is used as a best-effort parameter. - */ -const AVDVProfile *av_dv_codec_profile2(int width, int height, enum AVPixelFormat pix_fmt, AVRational frame_rate); - -#endif /* AVCODEC_DV_PROFILE_H */ diff --git a/gostream/ffmpeg/include/libavcodec/dxva2.h b/gostream/ffmpeg/include/libavcodec/dxva2.h deleted file mode 100644 index 22c93992f22..00000000000 --- a/gostream/ffmpeg/include/libavcodec/dxva2.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * DXVA2 HW acceleration - * - * copyright (c) 2009 Laurent Aimar - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_DXVA2_H -#define AVCODEC_DXVA2_H - -/** - * @file - * @ingroup lavc_codec_hwaccel_dxva2 - * Public libavcodec DXVA2 header. - */ - -#if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0602 -#undef _WIN32_WINNT -#define _WIN32_WINNT 0x0602 -#endif - -#include -#include -#include - -/** - * @defgroup lavc_codec_hwaccel_dxva2 DXVA2 - * @ingroup lavc_codec_hwaccel - * - * @{ - */ - -#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards -#define FF_DXVA2_WORKAROUND_INTEL_CLEARVIDEO 2 ///< Work around for DXVA2 and old Intel GPUs with ClearVideo interface - -/** - * This structure is used to provides the necessary configurations and data - * to the DXVA2 FFmpeg HWAccel implementation. - * - * The application must make it available as AVCodecContext.hwaccel_context. - */ -struct dxva_context { - /** - * DXVA2 decoder object - */ - IDirectXVideoDecoder *decoder; - - /** - * DXVA2 configuration used to create the decoder - */ - const DXVA2_ConfigPictureDecode *cfg; - - /** - * The number of surface in the surface array - */ - unsigned surface_count; - - /** - * The array of Direct3D surfaces used to create the decoder - */ - LPDIRECT3DSURFACE9 *surface; - - /** - * A bit field configuring the workarounds needed for using the decoder - */ - uint64_t workaround; - - /** - * Private to the FFmpeg AVHWAccel implementation - */ - unsigned report_id; -}; - -/** - * @} - */ - -#endif /* AVCODEC_DXVA2_H */ diff --git a/gostream/ffmpeg/include/libavcodec/jni.h b/gostream/ffmpeg/include/libavcodec/jni.h deleted file mode 100644 index dd99e926113..00000000000 --- a/gostream/ffmpeg/include/libavcodec/jni.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * JNI public API functions - * - * Copyright (c) 2015-2016 Matthieu Bouron - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_JNI_H -#define AVCODEC_JNI_H - -/* - * Manually set a Java virtual machine which will be used to retrieve the JNI - * environment. Once a Java VM is set it cannot be changed afterwards, meaning - * you can call multiple times av_jni_set_java_vm with the same Java VM pointer - * however it will error out if you try to set a different Java VM. - * - * @param vm Java virtual machine - * @param log_ctx context used for logging, can be NULL - * @return 0 on success, < 0 otherwise - */ -int av_jni_set_java_vm(void *vm, void *log_ctx); - -/* - * Get the Java virtual machine which has been set with av_jni_set_java_vm. - * - * @param vm Java virtual machine - * @return a pointer to the Java virtual machine - */ -void *av_jni_get_java_vm(void *log_ctx); - -#endif /* AVCODEC_JNI_H */ diff --git a/gostream/ffmpeg/include/libavcodec/mediacodec.h b/gostream/ffmpeg/include/libavcodec/mediacodec.h deleted file mode 100644 index 4e9b56a6184..00000000000 --- a/gostream/ffmpeg/include/libavcodec/mediacodec.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Android MediaCodec public API - * - * Copyright (c) 2016 Matthieu Bouron - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_MEDIACODEC_H -#define AVCODEC_MEDIACODEC_H - -#include "libavcodec/avcodec.h" - -/** - * This structure holds a reference to a android/view/Surface object that will - * be used as output by the decoder. - * - */ -typedef struct AVMediaCodecContext { - - /** - * android/view/Surface object reference. - */ - void *surface; - -} AVMediaCodecContext; - -/** - * Allocate and initialize a MediaCodec context. - * - * When decoding with MediaCodec is finished, the caller must free the - * MediaCodec context with av_mediacodec_default_free. - * - * @return a pointer to a newly allocated AVMediaCodecContext on success, NULL otherwise - */ -AVMediaCodecContext *av_mediacodec_alloc_context(void); - -/** - * Convenience function that sets up the MediaCodec context. - * - * @param avctx codec context - * @param ctx MediaCodec context to initialize - * @param surface reference to an android/view/Surface - * @return 0 on success, < 0 otherwise - */ -int av_mediacodec_default_init(AVCodecContext *avctx, AVMediaCodecContext *ctx, void *surface); - -/** - * This function must be called to free the MediaCodec context initialized with - * av_mediacodec_default_init(). - * - * @param avctx codec context - */ -void av_mediacodec_default_free(AVCodecContext *avctx); - -/** - * Opaque structure representing a MediaCodec buffer to render. - */ -typedef struct MediaCodecBuffer AVMediaCodecBuffer; - -/** - * Release a MediaCodec buffer and render it to the surface that is associated - * with the decoder. This function should only be called once on a given - * buffer, once released the underlying buffer returns to the codec, thus - * subsequent calls to this function will have no effect. - * - * @param buffer the buffer to render - * @param render 1 to release and render the buffer to the surface or 0 to - * discard the buffer - * @return 0 on success, < 0 otherwise - */ -int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render); - -/** - * Release a MediaCodec buffer and render it at the given time to the surface - * that is associated with the decoder. The timestamp must be within one second - * of the current `java/lang/System#nanoTime()` (which is implemented using - * `CLOCK_MONOTONIC` on Android). See the Android MediaCodec documentation - * of [`android/media/MediaCodec#releaseOutputBuffer(int,long)`][0] for more details. - * - * @param buffer the buffer to render - * @param time timestamp in nanoseconds of when to render the buffer - * @return 0 on success, < 0 otherwise - * - * [0]: https://developer.android.com/reference/android/media/MediaCodec#releaseOutputBuffer(int,%20long) - */ -int av_mediacodec_render_buffer_at_time(AVMediaCodecBuffer *buffer, int64_t time); - -#endif /* AVCODEC_MEDIACODEC_H */ diff --git a/gostream/ffmpeg/include/libavcodec/packet.h b/gostream/ffmpeg/include/libavcodec/packet.h deleted file mode 100644 index b19409b7192..00000000000 --- a/gostream/ffmpeg/include/libavcodec/packet.h +++ /dev/null @@ -1,846 +0,0 @@ -/* - * AVPacket public API - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_PACKET_H -#define AVCODEC_PACKET_H - -#include -#include - -#include "libavutil/attributes.h" -#include "libavutil/buffer.h" -#include "libavutil/dict.h" -#include "libavutil/rational.h" -#include "libavutil/version.h" - -#include "libavcodec/version_major.h" - -/** - * @defgroup lavc_packet_side_data AVPacketSideData - * - * Types and functions for working with AVPacketSideData. - * @{ - */ -enum AVPacketSideDataType { - /** - * An AV_PKT_DATA_PALETTE side data packet contains exactly AVPALETTE_SIZE - * bytes worth of palette. This side data signals that a new palette is - * present. - */ - AV_PKT_DATA_PALETTE, - - /** - * The AV_PKT_DATA_NEW_EXTRADATA is used to notify the codec or the format - * that the extradata buffer was changed and the receiving side should - * act upon it appropriately. The new extradata is embedded in the side - * data buffer and should be immediately used for processing the current - * frame or packet. - */ - AV_PKT_DATA_NEW_EXTRADATA, - - /** - * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows: - * @code - * u32le param_flags - * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT) - * s32le channel_count - * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT) - * u64le channel_layout - * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE) - * s32le sample_rate - * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS) - * s32le width - * s32le height - * @endcode - */ - AV_PKT_DATA_PARAM_CHANGE, - - /** - * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of - * structures with info about macroblocks relevant to splitting the - * packet into smaller packets on macroblock edges (e.g. as for RFC 2190). - * That is, it does not necessarily contain info about all macroblocks, - * as long as the distance between macroblocks in the info is smaller - * than the target payload size. - * Each MB info structure is 12 bytes, and is laid out as follows: - * @code - * u32le bit offset from the start of the packet - * u8 current quantizer at the start of the macroblock - * u8 GOB number - * u16le macroblock address within the GOB - * u8 horizontal MV predictor - * u8 vertical MV predictor - * u8 horizontal MV predictor for block number 3 - * u8 vertical MV predictor for block number 3 - * @endcode - */ - AV_PKT_DATA_H263_MB_INFO, - - /** - * This side data should be associated with an audio stream and contains - * ReplayGain information in form of the AVReplayGain struct. - */ - AV_PKT_DATA_REPLAYGAIN, - - /** - * This side data contains a 3x3 transformation matrix describing an affine - * transformation that needs to be applied to the decoded video frames for - * correct presentation. - * - * See libavutil/display.h for a detailed description of the data. - */ - AV_PKT_DATA_DISPLAYMATRIX, - - /** - * This side data should be associated with a video stream and contains - * Stereoscopic 3D information in form of the AVStereo3D struct. - */ - AV_PKT_DATA_STEREO3D, - - /** - * This side data should be associated with an audio stream and corresponds - * to enum AVAudioServiceType. - */ - AV_PKT_DATA_AUDIO_SERVICE_TYPE, - - /** - * This side data contains quality related information from the encoder. - * @code - * u32le quality factor of the compressed frame. Allowed range is between 1 (good) and FF_LAMBDA_MAX (bad). - * u8 picture type - * u8 error count - * u16 reserved - * u64le[error count] sum of squared differences between encoder in and output - * @endcode - */ - AV_PKT_DATA_QUALITY_STATS, - - /** - * This side data contains an integer value representing the stream index - * of a "fallback" track. A fallback track indicates an alternate - * track to use when the current track can not be decoded for some reason. - * e.g. no decoder available for codec. - */ - AV_PKT_DATA_FALLBACK_TRACK, - - /** - * This side data corresponds to the AVCPBProperties struct. - */ - AV_PKT_DATA_CPB_PROPERTIES, - - /** - * Recommmends skipping the specified number of samples - * @code - * u32le number of samples to skip from start of this packet - * u32le number of samples to skip from end of this packet - * u8 reason for start skip - * u8 reason for end skip (0=padding silence, 1=convergence) - * @endcode - */ - AV_PKT_DATA_SKIP_SAMPLES, - - /** - * An AV_PKT_DATA_JP_DUALMONO side data packet indicates that - * the packet may contain "dual mono" audio specific to Japanese DTV - * and if it is true, recommends only the selected channel to be used. - * @code - * u8 selected channels (0=main/left, 1=sub/right, 2=both) - * @endcode - */ - AV_PKT_DATA_JP_DUALMONO, - - /** - * A list of zero terminated key/value strings. There is no end marker for - * the list, so it is required to rely on the side data size to stop. - */ - AV_PKT_DATA_STRINGS_METADATA, - - /** - * Subtitle event position - * @code - * u32le x1 - * u32le y1 - * u32le x2 - * u32le y2 - * @endcode - */ - AV_PKT_DATA_SUBTITLE_POSITION, - - /** - * Data found in BlockAdditional element of matroska container. There is - * no end marker for the data, so it is required to rely on the side data - * size to recognize the end. 8 byte id (as found in BlockAddId) followed - * by data. - */ - AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, - - /** - * The optional first identifier line of a WebVTT cue. - */ - AV_PKT_DATA_WEBVTT_IDENTIFIER, - - /** - * The optional settings (rendering instructions) that immediately - * follow the timestamp specifier of a WebVTT cue. - */ - AV_PKT_DATA_WEBVTT_SETTINGS, - - /** - * A list of zero terminated key/value strings. There is no end marker for - * the list, so it is required to rely on the side data size to stop. This - * side data includes updated metadata which appeared in the stream. - */ - AV_PKT_DATA_METADATA_UPDATE, - - /** - * MPEGTS stream ID as uint8_t, this is required to pass the stream ID - * information from the demuxer to the corresponding muxer. - */ - AV_PKT_DATA_MPEGTS_STREAM_ID, - - /** - * Mastering display metadata (based on SMPTE-2086:2014). This metadata - * should be associated with a video stream and contains data in the form - * of the AVMasteringDisplayMetadata struct. - */ - AV_PKT_DATA_MASTERING_DISPLAY_METADATA, - - /** - * This side data should be associated with a video stream and corresponds - * to the AVSphericalMapping structure. - */ - AV_PKT_DATA_SPHERICAL, - - /** - * Content light level (based on CTA-861.3). This metadata should be - * associated with a video stream and contains data in the form of the - * AVContentLightMetadata struct. - */ - AV_PKT_DATA_CONTENT_LIGHT_LEVEL, - - /** - * ATSC A53 Part 4 Closed Captions. This metadata should be associated with - * a video stream. A53 CC bitstream is stored as uint8_t in AVPacketSideData.data. - * The number of bytes of CC data is AVPacketSideData.size. - */ - AV_PKT_DATA_A53_CC, - - /** - * This side data is encryption initialization data. - * The format is not part of ABI, use av_encryption_init_info_* methods to - * access. - */ - AV_PKT_DATA_ENCRYPTION_INIT_INFO, - - /** - * This side data contains encryption info for how to decrypt the packet. - * The format is not part of ABI, use av_encryption_info_* methods to access. - */ - AV_PKT_DATA_ENCRYPTION_INFO, - - /** - * Active Format Description data consisting of a single byte as specified - * in ETSI TS 101 154 using AVActiveFormatDescription enum. - */ - AV_PKT_DATA_AFD, - - /** - * Producer Reference Time data corresponding to the AVProducerReferenceTime struct, - * usually exported by some encoders (on demand through the prft flag set in the - * AVCodecContext export_side_data field). - */ - AV_PKT_DATA_PRFT, - - /** - * ICC profile data consisting of an opaque octet buffer following the - * format described by ISO 15076-1. - */ - AV_PKT_DATA_ICC_PROFILE, - - /** - * DOVI configuration - * ref: - * dolby-vision-bitstreams-within-the-iso-base-media-file-format-v2.1.2, section 2.2 - * dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2, section 3.3 - * Tags are stored in struct AVDOVIDecoderConfigurationRecord. - */ - AV_PKT_DATA_DOVI_CONF, - - /** - * Timecode which conforms to SMPTE ST 12-1:2014. The data is an array of 4 uint32_t - * where the first uint32_t describes how many (1-3) of the other timecodes are used. - * The timecode format is described in the documentation of av_timecode_get_smpte_from_framenum() - * function in libavutil/timecode.h. - */ - AV_PKT_DATA_S12M_TIMECODE, - - /** - * HDR10+ dynamic metadata associated with a video frame. The metadata is in - * the form of the AVDynamicHDRPlus struct and contains - * information for color volume transform - application 4 of - * SMPTE 2094-40:2016 standard. - */ - AV_PKT_DATA_DYNAMIC_HDR10_PLUS, - - /** - * The number of side data types. - * This is not part of the public API/ABI in the sense that it may - * change when new side data types are added. - * This must stay the last enum value. - * If its value becomes huge, some code using it - * needs to be updated as it assumes it to be smaller than other limits. - */ - AV_PKT_DATA_NB -}; - -#define AV_PKT_DATA_QUALITY_FACTOR AV_PKT_DATA_QUALITY_STATS //DEPRECATED - -/** - * This structure stores auxiliary information for decoding, presenting, or - * otherwise processing the coded stream. It is typically exported by demuxers - * and encoders and can be fed to decoders and muxers either in a per packet - * basis, or as global side data (applying to the entire coded stream). - * - * Global side data is handled as follows: - * - During demuxing, it may be exported through - * @ref AVStream.codecpar.side_data "AVStream's codec parameters", which can - * then be passed as input to decoders through the - * @ref AVCodecContext.coded_side_data "decoder context's side data", for - * initialization. - * - For muxing, it can be fed through @ref AVStream.codecpar.side_data - * "AVStream's codec parameters", typically the output of encoders through - * the @ref AVCodecContext.coded_side_data "encoder context's side data", for - * initialization. - * - * Packet specific side data is handled as follows: - * - During demuxing, it may be exported through @ref AVPacket.side_data - * "AVPacket's side data", which can then be passed as input to decoders. - * - For muxing, it can be fed through @ref AVPacket.side_data "AVPacket's - * side data", typically the output of encoders. - * - * Different modules may accept or export different types of side data - * depending on media type and codec. Refer to @ref AVPacketSideDataType for a - * list of defined types and where they may be found or used. - */ -typedef struct AVPacketSideData { - uint8_t *data; - size_t size; - enum AVPacketSideDataType type; -} AVPacketSideData; - -/** - * Allocate a new packet side data. - * - * @param sd pointer to an array of side data to which the side data should - * be added. *sd may be NULL, in which case the array will be - * initialized. - * @param nb_sd pointer to an integer containing the number of entries in - * the array. The integer value will be increased by 1 on success. - * @param type side data type - * @param size desired side data size - * @param flags currently unused. Must be zero - * - * @return pointer to freshly allocated side data on success, or NULL otherwise. - */ -AVPacketSideData *av_packet_side_data_new(AVPacketSideData **psd, int *pnb_sd, - enum AVPacketSideDataType type, - size_t size, int flags); - -/** - * Wrap existing data as packet side data. - * - * @param sd pointer to an array of side data to which the side data should - * be added. *sd may be NULL, in which case the array will be - * initialized - * @param nb_sd pointer to an integer containing the number of entries in - * the array. The integer value will be increased by 1 on success. - * @param type side data type - * @param data a data array. It must be allocated with the av_malloc() family - * of functions. The ownership of the data is transferred to the - * side data array on success - * @param size size of the data array - * @param flags currently unused. Must be zero - * - * @return pointer to freshly allocated side data on success, or NULL otherwise - * On failure, the side data array is unchanged and the data remains - * owned by the caller. - */ -AVPacketSideData *av_packet_side_data_add(AVPacketSideData **sd, int *nb_sd, - enum AVPacketSideDataType type, - void *data, size_t size, int flags); - -/** - * Get side information from a side data array. - * - * @param sd the array from which the side data should be fetched - * @param nb_sd value containing the number of entries in the array. - * @param type desired side information type - * - * @return pointer to side data if present or NULL otherwise - */ -const AVPacketSideData *av_packet_side_data_get(const AVPacketSideData *sd, - int nb_sd, - enum AVPacketSideDataType type); - -/** - * Remove side data of the given type from a side data array. - * - * @param sd the array from which the side data should be removed - * @param nb_sd pointer to an integer containing the number of entries in - * the array. Will be reduced by the amount of entries removed - * upon return - * @param type side information type - */ -void av_packet_side_data_remove(AVPacketSideData *sd, int *nb_sd, - enum AVPacketSideDataType type); - -/** - * Convenience function to free all the side data stored in an array, and - * the array itself. - * - * @param sd pointer to array of side data to free. Will be set to NULL - * upon return. - * @param nb_sd pointer to an integer containing the number of entries in - * the array. Will be set to 0 upon return. - */ -void av_packet_side_data_free(AVPacketSideData **sd, int *nb_sd); - -const char *av_packet_side_data_name(enum AVPacketSideDataType type); - -/** - * @} - */ - -/** - * @defgroup lavc_packet AVPacket - * - * Types and functions for working with AVPacket. - * @{ - */ - -/** - * This structure stores compressed data. It is typically exported by demuxers - * and then passed as input to decoders, or received as output from encoders and - * then passed to muxers. - * - * For video, it should typically contain one compressed frame. For audio it may - * contain several compressed frames. Encoders are allowed to output empty - * packets, with no compressed data, containing only side data - * (e.g. to update some stream parameters at the end of encoding). - * - * The semantics of data ownership depends on the buf field. - * If it is set, the packet data is dynamically allocated and is - * valid indefinitely until a call to av_packet_unref() reduces the - * reference count to 0. - * - * If the buf field is not set av_packet_ref() would make a copy instead - * of increasing the reference count. - * - * The side data is always allocated with av_malloc(), copied by - * av_packet_ref() and freed by av_packet_unref(). - * - * sizeof(AVPacket) being a part of the public ABI is deprecated. once - * av_init_packet() is removed, new packets will only be able to be allocated - * with av_packet_alloc(), and new fields may be added to the end of the struct - * with a minor bump. - * - * @see av_packet_alloc - * @see av_packet_ref - * @see av_packet_unref - */ -typedef struct AVPacket { - /** - * A reference to the reference-counted buffer where the packet data is - * stored. - * May be NULL, then the packet data is not reference-counted. - */ - AVBufferRef *buf; - /** - * Presentation timestamp in AVStream->time_base units; the time at which - * the decompressed packet will be presented to the user. - * Can be AV_NOPTS_VALUE if it is not stored in the file. - * pts MUST be larger or equal to dts as presentation cannot happen before - * decompression, unless one wants to view hex dumps. Some formats misuse - * the terms dts and pts/cts to mean something different. Such timestamps - * must be converted to true pts/dts before they are stored in AVPacket. - */ - int64_t pts; - /** - * Decompression timestamp in AVStream->time_base units; the time at which - * the packet is decompressed. - * Can be AV_NOPTS_VALUE if it is not stored in the file. - */ - int64_t dts; - uint8_t *data; - int size; - int stream_index; - /** - * A combination of AV_PKT_FLAG values - */ - int flags; - /** - * Additional packet data that can be provided by the container. - * Packet can contain several types of side information. - */ - AVPacketSideData *side_data; - int side_data_elems; - - /** - * Duration of this packet in AVStream->time_base units, 0 if unknown. - * Equals next_pts - this_pts in presentation order. - */ - int64_t duration; - - int64_t pos; ///< byte position in stream, -1 if unknown - - /** - * for some private data of the user - */ - void *opaque; - - /** - * AVBufferRef for free use by the API user. FFmpeg will never check the - * contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when - * the packet is unreferenced. av_packet_copy_props() calls create a new - * reference with av_buffer_ref() for the target packet's opaque_ref field. - * - * This is unrelated to the opaque field, although it serves a similar - * purpose. - */ - AVBufferRef *opaque_ref; - - /** - * Time base of the packet's timestamps. - * In the future, this field may be set on packets output by encoders or - * demuxers, but its value will be by default ignored on input to decoders - * or muxers. - */ - AVRational time_base; -} AVPacket; - -#if FF_API_INIT_PACKET -attribute_deprecated -typedef struct AVPacketList { - AVPacket pkt; - struct AVPacketList *next; -} AVPacketList; -#endif - -#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe -#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted -/** - * Flag is used to discard packets which are required to maintain valid - * decoder state but are not required for output and should be dropped - * after decoding. - **/ -#define AV_PKT_FLAG_DISCARD 0x0004 -/** - * The packet comes from a trusted source. - * - * Otherwise-unsafe constructs such as arbitrary pointers to data - * outside the packet may be followed. - */ -#define AV_PKT_FLAG_TRUSTED 0x0008 -/** - * Flag is used to indicate packets that contain frames that can - * be discarded by the decoder. I.e. Non-reference frames. - */ -#define AV_PKT_FLAG_DISPOSABLE 0x0010 - -enum AVSideDataParamChangeFlags { -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * @deprecated those are not used by any decoder - */ - AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001, - AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002, -#endif - AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004, - AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008, -}; - -/** - * Allocate an AVPacket and set its fields to default values. The resulting - * struct must be freed using av_packet_free(). - * - * @return An AVPacket filled with default values or NULL on failure. - * - * @note this only allocates the AVPacket itself, not the data buffers. Those - * must be allocated through other means such as av_new_packet. - * - * @see av_new_packet - */ -AVPacket *av_packet_alloc(void); - -/** - * Create a new packet that references the same data as src. - * - * This is a shortcut for av_packet_alloc()+av_packet_ref(). - * - * @return newly created AVPacket on success, NULL on error. - * - * @see av_packet_alloc - * @see av_packet_ref - */ -AVPacket *av_packet_clone(const AVPacket *src); - -/** - * Free the packet, if the packet is reference counted, it will be - * unreferenced first. - * - * @param pkt packet to be freed. The pointer will be set to NULL. - * @note passing NULL is a no-op. - */ -void av_packet_free(AVPacket **pkt); - -#if FF_API_INIT_PACKET -/** - * Initialize optional fields of a packet with default values. - * - * Note, this does not touch the data and size members, which have to be - * initialized separately. - * - * @param pkt packet - * - * @see av_packet_alloc - * @see av_packet_unref - * - * @deprecated This function is deprecated. Once it's removed, - sizeof(AVPacket) will not be a part of the ABI anymore. - */ -attribute_deprecated -void av_init_packet(AVPacket *pkt); -#endif - -/** - * Allocate the payload of a packet and initialize its fields with - * default values. - * - * @param pkt packet - * @param size wanted payload size - * @return 0 if OK, AVERROR_xxx otherwise - */ -int av_new_packet(AVPacket *pkt, int size); - -/** - * Reduce packet size, correctly zeroing padding - * - * @param pkt packet - * @param size new size - */ -void av_shrink_packet(AVPacket *pkt, int size); - -/** - * Increase packet size, correctly zeroing padding - * - * @param pkt packet - * @param grow_by number of bytes by which to increase the size of the packet - */ -int av_grow_packet(AVPacket *pkt, int grow_by); - -/** - * Initialize a reference-counted packet from av_malloc()ed data. - * - * @param pkt packet to be initialized. This function will set the data, size, - * and buf fields, all others are left untouched. - * @param data Data allocated by av_malloc() to be used as packet data. If this - * function returns successfully, the data is owned by the underlying AVBuffer. - * The caller may not access the data through other means. - * @param size size of data in bytes, without the padding. I.e. the full buffer - * size is assumed to be size + AV_INPUT_BUFFER_PADDING_SIZE. - * - * @return 0 on success, a negative AVERROR on error - */ -int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size); - -/** - * Allocate new information of a packet. - * - * @param pkt packet - * @param type side information type - * @param size side information size - * @return pointer to fresh allocated data or NULL otherwise - */ -uint8_t* av_packet_new_side_data(AVPacket *pkt, enum AVPacketSideDataType type, - size_t size); - -/** - * Wrap an existing array as a packet side data. - * - * @param pkt packet - * @param type side information type - * @param data the side data array. It must be allocated with the av_malloc() - * family of functions. The ownership of the data is transferred to - * pkt. - * @param size side information size - * @return a non-negative number on success, a negative AVERROR code on - * failure. On failure, the packet is unchanged and the data remains - * owned by the caller. - */ -int av_packet_add_side_data(AVPacket *pkt, enum AVPacketSideDataType type, - uint8_t *data, size_t size); - -/** - * Shrink the already allocated side data buffer - * - * @param pkt packet - * @param type side information type - * @param size new side information size - * @return 0 on success, < 0 on failure - */ -int av_packet_shrink_side_data(AVPacket *pkt, enum AVPacketSideDataType type, - size_t size); - -/** - * Get side information from packet. - * - * @param pkt packet - * @param type desired side information type - * @param size If supplied, *size will be set to the size of the side data - * or to zero if the desired side data is not present. - * @return pointer to data if present or NULL otherwise - */ -uint8_t* av_packet_get_side_data(const AVPacket *pkt, enum AVPacketSideDataType type, - size_t *size); - -/** - * Pack a dictionary for use in side_data. - * - * @param dict The dictionary to pack. - * @param size pointer to store the size of the returned data - * @return pointer to data if successful, NULL otherwise - */ -uint8_t *av_packet_pack_dictionary(AVDictionary *dict, size_t *size); -/** - * Unpack a dictionary from side_data. - * - * @param data data from side_data - * @param size size of the data - * @param dict the metadata storage dictionary - * @return 0 on success, < 0 on failure - */ -int av_packet_unpack_dictionary(const uint8_t *data, size_t size, - AVDictionary **dict); - -/** - * Convenience function to free all the side data stored. - * All the other fields stay untouched. - * - * @param pkt packet - */ -void av_packet_free_side_data(AVPacket *pkt); - -/** - * Setup a new reference to the data described by a given packet - * - * If src is reference-counted, setup dst as a new reference to the - * buffer in src. Otherwise allocate a new buffer in dst and copy the - * data from src into it. - * - * All the other fields are copied from src. - * - * @see av_packet_unref - * - * @param dst Destination packet. Will be completely overwritten. - * @param src Source packet - * - * @return 0 on success, a negative AVERROR on error. On error, dst - * will be blank (as if returned by av_packet_alloc()). - */ -int av_packet_ref(AVPacket *dst, const AVPacket *src); - -/** - * Wipe the packet. - * - * Unreference the buffer referenced by the packet and reset the - * remaining packet fields to their default values. - * - * @param pkt The packet to be unreferenced. - */ -void av_packet_unref(AVPacket *pkt); - -/** - * Move every field in src to dst and reset src. - * - * @see av_packet_unref - * - * @param src Source packet, will be reset - * @param dst Destination packet - */ -void av_packet_move_ref(AVPacket *dst, AVPacket *src); - -/** - * Copy only "properties" fields from src to dst. - * - * Properties for the purpose of this function are all the fields - * beside those related to the packet data (buf, data, size) - * - * @param dst Destination packet - * @param src Source packet - * - * @return 0 on success AVERROR on failure. - */ -int av_packet_copy_props(AVPacket *dst, const AVPacket *src); - -/** - * Ensure the data described by a given packet is reference counted. - * - * @note This function does not ensure that the reference will be writable. - * Use av_packet_make_writable instead for that purpose. - * - * @see av_packet_ref - * @see av_packet_make_writable - * - * @param pkt packet whose data should be made reference counted. - * - * @return 0 on success, a negative AVERROR on error. On failure, the - * packet is unchanged. - */ -int av_packet_make_refcounted(AVPacket *pkt); - -/** - * Create a writable reference for the data described by a given packet, - * avoiding data copy if possible. - * - * @param pkt Packet whose data should be made writable. - * - * @return 0 on success, a negative AVERROR on failure. On failure, the - * packet is unchanged. - */ -int av_packet_make_writable(AVPacket *pkt); - -/** - * Convert valid timing fields (timestamps / durations) in a packet from one - * timebase to another. Timestamps with unknown values (AV_NOPTS_VALUE) will be - * ignored. - * - * @param pkt packet on which the conversion will be performed - * @param tb_src source timebase, in which the timing fields in pkt are - * expressed - * @param tb_dst destination timebase, to which the timing fields will be - * converted - */ -void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst); - -/** - * @} - */ - -#endif // AVCODEC_PACKET_H diff --git a/gostream/ffmpeg/include/libavcodec/qsv.h b/gostream/ffmpeg/include/libavcodec/qsv.h deleted file mode 100644 index c156b08d073..00000000000 --- a/gostream/ffmpeg/include/libavcodec/qsv.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Intel MediaSDK QSV public API - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_QSV_H -#define AVCODEC_QSV_H - -#include - -#include "libavutil/buffer.h" - -/** - * This struct is used for communicating QSV parameters between libavcodec and - * the caller. It is managed by the caller and must be assigned to - * AVCodecContext.hwaccel_context. - * - decoding: hwaccel_context must be set on return from the get_format() - * callback - * - encoding: hwaccel_context must be set before avcodec_open2() - */ -typedef struct AVQSVContext { - /** - * If non-NULL, the session to use for encoding or decoding. - * Otherwise, libavcodec will try to create an internal session. - */ - mfxSession session; - - /** - * The IO pattern to use. - */ - int iopattern; - - /** - * Extra buffers to pass to encoder or decoder initialization. - */ - mfxExtBuffer **ext_buffers; - int nb_ext_buffers; - - /** - * Encoding only. If this field is set to non-zero by the caller, libavcodec - * will create an mfxExtOpaqueSurfaceAlloc extended buffer and pass it to - * the encoder initialization. This only makes sense if iopattern is also - * set to MFX_IOPATTERN_IN_OPAQUE_MEMORY. - * - * The number of allocated opaque surfaces will be the sum of the number - * required by the encoder and the user-provided value nb_opaque_surfaces. - * The array of the opaque surfaces will be exported to the caller through - * the opaque_surfaces field. - * - * The caller must set this field to zero for oneVPL (MFX_VERSION >= 2.0) - */ - int opaque_alloc; - - /** - * Encoding only, and only if opaque_alloc is set to non-zero. Before - * calling avcodec_open2(), the caller should set this field to the number - * of extra opaque surfaces to allocate beyond what is required by the - * encoder. - * - * On return from avcodec_open2(), this field will be set by libavcodec to - * the total number of allocated opaque surfaces. - */ - int nb_opaque_surfaces; - - /** - * Encoding only, and only if opaque_alloc is set to non-zero. On return - * from avcodec_open2(), this field will be used by libavcodec to export the - * array of the allocated opaque surfaces to the caller, so they can be - * passed to other parts of the pipeline. - * - * The buffer reference exported here is owned and managed by libavcodec, - * the callers should make their own reference with av_buffer_ref() and free - * it with av_buffer_unref() when it is no longer needed. - * - * The buffer data is an nb_opaque_surfaces-sized array of mfxFrameSurface1. - */ - AVBufferRef *opaque_surfaces; - - /** - * Encoding only, and only if opaque_alloc is set to non-zero. On return - * from avcodec_open2(), this field will be set to the surface type used in - * the opaque allocation request. - */ - int opaque_alloc_type; -} AVQSVContext; - -/** - * Allocate a new context. - * - * It must be freed by the caller with av_free(). - */ -AVQSVContext *av_qsv_alloc_context(void); - -#endif /* AVCODEC_QSV_H */ diff --git a/gostream/ffmpeg/include/libavcodec/vdpau.h b/gostream/ffmpeg/include/libavcodec/vdpau.h deleted file mode 100644 index 35c4b1096ba..00000000000 --- a/gostream/ffmpeg/include/libavcodec/vdpau.h +++ /dev/null @@ -1,157 +0,0 @@ -/* - * The Video Decode and Presentation API for UNIX (VDPAU) is used for - * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1. - * - * Copyright (C) 2008 NVIDIA - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_VDPAU_H -#define AVCODEC_VDPAU_H - -/** - * @file - * @ingroup lavc_codec_hwaccel_vdpau - * Public libavcodec VDPAU header. - */ - - -/** - * @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer - * @ingroup lavc_codec_hwaccel - * - * VDPAU hardware acceleration has two modules - * - VDPAU decoding - * - VDPAU presentation - * - * The VDPAU decoding module parses all headers using FFmpeg - * parsing mechanisms and uses VDPAU for the actual decoding. - * - * As per the current implementation, the actual decoding - * and rendering (API calls) are done as part of the VDPAU - * presentation (vo_vdpau.c) module. - * - * @{ - */ - -#include - -#include "libavutil/avconfig.h" -#include "libavutil/attributes.h" - -#include "avcodec.h" - -struct AVCodecContext; -struct AVFrame; - -typedef int (*AVVDPAU_Render2)(struct AVCodecContext *, struct AVFrame *, - const VdpPictureInfo *, uint32_t, - const VdpBitstreamBuffer *); - -/** - * This structure is used to share data between the libavcodec library and - * the client video application. - * The user shall allocate the structure via the av_alloc_vdpau_hwaccel - * function and make it available as - * AVCodecContext.hwaccel_context. Members can be set by the user once - * during initialization or through each AVCodecContext.get_buffer() - * function call. In any case, they must be valid prior to calling - * decoding functions. - * - * The size of this structure is not a part of the public ABI and must not - * be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an - * AVVDPAUContext. - */ -typedef struct AVVDPAUContext { - /** - * VDPAU decoder handle - * - * Set by user. - */ - VdpDecoder decoder; - - /** - * VDPAU decoder render callback - * - * Set by the user. - */ - VdpDecoderRender *render; - - AVVDPAU_Render2 render2; -} AVVDPAUContext; - -/** - * @brief allocation function for AVVDPAUContext - * - * Allows extending the struct without breaking API/ABI - */ -AVVDPAUContext *av_alloc_vdpaucontext(void); - -AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *); -void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2); - -/** - * Associate a VDPAU device with a codec context for hardware acceleration. - * This function is meant to be called from the get_format() codec callback, - * or earlier. It can also be called after avcodec_flush_buffers() to change - * the underlying VDPAU device mid-stream (e.g. to recover from non-transparent - * display preemption). - * - * @note get_format() must return AV_PIX_FMT_VDPAU if this function completes - * successfully. - * - * @param avctx decoding context whose get_format() callback is invoked - * @param device VDPAU device handle to use for hardware acceleration - * @param get_proc_address VDPAU device driver - * @param flags zero of more OR'd AV_HWACCEL_FLAG_* flags - * - * @return 0 on success, an AVERROR code on failure. - */ -int av_vdpau_bind_context(AVCodecContext *avctx, VdpDevice device, - VdpGetProcAddress *get_proc_address, unsigned flags); - -/** - * Gets the parameters to create an adequate VDPAU video surface for the codec - * context using VDPAU hardware decoding acceleration. - * - * @note Behavior is undefined if the context was not successfully bound to a - * VDPAU device using av_vdpau_bind_context(). - * - * @param avctx the codec context being used for decoding the stream - * @param type storage space for the VDPAU video surface chroma type - * (or NULL to ignore) - * @param width storage space for the VDPAU video surface pixel width - * (or NULL to ignore) - * @param height storage space for the VDPAU video surface pixel height - * (or NULL to ignore) - * - * @return 0 on success, a negative AVERROR code on failure. - */ -int av_vdpau_get_surface_parameters(AVCodecContext *avctx, VdpChromaType *type, - uint32_t *width, uint32_t *height); - -/** - * Allocate an AVVDPAUContext. - * - * @return Newly-allocated AVVDPAUContext or NULL on failure. - */ -AVVDPAUContext *av_vdpau_alloc_context(void); - -/** @} */ - -#endif /* AVCODEC_VDPAU_H */ diff --git a/gostream/ffmpeg/include/libavcodec/version.h b/gostream/ffmpeg/include/libavcodec/version.h deleted file mode 100644 index 1cf96513914..00000000000 --- a/gostream/ffmpeg/include/libavcodec/version.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_VERSION_H -#define AVCODEC_VERSION_H - -/** - * @file - * @ingroup libavc - * Libavcodec version macros. - */ - -#include "libavutil/version.h" - -#include "version_major.h" - -#define LIBAVCODEC_VERSION_MINOR 31 -#define LIBAVCODEC_VERSION_MICRO 102 - -#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \ - LIBAVCODEC_VERSION_MINOR, \ - LIBAVCODEC_VERSION_MICRO) -#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \ - LIBAVCODEC_VERSION_MINOR, \ - LIBAVCODEC_VERSION_MICRO) -#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT - -#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION) - -#endif /* AVCODEC_VERSION_H */ diff --git a/gostream/ffmpeg/include/libavcodec/version_major.h b/gostream/ffmpeg/include/libavcodec/version_major.h deleted file mode 100644 index b9164fe5c62..00000000000 --- a/gostream/ffmpeg/include/libavcodec/version_major.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_VERSION_MAJOR_H -#define AVCODEC_VERSION_MAJOR_H - -/** - * @file - * @ingroup libavc - * Libavcodec version macros. - */ - -#define LIBAVCODEC_VERSION_MAJOR 60 - -/** - * FF_API_* defines may be placed below to indicate public API that will be - * dropped at a future version bump. The defines themselves are not part of - * the public API and may change, break or disappear at any time. - * - * @note, when bumping the major version it is recommended to manually - * disable each FF_API_* in its own commit instead of disabling them all - * at once through the bump. This improves the git bisect-ability of the change. - */ - -#define FF_API_INIT_PACKET (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_IDCT_NONE (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_SVTAV1_OPTS (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_AYUV_CODECID (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_VT_OUTPUT_CALLBACK (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_AVCODEC_CHROMA_POS (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_VT_HWACCEL_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_AVCTX_FRAME_NUMBER (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_SLICE_OFFSET (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_SUBFRAMES (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_TICKS_PER_FRAME (LIBAVCODEC_VERSION_MAJOR < 61) -#define FF_API_DROPCHANGED (LIBAVCODEC_VERSION_MAJOR < 61) - -#define FF_API_AVFFT (LIBAVCODEC_VERSION_MAJOR < 62) -#define FF_API_FF_PROFILE_LEVEL (LIBAVCODEC_VERSION_MAJOR < 62) - -// reminder to remove CrystalHD decoders on next major bump -#define FF_CODEC_CRYSTAL_HD (LIBAVCODEC_VERSION_MAJOR < 61) - -#endif /* AVCODEC_VERSION_MAJOR_H */ diff --git a/gostream/ffmpeg/include/libavcodec/videotoolbox.h b/gostream/ffmpeg/include/libavcodec/videotoolbox.h deleted file mode 100644 index ba5eddbf460..00000000000 --- a/gostream/ffmpeg/include/libavcodec/videotoolbox.h +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Videotoolbox hardware acceleration - * - * copyright (c) 2012 Sebastien Zwickert - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_VIDEOTOOLBOX_H -#define AVCODEC_VIDEOTOOLBOX_H - -/** - * @file - * @ingroup lavc_codec_hwaccel_videotoolbox - * Public libavcodec Videotoolbox header. - */ - -/** - * @defgroup lavc_codec_hwaccel_videotoolbox VideoToolbox Decoder - * @ingroup lavc_codec_hwaccel - * - * Hardware accelerated decoding using VideoToolbox on Apple Platforms - * - * @{ - */ - -#include - -#define Picture QuickdrawPicture -#include -#undef Picture - -#include "libavcodec/avcodec.h" - -#include "libavutil/attributes.h" - -/** - * This struct holds all the information that needs to be passed - * between the caller and libavcodec for initializing Videotoolbox decoding. - * Its size is not a part of the public ABI, it must be allocated with - * av_videotoolbox_alloc_context() and freed with av_free(). - */ -typedef struct AVVideotoolboxContext { - /** - * Videotoolbox decompression session object. - */ - VTDecompressionSessionRef session; - -#if FF_API_VT_OUTPUT_CALLBACK - /** - * The output callback that must be passed to the session. - * Set by av_videottoolbox_default_init() - */ - attribute_deprecated - VTDecompressionOutputCallback output_callback; -#endif - - /** - * CVPixelBuffer Format Type that Videotoolbox will use for decoded frames. - * set by the caller. If this is set to 0, then no specific format is - * requested from the decoder, and its native format is output. - */ - OSType cv_pix_fmt_type; - - /** - * CoreMedia Format Description that Videotoolbox will use to create the decompression session. - */ - CMVideoFormatDescriptionRef cm_fmt_desc; - - /** - * CoreMedia codec type that Videotoolbox will use to create the decompression session. - */ - int cm_codec_type; -} AVVideotoolboxContext; - -#if FF_API_VT_HWACCEL_CONTEXT - -/** - * Allocate and initialize a Videotoolbox context. - * - * This function should be called from the get_format() callback when the caller - * selects the AV_PIX_FMT_VIDETOOLBOX format. The caller must then create - * the decoder object (using the output callback provided by libavcodec) that - * will be used for Videotoolbox-accelerated decoding. - * - * When decoding with Videotoolbox is finished, the caller must destroy the decoder - * object and free the Videotoolbox context using av_free(). - * - * @return the newly allocated context or NULL on failure - * @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead. - */ -attribute_deprecated -AVVideotoolboxContext *av_videotoolbox_alloc_context(void); - -/** - * This is a convenience function that creates and sets up the Videotoolbox context using - * an internal implementation. - * - * @param avctx the corresponding codec context - * - * @return >= 0 on success, a negative AVERROR code on failure - * @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead. - */ -attribute_deprecated -int av_videotoolbox_default_init(AVCodecContext *avctx); - -/** - * This is a convenience function that creates and sets up the Videotoolbox context using - * an internal implementation. - * - * @param avctx the corresponding codec context - * @param vtctx the Videotoolbox context to use - * - * @return >= 0 on success, a negative AVERROR code on failure - * @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead. - */ -attribute_deprecated -int av_videotoolbox_default_init2(AVCodecContext *avctx, AVVideotoolboxContext *vtctx); - -/** - * This function must be called to free the Videotoolbox context initialized with - * av_videotoolbox_default_init(). - * - * @param avctx the corresponding codec context - * @deprecated Use AVCodecContext.hw_frames_ctx or hw_device_ctx instead. - */ -attribute_deprecated -void av_videotoolbox_default_free(AVCodecContext *avctx); - -#endif /* FF_API_VT_HWACCEL_CONTEXT */ - -/** - * @} - */ - -#endif /* AVCODEC_VIDEOTOOLBOX_H */ diff --git a/gostream/ffmpeg/include/libavcodec/vorbis_parser.h b/gostream/ffmpeg/include/libavcodec/vorbis_parser.h deleted file mode 100644 index 789932ac492..00000000000 --- a/gostream/ffmpeg/include/libavcodec/vorbis_parser.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * A public API for Vorbis parsing - * - * Determines the duration for each packet. - */ - -#ifndef AVCODEC_VORBIS_PARSER_H -#define AVCODEC_VORBIS_PARSER_H - -#include - -typedef struct AVVorbisParseContext AVVorbisParseContext; - -/** - * Allocate and initialize the Vorbis parser using headers in the extradata. - */ -AVVorbisParseContext *av_vorbis_parse_init(const uint8_t *extradata, - int extradata_size); - -/** - * Free the parser and everything associated with it. - */ -void av_vorbis_parse_free(AVVorbisParseContext **s); - -#define VORBIS_FLAG_HEADER 0x00000001 -#define VORBIS_FLAG_COMMENT 0x00000002 -#define VORBIS_FLAG_SETUP 0x00000004 - -/** - * Get the duration for a Vorbis packet. - * - * If @p flags is @c NULL, - * special frames are considered invalid. - * - * @param s Vorbis parser context - * @param buf buffer containing a Vorbis frame - * @param buf_size size of the buffer - * @param flags flags for special frames - */ -int av_vorbis_parse_frame_flags(AVVorbisParseContext *s, const uint8_t *buf, - int buf_size, int *flags); - -/** - * Get the duration for a Vorbis packet. - * - * @param s Vorbis parser context - * @param buf buffer containing a Vorbis frame - * @param buf_size size of the buffer - */ -int av_vorbis_parse_frame(AVVorbisParseContext *s, const uint8_t *buf, - int buf_size); - -void av_vorbis_parse_reset(AVVorbisParseContext *s); - -#endif /* AVCODEC_VORBIS_PARSER_H */ diff --git a/gostream/ffmpeg/include/libavcodec/xvmc.h b/gostream/ffmpeg/include/libavcodec/xvmc.h deleted file mode 100644 index 52e70c0d77e..00000000000 --- a/gostream/ffmpeg/include/libavcodec/xvmc.h +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2003 Ivan Kalvachev - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVCODEC_XVMC_H -#define AVCODEC_XVMC_H - -/** - * @file - * @ingroup lavc_codec_hwaccel_xvmc - * Public libavcodec XvMC header. - */ - -#pragma message("XvMC is no longer supported; this header is deprecated and will be removed") - -#include - -#include "libavutil/attributes.h" -#include "avcodec.h" - -/** - * @defgroup lavc_codec_hwaccel_xvmc XvMC - * @ingroup lavc_codec_hwaccel - * - * @{ - */ - -#define AV_XVMC_ID 0x1DC711C0 /**< special value to ensure that regular pixel routines haven't corrupted the struct - the number is 1337 speak for the letters IDCT MCo (motion compensation) */ - -struct attribute_deprecated xvmc_pix_fmt { - /** The field contains the special constant value AV_XVMC_ID. - It is used as a test that the application correctly uses the API, - and that there is no corruption caused by pixel routines. - - application - set during initialization - - libavcodec - unchanged - */ - int xvmc_id; - - /** Pointer to the block array allocated by XvMCCreateBlocks(). - The array has to be freed by XvMCDestroyBlocks(). - Each group of 64 values represents one data block of differential - pixel information (in MoCo mode) or coefficients for IDCT. - - application - set the pointer during initialization - - libavcodec - fills coefficients/pixel data into the array - */ - short* data_blocks; - - /** Pointer to the macroblock description array allocated by - XvMCCreateMacroBlocks() and freed by XvMCDestroyMacroBlocks(). - - application - set the pointer during initialization - - libavcodec - fills description data into the array - */ - XvMCMacroBlock* mv_blocks; - - /** Number of macroblock descriptions that can be stored in the mv_blocks - array. - - application - set during initialization - - libavcodec - unchanged - */ - int allocated_mv_blocks; - - /** Number of blocks that can be stored at once in the data_blocks array. - - application - set during initialization - - libavcodec - unchanged - */ - int allocated_data_blocks; - - /** Indicate that the hardware would interpret data_blocks as IDCT - coefficients and perform IDCT on them. - - application - set during initialization - - libavcodec - unchanged - */ - int idct; - - /** In MoCo mode it indicates that intra macroblocks are assumed to be in - unsigned format; same as the XVMC_INTRA_UNSIGNED flag. - - application - set during initialization - - libavcodec - unchanged - */ - int unsigned_intra; - - /** Pointer to the surface allocated by XvMCCreateSurface(). - It has to be freed by XvMCDestroySurface() on application exit. - It identifies the frame and its state on the video hardware. - - application - set during initialization - - libavcodec - unchanged - */ - XvMCSurface* p_surface; - -/** Set by the decoder before calling ff_draw_horiz_band(), - needed by the XvMCRenderSurface function. */ -//@{ - /** Pointer to the surface used as past reference - - application - unchanged - - libavcodec - set - */ - XvMCSurface* p_past_surface; - - /** Pointer to the surface used as future reference - - application - unchanged - - libavcodec - set - */ - XvMCSurface* p_future_surface; - - /** top/bottom field or frame - - application - unchanged - - libavcodec - set - */ - unsigned int picture_structure; - - /** XVMC_SECOND_FIELD - 1st or 2nd field in the sequence - - application - unchanged - - libavcodec - set - */ - unsigned int flags; -//}@ - - /** Number of macroblock descriptions in the mv_blocks array - that have already been passed to the hardware. - - application - zeroes it on get_buffer(). - A successful ff_draw_horiz_band() may increment it - with filled_mb_block_num or zero both. - - libavcodec - unchanged - */ - int start_mv_blocks_num; - - /** Number of new macroblock descriptions in the mv_blocks array (after - start_mv_blocks_num) that are filled by libavcodec and have to be - passed to the hardware. - - application - zeroes it on get_buffer() or after successful - ff_draw_horiz_band(). - - libavcodec - increment with one of each stored MB - */ - int filled_mv_blocks_num; - - /** Number of the next free data block; one data block consists of - 64 short values in the data_blocks array. - All blocks before this one have already been claimed by placing their - position into the corresponding block description structure field, - that are part of the mv_blocks array. - - application - zeroes it on get_buffer(). - A successful ff_draw_horiz_band() may zero it together - with start_mb_blocks_num. - - libavcodec - each decoded macroblock increases it by the number - of coded blocks it contains. - */ - int next_free_data_block_num; -}; - -/** - * @} - */ - -#endif /* AVCODEC_XVMC_H */ diff --git a/gostream/ffmpeg/include/libavdevice/avdevice.h b/gostream/ffmpeg/include/libavdevice/avdevice.h deleted file mode 100644 index 887fd5e3c80..00000000000 --- a/gostream/ffmpeg/include/libavdevice/avdevice.h +++ /dev/null @@ -1,397 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVDEVICE_AVDEVICE_H -#define AVDEVICE_AVDEVICE_H - -#include "version_major.h" -#ifndef HAVE_AV_CONFIG_H -/* When included as part of the ffmpeg build, only include the major version - * to avoid unnecessary rebuilds. When included externally, keep including - * the full version information. */ -#include "version.h" -#endif - -/** - * @file - * @ingroup lavd - * Main libavdevice API header - */ - -/** - * @defgroup lavd libavdevice - * Special devices muxing/demuxing library. - * - * Libavdevice is a complementary library to @ref libavf "libavformat". It - * provides various "special" platform-specific muxers and demuxers, e.g. for - * grabbing devices, audio capture and playback etc. As a consequence, the - * (de)muxers in libavdevice are of the AVFMT_NOFILE type (they use their own - * I/O functions). The filename passed to avformat_open_input() often does not - * refer to an actually existing file, but has some special device-specific - * meaning - e.g. for xcbgrab it is the display name. - * - * To use libavdevice, simply call avdevice_register_all() to register all - * compiled muxers and demuxers. They all use standard libavformat API. - * - * @{ - */ - -#include "libavutil/log.h" -#include "libavutil/opt.h" -#include "libavutil/dict.h" -#include "libavformat/avformat.h" - -/** - * Return the LIBAVDEVICE_VERSION_INT constant. - */ -unsigned avdevice_version(void); - -/** - * Return the libavdevice build-time configuration. - */ -const char *avdevice_configuration(void); - -/** - * Return the libavdevice license. - */ -const char *avdevice_license(void); - -/** - * Initialize libavdevice and register all the input and output devices. - */ -void avdevice_register_all(void); - -/** - * Audio input devices iterator. - * - * If d is NULL, returns the first registered input audio/video device, - * if d is non-NULL, returns the next registered input audio/video device after d - * or NULL if d is the last one. - */ -const AVInputFormat *av_input_audio_device_next(const AVInputFormat *d); - -/** - * Video input devices iterator. - * - * If d is NULL, returns the first registered input audio/video device, - * if d is non-NULL, returns the next registered input audio/video device after d - * or NULL if d is the last one. - */ -const AVInputFormat *av_input_video_device_next(const AVInputFormat *d); - -/** - * Audio output devices iterator. - * - * If d is NULL, returns the first registered output audio/video device, - * if d is non-NULL, returns the next registered output audio/video device after d - * or NULL if d is the last one. - */ -const AVOutputFormat *av_output_audio_device_next(const AVOutputFormat *d); - -/** - * Video output devices iterator. - * - * If d is NULL, returns the first registered output audio/video device, - * if d is non-NULL, returns the next registered output audio/video device after d - * or NULL if d is the last one. - */ -const AVOutputFormat *av_output_video_device_next(const AVOutputFormat *d); - -typedef struct AVDeviceRect { - int x; /**< x coordinate of top left corner */ - int y; /**< y coordinate of top left corner */ - int width; /**< width */ - int height; /**< height */ -} AVDeviceRect; - -/** - * Message types used by avdevice_app_to_dev_control_message(). - */ -enum AVAppToDevMessageType { - /** - * Dummy message. - */ - AV_APP_TO_DEV_NONE = MKBETAG('N','O','N','E'), - - /** - * Window size change message. - * - * Message is sent to the device every time the application changes the size - * of the window device renders to. - * Message should also be sent right after window is created. - * - * data: AVDeviceRect: new window size. - */ - AV_APP_TO_DEV_WINDOW_SIZE = MKBETAG('G','E','O','M'), - - /** - * Repaint request message. - * - * Message is sent to the device when window has to be repainted. - * - * data: AVDeviceRect: area required to be repainted. - * NULL: whole area is required to be repainted. - */ - AV_APP_TO_DEV_WINDOW_REPAINT = MKBETAG('R','E','P','A'), - - /** - * Request pause/play. - * - * Application requests pause/unpause playback. - * Mostly usable with devices that have internal buffer. - * By default devices are not paused. - * - * data: NULL - */ - AV_APP_TO_DEV_PAUSE = MKBETAG('P', 'A', 'U', ' '), - AV_APP_TO_DEV_PLAY = MKBETAG('P', 'L', 'A', 'Y'), - AV_APP_TO_DEV_TOGGLE_PAUSE = MKBETAG('P', 'A', 'U', 'T'), - - /** - * Volume control message. - * - * Set volume level. It may be device-dependent if volume - * is changed per stream or system wide. Per stream volume - * change is expected when possible. - * - * data: double: new volume with range of 0.0 - 1.0. - */ - AV_APP_TO_DEV_SET_VOLUME = MKBETAG('S', 'V', 'O', 'L'), - - /** - * Mute control messages. - * - * Change mute state. It may be device-dependent if mute status - * is changed per stream or system wide. Per stream mute status - * change is expected when possible. - * - * data: NULL. - */ - AV_APP_TO_DEV_MUTE = MKBETAG(' ', 'M', 'U', 'T'), - AV_APP_TO_DEV_UNMUTE = MKBETAG('U', 'M', 'U', 'T'), - AV_APP_TO_DEV_TOGGLE_MUTE = MKBETAG('T', 'M', 'U', 'T'), - - /** - * Get volume/mute messages. - * - * Force the device to send AV_DEV_TO_APP_VOLUME_LEVEL_CHANGED or - * AV_DEV_TO_APP_MUTE_STATE_CHANGED command respectively. - * - * data: NULL. - */ - AV_APP_TO_DEV_GET_VOLUME = MKBETAG('G', 'V', 'O', 'L'), - AV_APP_TO_DEV_GET_MUTE = MKBETAG('G', 'M', 'U', 'T'), -}; - -/** - * Message types used by avdevice_dev_to_app_control_message(). - */ -enum AVDevToAppMessageType { - /** - * Dummy message. - */ - AV_DEV_TO_APP_NONE = MKBETAG('N','O','N','E'), - - /** - * Create window buffer message. - * - * Device requests to create a window buffer. Exact meaning is device- - * and application-dependent. Message is sent before rendering first - * frame and all one-shot initializations should be done here. - * Application is allowed to ignore preferred window buffer size. - * - * @note: Application is obligated to inform about window buffer size - * with AV_APP_TO_DEV_WINDOW_SIZE message. - * - * data: AVDeviceRect: preferred size of the window buffer. - * NULL: no preferred size of the window buffer. - */ - AV_DEV_TO_APP_CREATE_WINDOW_BUFFER = MKBETAG('B','C','R','E'), - - /** - * Prepare window buffer message. - * - * Device requests to prepare a window buffer for rendering. - * Exact meaning is device- and application-dependent. - * Message is sent before rendering of each frame. - * - * data: NULL. - */ - AV_DEV_TO_APP_PREPARE_WINDOW_BUFFER = MKBETAG('B','P','R','E'), - - /** - * Display window buffer message. - * - * Device requests to display a window buffer. - * Message is sent when new frame is ready to be displayed. - * Usually buffers need to be swapped in handler of this message. - * - * data: NULL. - */ - AV_DEV_TO_APP_DISPLAY_WINDOW_BUFFER = MKBETAG('B','D','I','S'), - - /** - * Destroy window buffer message. - * - * Device requests to destroy a window buffer. - * Message is sent when device is about to be destroyed and window - * buffer is not required anymore. - * - * data: NULL. - */ - AV_DEV_TO_APP_DESTROY_WINDOW_BUFFER = MKBETAG('B','D','E','S'), - - /** - * Buffer fullness status messages. - * - * Device signals buffer overflow/underflow. - * - * data: NULL. - */ - AV_DEV_TO_APP_BUFFER_OVERFLOW = MKBETAG('B','O','F','L'), - AV_DEV_TO_APP_BUFFER_UNDERFLOW = MKBETAG('B','U','F','L'), - - /** - * Buffer readable/writable. - * - * Device informs that buffer is readable/writable. - * When possible, device informs how many bytes can be read/write. - * - * @warning Device may not inform when number of bytes than can be read/write changes. - * - * data: int64_t: amount of bytes available to read/write. - * NULL: amount of bytes available to read/write is not known. - */ - AV_DEV_TO_APP_BUFFER_READABLE = MKBETAG('B','R','D',' '), - AV_DEV_TO_APP_BUFFER_WRITABLE = MKBETAG('B','W','R',' '), - - /** - * Mute state change message. - * - * Device informs that mute state has changed. - * - * data: int: 0 for not muted state, non-zero for muted state. - */ - AV_DEV_TO_APP_MUTE_STATE_CHANGED = MKBETAG('C','M','U','T'), - - /** - * Volume level change message. - * - * Device informs that volume level has changed. - * - * data: double: new volume with range of 0.0 - 1.0. - */ - AV_DEV_TO_APP_VOLUME_LEVEL_CHANGED = MKBETAG('C','V','O','L'), -}; - -/** - * Send control message from application to device. - * - * @param s device context. - * @param type message type. - * @param data message data. Exact type depends on message type. - * @param data_size size of message data. - * @return >= 0 on success, negative on error. - * AVERROR(ENOSYS) when device doesn't implement handler of the message. - */ -int avdevice_app_to_dev_control_message(struct AVFormatContext *s, - enum AVAppToDevMessageType type, - void *data, size_t data_size); - -/** - * Send control message from device to application. - * - * @param s device context. - * @param type message type. - * @param data message data. Can be NULL. - * @param data_size size of message data. - * @return >= 0 on success, negative on error. - * AVERROR(ENOSYS) when application doesn't implement handler of the message. - */ -int avdevice_dev_to_app_control_message(struct AVFormatContext *s, - enum AVDevToAppMessageType type, - void *data, size_t data_size); - -/** - * Structure describes basic parameters of the device. - */ -typedef struct AVDeviceInfo { - char *device_name; /**< device name, format depends on device */ - char *device_description; /**< human friendly name */ - enum AVMediaType *media_types; /**< array indicating what media types(s), if any, a device can provide. If null, cannot provide any */ - int nb_media_types; /**< length of media_types array, 0 if device cannot provide any media types */ -} AVDeviceInfo; - -/** - * List of devices. - */ -typedef struct AVDeviceInfoList { - AVDeviceInfo **devices; /**< list of autodetected devices */ - int nb_devices; /**< number of autodetected devices */ - int default_device; /**< index of default device or -1 if no default */ -} AVDeviceInfoList; - -/** - * List devices. - * - * Returns available device names and their parameters. - * - * @note: Some devices may accept system-dependent device names that cannot be - * autodetected. The list returned by this function cannot be assumed to - * be always completed. - * - * @param s device context. - * @param[out] device_list list of autodetected devices. - * @return count of autodetected devices, negative on error. - */ -int avdevice_list_devices(struct AVFormatContext *s, AVDeviceInfoList **device_list); - -/** - * Convenient function to free result of avdevice_list_devices(). - * - * @param device_list device list to be freed. - */ -void avdevice_free_list_devices(AVDeviceInfoList **device_list); - -/** - * List devices. - * - * Returns available device names and their parameters. - * These are convinient wrappers for avdevice_list_devices(). - * Device context is allocated and deallocated internally. - * - * @param device device format. May be NULL if device name is set. - * @param device_name device name. May be NULL if device format is set. - * @param device_options An AVDictionary filled with device-private options. May be NULL. - * The same options must be passed later to avformat_write_header() for output - * devices or avformat_open_input() for input devices, or at any other place - * that affects device-private options. - * @param[out] device_list list of autodetected devices - * @return count of autodetected devices, negative on error. - * @note device argument takes precedence over device_name when both are set. - */ -int avdevice_list_input_sources(const AVInputFormat *device, const char *device_name, - AVDictionary *device_options, AVDeviceInfoList **device_list); -int avdevice_list_output_sinks(const AVOutputFormat *device, const char *device_name, - AVDictionary *device_options, AVDeviceInfoList **device_list); - -/** - * @} - */ - -#endif /* AVDEVICE_AVDEVICE_H */ diff --git a/gostream/ffmpeg/include/libavdevice/version.h b/gostream/ffmpeg/include/libavdevice/version.h deleted file mode 100644 index 7608a8602c2..00000000000 --- a/gostream/ffmpeg/include/libavdevice/version.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVDEVICE_VERSION_H -#define AVDEVICE_VERSION_H - -/** - * @file - * @ingroup lavd - * Libavdevice version macros - */ - -#include "libavutil/version.h" - -#include "version_major.h" - -#define LIBAVDEVICE_VERSION_MINOR 3 -#define LIBAVDEVICE_VERSION_MICRO 100 - -#define LIBAVDEVICE_VERSION_INT AV_VERSION_INT(LIBAVDEVICE_VERSION_MAJOR, \ - LIBAVDEVICE_VERSION_MINOR, \ - LIBAVDEVICE_VERSION_MICRO) -#define LIBAVDEVICE_VERSION AV_VERSION(LIBAVDEVICE_VERSION_MAJOR, \ - LIBAVDEVICE_VERSION_MINOR, \ - LIBAVDEVICE_VERSION_MICRO) -#define LIBAVDEVICE_BUILD LIBAVDEVICE_VERSION_INT - -#define LIBAVDEVICE_IDENT "Lavd" AV_STRINGIFY(LIBAVDEVICE_VERSION) - -#endif /* AVDEVICE_VERSION_H */ diff --git a/gostream/ffmpeg/include/libavdevice/version_major.h b/gostream/ffmpeg/include/libavdevice/version_major.h deleted file mode 100644 index b884fd42246..00000000000 --- a/gostream/ffmpeg/include/libavdevice/version_major.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVDEVICE_VERSION_MAJOR_H -#define AVDEVICE_VERSION_MAJOR_H - -/** - * @file - * @ingroup lavd - * Libavdevice version macros - */ - -#define LIBAVDEVICE_VERSION_MAJOR 60 - -/** - * FF_API_* defines may be placed below to indicate public API that will be - * dropped at a future version bump. The defines themselves are not part of - * the public API and may change, break or disappear at any time. - */ - -#endif /* AVDEVICE_VERSION_MAJOR_H */ diff --git a/gostream/ffmpeg/include/libavfilter/avfilter.h b/gostream/ffmpeg/include/libavfilter/avfilter.h deleted file mode 100644 index d69381aed4f..00000000000 --- a/gostream/ffmpeg/include/libavfilter/avfilter.h +++ /dev/null @@ -1,1500 +0,0 @@ -/* - * filter layer - * Copyright (c) 2007 Bobby Bingham - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFILTER_AVFILTER_H -#define AVFILTER_AVFILTER_H - -/** - * @file - * @ingroup lavfi - * Main libavfilter public API header - */ - -/** - * @defgroup lavfi libavfilter - * Graph-based frame editing library. - * - * @{ - */ - -#include - -#include "libavutil/attributes.h" -#include "libavutil/avutil.h" -#include "libavutil/buffer.h" -#include "libavutil/dict.h" -#include "libavutil/frame.h" -#include "libavutil/log.h" -#include "libavutil/samplefmt.h" -#include "libavutil/pixfmt.h" -#include "libavutil/rational.h" - -#include "libavfilter/version_major.h" -#ifndef HAVE_AV_CONFIG_H -/* When included as part of the ffmpeg build, only include the major version - * to avoid unnecessary rebuilds. When included externally, keep including - * the full version information. */ -#include "libavfilter/version.h" -#endif - -/** - * Return the LIBAVFILTER_VERSION_INT constant. - */ -unsigned avfilter_version(void); - -/** - * Return the libavfilter build-time configuration. - */ -const char *avfilter_configuration(void); - -/** - * Return the libavfilter license. - */ -const char *avfilter_license(void); - -typedef struct AVFilterContext AVFilterContext; -typedef struct AVFilterLink AVFilterLink; -typedef struct AVFilterPad AVFilterPad; -typedef struct AVFilterFormats AVFilterFormats; -typedef struct AVFilterChannelLayouts AVFilterChannelLayouts; - -/** - * Get the name of an AVFilterPad. - * - * @param pads an array of AVFilterPads - * @param pad_idx index of the pad in the array; it is the caller's - * responsibility to ensure the index is valid - * - * @return name of the pad_idx'th pad in pads - */ -const char *avfilter_pad_get_name(const AVFilterPad *pads, int pad_idx); - -/** - * Get the type of an AVFilterPad. - * - * @param pads an array of AVFilterPads - * @param pad_idx index of the pad in the array; it is the caller's - * responsibility to ensure the index is valid - * - * @return type of the pad_idx'th pad in pads - */ -enum AVMediaType avfilter_pad_get_type(const AVFilterPad *pads, int pad_idx); - -/** - * The number of the filter inputs is not determined just by AVFilter.inputs. - * The filter might add additional inputs during initialization depending on the - * options supplied to it. - */ -#define AVFILTER_FLAG_DYNAMIC_INPUTS (1 << 0) -/** - * The number of the filter outputs is not determined just by AVFilter.outputs. - * The filter might add additional outputs during initialization depending on - * the options supplied to it. - */ -#define AVFILTER_FLAG_DYNAMIC_OUTPUTS (1 << 1) -/** - * The filter supports multithreading by splitting frames into multiple parts - * and processing them concurrently. - */ -#define AVFILTER_FLAG_SLICE_THREADS (1 << 2) -/** - * The filter is a "metadata" filter - it does not modify the frame data in any - * way. It may only affect the metadata (i.e. those fields copied by - * av_frame_copy_props()). - * - * More precisely, this means: - * - video: the data of any frame output by the filter must be exactly equal to - * some frame that is received on one of its inputs. Furthermore, all frames - * produced on a given output must correspond to frames received on the same - * input and their order must be unchanged. Note that the filter may still - * drop or duplicate the frames. - * - audio: the data produced by the filter on any of its outputs (viewed e.g. - * as an array of interleaved samples) must be exactly equal to the data - * received by the filter on one of its inputs. - */ -#define AVFILTER_FLAG_METADATA_ONLY (1 << 3) - -/** - * The filter can create hardware frames using AVFilterContext.hw_device_ctx. - */ -#define AVFILTER_FLAG_HWDEVICE (1 << 4) -/** - * Some filters support a generic "enable" expression option that can be used - * to enable or disable a filter in the timeline. Filters supporting this - * option have this flag set. When the enable expression is false, the default - * no-op filter_frame() function is called in place of the filter_frame() - * callback defined on each input pad, thus the frame is passed unchanged to - * the next filters. - */ -#define AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC (1 << 16) -/** - * Same as AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC, except that the filter will - * have its filter_frame() callback(s) called as usual even when the enable - * expression is false. The filter will disable filtering within the - * filter_frame() callback(s) itself, for example executing code depending on - * the AVFilterContext->is_disabled value. - */ -#define AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL (1 << 17) -/** - * Handy mask to test whether the filter supports or no the timeline feature - * (internally or generically). - */ -#define AVFILTER_FLAG_SUPPORT_TIMELINE (AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC | AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL) - -/** - * Filter definition. This defines the pads a filter contains, and all the - * callback functions used to interact with the filter. - */ -typedef struct AVFilter { - /** - * Filter name. Must be non-NULL and unique among filters. - */ - const char *name; - - /** - * A description of the filter. May be NULL. - * - * You should use the NULL_IF_CONFIG_SMALL() macro to define it. - */ - const char *description; - - /** - * List of static inputs. - * - * NULL if there are no (static) inputs. Instances of filters with - * AVFILTER_FLAG_DYNAMIC_INPUTS set may have more inputs than present in - * this list. - */ - const AVFilterPad *inputs; - - /** - * List of static outputs. - * - * NULL if there are no (static) outputs. Instances of filters with - * AVFILTER_FLAG_DYNAMIC_OUTPUTS set may have more outputs than present in - * this list. - */ - const AVFilterPad *outputs; - - /** - * A class for the private data, used to declare filter private AVOptions. - * This field is NULL for filters that do not declare any options. - * - * If this field is non-NULL, the first member of the filter private data - * must be a pointer to AVClass, which will be set by libavfilter generic - * code to this class. - */ - const AVClass *priv_class; - - /** - * A combination of AVFILTER_FLAG_* - */ - int flags; - - /***************************************************************** - * All fields below this line are not part of the public API. They - * may not be used outside of libavfilter and can be changed and - * removed at will. - * New public fields should be added right above. - ***************************************************************** - */ - - /** - * The number of entries in the list of inputs. - */ - uint8_t nb_inputs; - - /** - * The number of entries in the list of outputs. - */ - uint8_t nb_outputs; - - /** - * This field determines the state of the formats union. - * It is an enum FilterFormatsState value. - */ - uint8_t formats_state; - - /** - * Filter pre-initialization function - * - * This callback will be called immediately after the filter context is - * allocated, to allow allocating and initing sub-objects. - * - * If this callback is not NULL, the uninit callback will be called on - * allocation failure. - * - * @return 0 on success, - * AVERROR code on failure (but the code will be - * dropped and treated as ENOMEM by the calling code) - */ - int (*preinit)(AVFilterContext *ctx); - - /** - * Filter initialization function. - * - * This callback will be called only once during the filter lifetime, after - * all the options have been set, but before links between filters are - * established and format negotiation is done. - * - * Basic filter initialization should be done here. Filters with dynamic - * inputs and/or outputs should create those inputs/outputs here based on - * provided options. No more changes to this filter's inputs/outputs can be - * done after this callback. - * - * This callback must not assume that the filter links exist or frame - * parameters are known. - * - * @ref AVFilter.uninit "uninit" is guaranteed to be called even if - * initialization fails, so this callback does not have to clean up on - * failure. - * - * @return 0 on success, a negative AVERROR on failure - */ - int (*init)(AVFilterContext *ctx); - - /** - * Filter uninitialization function. - * - * Called only once right before the filter is freed. Should deallocate any - * memory held by the filter, release any buffer references, etc. It does - * not need to deallocate the AVFilterContext.priv memory itself. - * - * This callback may be called even if @ref AVFilter.init "init" was not - * called or failed, so it must be prepared to handle such a situation. - */ - void (*uninit)(AVFilterContext *ctx); - - /** - * The state of the following union is determined by formats_state. - * See the documentation of enum FilterFormatsState in internal.h. - */ - union { - /** - * Query formats supported by the filter on its inputs and outputs. - * - * This callback is called after the filter is initialized (so the inputs - * and outputs are fixed), shortly before the format negotiation. This - * callback may be called more than once. - * - * This callback must set ::AVFilterLink's - * @ref AVFilterFormatsConfig.formats "outcfg.formats" - * on every input link and - * @ref AVFilterFormatsConfig.formats "incfg.formats" - * on every output link to a list of pixel/sample formats that the filter - * supports on that link. - * For audio links, this filter must also set - * @ref AVFilterFormatsConfig.samplerates "incfg.samplerates" - * / - * @ref AVFilterFormatsConfig.samplerates "outcfg.samplerates" - * and @ref AVFilterFormatsConfig.channel_layouts "incfg.channel_layouts" - * / - * @ref AVFilterFormatsConfig.channel_layouts "outcfg.channel_layouts" - * analogously. - * - * This callback must never be NULL if the union is in this state. - * - * @return zero on success, a negative value corresponding to an - * AVERROR code otherwise - */ - int (*query_func)(AVFilterContext *); - /** - * A pointer to an array of admissible pixel formats delimited - * by AV_PIX_FMT_NONE. The generic code will use this list - * to indicate that this filter supports each of these pixel formats, - * provided that all inputs and outputs use the same pixel format. - * - * This list must never be NULL if the union is in this state. - * The type of all inputs and outputs of filters using this must - * be AVMEDIA_TYPE_VIDEO. - */ - const enum AVPixelFormat *pixels_list; - /** - * Analogous to pixels, but delimited by AV_SAMPLE_FMT_NONE - * and restricted to filters that only have AVMEDIA_TYPE_AUDIO - * inputs and outputs. - * - * In addition to that the generic code will mark all inputs - * and all outputs as supporting all sample rates and every - * channel count and channel layout, as long as all inputs - * and outputs use the same sample rate and channel count/layout. - */ - const enum AVSampleFormat *samples_list; - /** - * Equivalent to { pix_fmt, AV_PIX_FMT_NONE } as pixels_list. - */ - enum AVPixelFormat pix_fmt; - /** - * Equivalent to { sample_fmt, AV_SAMPLE_FMT_NONE } as samples_list. - */ - enum AVSampleFormat sample_fmt; - } formats; - - int priv_size; ///< size of private data to allocate for the filter - - int flags_internal; ///< Additional flags for avfilter internal use only. - - /** - * Make the filter instance process a command. - * - * @param cmd the command to process, for handling simplicity all commands must be alphanumeric only - * @param arg the argument for the command - * @param res a buffer with size res_size where the filter(s) can return a response. This must not change when the command is not supported. - * @param flags if AVFILTER_CMD_FLAG_FAST is set and the command would be - * time consuming then a filter should treat it like an unsupported command - * - * @returns >=0 on success otherwise an error code. - * AVERROR(ENOSYS) on unsupported commands - */ - int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags); - - /** - * Filter activation function. - * - * Called when any processing is needed from the filter, instead of any - * filter_frame and request_frame on pads. - * - * The function must examine inlinks and outlinks and perform a single - * step of processing. If there is nothing to do, the function must do - * nothing and not return an error. If more steps are or may be - * possible, it must use ff_filter_set_ready() to schedule another - * activation. - */ - int (*activate)(AVFilterContext *ctx); -} AVFilter; - -/** - * Get the number of elements in an AVFilter's inputs or outputs array. - */ -unsigned avfilter_filter_pad_count(const AVFilter *filter, int is_output); - -/** - * Process multiple parts of the frame concurrently. - */ -#define AVFILTER_THREAD_SLICE (1 << 0) - -typedef struct AVFilterInternal AVFilterInternal; - -/** An instance of a filter */ -struct AVFilterContext { - const AVClass *av_class; ///< needed for av_log() and filters common options - - const AVFilter *filter; ///< the AVFilter of which this is an instance - - char *name; ///< name of this filter instance - - AVFilterPad *input_pads; ///< array of input pads - AVFilterLink **inputs; ///< array of pointers to input links - unsigned nb_inputs; ///< number of input pads - - AVFilterPad *output_pads; ///< array of output pads - AVFilterLink **outputs; ///< array of pointers to output links - unsigned nb_outputs; ///< number of output pads - - void *priv; ///< private data for use by the filter - - struct AVFilterGraph *graph; ///< filtergraph this filter belongs to - - /** - * Type of multithreading being allowed/used. A combination of - * AVFILTER_THREAD_* flags. - * - * May be set by the caller before initializing the filter to forbid some - * or all kinds of multithreading for this filter. The default is allowing - * everything. - * - * When the filter is initialized, this field is combined using bit AND with - * AVFilterGraph.thread_type to get the final mask used for determining - * allowed threading types. I.e. a threading type needs to be set in both - * to be allowed. - * - * After the filter is initialized, libavfilter sets this field to the - * threading type that is actually used (0 for no multithreading). - */ - int thread_type; - - /** - * An opaque struct for libavfilter internal use. - */ - AVFilterInternal *internal; - - struct AVFilterCommand *command_queue; - - char *enable_str; ///< enable expression string - void *enable; ///< parsed expression (AVExpr*) - double *var_values; ///< variable values for the enable expression - int is_disabled; ///< the enabled state from the last expression evaluation - - /** - * For filters which will create hardware frames, sets the device the - * filter should create them in. All other filters will ignore this field: - * in particular, a filter which consumes or processes hardware frames will - * instead use the hw_frames_ctx field in AVFilterLink to carry the - * hardware context information. - * - * May be set by the caller on filters flagged with AVFILTER_FLAG_HWDEVICE - * before initializing the filter with avfilter_init_str() or - * avfilter_init_dict(). - */ - AVBufferRef *hw_device_ctx; - - /** - * Max number of threads allowed in this filter instance. - * If <= 0, its value is ignored. - * Overrides global number of threads set per filter graph. - */ - int nb_threads; - - /** - * Ready status of the filter. - * A non-0 value means that the filter needs activating; - * a higher value suggests a more urgent activation. - */ - unsigned ready; - - /** - * Sets the number of extra hardware frames which the filter will - * allocate on its output links for use in following filters or by - * the caller. - * - * Some hardware filters require all frames that they will use for - * output to be defined in advance before filtering starts. For such - * filters, any hardware frame pools used for output must therefore be - * of fixed size. The extra frames set here are on top of any number - * that the filter needs internally in order to operate normally. - * - * This field must be set before the graph containing this filter is - * configured. - */ - int extra_hw_frames; -}; - -/** - * Lists of formats / etc. supported by an end of a link. - * - * This structure is directly part of AVFilterLink, in two copies: - * one for the source filter, one for the destination filter. - - * These lists are used for negotiating the format to actually be used, - * which will be loaded into the format and channel_layout members of - * AVFilterLink, when chosen. - */ -typedef struct AVFilterFormatsConfig { - - /** - * List of supported formats (pixel or sample). - */ - AVFilterFormats *formats; - - /** - * Lists of supported sample rates, only for audio. - */ - AVFilterFormats *samplerates; - - /** - * Lists of supported channel layouts, only for audio. - */ - AVFilterChannelLayouts *channel_layouts; - -} AVFilterFormatsConfig; - -/** - * A link between two filters. This contains pointers to the source and - * destination filters between which this link exists, and the indexes of - * the pads involved. In addition, this link also contains the parameters - * which have been negotiated and agreed upon between the filter, such as - * image dimensions, format, etc. - * - * Applications must not normally access the link structure directly. - * Use the buffersrc and buffersink API instead. - * In the future, access to the header may be reserved for filters - * implementation. - */ -struct AVFilterLink { - AVFilterContext *src; ///< source filter - AVFilterPad *srcpad; ///< output pad on the source filter - - AVFilterContext *dst; ///< dest filter - AVFilterPad *dstpad; ///< input pad on the dest filter - - enum AVMediaType type; ///< filter media type - - /* These parameters apply only to video */ - int w; ///< agreed upon image width - int h; ///< agreed upon image height - AVRational sample_aspect_ratio; ///< agreed upon sample aspect ratio - /* These parameters apply only to audio */ -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * channel layout of current buffer (see libavutil/channel_layout.h) - * @deprecated use ch_layout - */ - attribute_deprecated - uint64_t channel_layout; -#endif - int sample_rate; ///< samples per second - - int format; ///< agreed upon media format - - /** - * Define the time base used by the PTS of the frames/samples - * which will pass through this link. - * During the configuration stage, each filter is supposed to - * change only the output timebase, while the timebase of the - * input link is assumed to be an unchangeable property. - */ - AVRational time_base; - - AVChannelLayout ch_layout; ///< channel layout of current buffer (see libavutil/channel_layout.h) - - /***************************************************************** - * All fields below this line are not part of the public API. They - * may not be used outside of libavfilter and can be changed and - * removed at will. - * New public fields should be added right above. - ***************************************************************** - */ - - /** - * Lists of supported formats / etc. supported by the input filter. - */ - AVFilterFormatsConfig incfg; - - /** - * Lists of supported formats / etc. supported by the output filter. - */ - AVFilterFormatsConfig outcfg; - - /** stage of the initialization of the link properties (dimensions, etc) */ - enum { - AVLINK_UNINIT = 0, ///< not started - AVLINK_STARTINIT, ///< started, but incomplete - AVLINK_INIT ///< complete - } init_state; - - /** - * Graph the filter belongs to. - */ - struct AVFilterGraph *graph; - - /** - * Current timestamp of the link, as defined by the most recent - * frame(s), in link time_base units. - */ - int64_t current_pts; - - /** - * Current timestamp of the link, as defined by the most recent - * frame(s), in AV_TIME_BASE units. - */ - int64_t current_pts_us; - - /** - * Index in the age array. - */ - int age_index; - - /** - * Frame rate of the stream on the link, or 1/0 if unknown or variable; - * if left to 0/0, will be automatically copied from the first input - * of the source filter if it exists. - * - * Sources should set it to the best estimation of the real frame rate. - * If the source frame rate is unknown or variable, set this to 1/0. - * Filters should update it if necessary depending on their function. - * Sinks can use it to set a default output frame rate. - * It is similar to the r_frame_rate field in AVStream. - */ - AVRational frame_rate; - - /** - * Minimum number of samples to filter at once. If filter_frame() is - * called with fewer samples, it will accumulate them in fifo. - * This field and the related ones must not be changed after filtering - * has started. - * If 0, all related fields are ignored. - */ - int min_samples; - - /** - * Maximum number of samples to filter at once. If filter_frame() is - * called with more samples, it will split them. - */ - int max_samples; - - /** - * Number of past frames sent through the link. - */ - int64_t frame_count_in, frame_count_out; - - /** - * Number of past samples sent through the link. - */ - int64_t sample_count_in, sample_count_out; - - /** - * A pointer to a FFFramePool struct. - */ - void *frame_pool; - - /** - * True if a frame is currently wanted on the output of this filter. - * Set when ff_request_frame() is called by the output, - * cleared when a frame is filtered. - */ - int frame_wanted_out; - - /** - * For hwaccel pixel formats, this should be a reference to the - * AVHWFramesContext describing the frames. - */ - AVBufferRef *hw_frames_ctx; - -#ifndef FF_INTERNAL_FIELDS - - /** - * Internal structure members. - * The fields below this limit are internal for libavfilter's use - * and must in no way be accessed by applications. - */ - char reserved[0xF000]; - -#else /* FF_INTERNAL_FIELDS */ - - /** - * Queue of frames waiting to be filtered. - */ - FFFrameQueue fifo; - - /** - * If set, the source filter can not generate a frame as is. - * The goal is to avoid repeatedly calling the request_frame() method on - * the same link. - */ - int frame_blocked_in; - - /** - * Link input status. - * If not zero, all attempts of filter_frame will fail with the - * corresponding code. - */ - int status_in; - - /** - * Timestamp of the input status change. - */ - int64_t status_in_pts; - - /** - * Link output status. - * If not zero, all attempts of request_frame will fail with the - * corresponding code. - */ - int status_out; - -#endif /* FF_INTERNAL_FIELDS */ - -}; - -/** - * Link two filters together. - * - * @param src the source filter - * @param srcpad index of the output pad on the source filter - * @param dst the destination filter - * @param dstpad index of the input pad on the destination filter - * @return zero on success - */ -int avfilter_link(AVFilterContext *src, unsigned srcpad, - AVFilterContext *dst, unsigned dstpad); - -/** - * Free the link in *link, and set its pointer to NULL. - */ -void avfilter_link_free(AVFilterLink **link); - -/** - * Negotiate the media format, dimensions, etc of all inputs to a filter. - * - * @param filter the filter to negotiate the properties for its inputs - * @return zero on successful negotiation - */ -int avfilter_config_links(AVFilterContext *filter); - -#define AVFILTER_CMD_FLAG_ONE 1 ///< Stop once a filter understood the command (for target=all for example), fast filters are favored automatically -#define AVFILTER_CMD_FLAG_FAST 2 ///< Only execute command when its fast (like a video out that supports contrast adjustment in hw) - -/** - * Make the filter instance process a command. - * It is recommended to use avfilter_graph_send_command(). - */ -int avfilter_process_command(AVFilterContext *filter, const char *cmd, const char *arg, char *res, int res_len, int flags); - -/** - * Iterate over all registered filters. - * - * @param opaque a pointer where libavfilter will store the iteration state. Must - * point to NULL to start the iteration. - * - * @return the next registered filter or NULL when the iteration is - * finished - */ -const AVFilter *av_filter_iterate(void **opaque); - -/** - * Get a filter definition matching the given name. - * - * @param name the filter name to find - * @return the filter definition, if any matching one is registered. - * NULL if none found. - */ -const AVFilter *avfilter_get_by_name(const char *name); - - -/** - * Initialize a filter with the supplied parameters. - * - * @param ctx uninitialized filter context to initialize - * @param args Options to initialize the filter with. This must be a - * ':'-separated list of options in the 'key=value' form. - * May be NULL if the options have been set directly using the - * AVOptions API or there are no options that need to be set. - * @return 0 on success, a negative AVERROR on failure - */ -int avfilter_init_str(AVFilterContext *ctx, const char *args); - -/** - * Initialize a filter with the supplied dictionary of options. - * - * @param ctx uninitialized filter context to initialize - * @param options An AVDictionary filled with options for this filter. On - * return this parameter will be destroyed and replaced with - * a dict containing options that were not found. This dictionary - * must be freed by the caller. - * May be NULL, then this function is equivalent to - * avfilter_init_str() with the second parameter set to NULL. - * @return 0 on success, a negative AVERROR on failure - * - * @note This function and avfilter_init_str() do essentially the same thing, - * the difference is in manner in which the options are passed. It is up to the - * calling code to choose whichever is more preferable. The two functions also - * behave differently when some of the provided options are not declared as - * supported by the filter. In such a case, avfilter_init_str() will fail, but - * this function will leave those extra options in the options AVDictionary and - * continue as usual. - */ -int avfilter_init_dict(AVFilterContext *ctx, AVDictionary **options); - -/** - * Free a filter context. This will also remove the filter from its - * filtergraph's list of filters. - * - * @param filter the filter to free - */ -void avfilter_free(AVFilterContext *filter); - -/** - * Insert a filter in the middle of an existing link. - * - * @param link the link into which the filter should be inserted - * @param filt the filter to be inserted - * @param filt_srcpad_idx the input pad on the filter to connect - * @param filt_dstpad_idx the output pad on the filter to connect - * @return zero on success - */ -int avfilter_insert_filter(AVFilterLink *link, AVFilterContext *filt, - unsigned filt_srcpad_idx, unsigned filt_dstpad_idx); - -/** - * @return AVClass for AVFilterContext. - * - * @see av_opt_find(). - */ -const AVClass *avfilter_get_class(void); - -typedef struct AVFilterGraphInternal AVFilterGraphInternal; - -/** - * A function pointer passed to the @ref AVFilterGraph.execute callback to be - * executed multiple times, possibly in parallel. - * - * @param ctx the filter context the job belongs to - * @param arg an opaque parameter passed through from @ref - * AVFilterGraph.execute - * @param jobnr the index of the job being executed - * @param nb_jobs the total number of jobs - * - * @return 0 on success, a negative AVERROR on error - */ -typedef int (avfilter_action_func)(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs); - -/** - * A function executing multiple jobs, possibly in parallel. - * - * @param ctx the filter context to which the jobs belong - * @param func the function to be called multiple times - * @param arg the argument to be passed to func - * @param ret a nb_jobs-sized array to be filled with return values from each - * invocation of func - * @param nb_jobs the number of jobs to execute - * - * @return 0 on success, a negative AVERROR on error - */ -typedef int (avfilter_execute_func)(AVFilterContext *ctx, avfilter_action_func *func, - void *arg, int *ret, int nb_jobs); - -typedef struct AVFilterGraph { - const AVClass *av_class; - AVFilterContext **filters; - unsigned nb_filters; - - char *scale_sws_opts; ///< sws options to use for the auto-inserted scale filters - - /** - * Type of multithreading allowed for filters in this graph. A combination - * of AVFILTER_THREAD_* flags. - * - * May be set by the caller at any point, the setting will apply to all - * filters initialized after that. The default is allowing everything. - * - * When a filter in this graph is initialized, this field is combined using - * bit AND with AVFilterContext.thread_type to get the final mask used for - * determining allowed threading types. I.e. a threading type needs to be - * set in both to be allowed. - */ - int thread_type; - - /** - * Maximum number of threads used by filters in this graph. May be set by - * the caller before adding any filters to the filtergraph. Zero (the - * default) means that the number of threads is determined automatically. - */ - int nb_threads; - - /** - * Opaque object for libavfilter internal use. - */ - AVFilterGraphInternal *internal; - - /** - * Opaque user data. May be set by the caller to an arbitrary value, e.g. to - * be used from callbacks like @ref AVFilterGraph.execute. - * Libavfilter will not touch this field in any way. - */ - void *opaque; - - /** - * This callback may be set by the caller immediately after allocating the - * graph and before adding any filters to it, to provide a custom - * multithreading implementation. - * - * If set, filters with slice threading capability will call this callback - * to execute multiple jobs in parallel. - * - * If this field is left unset, libavfilter will use its internal - * implementation, which may or may not be multithreaded depending on the - * platform and build options. - */ - avfilter_execute_func *execute; - - char *aresample_swr_opts; ///< swr options to use for the auto-inserted aresample filters, Access ONLY through AVOptions - - /** - * Private fields - * - * The following fields are for internal use only. - * Their type, offset, number and semantic can change without notice. - */ - - AVFilterLink **sink_links; - int sink_links_count; - - unsigned disable_auto_convert; -} AVFilterGraph; - -/** - * Allocate a filter graph. - * - * @return the allocated filter graph on success or NULL. - */ -AVFilterGraph *avfilter_graph_alloc(void); - -/** - * Create a new filter instance in a filter graph. - * - * @param graph graph in which the new filter will be used - * @param filter the filter to create an instance of - * @param name Name to give to the new instance (will be copied to - * AVFilterContext.name). This may be used by the caller to identify - * different filters, libavfilter itself assigns no semantics to - * this parameter. May be NULL. - * - * @return the context of the newly created filter instance (note that it is - * also retrievable directly through AVFilterGraph.filters or with - * avfilter_graph_get_filter()) on success or NULL on failure. - */ -AVFilterContext *avfilter_graph_alloc_filter(AVFilterGraph *graph, - const AVFilter *filter, - const char *name); - -/** - * Get a filter instance identified by instance name from graph. - * - * @param graph filter graph to search through. - * @param name filter instance name (should be unique in the graph). - * @return the pointer to the found filter instance or NULL if it - * cannot be found. - */ -AVFilterContext *avfilter_graph_get_filter(AVFilterGraph *graph, const char *name); - -/** - * Create and add a filter instance into an existing graph. - * The filter instance is created from the filter filt and inited - * with the parameter args. opaque is currently ignored. - * - * In case of success put in *filt_ctx the pointer to the created - * filter instance, otherwise set *filt_ctx to NULL. - * - * @param name the instance name to give to the created filter instance - * @param graph_ctx the filter graph - * @return a negative AVERROR error code in case of failure, a non - * negative value otherwise - */ -int avfilter_graph_create_filter(AVFilterContext **filt_ctx, const AVFilter *filt, - const char *name, const char *args, void *opaque, - AVFilterGraph *graph_ctx); - -/** - * Enable or disable automatic format conversion inside the graph. - * - * Note that format conversion can still happen inside explicitly inserted - * scale and aresample filters. - * - * @param flags any of the AVFILTER_AUTO_CONVERT_* constants - */ -void avfilter_graph_set_auto_convert(AVFilterGraph *graph, unsigned flags); - -enum { - AVFILTER_AUTO_CONVERT_ALL = 0, /**< all automatic conversions enabled */ - AVFILTER_AUTO_CONVERT_NONE = -1, /**< all automatic conversions disabled */ -}; - -/** - * Check validity and configure all the links and formats in the graph. - * - * @param graphctx the filter graph - * @param log_ctx context used for logging - * @return >= 0 in case of success, a negative AVERROR code otherwise - */ -int avfilter_graph_config(AVFilterGraph *graphctx, void *log_ctx); - -/** - * Free a graph, destroy its links, and set *graph to NULL. - * If *graph is NULL, do nothing. - */ -void avfilter_graph_free(AVFilterGraph **graph); - -/** - * A linked-list of the inputs/outputs of the filter chain. - * - * This is mainly useful for avfilter_graph_parse() / avfilter_graph_parse2(), - * where it is used to communicate open (unlinked) inputs and outputs from and - * to the caller. - * This struct specifies, per each not connected pad contained in the graph, the - * filter context and the pad index required for establishing a link. - */ -typedef struct AVFilterInOut { - /** unique name for this input/output in the list */ - char *name; - - /** filter context associated to this input/output */ - AVFilterContext *filter_ctx; - - /** index of the filt_ctx pad to use for linking */ - int pad_idx; - - /** next input/input in the list, NULL if this is the last */ - struct AVFilterInOut *next; -} AVFilterInOut; - -/** - * Allocate a single AVFilterInOut entry. - * Must be freed with avfilter_inout_free(). - * @return allocated AVFilterInOut on success, NULL on failure. - */ -AVFilterInOut *avfilter_inout_alloc(void); - -/** - * Free the supplied list of AVFilterInOut and set *inout to NULL. - * If *inout is NULL, do nothing. - */ -void avfilter_inout_free(AVFilterInOut **inout); - -/** - * Add a graph described by a string to a graph. - * - * @note The caller must provide the lists of inputs and outputs, - * which therefore must be known before calling the function. - * - * @note The inputs parameter describes inputs of the already existing - * part of the graph; i.e. from the point of view of the newly created - * part, they are outputs. Similarly the outputs parameter describes - * outputs of the already existing filters, which are provided as - * inputs to the parsed filters. - * - * @param graph the filter graph where to link the parsed graph context - * @param filters string to be parsed - * @param inputs linked list to the inputs of the graph - * @param outputs linked list to the outputs of the graph - * @return zero on success, a negative AVERROR code on error - */ -int avfilter_graph_parse(AVFilterGraph *graph, const char *filters, - AVFilterInOut *inputs, AVFilterInOut *outputs, - void *log_ctx); - -/** - * Add a graph described by a string to a graph. - * - * In the graph filters description, if the input label of the first - * filter is not specified, "in" is assumed; if the output label of - * the last filter is not specified, "out" is assumed. - * - * @param graph the filter graph where to link the parsed graph context - * @param filters string to be parsed - * @param inputs pointer to a linked list to the inputs of the graph, may be NULL. - * If non-NULL, *inputs is updated to contain the list of open inputs - * after the parsing, should be freed with avfilter_inout_free(). - * @param outputs pointer to a linked list to the outputs of the graph, may be NULL. - * If non-NULL, *outputs is updated to contain the list of open outputs - * after the parsing, should be freed with avfilter_inout_free(). - * @return non negative on success, a negative AVERROR code on error - */ -int avfilter_graph_parse_ptr(AVFilterGraph *graph, const char *filters, - AVFilterInOut **inputs, AVFilterInOut **outputs, - void *log_ctx); - -/** - * Add a graph described by a string to a graph. - * - * @param[in] graph the filter graph where to link the parsed graph context - * @param[in] filters string to be parsed - * @param[out] inputs a linked list of all free (unlinked) inputs of the - * parsed graph will be returned here. It is to be freed - * by the caller using avfilter_inout_free(). - * @param[out] outputs a linked list of all free (unlinked) outputs of the - * parsed graph will be returned here. It is to be freed by the - * caller using avfilter_inout_free(). - * @return zero on success, a negative AVERROR code on error - * - * @note This function returns the inputs and outputs that are left - * unlinked after parsing the graph and the caller then deals with - * them. - * @note This function makes no reference whatsoever to already - * existing parts of the graph and the inputs parameter will on return - * contain inputs of the newly parsed part of the graph. Analogously - * the outputs parameter will contain outputs of the newly created - * filters. - */ -int avfilter_graph_parse2(AVFilterGraph *graph, const char *filters, - AVFilterInOut **inputs, - AVFilterInOut **outputs); - -/** - * Parameters of a filter's input or output pad. - * - * Created as a child of AVFilterParams by avfilter_graph_segment_parse(). - * Freed in avfilter_graph_segment_free(). - */ -typedef struct AVFilterPadParams { - /** - * An av_malloc()'ed string containing the pad label. - * - * May be av_free()'d and set to NULL by the caller, in which case this pad - * will be treated as unlabeled for linking. - * May also be replaced by another av_malloc()'ed string. - */ - char *label; -} AVFilterPadParams; - -/** - * Parameters describing a filter to be created in a filtergraph. - * - * Created as a child of AVFilterGraphSegment by avfilter_graph_segment_parse(). - * Freed in avfilter_graph_segment_free(). - */ -typedef struct AVFilterParams { - /** - * The filter context. - * - * Created by avfilter_graph_segment_create_filters() based on - * AVFilterParams.filter_name and instance_name. - * - * Callers may also create the filter context manually, then they should - * av_free() filter_name and set it to NULL. Such AVFilterParams instances - * are then skipped by avfilter_graph_segment_create_filters(). - */ - AVFilterContext *filter; - - /** - * Name of the AVFilter to be used. - * - * An av_malloc()'ed string, set by avfilter_graph_segment_parse(). Will be - * passed to avfilter_get_by_name() by - * avfilter_graph_segment_create_filters(). - * - * Callers may av_free() this string and replace it with another one or - * NULL. If the caller creates the filter instance manually, this string - * MUST be set to NULL. - * - * When both AVFilterParams.filter an AVFilterParams.filter_name are NULL, - * this AVFilterParams instance is skipped by avfilter_graph_segment_*() - * functions. - */ - char *filter_name; - /** - * Name to be used for this filter instance. - * - * An av_malloc()'ed string, may be set by avfilter_graph_segment_parse() or - * left NULL. The caller may av_free() this string and replace with another - * one or NULL. - * - * Will be used by avfilter_graph_segment_create_filters() - passed as the - * third argument to avfilter_graph_alloc_filter(), then freed and set to - * NULL. - */ - char *instance_name; - - /** - * Options to be apllied to the filter. - * - * Filled by avfilter_graph_segment_parse(). Afterwards may be freely - * modified by the caller. - * - * Will be applied to the filter by avfilter_graph_segment_apply_opts() - * with an equivalent of av_opt_set_dict2(filter, &opts, AV_OPT_SEARCH_CHILDREN), - * i.e. any unapplied options will be left in this dictionary. - */ - AVDictionary *opts; - - AVFilterPadParams **inputs; - unsigned nb_inputs; - - AVFilterPadParams **outputs; - unsigned nb_outputs; -} AVFilterParams; - -/** - * A filterchain is a list of filter specifications. - * - * Created as a child of AVFilterGraphSegment by avfilter_graph_segment_parse(). - * Freed in avfilter_graph_segment_free(). - */ -typedef struct AVFilterChain { - AVFilterParams **filters; - size_t nb_filters; -} AVFilterChain; - -/** - * A parsed representation of a filtergraph segment. - * - * A filtergraph segment is conceptually a list of filterchains, with some - * supplementary information (e.g. format conversion flags). - * - * Created by avfilter_graph_segment_parse(). Must be freed with - * avfilter_graph_segment_free(). - */ -typedef struct AVFilterGraphSegment { - /** - * The filtergraph this segment is associated with. - * Set by avfilter_graph_segment_parse(). - */ - AVFilterGraph *graph; - - /** - * A list of filter chain contained in this segment. - * Set in avfilter_graph_segment_parse(). - */ - AVFilterChain **chains; - size_t nb_chains; - - /** - * A string containing a colon-separated list of key=value options applied - * to all scale filters in this segment. - * - * May be set by avfilter_graph_segment_parse(). - * The caller may free this string with av_free() and replace it with a - * different av_malloc()'ed string. - */ - char *scale_sws_opts; -} AVFilterGraphSegment; - -/** - * Parse a textual filtergraph description into an intermediate form. - * - * This intermediate representation is intended to be modified by the caller as - * described in the documentation of AVFilterGraphSegment and its children, and - * then applied to the graph either manually or with other - * avfilter_graph_segment_*() functions. See the documentation for - * avfilter_graph_segment_apply() for the canonical way to apply - * AVFilterGraphSegment. - * - * @param graph Filter graph the parsed segment is associated with. Will only be - * used for logging and similar auxiliary purposes. The graph will - * not be actually modified by this function - the parsing results - * are instead stored in seg for further processing. - * @param graph_str a string describing the filtergraph segment - * @param flags reserved for future use, caller must set to 0 for now - * @param seg A pointer to the newly-created AVFilterGraphSegment is written - * here on success. The graph segment is owned by the caller and must - * be freed with avfilter_graph_segment_free() before graph itself is - * freed. - * - * @retval "non-negative number" success - * @retval "negative error code" failure - */ -int avfilter_graph_segment_parse(AVFilterGraph *graph, const char *graph_str, - int flags, AVFilterGraphSegment **seg); - -/** - * Create filters specified in a graph segment. - * - * Walk through the creation-pending AVFilterParams in the segment and create - * new filter instances for them. - * Creation-pending params are those where AVFilterParams.filter_name is - * non-NULL (and hence AVFilterParams.filter is NULL). All other AVFilterParams - * instances are ignored. - * - * For any filter created by this function, the corresponding - * AVFilterParams.filter is set to the newly-created filter context, - * AVFilterParams.filter_name and AVFilterParams.instance_name are freed and set - * to NULL. - * - * @param seg the filtergraph segment to process - * @param flags reserved for future use, caller must set to 0 for now - * - * @retval "non-negative number" Success, all creation-pending filters were - * successfully created - * @retval AVERROR_FILTER_NOT_FOUND some filter's name did not correspond to a - * known filter - * @retval "another negative error code" other failures - * - * @note Calling this function multiple times is safe, as it is idempotent. - */ -int avfilter_graph_segment_create_filters(AVFilterGraphSegment *seg, int flags); - -/** - * Apply parsed options to filter instances in a graph segment. - * - * Walk through all filter instances in the graph segment that have option - * dictionaries associated with them and apply those options with - * av_opt_set_dict2(..., AV_OPT_SEARCH_CHILDREN). AVFilterParams.opts is - * replaced by the dictionary output by av_opt_set_dict2(), which should be - * empty (NULL) if all options were successfully applied. - * - * If any options could not be found, this function will continue processing all - * other filters and finally return AVERROR_OPTION_NOT_FOUND (unless another - * error happens). The calling program may then deal with unapplied options as - * it wishes. - * - * Any creation-pending filters (see avfilter_graph_segment_create_filters()) - * present in the segment will cause this function to fail. AVFilterParams with - * no associated filter context are simply skipped. - * - * @param seg the filtergraph segment to process - * @param flags reserved for future use, caller must set to 0 for now - * - * @retval "non-negative number" Success, all options were successfully applied. - * @retval AVERROR_OPTION_NOT_FOUND some options were not found in a filter - * @retval "another negative error code" other failures - * - * @note Calling this function multiple times is safe, as it is idempotent. - */ -int avfilter_graph_segment_apply_opts(AVFilterGraphSegment *seg, int flags); - -/** - * Initialize all filter instances in a graph segment. - * - * Walk through all filter instances in the graph segment and call - * avfilter_init_dict(..., NULL) on those that have not been initialized yet. - * - * Any creation-pending filters (see avfilter_graph_segment_create_filters()) - * present in the segment will cause this function to fail. AVFilterParams with - * no associated filter context or whose filter context is already initialized, - * are simply skipped. - * - * @param seg the filtergraph segment to process - * @param flags reserved for future use, caller must set to 0 for now - * - * @retval "non-negative number" Success, all filter instances were successfully - * initialized - * @retval "negative error code" failure - * - * @note Calling this function multiple times is safe, as it is idempotent. - */ -int avfilter_graph_segment_init(AVFilterGraphSegment *seg, int flags); - -/** - * Link filters in a graph segment. - * - * Walk through all filter instances in the graph segment and try to link all - * unlinked input and output pads. Any creation-pending filters (see - * avfilter_graph_segment_create_filters()) present in the segment will cause - * this function to fail. Disabled filters and already linked pads are skipped. - * - * Every filter output pad that has a corresponding AVFilterPadParams with a - * non-NULL label is - * - linked to the input with the matching label, if one exists; - * - exported in the outputs linked list otherwise, with the label preserved. - * Unlabeled outputs are - * - linked to the first unlinked unlabeled input in the next non-disabled - * filter in the chain, if one exists - * - exported in the ouputs linked list otherwise, with NULL label - * - * Similarly, unlinked input pads are exported in the inputs linked list. - * - * @param seg the filtergraph segment to process - * @param flags reserved for future use, caller must set to 0 for now - * @param[out] inputs a linked list of all free (unlinked) inputs of the - * filters in this graph segment will be returned here. It - * is to be freed by the caller using avfilter_inout_free(). - * @param[out] outputs a linked list of all free (unlinked) outputs of the - * filters in this graph segment will be returned here. It - * is to be freed by the caller using avfilter_inout_free(). - * - * @retval "non-negative number" success - * @retval "negative error code" failure - * - * @note Calling this function multiple times is safe, as it is idempotent. - */ -int avfilter_graph_segment_link(AVFilterGraphSegment *seg, int flags, - AVFilterInOut **inputs, - AVFilterInOut **outputs); - -/** - * Apply all filter/link descriptions from a graph segment to the associated filtergraph. - * - * This functions is currently equivalent to calling the following in sequence: - * - avfilter_graph_segment_create_filters(); - * - avfilter_graph_segment_apply_opts(); - * - avfilter_graph_segment_init(); - * - avfilter_graph_segment_link(); - * failing if any of them fails. This list may be extended in the future. - * - * Since the above functions are idempotent, the caller may call some of them - * manually, then do some custom processing on the filtergraph, then call this - * function to do the rest. - * - * @param seg the filtergraph segment to process - * @param flags reserved for future use, caller must set to 0 for now - * @param[out] inputs passed to avfilter_graph_segment_link() - * @param[out] outputs passed to avfilter_graph_segment_link() - * - * @retval "non-negative number" success - * @retval "negative error code" failure - * - * @note Calling this function multiple times is safe, as it is idempotent. - */ -int avfilter_graph_segment_apply(AVFilterGraphSegment *seg, int flags, - AVFilterInOut **inputs, - AVFilterInOut **outputs); - -/** - * Free the provided AVFilterGraphSegment and everything associated with it. - * - * @param seg double pointer to the AVFilterGraphSegment to be freed. NULL will - * be written to this pointer on exit from this function. - * - * @note - * The filter contexts (AVFilterParams.filter) are owned by AVFilterGraph rather - * than AVFilterGraphSegment, so they are not freed. - */ -void avfilter_graph_segment_free(AVFilterGraphSegment **seg); - -/** - * Send a command to one or more filter instances. - * - * @param graph the filter graph - * @param target the filter(s) to which the command should be sent - * "all" sends to all filters - * otherwise it can be a filter or filter instance name - * which will send the command to all matching filters. - * @param cmd the command to send, for handling simplicity all commands must be alphanumeric only - * @param arg the argument for the command - * @param res a buffer with size res_size where the filter(s) can return a response. - * - * @returns >=0 on success otherwise an error code. - * AVERROR(ENOSYS) on unsupported commands - */ -int avfilter_graph_send_command(AVFilterGraph *graph, const char *target, const char *cmd, const char *arg, char *res, int res_len, int flags); - -/** - * Queue a command for one or more filter instances. - * - * @param graph the filter graph - * @param target the filter(s) to which the command should be sent - * "all" sends to all filters - * otherwise it can be a filter or filter instance name - * which will send the command to all matching filters. - * @param cmd the command to sent, for handling simplicity all commands must be alphanumeric only - * @param arg the argument for the command - * @param ts time at which the command should be sent to the filter - * - * @note As this executes commands after this function returns, no return code - * from the filter is provided, also AVFILTER_CMD_FLAG_ONE is not supported. - */ -int avfilter_graph_queue_command(AVFilterGraph *graph, const char *target, const char *cmd, const char *arg, int flags, double ts); - - -/** - * Dump a graph into a human-readable string representation. - * - * @param graph the graph to dump - * @param options formatting options; currently ignored - * @return a string, or NULL in case of memory allocation failure; - * the string must be freed using av_free - */ -char *avfilter_graph_dump(AVFilterGraph *graph, const char *options); - -/** - * Request a frame on the oldest sink link. - * - * If the request returns AVERROR_EOF, try the next. - * - * Note that this function is not meant to be the sole scheduling mechanism - * of a filtergraph, only a convenience function to help drain a filtergraph - * in a balanced way under normal circumstances. - * - * Also note that AVERROR_EOF does not mean that frames did not arrive on - * some of the sinks during the process. - * When there are multiple sink links, in case the requested link - * returns an EOF, this may cause a filter to flush pending frames - * which are sent to another sink link, although unrequested. - * - * @return the return value of ff_request_frame(), - * or AVERROR_EOF if all links returned AVERROR_EOF - */ -int avfilter_graph_request_oldest(AVFilterGraph *graph); - -/** - * @} - */ - -#endif /* AVFILTER_AVFILTER_H */ diff --git a/gostream/ffmpeg/include/libavfilter/buffersink.h b/gostream/ffmpeg/include/libavfilter/buffersink.h deleted file mode 100644 index 64e08de53ee..00000000000 --- a/gostream/ffmpeg/include/libavfilter/buffersink.h +++ /dev/null @@ -1,173 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFILTER_BUFFERSINK_H -#define AVFILTER_BUFFERSINK_H - -/** - * @file - * @ingroup lavfi_buffersink - * memory buffer sink API for audio and video - */ - -#include "avfilter.h" - -/** - * @defgroup lavfi_buffersink Buffer sink API - * @ingroup lavfi - * @{ - * - * The buffersink and abuffersink filters are there to connect filter graphs - * to applications. They have a single input, connected to the graph, and no - * output. Frames must be extracted using av_buffersink_get_frame() or - * av_buffersink_get_samples(). - * - * The format negotiated by the graph during configuration can be obtained - * using the accessor functions: - * - av_buffersink_get_time_base(), - * - av_buffersink_get_format(), - * - av_buffersink_get_frame_rate(), - * - av_buffersink_get_w(), - * - av_buffersink_get_h(), - * - av_buffersink_get_sample_aspect_ratio(), - * - av_buffersink_get_channels(), - * - av_buffersink_get_ch_layout(), - * - av_buffersink_get_sample_rate(). - * - * The layout returned by av_buffersink_get_ch_layout() must de uninitialized - * by the caller. - * - * The format can be constrained by setting options, using av_opt_set() and - * related functions with the AV_OPT_SEARCH_CHILDREN flag. - * - pix_fmts (int list), - * - sample_fmts (int list), - * - sample_rates (int list), - * - ch_layouts (string), - * - channel_counts (int list), - * - all_channel_counts (bool). - * Most of these options are of type binary, and should be set using - * av_opt_set_int_list() or av_opt_set_bin(). If they are not set, all - * corresponding formats are accepted. - * - * As a special case, if ch_layouts is not set, all valid channel layouts are - * accepted except for UNSPEC layouts, unless all_channel_counts is set. - */ - -/** - * Get a frame with filtered data from sink and put it in frame. - * - * @param ctx pointer to a buffersink or abuffersink filter context. - * @param frame pointer to an allocated frame that will be filled with data. - * The data must be freed using av_frame_unref() / av_frame_free() - * @param flags a combination of AV_BUFFERSINK_FLAG_* flags - * - * @return >= 0 in for success, a negative AVERROR code for failure. - */ -int av_buffersink_get_frame_flags(AVFilterContext *ctx, AVFrame *frame, int flags); - -/** - * Tell av_buffersink_get_buffer_ref() to read video/samples buffer - * reference, but not remove it from the buffer. This is useful if you - * need only to read a video/samples buffer, without to fetch it. - */ -#define AV_BUFFERSINK_FLAG_PEEK 1 - -/** - * Tell av_buffersink_get_buffer_ref() not to request a frame from its input. - * If a frame is already buffered, it is read (and removed from the buffer), - * but if no frame is present, return AVERROR(EAGAIN). - */ -#define AV_BUFFERSINK_FLAG_NO_REQUEST 2 - -/** - * Set the frame size for an audio buffer sink. - * - * All calls to av_buffersink_get_buffer_ref will return a buffer with - * exactly the specified number of samples, or AVERROR(EAGAIN) if there is - * not enough. The last buffer at EOF will be padded with 0. - */ -void av_buffersink_set_frame_size(AVFilterContext *ctx, unsigned frame_size); - -/** - * @defgroup lavfi_buffersink_accessors Buffer sink accessors - * Get the properties of the stream - * @{ - */ - -enum AVMediaType av_buffersink_get_type (const AVFilterContext *ctx); -AVRational av_buffersink_get_time_base (const AVFilterContext *ctx); -int av_buffersink_get_format (const AVFilterContext *ctx); - -AVRational av_buffersink_get_frame_rate (const AVFilterContext *ctx); -int av_buffersink_get_w (const AVFilterContext *ctx); -int av_buffersink_get_h (const AVFilterContext *ctx); -AVRational av_buffersink_get_sample_aspect_ratio (const AVFilterContext *ctx); - -int av_buffersink_get_channels (const AVFilterContext *ctx); -#if FF_API_OLD_CHANNEL_LAYOUT -attribute_deprecated -uint64_t av_buffersink_get_channel_layout (const AVFilterContext *ctx); -#endif -int av_buffersink_get_ch_layout (const AVFilterContext *ctx, - AVChannelLayout *ch_layout); -int av_buffersink_get_sample_rate (const AVFilterContext *ctx); - -AVBufferRef * av_buffersink_get_hw_frames_ctx (const AVFilterContext *ctx); - -/** @} */ - -/** - * Get a frame with filtered data from sink and put it in frame. - * - * @param ctx pointer to a context of a buffersink or abuffersink AVFilter. - * @param frame pointer to an allocated frame that will be filled with data. - * The data must be freed using av_frame_unref() / av_frame_free() - * - * @return - * - >= 0 if a frame was successfully returned. - * - AVERROR(EAGAIN) if no frames are available at this point; more - * input frames must be added to the filtergraph to get more output. - * - AVERROR_EOF if there will be no more output frames on this sink. - * - A different negative AVERROR code in other failure cases. - */ -int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame); - -/** - * Same as av_buffersink_get_frame(), but with the ability to specify the number - * of samples read. This function is less efficient than - * av_buffersink_get_frame(), because it copies the data around. - * - * @param ctx pointer to a context of the abuffersink AVFilter. - * @param frame pointer to an allocated frame that will be filled with data. - * The data must be freed using av_frame_unref() / av_frame_free() - * frame will contain exactly nb_samples audio samples, except at - * the end of stream, when it can contain less than nb_samples. - * - * @return The return codes have the same meaning as for - * av_buffersink_get_frame(). - * - * @warning do not mix this function with av_buffersink_get_frame(). Use only one or - * the other with a single sink, not both. - */ -int av_buffersink_get_samples(AVFilterContext *ctx, AVFrame *frame, int nb_samples); - -/** - * @} - */ - -#endif /* AVFILTER_BUFFERSINK_H */ diff --git a/gostream/ffmpeg/include/libavfilter/buffersrc.h b/gostream/ffmpeg/include/libavfilter/buffersrc.h deleted file mode 100644 index 3b248b37cd8..00000000000 --- a/gostream/ffmpeg/include/libavfilter/buffersrc.h +++ /dev/null @@ -1,218 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFILTER_BUFFERSRC_H -#define AVFILTER_BUFFERSRC_H - -/** - * @file - * @ingroup lavfi_buffersrc - * Memory buffer source API. - */ - -#include "avfilter.h" - -/** - * @defgroup lavfi_buffersrc Buffer source API - * @ingroup lavfi - * @{ - */ - -enum { - - /** - * Do not check for format changes. - */ - AV_BUFFERSRC_FLAG_NO_CHECK_FORMAT = 1, - - /** - * Immediately push the frame to the output. - */ - AV_BUFFERSRC_FLAG_PUSH = 4, - - /** - * Keep a reference to the frame. - * If the frame if reference-counted, create a new reference; otherwise - * copy the frame data. - */ - AV_BUFFERSRC_FLAG_KEEP_REF = 8, - -}; - -/** - * Get the number of failed requests. - * - * A failed request is when the request_frame method is called while no - * frame is present in the buffer. - * The number is reset when a frame is added. - */ -unsigned av_buffersrc_get_nb_failed_requests(AVFilterContext *buffer_src); - -/** - * This structure contains the parameters describing the frames that will be - * passed to this filter. - * - * It should be allocated with av_buffersrc_parameters_alloc() and freed with - * av_free(). All the allocated fields in it remain owned by the caller. - */ -typedef struct AVBufferSrcParameters { - /** - * video: the pixel format, value corresponds to enum AVPixelFormat - * audio: the sample format, value corresponds to enum AVSampleFormat - */ - int format; - /** - * The timebase to be used for the timestamps on the input frames. - */ - AVRational time_base; - - /** - * Video only, the display dimensions of the input frames. - */ - int width, height; - - /** - * Video only, the sample (pixel) aspect ratio. - */ - AVRational sample_aspect_ratio; - - /** - * Video only, the frame rate of the input video. This field must only be - * set to a non-zero value if input stream has a known constant framerate - * and should be left at its initial value if the framerate is variable or - * unknown. - */ - AVRational frame_rate; - - /** - * Video with a hwaccel pixel format only. This should be a reference to an - * AVHWFramesContext instance describing the input frames. - */ - AVBufferRef *hw_frames_ctx; - - /** - * Audio only, the audio sampling rate in samples per second. - */ - int sample_rate; - -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * Audio only, the audio channel layout - * @deprecated use ch_layout - */ - attribute_deprecated - uint64_t channel_layout; -#endif - - /** - * Audio only, the audio channel layout - */ - AVChannelLayout ch_layout; -} AVBufferSrcParameters; - -/** - * Allocate a new AVBufferSrcParameters instance. It should be freed by the - * caller with av_free(). - */ -AVBufferSrcParameters *av_buffersrc_parameters_alloc(void); - -/** - * Initialize the buffersrc or abuffersrc filter with the provided parameters. - * This function may be called multiple times, the later calls override the - * previous ones. Some of the parameters may also be set through AVOptions, then - * whatever method is used last takes precedence. - * - * @param ctx an instance of the buffersrc or abuffersrc filter - * @param param the stream parameters. The frames later passed to this filter - * must conform to those parameters. All the allocated fields in - * param remain owned by the caller, libavfilter will make internal - * copies or references when necessary. - * @return 0 on success, a negative AVERROR code on failure. - */ -int av_buffersrc_parameters_set(AVFilterContext *ctx, AVBufferSrcParameters *param); - -/** - * Add a frame to the buffer source. - * - * @param ctx an instance of the buffersrc filter - * @param frame frame to be added. If the frame is reference counted, this - * function will make a new reference to it. Otherwise the frame data will be - * copied. - * - * @return 0 on success, a negative AVERROR on error - * - * This function is equivalent to av_buffersrc_add_frame_flags() with the - * AV_BUFFERSRC_FLAG_KEEP_REF flag. - */ -av_warn_unused_result -int av_buffersrc_write_frame(AVFilterContext *ctx, const AVFrame *frame); - -/** - * Add a frame to the buffer source. - * - * @param ctx an instance of the buffersrc filter - * @param frame frame to be added. If the frame is reference counted, this - * function will take ownership of the reference(s) and reset the frame. - * Otherwise the frame data will be copied. If this function returns an error, - * the input frame is not touched. - * - * @return 0 on success, a negative AVERROR on error. - * - * @note the difference between this function and av_buffersrc_write_frame() is - * that av_buffersrc_write_frame() creates a new reference to the input frame, - * while this function takes ownership of the reference passed to it. - * - * This function is equivalent to av_buffersrc_add_frame_flags() without the - * AV_BUFFERSRC_FLAG_KEEP_REF flag. - */ -av_warn_unused_result -int av_buffersrc_add_frame(AVFilterContext *ctx, AVFrame *frame); - -/** - * Add a frame to the buffer source. - * - * By default, if the frame is reference-counted, this function will take - * ownership of the reference(s) and reset the frame. This can be controlled - * using the flags. - * - * If this function returns an error, the input frame is not touched. - * - * @param buffer_src pointer to a buffer source context - * @param frame a frame, or NULL to mark EOF - * @param flags a combination of AV_BUFFERSRC_FLAG_* - * @return >= 0 in case of success, a negative AVERROR code - * in case of failure - */ -av_warn_unused_result -int av_buffersrc_add_frame_flags(AVFilterContext *buffer_src, - AVFrame *frame, int flags); - -/** - * Close the buffer source after EOF. - * - * This is similar to passing NULL to av_buffersrc_add_frame_flags() - * except it takes the timestamp of the EOF, i.e. the timestamp of the end - * of the last frame. - */ -int av_buffersrc_close(AVFilterContext *ctx, int64_t pts, unsigned flags); - -/** - * @} - */ - -#endif /* AVFILTER_BUFFERSRC_H */ diff --git a/gostream/ffmpeg/include/libavfilter/version.h b/gostream/ffmpeg/include/libavfilter/version.h deleted file mode 100644 index 64cd692ab67..00000000000 --- a/gostream/ffmpeg/include/libavfilter/version.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Version macros. - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFILTER_VERSION_H -#define AVFILTER_VERSION_H - -/** - * @file - * @ingroup lavfi - * Libavfilter version macros - */ - -#include "libavutil/version.h" - -#include "version_major.h" - -#define LIBAVFILTER_VERSION_MINOR 12 -#define LIBAVFILTER_VERSION_MICRO 100 - - -#define LIBAVFILTER_VERSION_INT AV_VERSION_INT(LIBAVFILTER_VERSION_MAJOR, \ - LIBAVFILTER_VERSION_MINOR, \ - LIBAVFILTER_VERSION_MICRO) -#define LIBAVFILTER_VERSION AV_VERSION(LIBAVFILTER_VERSION_MAJOR, \ - LIBAVFILTER_VERSION_MINOR, \ - LIBAVFILTER_VERSION_MICRO) -#define LIBAVFILTER_BUILD LIBAVFILTER_VERSION_INT - -#define LIBAVFILTER_IDENT "Lavfi" AV_STRINGIFY(LIBAVFILTER_VERSION) - -#endif /* AVFILTER_VERSION_H */ diff --git a/gostream/ffmpeg/include/libavfilter/version_major.h b/gostream/ffmpeg/include/libavfilter/version_major.h deleted file mode 100644 index 1decc4012ee..00000000000 --- a/gostream/ffmpeg/include/libavfilter/version_major.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Version macros. - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFILTER_VERSION_MAJOR_H -#define AVFILTER_VERSION_MAJOR_H - -/** - * @file - * @ingroup lavfi - * Libavfilter version macros - */ - -#define LIBAVFILTER_VERSION_MAJOR 9 - -/** - * FF_API_* defines may be placed below to indicate public API that will be - * dropped at a future version bump. The defines themselves are not part of - * the public API and may change, break or disappear at any time. - */ - -#define FF_API_LIBPLACEBO_OPTS (LIBAVFILTER_VERSION_MAJOR < 10) - -#endif /* AVFILTER_VERSION_MAJOR_H */ diff --git a/gostream/ffmpeg/include/libavformat/avformat.h b/gostream/ffmpeg/include/libavformat/avformat.h deleted file mode 100644 index 9e7eca007ee..00000000000 --- a/gostream/ffmpeg/include/libavformat/avformat.h +++ /dev/null @@ -1,2858 +0,0 @@ -/* - * copyright (c) 2001 Fabrice Bellard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFORMAT_AVFORMAT_H -#define AVFORMAT_AVFORMAT_H - -/** - * @file - * @ingroup libavf - * Main libavformat public API header - */ - -/** - * @defgroup libavf libavformat - * I/O and Muxing/Demuxing Library - * - * Libavformat (lavf) is a library for dealing with various media container - * formats. Its main two purposes are demuxing - i.e. splitting a media file - * into component streams, and the reverse process of muxing - writing supplied - * data in a specified container format. It also has an @ref lavf_io - * "I/O module" which supports a number of protocols for accessing the data (e.g. - * file, tcp, http and others). - * Unless you are absolutely sure you won't use libavformat's network - * capabilities, you should also call avformat_network_init(). - * - * A supported input format is described by an AVInputFormat struct, conversely - * an output format is described by AVOutputFormat. You can iterate over all - * input/output formats using the av_demuxer_iterate / av_muxer_iterate() functions. - * The protocols layer is not part of the public API, so you can only get the names - * of supported protocols with the avio_enum_protocols() function. - * - * Main lavf structure used for both muxing and demuxing is AVFormatContext, - * which exports all information about the file being read or written. As with - * most Libavformat structures, its size is not part of public ABI, so it cannot be - * allocated on stack or directly with av_malloc(). To create an - * AVFormatContext, use avformat_alloc_context() (some functions, like - * avformat_open_input() might do that for you). - * - * Most importantly an AVFormatContext contains: - * @li the @ref AVFormatContext.iformat "input" or @ref AVFormatContext.oformat - * "output" format. It is either autodetected or set by user for input; - * always set by user for output. - * @li an @ref AVFormatContext.streams "array" of AVStreams, which describe all - * elementary streams stored in the file. AVStreams are typically referred to - * using their index in this array. - * @li an @ref AVFormatContext.pb "I/O context". It is either opened by lavf or - * set by user for input, always set by user for output (unless you are dealing - * with an AVFMT_NOFILE format). - * - * @section lavf_options Passing options to (de)muxers - * It is possible to configure lavf muxers and demuxers using the @ref avoptions - * mechanism. Generic (format-independent) libavformat options are provided by - * AVFormatContext, they can be examined from a user program by calling - * av_opt_next() / av_opt_find() on an allocated AVFormatContext (or its AVClass - * from avformat_get_class()). Private (format-specific) options are provided by - * AVFormatContext.priv_data if and only if AVInputFormat.priv_class / - * AVOutputFormat.priv_class of the corresponding format struct is non-NULL. - * Further options may be provided by the @ref AVFormatContext.pb "I/O context", - * if its AVClass is non-NULL, and the protocols layer. See the discussion on - * nesting in @ref avoptions documentation to learn how to access those. - * - * @section urls - * URL strings in libavformat are made of a scheme/protocol, a ':', and a - * scheme specific string. URLs without a scheme and ':' used for local files - * are supported but deprecated. "file:" should be used for local files. - * - * It is important that the scheme string is not taken from untrusted - * sources without checks. - * - * Note that some schemes/protocols are quite powerful, allowing access to - * both local and remote files, parts of them, concatenations of them, local - * audio and video devices and so on. - * - * @{ - * - * @defgroup lavf_decoding Demuxing - * @{ - * Demuxers read a media file and split it into chunks of data (@em packets). A - * @ref AVPacket "packet" contains one or more encoded frames which belongs to a - * single elementary stream. In the lavf API this process is represented by the - * avformat_open_input() function for opening a file, av_read_frame() for - * reading a single packet and finally avformat_close_input(), which does the - * cleanup. - * - * @section lavf_decoding_open Opening a media file - * The minimum information required to open a file is its URL, which - * is passed to avformat_open_input(), as in the following code: - * @code - * const char *url = "file:in.mp3"; - * AVFormatContext *s = NULL; - * int ret = avformat_open_input(&s, url, NULL, NULL); - * if (ret < 0) - * abort(); - * @endcode - * The above code attempts to allocate an AVFormatContext, open the - * specified file (autodetecting the format) and read the header, exporting the - * information stored there into s. Some formats do not have a header or do not - * store enough information there, so it is recommended that you call the - * avformat_find_stream_info() function which tries to read and decode a few - * frames to find missing information. - * - * In some cases you might want to preallocate an AVFormatContext yourself with - * avformat_alloc_context() and do some tweaking on it before passing it to - * avformat_open_input(). One such case is when you want to use custom functions - * for reading input data instead of lavf internal I/O layer. - * To do that, create your own AVIOContext with avio_alloc_context(), passing - * your reading callbacks to it. Then set the @em pb field of your - * AVFormatContext to newly created AVIOContext. - * - * Since the format of the opened file is in general not known until after - * avformat_open_input() has returned, it is not possible to set demuxer private - * options on a preallocated context. Instead, the options should be passed to - * avformat_open_input() wrapped in an AVDictionary: - * @code - * AVDictionary *options = NULL; - * av_dict_set(&options, "video_size", "640x480", 0); - * av_dict_set(&options, "pixel_format", "rgb24", 0); - * - * if (avformat_open_input(&s, url, NULL, &options) < 0) - * abort(); - * av_dict_free(&options); - * @endcode - * This code passes the private options 'video_size' and 'pixel_format' to the - * demuxer. They would be necessary for e.g. the rawvideo demuxer, since it - * cannot know how to interpret raw video data otherwise. If the format turns - * out to be something different than raw video, those options will not be - * recognized by the demuxer and therefore will not be applied. Such unrecognized - * options are then returned in the options dictionary (recognized options are - * consumed). The calling program can handle such unrecognized options as it - * wishes, e.g. - * @code - * AVDictionaryEntry *e; - * if (e = av_dict_get(options, "", NULL, AV_DICT_IGNORE_SUFFIX)) { - * fprintf(stderr, "Option %s not recognized by the demuxer.\n", e->key); - * abort(); - * } - * @endcode - * - * After you have finished reading the file, you must close it with - * avformat_close_input(). It will free everything associated with the file. - * - * @section lavf_decoding_read Reading from an opened file - * Reading data from an opened AVFormatContext is done by repeatedly calling - * av_read_frame() on it. Each call, if successful, will return an AVPacket - * containing encoded data for one AVStream, identified by - * AVPacket.stream_index. This packet may be passed straight into the libavcodec - * decoding functions avcodec_send_packet() or avcodec_decode_subtitle2() if the - * caller wishes to decode the data. - * - * AVPacket.pts, AVPacket.dts and AVPacket.duration timing information will be - * set if known. They may also be unset (i.e. AV_NOPTS_VALUE for - * pts/dts, 0 for duration) if the stream does not provide them. The timing - * information will be in AVStream.time_base units, i.e. it has to be - * multiplied by the timebase to convert them to seconds. - * - * A packet returned by av_read_frame() is always reference-counted, - * i.e. AVPacket.buf is set and the user may keep it indefinitely. - * The packet must be freed with av_packet_unref() when it is no - * longer needed. - * - * @section lavf_decoding_seek Seeking - * @} - * - * @defgroup lavf_encoding Muxing - * @{ - * Muxers take encoded data in the form of @ref AVPacket "AVPackets" and write - * it into files or other output bytestreams in the specified container format. - * - * The main API functions for muxing are avformat_write_header() for writing the - * file header, av_write_frame() / av_interleaved_write_frame() for writing the - * packets and av_write_trailer() for finalizing the file. - * - * At the beginning of the muxing process, the caller must first call - * avformat_alloc_context() to create a muxing context. The caller then sets up - * the muxer by filling the various fields in this context: - * - * - The @ref AVFormatContext.oformat "oformat" field must be set to select the - * muxer that will be used. - * - Unless the format is of the AVFMT_NOFILE type, the @ref AVFormatContext.pb - * "pb" field must be set to an opened IO context, either returned from - * avio_open2() or a custom one. - * - Unless the format is of the AVFMT_NOSTREAMS type, at least one stream must - * be created with the avformat_new_stream() function. The caller should fill - * the @ref AVStream.codecpar "stream codec parameters" information, such as the - * codec @ref AVCodecParameters.codec_type "type", @ref AVCodecParameters.codec_id - * "id" and other parameters (e.g. width / height, the pixel or sample format, - * etc.) as known. The @ref AVStream.time_base "stream timebase" should - * be set to the timebase that the caller desires to use for this stream (note - * that the timebase actually used by the muxer can be different, as will be - * described later). - * - It is advised to manually initialize only the relevant fields in - * AVCodecParameters, rather than using @ref avcodec_parameters_copy() during - * remuxing: there is no guarantee that the codec context values remain valid - * for both input and output format contexts. - * - The caller may fill in additional information, such as @ref - * AVFormatContext.metadata "global" or @ref AVStream.metadata "per-stream" - * metadata, @ref AVFormatContext.chapters "chapters", @ref - * AVFormatContext.programs "programs", etc. as described in the - * AVFormatContext documentation. Whether such information will actually be - * stored in the output depends on what the container format and the muxer - * support. - * - * When the muxing context is fully set up, the caller must call - * avformat_write_header() to initialize the muxer internals and write the file - * header. Whether anything actually is written to the IO context at this step - * depends on the muxer, but this function must always be called. Any muxer - * private options must be passed in the options parameter to this function. - * - * The data is then sent to the muxer by repeatedly calling av_write_frame() or - * av_interleaved_write_frame() (consult those functions' documentation for - * discussion on the difference between them; only one of them may be used with - * a single muxing context, they should not be mixed). Do note that the timing - * information on the packets sent to the muxer must be in the corresponding - * AVStream's timebase. That timebase is set by the muxer (in the - * avformat_write_header() step) and may be different from the timebase - * requested by the caller. - * - * Once all the data has been written, the caller must call av_write_trailer() - * to flush any buffered packets and finalize the output file, then close the IO - * context (if any) and finally free the muxing context with - * avformat_free_context(). - * @} - * - * @defgroup lavf_io I/O Read/Write - * @{ - * @section lavf_io_dirlist Directory listing - * The directory listing API makes it possible to list files on remote servers. - * - * Some of possible use cases: - * - an "open file" dialog to choose files from a remote location, - * - a recursive media finder providing a player with an ability to play all - * files from a given directory. - * - * @subsection lavf_io_dirlist_open Opening a directory - * At first, a directory needs to be opened by calling avio_open_dir() - * supplied with a URL and, optionally, ::AVDictionary containing - * protocol-specific parameters. The function returns zero or positive - * integer and allocates AVIODirContext on success. - * - * @code - * AVIODirContext *ctx = NULL; - * if (avio_open_dir(&ctx, "smb://example.com/some_dir", NULL) < 0) { - * fprintf(stderr, "Cannot open directory.\n"); - * abort(); - * } - * @endcode - * - * This code tries to open a sample directory using smb protocol without - * any additional parameters. - * - * @subsection lavf_io_dirlist_read Reading entries - * Each directory's entry (i.e. file, another directory, anything else - * within ::AVIODirEntryType) is represented by AVIODirEntry. - * Reading consecutive entries from an opened AVIODirContext is done by - * repeatedly calling avio_read_dir() on it. Each call returns zero or - * positive integer if successful. Reading can be stopped right after the - * NULL entry has been read -- it means there are no entries left to be - * read. The following code reads all entries from a directory associated - * with ctx and prints their names to standard output. - * @code - * AVIODirEntry *entry = NULL; - * for (;;) { - * if (avio_read_dir(ctx, &entry) < 0) { - * fprintf(stderr, "Cannot list directory.\n"); - * abort(); - * } - * if (!entry) - * break; - * printf("%s\n", entry->name); - * avio_free_directory_entry(&entry); - * } - * @endcode - * @} - * - * @defgroup lavf_codec Demuxers - * @{ - * @defgroup lavf_codec_native Native Demuxers - * @{ - * @} - * @defgroup lavf_codec_wrappers External library wrappers - * @{ - * @} - * @} - * @defgroup lavf_protos I/O Protocols - * @{ - * @} - * @defgroup lavf_internal Internal - * @{ - * @} - * @} - */ - -#include /* FILE */ - -#include "libavcodec/codec_par.h" -#include "libavcodec/defs.h" -#include "libavcodec/packet.h" - -#include "libavutil/dict.h" -#include "libavutil/log.h" - -#include "avio.h" -#include "libavformat/version_major.h" -#ifndef HAVE_AV_CONFIG_H -/* When included as part of the ffmpeg build, only include the major version - * to avoid unnecessary rebuilds. When included externally, keep including - * the full version information. */ -#include "libavformat/version.h" - -#include "libavutil/frame.h" -#include "libavcodec/codec.h" -#endif - -struct AVFormatContext; -struct AVFrame; -struct AVDeviceInfoList; - -/** - * @defgroup metadata_api Public Metadata API - * @{ - * @ingroup libavf - * The metadata API allows libavformat to export metadata tags to a client - * application when demuxing. Conversely it allows a client application to - * set metadata when muxing. - * - * Metadata is exported or set as pairs of key/value strings in the 'metadata' - * fields of the AVFormatContext, AVStream, AVChapter and AVProgram structs - * using the @ref lavu_dict "AVDictionary" API. Like all strings in FFmpeg, - * metadata is assumed to be UTF-8 encoded Unicode. Note that metadata - * exported by demuxers isn't checked to be valid UTF-8 in most cases. - * - * Important concepts to keep in mind: - * - Keys are unique; there can never be 2 tags with the same key. This is - * also meant semantically, i.e., a demuxer should not knowingly produce - * several keys that are literally different but semantically identical. - * E.g., key=Author5, key=Author6. In this example, all authors must be - * placed in the same tag. - * - Metadata is flat, not hierarchical; there are no subtags. If you - * want to store, e.g., the email address of the child of producer Alice - * and actor Bob, that could have key=alice_and_bobs_childs_email_address. - * - Several modifiers can be applied to the tag name. This is done by - * appending a dash character ('-') and the modifier name in the order - * they appear in the list below -- e.g. foo-eng-sort, not foo-sort-eng. - * - language -- a tag whose value is localized for a particular language - * is appended with the ISO 639-2/B 3-letter language code. - * For example: Author-ger=Michael, Author-eng=Mike - * The original/default language is in the unqualified "Author" tag. - * A demuxer should set a default if it sets any translated tag. - * - sorting -- a modified version of a tag that should be used for - * sorting will have '-sort' appended. E.g. artist="The Beatles", - * artist-sort="Beatles, The". - * - Some protocols and demuxers support metadata updates. After a successful - * call to av_read_frame(), AVFormatContext.event_flags or AVStream.event_flags - * will be updated to indicate if metadata changed. In order to detect metadata - * changes on a stream, you need to loop through all streams in the AVFormatContext - * and check their individual event_flags. - * - * - Demuxers attempt to export metadata in a generic format, however tags - * with no generic equivalents are left as they are stored in the container. - * Follows a list of generic tag names: - * - @verbatim - album -- name of the set this work belongs to - album_artist -- main creator of the set/album, if different from artist. - e.g. "Various Artists" for compilation albums. - artist -- main creator of the work - comment -- any additional description of the file. - composer -- who composed the work, if different from artist. - copyright -- name of copyright holder. - creation_time-- date when the file was created, preferably in ISO 8601. - date -- date when the work was created, preferably in ISO 8601. - disc -- number of a subset, e.g. disc in a multi-disc collection. - encoder -- name/settings of the software/hardware that produced the file. - encoded_by -- person/group who created the file. - filename -- original name of the file. - genre -- . - language -- main language in which the work is performed, preferably - in ISO 639-2 format. Multiple languages can be specified by - separating them with commas. - performer -- artist who performed the work, if different from artist. - E.g for "Also sprach Zarathustra", artist would be "Richard - Strauss" and performer "London Philharmonic Orchestra". - publisher -- name of the label/publisher. - service_name -- name of the service in broadcasting (channel name). - service_provider -- name of the service provider in broadcasting. - title -- name of the work. - track -- number of this work in the set, can be in form current/total. - variant_bitrate -- the total bitrate of the bitrate variant that the current stream is part of - @endverbatim - * - * Look in the examples section for an application example how to use the Metadata API. - * - * @} - */ - -/* packet functions */ - - -/** - * Allocate and read the payload of a packet and initialize its - * fields with default values. - * - * @param s associated IO context - * @param pkt packet - * @param size desired payload size - * @return >0 (read size) if OK, AVERROR_xxx otherwise - */ -int av_get_packet(AVIOContext *s, AVPacket *pkt, int size); - - -/** - * Read data and append it to the current content of the AVPacket. - * If pkt->size is 0 this is identical to av_get_packet. - * Note that this uses av_grow_packet and thus involves a realloc - * which is inefficient. Thus this function should only be used - * when there is no reasonable way to know (an upper bound of) - * the final size. - * - * @param s associated IO context - * @param pkt packet - * @param size amount of data to read - * @return >0 (read size) if OK, AVERROR_xxx otherwise, previous data - * will not be lost even if an error occurs. - */ -int av_append_packet(AVIOContext *s, AVPacket *pkt, int size); - -/*************************************************/ -/* input/output formats */ - -struct AVCodecTag; - -/** - * This structure contains the data a format has to probe a file. - */ -typedef struct AVProbeData { - const char *filename; - unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. */ - int buf_size; /**< Size of buf except extra allocated bytes */ - const char *mime_type; /**< mime_type, when known. */ -} AVProbeData; - -#define AVPROBE_SCORE_RETRY (AVPROBE_SCORE_MAX/4) -#define AVPROBE_SCORE_STREAM_RETRY (AVPROBE_SCORE_MAX/4-1) - -#define AVPROBE_SCORE_EXTENSION 50 ///< score for file extension -#define AVPROBE_SCORE_MIME 75 ///< score for file mime type -#define AVPROBE_SCORE_MAX 100 ///< maximum score - -#define AVPROBE_PADDING_SIZE 32 ///< extra allocated bytes at the end of the probe buffer - -/// Demuxer will use avio_open, no opened file should be provided by the caller. -#define AVFMT_NOFILE 0x0001 -#define AVFMT_NEEDNUMBER 0x0002 /**< Needs '%d' in filename. */ -/** - * The muxer/demuxer is experimental and should be used with caution. - * - * - demuxers: will not be selected automatically by probing, must be specified - * explicitly. - */ -#define AVFMT_EXPERIMENTAL 0x0004 -#define AVFMT_SHOW_IDS 0x0008 /**< Show format stream IDs numbers. */ -#define AVFMT_GLOBALHEADER 0x0040 /**< Format wants global header. */ -#define AVFMT_NOTIMESTAMPS 0x0080 /**< Format does not need / have any timestamps. */ -#define AVFMT_GENERIC_INDEX 0x0100 /**< Use generic index building code. */ -#define AVFMT_TS_DISCONT 0x0200 /**< Format allows timestamp discontinuities. Note, muxers always require valid (monotone) timestamps */ -#define AVFMT_VARIABLE_FPS 0x0400 /**< Format allows variable fps. */ -#define AVFMT_NODIMENSIONS 0x0800 /**< Format does not need width/height */ -#define AVFMT_NOSTREAMS 0x1000 /**< Format does not require any streams */ -#define AVFMT_NOBINSEARCH 0x2000 /**< Format does not allow to fall back on binary search via read_timestamp */ -#define AVFMT_NOGENSEARCH 0x4000 /**< Format does not allow to fall back on generic search */ -#define AVFMT_NO_BYTE_SEEK 0x8000 /**< Format does not allow seeking by bytes */ -#if FF_API_ALLOW_FLUSH -#define AVFMT_ALLOW_FLUSH 0x10000 /**< @deprecated: Just send a NULL packet if you want to flush a muxer. */ -#endif -#define AVFMT_TS_NONSTRICT 0x20000 /**< Format does not require strictly - increasing timestamps, but they must - still be monotonic */ -#define AVFMT_TS_NEGATIVE 0x40000 /**< Format allows muxing negative - timestamps. If not set the timestamp - will be shifted in av_write_frame and - av_interleaved_write_frame so they - start from 0. - The user or muxer can override this through - AVFormatContext.avoid_negative_ts - */ - -#define AVFMT_SEEK_TO_PTS 0x4000000 /**< Seeking is based on PTS */ - -/** - * @addtogroup lavf_encoding - * @{ - */ -typedef struct AVOutputFormat { - const char *name; - /** - * Descriptive name for the format, meant to be more human-readable - * than name. You should use the NULL_IF_CONFIG_SMALL() macro - * to define it. - */ - const char *long_name; - const char *mime_type; - const char *extensions; /**< comma-separated filename extensions */ - /* output support */ - enum AVCodecID audio_codec; /**< default audio codec */ - enum AVCodecID video_codec; /**< default video codec */ - enum AVCodecID subtitle_codec; /**< default subtitle codec */ - /** - * can use flags: AVFMT_NOFILE, AVFMT_NEEDNUMBER, - * AVFMT_GLOBALHEADER, AVFMT_NOTIMESTAMPS, AVFMT_VARIABLE_FPS, - * AVFMT_NODIMENSIONS, AVFMT_NOSTREAMS, - * AVFMT_TS_NONSTRICT, AVFMT_TS_NEGATIVE - */ - int flags; - - /** - * List of supported codec_id-codec_tag pairs, ordered by "better - * choice first". The arrays are all terminated by AV_CODEC_ID_NONE. - */ - const struct AVCodecTag * const *codec_tag; - - - const AVClass *priv_class; ///< AVClass for the private context -} AVOutputFormat; -/** - * @} - */ - -/** - * @addtogroup lavf_decoding - * @{ - */ -typedef struct AVInputFormat { - /** - * A comma separated list of short names for the format. New names - * may be appended with a minor bump. - */ - const char *name; - - /** - * Descriptive name for the format, meant to be more human-readable - * than name. You should use the NULL_IF_CONFIG_SMALL() macro - * to define it. - */ - const char *long_name; - - /** - * Can use flags: AVFMT_NOFILE, AVFMT_NEEDNUMBER, AVFMT_SHOW_IDS, - * AVFMT_NOTIMESTAMPS, AVFMT_GENERIC_INDEX, AVFMT_TS_DISCONT, AVFMT_NOBINSEARCH, - * AVFMT_NOGENSEARCH, AVFMT_NO_BYTE_SEEK, AVFMT_SEEK_TO_PTS. - */ - int flags; - - /** - * If extensions are defined, then no probe is done. You should - * usually not use extension format guessing because it is not - * reliable enough - */ - const char *extensions; - - const struct AVCodecTag * const *codec_tag; - - const AVClass *priv_class; ///< AVClass for the private context - - /** - * Comma-separated list of mime types. - * It is used check for matching mime types while probing. - * @see av_probe_input_format2 - */ - const char *mime_type; - - /***************************************************************** - * No fields below this line are part of the public API. They - * may not be used outside of libavformat and can be changed and - * removed at will. - * New public fields should be added right above. - ***************************************************************** - */ - /** - * Raw demuxers store their codec ID here. - */ - int raw_codec_id; - - /** - * Size of private data so that it can be allocated in the wrapper. - */ - int priv_data_size; - - /** - * Internal flags. See FF_FMT_FLAG_* in internal.h. - */ - int flags_internal; - - /** - * Tell if a given file has a chance of being parsed as this format. - * The buffer provided is guaranteed to be AVPROBE_PADDING_SIZE bytes - * big so you do not have to check for that unless you need more. - */ - int (*read_probe)(const AVProbeData *); - - /** - * Read the format header and initialize the AVFormatContext - * structure. Return 0 if OK. 'avformat_new_stream' should be - * called to create new streams. - */ - int (*read_header)(struct AVFormatContext *); - - /** - * Read one packet and put it in 'pkt'. pts and flags are also - * set. 'avformat_new_stream' can be called only if the flag - * AVFMTCTX_NOHEADER is used and only in the calling thread (not in a - * background thread). - * @return 0 on success, < 0 on error. - * Upon returning an error, pkt must be unreferenced by the caller. - */ - int (*read_packet)(struct AVFormatContext *, AVPacket *pkt); - - /** - * Close the stream. The AVFormatContext and AVStreams are not - * freed by this function - */ - int (*read_close)(struct AVFormatContext *); - - /** - * Seek to a given timestamp relative to the frames in - * stream component stream_index. - * @param stream_index Must not be -1. - * @param flags Selects which direction should be preferred if no exact - * match is available. - * @return >= 0 on success (but not necessarily the new offset) - */ - int (*read_seek)(struct AVFormatContext *, - int stream_index, int64_t timestamp, int flags); - - /** - * Get the next timestamp in stream[stream_index].time_base units. - * @return the timestamp or AV_NOPTS_VALUE if an error occurred - */ - int64_t (*read_timestamp)(struct AVFormatContext *s, int stream_index, - int64_t *pos, int64_t pos_limit); - - /** - * Start/resume playing - only meaningful if using a network-based format - * (RTSP). - */ - int (*read_play)(struct AVFormatContext *); - - /** - * Pause playing - only meaningful if using a network-based format - * (RTSP). - */ - int (*read_pause)(struct AVFormatContext *); - - /** - * Seek to timestamp ts. - * Seeking will be done so that the point from which all active streams - * can be presented successfully will be closest to ts and within min/max_ts. - * Active streams are all streams that have AVStream.discard < AVDISCARD_ALL. - */ - int (*read_seek2)(struct AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags); - - /** - * Returns device list with it properties. - * @see avdevice_list_devices() for more details. - */ - int (*get_device_list)(struct AVFormatContext *s, struct AVDeviceInfoList *device_list); - -} AVInputFormat; -/** - * @} - */ - -enum AVStreamParseType { - AVSTREAM_PARSE_NONE, - AVSTREAM_PARSE_FULL, /**< full parsing and repack */ - AVSTREAM_PARSE_HEADERS, /**< Only parse headers, do not repack. */ - AVSTREAM_PARSE_TIMESTAMPS, /**< full parsing and interpolation of timestamps for frames not starting on a packet boundary */ - AVSTREAM_PARSE_FULL_ONCE, /**< full parsing and repack of the first frame only, only implemented for H.264 currently */ - AVSTREAM_PARSE_FULL_RAW, /**< full parsing and repack with timestamp and position generation by parser for raw - this assumes that each packet in the file contains no demuxer level headers and - just codec level data, otherwise position generation would fail */ -}; - -typedef struct AVIndexEntry { - int64_t pos; - int64_t timestamp; /**< - * Timestamp in AVStream.time_base units, preferably the time from which on correctly decoded frames are available - * when seeking to this entry. That means preferable PTS on keyframe based formats. - * But demuxers can choose to store a different timestamp, if it is more convenient for the implementation or nothing better - * is known - */ -#define AVINDEX_KEYFRAME 0x0001 -#define AVINDEX_DISCARD_FRAME 0x0002 /** - * Flag is used to indicate which frame should be discarded after decoding. - */ - int flags:2; - int size:30; //Yeah, trying to keep the size of this small to reduce memory requirements (it is 24 vs. 32 bytes due to possible 8-byte alignment). - int min_distance; /**< Minimum distance between this and the previous keyframe, used to avoid unneeded searching. */ -} AVIndexEntry; - -/** - * The stream should be chosen by default among other streams of the same type, - * unless the user has explicitly specified otherwise. - */ -#define AV_DISPOSITION_DEFAULT (1 << 0) -/** - * The stream is not in original language. - * - * @note AV_DISPOSITION_ORIGINAL is the inverse of this disposition. At most - * one of them should be set in properly tagged streams. - * @note This disposition may apply to any stream type, not just audio. - */ -#define AV_DISPOSITION_DUB (1 << 1) -/** - * The stream is in original language. - * - * @see the notes for AV_DISPOSITION_DUB - */ -#define AV_DISPOSITION_ORIGINAL (1 << 2) -/** - * The stream is a commentary track. - */ -#define AV_DISPOSITION_COMMENT (1 << 3) -/** - * The stream contains song lyrics. - */ -#define AV_DISPOSITION_LYRICS (1 << 4) -/** - * The stream contains karaoke audio. - */ -#define AV_DISPOSITION_KARAOKE (1 << 5) - -/** - * Track should be used during playback by default. - * Useful for subtitle track that should be displayed - * even when user did not explicitly ask for subtitles. - */ -#define AV_DISPOSITION_FORCED (1 << 6) -/** - * The stream is intended for hearing impaired audiences. - */ -#define AV_DISPOSITION_HEARING_IMPAIRED (1 << 7) -/** - * The stream is intended for visually impaired audiences. - */ -#define AV_DISPOSITION_VISUAL_IMPAIRED (1 << 8) -/** - * The audio stream contains music and sound effects without voice. - */ -#define AV_DISPOSITION_CLEAN_EFFECTS (1 << 9) -/** - * The stream is stored in the file as an attached picture/"cover art" (e.g. - * APIC frame in ID3v2). The first (usually only) packet associated with it - * will be returned among the first few packets read from the file unless - * seeking takes place. It can also be accessed at any time in - * AVStream.attached_pic. - */ -#define AV_DISPOSITION_ATTACHED_PIC (1 << 10) -/** - * The stream is sparse, and contains thumbnail images, often corresponding - * to chapter markers. Only ever used with AV_DISPOSITION_ATTACHED_PIC. - */ -#define AV_DISPOSITION_TIMED_THUMBNAILS (1 << 11) - -/** - * The stream is intended to be mixed with a spatial audio track. For example, - * it could be used for narration or stereo music, and may remain unchanged by - * listener head rotation. - */ -#define AV_DISPOSITION_NON_DIEGETIC (1 << 12) - -/** - * The subtitle stream contains captions, providing a transcription and possibly - * a translation of audio. Typically intended for hearing-impaired audiences. - */ -#define AV_DISPOSITION_CAPTIONS (1 << 16) -/** - * The subtitle stream contains a textual description of the video content. - * Typically intended for visually-impaired audiences or for the cases where the - * video cannot be seen. - */ -#define AV_DISPOSITION_DESCRIPTIONS (1 << 17) -/** - * The subtitle stream contains time-aligned metadata that is not intended to be - * directly presented to the user. - */ -#define AV_DISPOSITION_METADATA (1 << 18) -/** - * The audio stream is intended to be mixed with another stream before - * presentation. - * Corresponds to mix_type=0 in mpegts. - */ -#define AV_DISPOSITION_DEPENDENT (1 << 19) -/** - * The video stream contains still images. - */ -#define AV_DISPOSITION_STILL_IMAGE (1 << 20) - -/** - * @return The AV_DISPOSITION_* flag corresponding to disp or a negative error - * code if disp does not correspond to a known stream disposition. - */ -int av_disposition_from_string(const char *disp); - -/** - * @param disposition a combination of AV_DISPOSITION_* values - * @return The string description corresponding to the lowest set bit in - * disposition. NULL when the lowest set bit does not correspond - * to a known disposition or when disposition is 0. - */ -const char *av_disposition_to_string(int disposition); - -/** - * Options for behavior on timestamp wrap detection. - */ -#define AV_PTS_WRAP_IGNORE 0 ///< ignore the wrap -#define AV_PTS_WRAP_ADD_OFFSET 1 ///< add the format specific offset on wrap detection -#define AV_PTS_WRAP_SUB_OFFSET -1 ///< subtract the format specific offset on wrap detection - -/** - * Stream structure. - * New fields can be added to the end with minor version bumps. - * Removal, reordering and changes to existing fields require a major - * version bump. - * sizeof(AVStream) must not be used outside libav*. - */ -typedef struct AVStream { - /** - * A class for @ref avoptions. Set on stream creation. - */ - const AVClass *av_class; - - int index; /**< stream index in AVFormatContext */ - /** - * Format-specific stream ID. - * decoding: set by libavformat - * encoding: set by the user, replaced by libavformat if left unset - */ - int id; - - /** - * Codec parameters associated with this stream. Allocated and freed by - * libavformat in avformat_new_stream() and avformat_free_context() - * respectively. - * - * - demuxing: filled by libavformat on stream creation or in - * avformat_find_stream_info() - * - muxing: filled by the caller before avformat_write_header() - */ - AVCodecParameters *codecpar; - - void *priv_data; - - /** - * This is the fundamental unit of time (in seconds) in terms - * of which frame timestamps are represented. - * - * decoding: set by libavformat - * encoding: May be set by the caller before avformat_write_header() to - * provide a hint to the muxer about the desired timebase. In - * avformat_write_header(), the muxer will overwrite this field - * with the timebase that will actually be used for the timestamps - * written into the file (which may or may not be related to the - * user-provided one, depending on the format). - */ - AVRational time_base; - - /** - * Decoding: pts of the first frame of the stream in presentation order, in stream time base. - * Only set this if you are absolutely 100% sure that the value you set - * it to really is the pts of the first frame. - * This may be undefined (AV_NOPTS_VALUE). - * @note The ASF header does NOT contain a correct start_time the ASF - * demuxer must NOT set this. - */ - int64_t start_time; - - /** - * Decoding: duration of the stream, in stream time base. - * If a source file does not specify a duration, but does specify - * a bitrate, this value will be estimated from bitrate and file size. - * - * Encoding: May be set by the caller before avformat_write_header() to - * provide a hint to the muxer about the estimated duration. - */ - int64_t duration; - - int64_t nb_frames; ///< number of frames in this stream if known or 0 - - /** - * Stream disposition - a combination of AV_DISPOSITION_* flags. - * - demuxing: set by libavformat when creating the stream or in - * avformat_find_stream_info(). - * - muxing: may be set by the caller before avformat_write_header(). - */ - int disposition; - - enum AVDiscard discard; ///< Selects which packets can be discarded at will and do not need to be demuxed. - - /** - * sample aspect ratio (0 if unknown) - * - encoding: Set by user. - * - decoding: Set by libavformat. - */ - AVRational sample_aspect_ratio; - - AVDictionary *metadata; - - /** - * Average framerate - * - * - demuxing: May be set by libavformat when creating the stream or in - * avformat_find_stream_info(). - * - muxing: May be set by the caller before avformat_write_header(). - */ - AVRational avg_frame_rate; - - /** - * For streams with AV_DISPOSITION_ATTACHED_PIC disposition, this packet - * will contain the attached picture. - * - * decoding: set by libavformat, must not be modified by the caller. - * encoding: unused - */ - AVPacket attached_pic; - -#if FF_API_AVSTREAM_SIDE_DATA - /** - * An array of side data that applies to the whole stream (i.e. the - * container does not allow it to change between packets). - * - * There may be no overlap between the side data in this array and side data - * in the packets. I.e. a given side data is either exported by the muxer - * (demuxing) / set by the caller (muxing) in this array, then it never - * appears in the packets, or the side data is exported / sent through - * the packets (always in the first packet where the value becomes known or - * changes), then it does not appear in this array. - * - * - demuxing: Set by libavformat when the stream is created. - * - muxing: May be set by the caller before avformat_write_header(). - * - * Freed by libavformat in avformat_free_context(). - * - * @deprecated use AVStream's @ref AVCodecParameters.coded_side_data - * "codecpar side data". - */ - attribute_deprecated - AVPacketSideData *side_data; - /** - * The number of elements in the AVStream.side_data array. - * - * @deprecated use AVStream's @ref AVCodecParameters.nb_coded_side_data - * "codecpar side data". - */ - attribute_deprecated - int nb_side_data; -#endif - - /** - * Flags indicating events happening on the stream, a combination of - * AVSTREAM_EVENT_FLAG_*. - * - * - demuxing: may be set by the demuxer in avformat_open_input(), - * avformat_find_stream_info() and av_read_frame(). Flags must be cleared - * by the user once the event has been handled. - * - muxing: may be set by the user after avformat_write_header(). to - * indicate a user-triggered event. The muxer will clear the flags for - * events it has handled in av_[interleaved]_write_frame(). - */ - int event_flags; -/** - * - demuxing: the demuxer read new metadata from the file and updated - * AVStream.metadata accordingly - * - muxing: the user updated AVStream.metadata and wishes the muxer to write - * it into the file - */ -#define AVSTREAM_EVENT_FLAG_METADATA_UPDATED 0x0001 -/** - * - demuxing: new packets for this stream were read from the file. This - * event is informational only and does not guarantee that new packets - * for this stream will necessarily be returned from av_read_frame(). - */ -#define AVSTREAM_EVENT_FLAG_NEW_PACKETS (1 << 1) - - /** - * Real base framerate of the stream. - * This is the lowest framerate with which all timestamps can be - * represented accurately (it is the least common multiple of all - * framerates in the stream). Note, this value is just a guess! - * For example, if the time base is 1/90000 and all frames have either - * approximately 3600 or 1800 timer ticks, then r_frame_rate will be 50/1. - */ - AVRational r_frame_rate; - - /** - * Number of bits in timestamps. Used for wrapping control. - * - * - demuxing: set by libavformat - * - muxing: set by libavformat - * - */ - int pts_wrap_bits; -} AVStream; - -struct AVCodecParserContext *av_stream_get_parser(const AVStream *s); - -#if FF_API_GET_END_PTS -/** - * Returns the pts of the last muxed packet + its duration - * - * the retuned value is undefined when used with a demuxer. - */ -attribute_deprecated -int64_t av_stream_get_end_pts(const AVStream *st); -#endif - -#define AV_PROGRAM_RUNNING 1 - -/** - * New fields can be added to the end with minor version bumps. - * Removal, reordering and changes to existing fields require a major - * version bump. - * sizeof(AVProgram) must not be used outside libav*. - */ -typedef struct AVProgram { - int id; - int flags; - enum AVDiscard discard; ///< selects which program to discard and which to feed to the caller - unsigned int *stream_index; - unsigned int nb_stream_indexes; - AVDictionary *metadata; - - int program_num; - int pmt_pid; - int pcr_pid; - int pmt_version; - - /***************************************************************** - * All fields below this line are not part of the public API. They - * may not be used outside of libavformat and can be changed and - * removed at will. - * New public fields should be added right above. - ***************************************************************** - */ - int64_t start_time; - int64_t end_time; - - int64_t pts_wrap_reference; ///< reference dts for wrap detection - int pts_wrap_behavior; ///< behavior on wrap detection -} AVProgram; - -#define AVFMTCTX_NOHEADER 0x0001 /**< signal that no header is present - (streams are added dynamically) */ -#define AVFMTCTX_UNSEEKABLE 0x0002 /**< signal that the stream is definitely - not seekable, and attempts to call the - seek function will fail. For some - network protocols (e.g. HLS), this can - change dynamically at runtime. */ - -typedef struct AVChapter { - int64_t id; ///< unique ID to identify the chapter - AVRational time_base; ///< time base in which the start/end timestamps are specified - int64_t start, end; ///< chapter start/end time in time_base units - AVDictionary *metadata; -} AVChapter; - - -/** - * Callback used by devices to communicate with application. - */ -typedef int (*av_format_control_message)(struct AVFormatContext *s, int type, - void *data, size_t data_size); - -typedef int (*AVOpenCallback)(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, - const AVIOInterruptCB *int_cb, AVDictionary **options); - -/** - * The duration of a video can be estimated through various ways, and this enum can be used - * to know how the duration was estimated. - */ -enum AVDurationEstimationMethod { - AVFMT_DURATION_FROM_PTS, ///< Duration accurately estimated from PTSes - AVFMT_DURATION_FROM_STREAM, ///< Duration estimated from a stream with a known duration - AVFMT_DURATION_FROM_BITRATE ///< Duration estimated from bitrate (less accurate) -}; - -/** - * Format I/O context. - * New fields can be added to the end with minor version bumps. - * Removal, reordering and changes to existing fields require a major - * version bump. - * sizeof(AVFormatContext) must not be used outside libav*, use - * avformat_alloc_context() to create an AVFormatContext. - * - * Fields can be accessed through AVOptions (av_opt*), - * the name string used matches the associated command line parameter name and - * can be found in libavformat/options_table.h. - * The AVOption/command line parameter names differ in some cases from the C - * structure field names for historic reasons or brevity. - */ -typedef struct AVFormatContext { - /** - * A class for logging and @ref avoptions. Set by avformat_alloc_context(). - * Exports (de)muxer private options if they exist. - */ - const AVClass *av_class; - - /** - * The input container format. - * - * Demuxing only, set by avformat_open_input(). - */ - const struct AVInputFormat *iformat; - - /** - * The output container format. - * - * Muxing only, must be set by the caller before avformat_write_header(). - */ - const struct AVOutputFormat *oformat; - - /** - * Format private data. This is an AVOptions-enabled struct - * if and only if iformat/oformat.priv_class is not NULL. - * - * - muxing: set by avformat_write_header() - * - demuxing: set by avformat_open_input() - */ - void *priv_data; - - /** - * I/O context. - * - * - demuxing: either set by the user before avformat_open_input() (then - * the user must close it manually) or set by avformat_open_input(). - * - muxing: set by the user before avformat_write_header(). The caller must - * take care of closing / freeing the IO context. - * - * Do NOT set this field if AVFMT_NOFILE flag is set in - * iformat/oformat.flags. In such a case, the (de)muxer will handle - * I/O in some other way and this field will be NULL. - */ - AVIOContext *pb; - - /* stream info */ - /** - * Flags signalling stream properties. A combination of AVFMTCTX_*. - * Set by libavformat. - */ - int ctx_flags; - - /** - * Number of elements in AVFormatContext.streams. - * - * Set by avformat_new_stream(), must not be modified by any other code. - */ - unsigned int nb_streams; - /** - * A list of all streams in the file. New streams are created with - * avformat_new_stream(). - * - * - demuxing: streams are created by libavformat in avformat_open_input(). - * If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams may also - * appear in av_read_frame(). - * - muxing: streams are created by the user before avformat_write_header(). - * - * Freed by libavformat in avformat_free_context(). - */ - AVStream **streams; - - /** - * input or output URL. Unlike the old filename field, this field has no - * length restriction. - * - * - demuxing: set by avformat_open_input(), initialized to an empty - * string if url parameter was NULL in avformat_open_input(). - * - muxing: may be set by the caller before calling avformat_write_header() - * (or avformat_init_output() if that is called first) to a string - * which is freeable by av_free(). Set to an empty string if it - * was NULL in avformat_init_output(). - * - * Freed by libavformat in avformat_free_context(). - */ - char *url; - - /** - * Position of the first frame of the component, in - * AV_TIME_BASE fractional seconds. NEVER set this value directly: - * It is deduced from the AVStream values. - * - * Demuxing only, set by libavformat. - */ - int64_t start_time; - - /** - * Duration of the stream, in AV_TIME_BASE fractional - * seconds. Only set this value if you know none of the individual stream - * durations and also do not set any of them. This is deduced from the - * AVStream values if not set. - * - * Demuxing only, set by libavformat. - */ - int64_t duration; - - /** - * Total stream bitrate in bit/s, 0 if not - * available. Never set it directly if the file_size and the - * duration are known as FFmpeg can compute it automatically. - */ - int64_t bit_rate; - - unsigned int packet_size; - int max_delay; - - /** - * Flags modifying the (de)muxer behaviour. A combination of AVFMT_FLAG_*. - * Set by the user before avformat_open_input() / avformat_write_header(). - */ - int flags; -#define AVFMT_FLAG_GENPTS 0x0001 ///< Generate missing pts even if it requires parsing future frames. -#define AVFMT_FLAG_IGNIDX 0x0002 ///< Ignore index. -#define AVFMT_FLAG_NONBLOCK 0x0004 ///< Do not block when reading packets from input. -#define AVFMT_FLAG_IGNDTS 0x0008 ///< Ignore DTS on frames that contain both DTS & PTS -#define AVFMT_FLAG_NOFILLIN 0x0010 ///< Do not infer any values from other values, just return what is stored in the container -#define AVFMT_FLAG_NOPARSE 0x0020 ///< Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled -#define AVFMT_FLAG_NOBUFFER 0x0040 ///< Do not buffer frames when possible -#define AVFMT_FLAG_CUSTOM_IO 0x0080 ///< The caller has supplied a custom AVIOContext, don't avio_close() it. -#define AVFMT_FLAG_DISCARD_CORRUPT 0x0100 ///< Discard frames marked corrupted -#define AVFMT_FLAG_FLUSH_PACKETS 0x0200 ///< Flush the AVIOContext every packet. -/** - * When muxing, try to avoid writing any random/volatile data to the output. - * This includes any random IDs, real-time timestamps/dates, muxer version, etc. - * - * This flag is mainly intended for testing. - */ -#define AVFMT_FLAG_BITEXACT 0x0400 -#define AVFMT_FLAG_SORT_DTS 0x10000 ///< try to interleave outputted packets by dts (using this flag can slow demuxing down) -#define AVFMT_FLAG_FAST_SEEK 0x80000 ///< Enable fast, but inaccurate seeks for some formats -#if FF_API_LAVF_SHORTEST -#define AVFMT_FLAG_SHORTEST 0x100000 ///< Stop muxing when the shortest stream stops. -#endif -#define AVFMT_FLAG_AUTO_BSF 0x200000 ///< Add bitstream filters as requested by the muxer - - /** - * Maximum number of bytes read from input in order to determine stream - * properties. Used when reading the global header and in - * avformat_find_stream_info(). - * - * Demuxing only, set by the caller before avformat_open_input(). - * - * @note this is \e not used for determining the \ref AVInputFormat - * "input format" - * @sa format_probesize - */ - int64_t probesize; - - /** - * Maximum duration (in AV_TIME_BASE units) of the data read - * from input in avformat_find_stream_info(). - * Demuxing only, set by the caller before avformat_find_stream_info(). - * Can be set to 0 to let avformat choose using a heuristic. - */ - int64_t max_analyze_duration; - - const uint8_t *key; - int keylen; - - unsigned int nb_programs; - AVProgram **programs; - - /** - * Forced video codec_id. - * Demuxing: Set by user. - */ - enum AVCodecID video_codec_id; - - /** - * Forced audio codec_id. - * Demuxing: Set by user. - */ - enum AVCodecID audio_codec_id; - - /** - * Forced subtitle codec_id. - * Demuxing: Set by user. - */ - enum AVCodecID subtitle_codec_id; - - /** - * Maximum amount of memory in bytes to use for the index of each stream. - * If the index exceeds this size, entries will be discarded as - * needed to maintain a smaller size. This can lead to slower or less - * accurate seeking (depends on demuxer). - * Demuxers for which a full in-memory index is mandatory will ignore - * this. - * - muxing: unused - * - demuxing: set by user - */ - unsigned int max_index_size; - - /** - * Maximum amount of memory in bytes to use for buffering frames - * obtained from realtime capture devices. - */ - unsigned int max_picture_buffer; - - /** - * Number of chapters in AVChapter array. - * When muxing, chapters are normally written in the file header, - * so nb_chapters should normally be initialized before write_header - * is called. Some muxers (e.g. mov and mkv) can also write chapters - * in the trailer. To write chapters in the trailer, nb_chapters - * must be zero when write_header is called and non-zero when - * write_trailer is called. - * - muxing: set by user - * - demuxing: set by libavformat - */ - unsigned int nb_chapters; - AVChapter **chapters; - - /** - * Metadata that applies to the whole file. - * - * - demuxing: set by libavformat in avformat_open_input() - * - muxing: may be set by the caller before avformat_write_header() - * - * Freed by libavformat in avformat_free_context(). - */ - AVDictionary *metadata; - - /** - * Start time of the stream in real world time, in microseconds - * since the Unix epoch (00:00 1st January 1970). That is, pts=0 in the - * stream was captured at this real world time. - * - muxing: Set by the caller before avformat_write_header(). If set to - * either 0 or AV_NOPTS_VALUE, then the current wall-time will - * be used. - * - demuxing: Set by libavformat. AV_NOPTS_VALUE if unknown. Note that - * the value may become known after some number of frames - * have been received. - */ - int64_t start_time_realtime; - - /** - * The number of frames used for determining the framerate in - * avformat_find_stream_info(). - * Demuxing only, set by the caller before avformat_find_stream_info(). - */ - int fps_probe_size; - - /** - * Error recognition; higher values will detect more errors but may - * misdetect some more or less valid parts as errors. - * Demuxing only, set by the caller before avformat_open_input(). - */ - int error_recognition; - - /** - * Custom interrupt callbacks for the I/O layer. - * - * demuxing: set by the user before avformat_open_input(). - * muxing: set by the user before avformat_write_header() - * (mainly useful for AVFMT_NOFILE formats). The callback - * should also be passed to avio_open2() if it's used to - * open the file. - */ - AVIOInterruptCB interrupt_callback; - - /** - * Flags to enable debugging. - */ - int debug; -#define FF_FDEBUG_TS 0x0001 - - /** - * Maximum buffering duration for interleaving. - * - * To ensure all the streams are interleaved correctly, - * av_interleaved_write_frame() will wait until it has at least one packet - * for each stream before actually writing any packets to the output file. - * When some streams are "sparse" (i.e. there are large gaps between - * successive packets), this can result in excessive buffering. - * - * This field specifies the maximum difference between the timestamps of the - * first and the last packet in the muxing queue, above which libavformat - * will output a packet regardless of whether it has queued a packet for all - * the streams. - * - * Muxing only, set by the caller before avformat_write_header(). - */ - int64_t max_interleave_delta; - - /** - * Allow non-standard and experimental extension - * @see AVCodecContext.strict_std_compliance - */ - int strict_std_compliance; - - /** - * Flags indicating events happening on the file, a combination of - * AVFMT_EVENT_FLAG_*. - * - * - demuxing: may be set by the demuxer in avformat_open_input(), - * avformat_find_stream_info() and av_read_frame(). Flags must be cleared - * by the user once the event has been handled. - * - muxing: may be set by the user after avformat_write_header() to - * indicate a user-triggered event. The muxer will clear the flags for - * events it has handled in av_[interleaved]_write_frame(). - */ - int event_flags; -/** - * - demuxing: the demuxer read new metadata from the file and updated - * AVFormatContext.metadata accordingly - * - muxing: the user updated AVFormatContext.metadata and wishes the muxer to - * write it into the file - */ -#define AVFMT_EVENT_FLAG_METADATA_UPDATED 0x0001 - - /** - * Maximum number of packets to read while waiting for the first timestamp. - * Decoding only. - */ - int max_ts_probe; - - /** - * Avoid negative timestamps during muxing. - * Any value of the AVFMT_AVOID_NEG_TS_* constants. - * Note, this works better when using av_interleaved_write_frame(). - * - muxing: Set by user - * - demuxing: unused - */ - int avoid_negative_ts; -#define AVFMT_AVOID_NEG_TS_AUTO -1 ///< Enabled when required by target format -#define AVFMT_AVOID_NEG_TS_DISABLED 0 ///< Do not shift timestamps even when they are negative. -#define AVFMT_AVOID_NEG_TS_MAKE_NON_NEGATIVE 1 ///< Shift timestamps so they are non negative -#define AVFMT_AVOID_NEG_TS_MAKE_ZERO 2 ///< Shift timestamps so that they start at 0 - - /** - * Transport stream id. - * This will be moved into demuxer private options. Thus no API/ABI compatibility - */ - int ts_id; - - /** - * Audio preload in microseconds. - * Note, not all formats support this and unpredictable things may happen if it is used when not supported. - * - encoding: Set by user - * - decoding: unused - */ - int audio_preload; - - /** - * Max chunk time in microseconds. - * Note, not all formats support this and unpredictable things may happen if it is used when not supported. - * - encoding: Set by user - * - decoding: unused - */ - int max_chunk_duration; - - /** - * Max chunk size in bytes - * Note, not all formats support this and unpredictable things may happen if it is used when not supported. - * - encoding: Set by user - * - decoding: unused - */ - int max_chunk_size; - - /** - * forces the use of wallclock timestamps as pts/dts of packets - * This has undefined results in the presence of B frames. - * - encoding: unused - * - decoding: Set by user - */ - int use_wallclock_as_timestamps; - - /** - * avio flags, used to force AVIO_FLAG_DIRECT. - * - encoding: unused - * - decoding: Set by user - */ - int avio_flags; - - /** - * The duration field can be estimated through various ways, and this field can be used - * to know how the duration was estimated. - * - encoding: unused - * - decoding: Read by user - */ - enum AVDurationEstimationMethod duration_estimation_method; - - /** - * Skip initial bytes when opening stream - * - encoding: unused - * - decoding: Set by user - */ - int64_t skip_initial_bytes; - - /** - * Correct single timestamp overflows - * - encoding: unused - * - decoding: Set by user - */ - unsigned int correct_ts_overflow; - - /** - * Force seeking to any (also non key) frames. - * - encoding: unused - * - decoding: Set by user - */ - int seek2any; - - /** - * Flush the I/O context after each packet. - * - encoding: Set by user - * - decoding: unused - */ - int flush_packets; - - /** - * format probing score. - * The maximal score is AVPROBE_SCORE_MAX, its set when the demuxer probes - * the format. - * - encoding: unused - * - decoding: set by avformat, read by user - */ - int probe_score; - - /** - * Maximum number of bytes read from input in order to identify the - * \ref AVInputFormat "input format". Only used when the format is not set - * explicitly by the caller. - * - * Demuxing only, set by the caller before avformat_open_input(). - * - * @sa probesize - */ - int format_probesize; - - /** - * ',' separated list of allowed decoders. - * If NULL then all are allowed - * - encoding: unused - * - decoding: set by user - */ - char *codec_whitelist; - - /** - * ',' separated list of allowed demuxers. - * If NULL then all are allowed - * - encoding: unused - * - decoding: set by user - */ - char *format_whitelist; - - /** - * IO repositioned flag. - * This is set by avformat when the underlaying IO context read pointer - * is repositioned, for example when doing byte based seeking. - * Demuxers can use the flag to detect such changes. - */ - int io_repositioned; - - /** - * Forced video codec. - * This allows forcing a specific decoder, even when there are multiple with - * the same codec_id. - * Demuxing: Set by user - */ - const struct AVCodec *video_codec; - - /** - * Forced audio codec. - * This allows forcing a specific decoder, even when there are multiple with - * the same codec_id. - * Demuxing: Set by user - */ - const struct AVCodec *audio_codec; - - /** - * Forced subtitle codec. - * This allows forcing a specific decoder, even when there are multiple with - * the same codec_id. - * Demuxing: Set by user - */ - const struct AVCodec *subtitle_codec; - - /** - * Forced data codec. - * This allows forcing a specific decoder, even when there are multiple with - * the same codec_id. - * Demuxing: Set by user - */ - const struct AVCodec *data_codec; - - /** - * Number of bytes to be written as padding in a metadata header. - * Demuxing: Unused. - * Muxing: Set by user via av_format_set_metadata_header_padding. - */ - int metadata_header_padding; - - /** - * User data. - * This is a place for some private data of the user. - */ - void *opaque; - - /** - * Callback used by devices to communicate with application. - */ - av_format_control_message control_message_cb; - - /** - * Output timestamp offset, in microseconds. - * Muxing: set by user - */ - int64_t output_ts_offset; - - /** - * dump format separator. - * can be ", " or "\n " or anything else - * - muxing: Set by user. - * - demuxing: Set by user. - */ - uint8_t *dump_separator; - - /** - * Forced Data codec_id. - * Demuxing: Set by user. - */ - enum AVCodecID data_codec_id; - - /** - * ',' separated list of allowed protocols. - * - encoding: unused - * - decoding: set by user - */ - char *protocol_whitelist; - - /** - * A callback for opening new IO streams. - * - * Whenever a muxer or a demuxer needs to open an IO stream (typically from - * avformat_open_input() for demuxers, but for certain formats can happen at - * other times as well), it will call this callback to obtain an IO context. - * - * @param s the format context - * @param pb on success, the newly opened IO context should be returned here - * @param url the url to open - * @param flags a combination of AVIO_FLAG_* - * @param options a dictionary of additional options, with the same - * semantics as in avio_open2() - * @return 0 on success, a negative AVERROR code on failure - * - * @note Certain muxers and demuxers do nesting, i.e. they open one or more - * additional internal format contexts. Thus the AVFormatContext pointer - * passed to this callback may be different from the one facing the caller. - * It will, however, have the same 'opaque' field. - */ - int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url, - int flags, AVDictionary **options); - -#if FF_API_AVFORMAT_IO_CLOSE - /** - * A callback for closing the streams opened with AVFormatContext.io_open(). - * - * @deprecated use io_close2 - */ - attribute_deprecated - void (*io_close)(struct AVFormatContext *s, AVIOContext *pb); -#endif - - /** - * ',' separated list of disallowed protocols. - * - encoding: unused - * - decoding: set by user - */ - char *protocol_blacklist; - - /** - * The maximum number of streams. - * - encoding: unused - * - decoding: set by user - */ - int max_streams; - - /** - * Skip duration calcuation in estimate_timings_from_pts. - * - encoding: unused - * - decoding: set by user - */ - int skip_estimate_duration_from_pts; - - /** - * Maximum number of packets that can be probed - * - encoding: unused - * - decoding: set by user - */ - int max_probe_packets; - - /** - * A callback for closing the streams opened with AVFormatContext.io_open(). - * - * Using this is preferred over io_close, because this can return an error. - * Therefore this callback is used instead of io_close by the generic - * libavformat code if io_close is NULL or the default. - * - * @param s the format context - * @param pb IO context to be closed and freed - * @return 0 on success, a negative AVERROR code on failure - */ - int (*io_close2)(struct AVFormatContext *s, AVIOContext *pb); -} AVFormatContext; - -/** - * This function will cause global side data to be injected in the next packet - * of each stream as well as after any subsequent seek. - * - * @note global side data is always available in every AVStream's - * @ref AVCodecParameters.coded_side_data "codecpar side data" array, and - * in a @ref AVCodecContext.coded_side_data "decoder's side data" array if - * initialized with said stream's codecpar. - * @see av_packet_side_data_get() - */ -void av_format_inject_global_side_data(AVFormatContext *s); - -/** - * Returns the method used to set ctx->duration. - * - * @return AVFMT_DURATION_FROM_PTS, AVFMT_DURATION_FROM_STREAM, or AVFMT_DURATION_FROM_BITRATE. - */ -enum AVDurationEstimationMethod av_fmt_ctx_get_duration_estimation_method(const AVFormatContext* ctx); - -/** - * @defgroup lavf_core Core functions - * @ingroup libavf - * - * Functions for querying libavformat capabilities, allocating core structures, - * etc. - * @{ - */ - -/** - * Return the LIBAVFORMAT_VERSION_INT constant. - */ -unsigned avformat_version(void); - -/** - * Return the libavformat build-time configuration. - */ -const char *avformat_configuration(void); - -/** - * Return the libavformat license. - */ -const char *avformat_license(void); - -/** - * Do global initialization of network libraries. This is optional, - * and not recommended anymore. - * - * This functions only exists to work around thread-safety issues - * with older GnuTLS or OpenSSL libraries. If libavformat is linked - * to newer versions of those libraries, or if you do not use them, - * calling this function is unnecessary. Otherwise, you need to call - * this function before any other threads using them are started. - * - * This function will be deprecated once support for older GnuTLS and - * OpenSSL libraries is removed, and this function has no purpose - * anymore. - */ -int avformat_network_init(void); - -/** - * Undo the initialization done by avformat_network_init. Call it only - * once for each time you called avformat_network_init. - */ -int avformat_network_deinit(void); - -/** - * Iterate over all registered muxers. - * - * @param opaque a pointer where libavformat will store the iteration state. Must - * point to NULL to start the iteration. - * - * @return the next registered muxer or NULL when the iteration is - * finished - */ -const AVOutputFormat *av_muxer_iterate(void **opaque); - -/** - * Iterate over all registered demuxers. - * - * @param opaque a pointer where libavformat will store the iteration state. - * Must point to NULL to start the iteration. - * - * @return the next registered demuxer or NULL when the iteration is - * finished - */ -const AVInputFormat *av_demuxer_iterate(void **opaque); - -/** - * Allocate an AVFormatContext. - * avformat_free_context() can be used to free the context and everything - * allocated by the framework within it. - */ -AVFormatContext *avformat_alloc_context(void); - -/** - * Free an AVFormatContext and all its streams. - * @param s context to free - */ -void avformat_free_context(AVFormatContext *s); - -/** - * Get the AVClass for AVFormatContext. It can be used in combination with - * AV_OPT_SEARCH_FAKE_OBJ for examining options. - * - * @see av_opt_find(). - */ -const AVClass *avformat_get_class(void); - -/** - * Get the AVClass for AVStream. It can be used in combination with - * AV_OPT_SEARCH_FAKE_OBJ for examining options. - * - * @see av_opt_find(). - */ -const AVClass *av_stream_get_class(void); - -/** - * Add a new stream to a media file. - * - * When demuxing, it is called by the demuxer in read_header(). If the - * flag AVFMTCTX_NOHEADER is set in s.ctx_flags, then it may also - * be called in read_packet(). - * - * When muxing, should be called by the user before avformat_write_header(). - * - * User is required to call avformat_free_context() to clean up the allocation - * by avformat_new_stream(). - * - * @param s media file handle - * @param c unused, does nothing - * - * @return newly created stream or NULL on error. - */ -AVStream *avformat_new_stream(AVFormatContext *s, const struct AVCodec *c); - -#if FF_API_AVSTREAM_SIDE_DATA -/** - * Wrap an existing array as stream side data. - * - * @param st stream - * @param type side information type - * @param data the side data array. It must be allocated with the av_malloc() - * family of functions. The ownership of the data is transferred to - * st. - * @param size side information size - * - * @return zero on success, a negative AVERROR code on failure. On failure, - * the stream is unchanged and the data remains owned by the caller. - * @deprecated use av_packet_side_data_add() with the stream's - * @ref AVCodecParameters.coded_side_data "codecpar side data" - */ -attribute_deprecated -int av_stream_add_side_data(AVStream *st, enum AVPacketSideDataType type, - uint8_t *data, size_t size); - -/** - * Allocate new information from stream. - * - * @param stream stream - * @param type desired side information type - * @param size side information size - * - * @return pointer to fresh allocated data or NULL otherwise - * @deprecated use av_packet_side_data_new() with the stream's - * @ref AVCodecParameters.coded_side_data "codecpar side data" - */ -attribute_deprecated -uint8_t *av_stream_new_side_data(AVStream *stream, - enum AVPacketSideDataType type, size_t size); -/** - * Get side information from stream. - * - * @param stream stream - * @param type desired side information type - * @param size If supplied, *size will be set to the size of the side data - * or to zero if the desired side data is not present. - * - * @return pointer to data if present or NULL otherwise - * @deprecated use av_packet_side_data_get() with the stream's - * @ref AVCodecParameters.coded_side_data "codecpar side data" - */ -attribute_deprecated -uint8_t *av_stream_get_side_data(const AVStream *stream, - enum AVPacketSideDataType type, size_t *size); -#endif - -AVProgram *av_new_program(AVFormatContext *s, int id); - -/** - * @} - */ - - -/** - * Allocate an AVFormatContext for an output format. - * avformat_free_context() can be used to free the context and - * everything allocated by the framework within it. - * - * @param ctx pointee is set to the created format context, - * or to NULL in case of failure - * @param oformat format to use for allocating the context, if NULL - * format_name and filename are used instead - * @param format_name the name of output format to use for allocating the - * context, if NULL filename is used instead - * @param filename the name of the filename to use for allocating the - * context, may be NULL - * - * @return >= 0 in case of success, a negative AVERROR code in case of - * failure - */ -int avformat_alloc_output_context2(AVFormatContext **ctx, const AVOutputFormat *oformat, - const char *format_name, const char *filename); - -/** - * @addtogroup lavf_decoding - * @{ - */ - -/** - * Find AVInputFormat based on the short name of the input format. - */ -const AVInputFormat *av_find_input_format(const char *short_name); - -/** - * Guess the file format. - * - * @param pd data to be probed - * @param is_opened Whether the file is already opened; determines whether - * demuxers with or without AVFMT_NOFILE are probed. - */ -const AVInputFormat *av_probe_input_format(const AVProbeData *pd, int is_opened); - -/** - * Guess the file format. - * - * @param pd data to be probed - * @param is_opened Whether the file is already opened; determines whether - * demuxers with or without AVFMT_NOFILE are probed. - * @param score_max A probe score larger that this is required to accept a - * detection, the variable is set to the actual detection - * score afterwards. - * If the score is <= AVPROBE_SCORE_MAX / 4 it is recommended - * to retry with a larger probe buffer. - */ -const AVInputFormat *av_probe_input_format2(const AVProbeData *pd, - int is_opened, int *score_max); - -/** - * Guess the file format. - * - * @param is_opened Whether the file is already opened; determines whether - * demuxers with or without AVFMT_NOFILE are probed. - * @param score_ret The score of the best detection. - */ -const AVInputFormat *av_probe_input_format3(const AVProbeData *pd, - int is_opened, int *score_ret); - -/** - * Probe a bytestream to determine the input format. Each time a probe returns - * with a score that is too low, the probe buffer size is increased and another - * attempt is made. When the maximum probe size is reached, the input format - * with the highest score is returned. - * - * @param pb the bytestream to probe - * @param fmt the input format is put here - * @param url the url of the stream - * @param logctx the log context - * @param offset the offset within the bytestream to probe from - * @param max_probe_size the maximum probe buffer size (zero for default) - * - * @return the score in case of success, a negative value corresponding to an - * the maximal score is AVPROBE_SCORE_MAX - * AVERROR code otherwise - */ -int av_probe_input_buffer2(AVIOContext *pb, const AVInputFormat **fmt, - const char *url, void *logctx, - unsigned int offset, unsigned int max_probe_size); - -/** - * Like av_probe_input_buffer2() but returns 0 on success - */ -int av_probe_input_buffer(AVIOContext *pb, const AVInputFormat **fmt, - const char *url, void *logctx, - unsigned int offset, unsigned int max_probe_size); - -/** - * Open an input stream and read the header. The codecs are not opened. - * The stream must be closed with avformat_close_input(). - * - * @param ps Pointer to user-supplied AVFormatContext (allocated by - * avformat_alloc_context). May be a pointer to NULL, in - * which case an AVFormatContext is allocated by this - * function and written into ps. - * Note that a user-supplied AVFormatContext will be freed - * on failure. - * @param url URL of the stream to open. - * @param fmt If non-NULL, this parameter forces a specific input format. - * Otherwise the format is autodetected. - * @param options A dictionary filled with AVFormatContext and demuxer-private - * options. - * On return this parameter will be destroyed and replaced with - * a dict containing options that were not found. May be NULL. - * - * @return 0 on success, a negative AVERROR on failure. - * - * @note If you want to use custom IO, preallocate the format context and set its pb field. - */ -int avformat_open_input(AVFormatContext **ps, const char *url, - const AVInputFormat *fmt, AVDictionary **options); - -/** - * Read packets of a media file to get stream information. This - * is useful for file formats with no headers such as MPEG. This - * function also computes the real framerate in case of MPEG-2 repeat - * frame mode. - * The logical file position is not changed by this function; - * examined packets may be buffered for later processing. - * - * @param ic media file handle - * @param options If non-NULL, an ic.nb_streams long array of pointers to - * dictionaries, where i-th member contains options for - * codec corresponding to i-th stream. - * On return each dictionary will be filled with options that were not found. - * @return >=0 if OK, AVERROR_xxx on error - * - * @note this function isn't guaranteed to open all the codecs, so - * options being non-empty at return is a perfectly normal behavior. - * - * @todo Let the user decide somehow what information is needed so that - * we do not waste time getting stuff the user does not need. - */ -int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options); - -/** - * Find the programs which belong to a given stream. - * - * @param ic media file handle - * @param last the last found program, the search will start after this - * program, or from the beginning if it is NULL - * @param s stream index - * - * @return the next program which belongs to s, NULL if no program is found or - * the last program is not among the programs of ic. - */ -AVProgram *av_find_program_from_stream(AVFormatContext *ic, AVProgram *last, int s); - -void av_program_add_stream_index(AVFormatContext *ac, int progid, unsigned int idx); - -/** - * Find the "best" stream in the file. - * The best stream is determined according to various heuristics as the most - * likely to be what the user expects. - * If the decoder parameter is non-NULL, av_find_best_stream will find the - * default decoder for the stream's codec; streams for which no decoder can - * be found are ignored. - * - * @param ic media file handle - * @param type stream type: video, audio, subtitles, etc. - * @param wanted_stream_nb user-requested stream number, - * or -1 for automatic selection - * @param related_stream try to find a stream related (eg. in the same - * program) to this one, or -1 if none - * @param decoder_ret if non-NULL, returns the decoder for the - * selected stream - * @param flags flags; none are currently defined - * - * @return the non-negative stream number in case of success, - * AVERROR_STREAM_NOT_FOUND if no stream with the requested type - * could be found, - * AVERROR_DECODER_NOT_FOUND if streams were found but no decoder - * - * @note If av_find_best_stream returns successfully and decoder_ret is not - * NULL, then *decoder_ret is guaranteed to be set to a valid AVCodec. - */ -int av_find_best_stream(AVFormatContext *ic, - enum AVMediaType type, - int wanted_stream_nb, - int related_stream, - const struct AVCodec **decoder_ret, - int flags); - -/** - * Return the next frame of a stream. - * This function returns what is stored in the file, and does not validate - * that what is there are valid frames for the decoder. It will split what is - * stored in the file into frames and return one for each call. It will not - * omit invalid data between valid frames so as to give the decoder the maximum - * information possible for decoding. - * - * On success, the returned packet is reference-counted (pkt->buf is set) and - * valid indefinitely. The packet must be freed with av_packet_unref() when - * it is no longer needed. For video, the packet contains exactly one frame. - * For audio, it contains an integer number of frames if each frame has - * a known fixed size (e.g. PCM or ADPCM data). If the audio frames have - * a variable size (e.g. MPEG audio), then it contains one frame. - * - * pkt->pts, pkt->dts and pkt->duration are always set to correct - * values in AVStream.time_base units (and guessed if the format cannot - * provide them). pkt->pts can be AV_NOPTS_VALUE if the video format - * has B-frames, so it is better to rely on pkt->dts if you do not - * decompress the payload. - * - * @return 0 if OK, < 0 on error or end of file. On error, pkt will be blank - * (as if it came from av_packet_alloc()). - * - * @note pkt will be initialized, so it may be uninitialized, but it must not - * contain data that needs to be freed. - */ -int av_read_frame(AVFormatContext *s, AVPacket *pkt); - -/** - * Seek to the keyframe at timestamp. - * 'timestamp' in 'stream_index'. - * - * @param s media file handle - * @param stream_index If stream_index is (-1), a default stream is selected, - * and timestamp is automatically converted from - * AV_TIME_BASE units to the stream specific time_base. - * @param timestamp Timestamp in AVStream.time_base units or, if no stream - * is specified, in AV_TIME_BASE units. - * @param flags flags which select direction and seeking mode - * - * @return >= 0 on success - */ -int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, - int flags); - -/** - * Seek to timestamp ts. - * Seeking will be done so that the point from which all active streams - * can be presented successfully will be closest to ts and within min/max_ts. - * Active streams are all streams that have AVStream.discard < AVDISCARD_ALL. - * - * If flags contain AVSEEK_FLAG_BYTE, then all timestamps are in bytes and - * are the file position (this may not be supported by all demuxers). - * If flags contain AVSEEK_FLAG_FRAME, then all timestamps are in frames - * in the stream with stream_index (this may not be supported by all demuxers). - * Otherwise all timestamps are in units of the stream selected by stream_index - * or if stream_index is -1, in AV_TIME_BASE units. - * If flags contain AVSEEK_FLAG_ANY, then non-keyframes are treated as - * keyframes (this may not be supported by all demuxers). - * If flags contain AVSEEK_FLAG_BACKWARD, it is ignored. - * - * @param s media file handle - * @param stream_index index of the stream which is used as time base reference - * @param min_ts smallest acceptable timestamp - * @param ts target timestamp - * @param max_ts largest acceptable timestamp - * @param flags flags - * @return >=0 on success, error code otherwise - * - * @note This is part of the new seek API which is still under construction. - */ -int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags); - -/** - * Discard all internally buffered data. This can be useful when dealing with - * discontinuities in the byte stream. Generally works only with formats that - * can resync. This includes headerless formats like MPEG-TS/TS but should also - * work with NUT, Ogg and in a limited way AVI for example. - * - * The set of streams, the detected duration, stream parameters and codecs do - * not change when calling this function. If you want a complete reset, it's - * better to open a new AVFormatContext. - * - * This does not flush the AVIOContext (s->pb). If necessary, call - * avio_flush(s->pb) before calling this function. - * - * @param s media file handle - * @return >=0 on success, error code otherwise - */ -int avformat_flush(AVFormatContext *s); - -/** - * Start playing a network-based stream (e.g. RTSP stream) at the - * current position. - */ -int av_read_play(AVFormatContext *s); - -/** - * Pause a network-based stream (e.g. RTSP stream). - * - * Use av_read_play() to resume it. - */ -int av_read_pause(AVFormatContext *s); - -/** - * Close an opened input AVFormatContext. Free it and all its contents - * and set *s to NULL. - */ -void avformat_close_input(AVFormatContext **s); -/** - * @} - */ - -#define AVSEEK_FLAG_BACKWARD 1 ///< seek backward -#define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes -#define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes -#define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number - -/** - * @addtogroup lavf_encoding - * @{ - */ - -#define AVSTREAM_INIT_IN_WRITE_HEADER 0 ///< stream parameters initialized in avformat_write_header -#define AVSTREAM_INIT_IN_INIT_OUTPUT 1 ///< stream parameters initialized in avformat_init_output - -/** - * Allocate the stream private data and write the stream header to - * an output media file. - * - * @param s Media file handle, must be allocated with - * avformat_alloc_context(). - * Its \ref AVFormatContext.oformat "oformat" field must be set - * to the desired output format; - * Its \ref AVFormatContext.pb "pb" field must be set to an - * already opened ::AVIOContext. - * @param options An ::AVDictionary filled with AVFormatContext and - * muxer-private options. - * On return this parameter will be destroyed and replaced with - * a dict containing options that were not found. May be NULL. - * - * @retval AVSTREAM_INIT_IN_WRITE_HEADER On success, if the codec had not already been - * fully initialized in avformat_init_output(). - * @retval AVSTREAM_INIT_IN_INIT_OUTPUT On success, if the codec had already been fully - * initialized in avformat_init_output(). - * @retval AVERROR A negative AVERROR on failure. - * - * @see av_opt_find, av_dict_set, avio_open, av_oformat_next, avformat_init_output. - */ -av_warn_unused_result -int avformat_write_header(AVFormatContext *s, AVDictionary **options); - -/** - * Allocate the stream private data and initialize the codec, but do not write the header. - * May optionally be used before avformat_write_header() to initialize stream parameters - * before actually writing the header. - * If using this function, do not pass the same options to avformat_write_header(). - * - * @param s Media file handle, must be allocated with - * avformat_alloc_context(). - * Its \ref AVFormatContext.oformat "oformat" field must be set - * to the desired output format; - * Its \ref AVFormatContext.pb "pb" field must be set to an - * already opened ::AVIOContext. - * @param options An ::AVDictionary filled with AVFormatContext and - * muxer-private options. - * On return this parameter will be destroyed and replaced with - * a dict containing options that were not found. May be NULL. - * - * @retval AVSTREAM_INIT_IN_WRITE_HEADER On success, if the codec requires - * avformat_write_header to fully initialize. - * @retval AVSTREAM_INIT_IN_INIT_OUTPUT On success, if the codec has been fully - * initialized. - * @retval AVERROR Anegative AVERROR on failure. - * - * @see av_opt_find, av_dict_set, avio_open, av_oformat_next, avformat_write_header. - */ -av_warn_unused_result -int avformat_init_output(AVFormatContext *s, AVDictionary **options); - -/** - * Write a packet to an output media file. - * - * This function passes the packet directly to the muxer, without any buffering - * or reordering. The caller is responsible for correctly interleaving the - * packets if the format requires it. Callers that want libavformat to handle - * the interleaving should call av_interleaved_write_frame() instead of this - * function. - * - * @param s media file handle - * @param pkt The packet containing the data to be written. Note that unlike - * av_interleaved_write_frame(), this function does not take - * ownership of the packet passed to it (though some muxers may make - * an internal reference to the input packet). - *
- * This parameter can be NULL (at any time, not just at the end), in - * order to immediately flush data buffered within the muxer, for - * muxers that buffer up data internally before writing it to the - * output. - *
- * Packet's @ref AVPacket.stream_index "stream_index" field must be - * set to the index of the corresponding stream in @ref - * AVFormatContext.streams "s->streams". - *
- * The timestamps (@ref AVPacket.pts "pts", @ref AVPacket.dts "dts") - * must be set to correct values in the stream's timebase (unless the - * output format is flagged with the AVFMT_NOTIMESTAMPS flag, then - * they can be set to AV_NOPTS_VALUE). - * The dts for subsequent packets passed to this function must be strictly - * increasing when compared in their respective timebases (unless the - * output format is flagged with the AVFMT_TS_NONSTRICT, then they - * merely have to be nondecreasing). @ref AVPacket.duration - * "duration") should also be set if known. - * @return < 0 on error, = 0 if OK, 1 if flushed and there is no more data to flush - * - * @see av_interleaved_write_frame() - */ -int av_write_frame(AVFormatContext *s, AVPacket *pkt); - -/** - * Write a packet to an output media file ensuring correct interleaving. - * - * This function will buffer the packets internally as needed to make sure the - * packets in the output file are properly interleaved, usually ordered by - * increasing dts. Callers doing their own interleaving should call - * av_write_frame() instead of this function. - * - * Using this function instead of av_write_frame() can give muxers advance - * knowledge of future packets, improving e.g. the behaviour of the mp4 - * muxer for VFR content in fragmenting mode. - * - * @param s media file handle - * @param pkt The packet containing the data to be written. - *
- * If the packet is reference-counted, this function will take - * ownership of this reference and unreference it later when it sees - * fit. If the packet is not reference-counted, libavformat will - * make a copy. - * The returned packet will be blank (as if returned from - * av_packet_alloc()), even on error. - *
- * This parameter can be NULL (at any time, not just at the end), to - * flush the interleaving queues. - *
- * Packet's @ref AVPacket.stream_index "stream_index" field must be - * set to the index of the corresponding stream in @ref - * AVFormatContext.streams "s->streams". - *
- * The timestamps (@ref AVPacket.pts "pts", @ref AVPacket.dts "dts") - * must be set to correct values in the stream's timebase (unless the - * output format is flagged with the AVFMT_NOTIMESTAMPS flag, then - * they can be set to AV_NOPTS_VALUE). - * The dts for subsequent packets in one stream must be strictly - * increasing (unless the output format is flagged with the - * AVFMT_TS_NONSTRICT, then they merely have to be nondecreasing). - * @ref AVPacket.duration "duration" should also be set if known. - * - * @return 0 on success, a negative AVERROR on error. - * - * @see av_write_frame(), AVFormatContext.max_interleave_delta - */ -int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt); - -/** - * Write an uncoded frame to an output media file. - * - * The frame must be correctly interleaved according to the container - * specification; if not, av_interleaved_write_uncoded_frame() must be used. - * - * See av_interleaved_write_uncoded_frame() for details. - */ -int av_write_uncoded_frame(AVFormatContext *s, int stream_index, - struct AVFrame *frame); - -/** - * Write an uncoded frame to an output media file. - * - * If the muxer supports it, this function makes it possible to write an AVFrame - * structure directly, without encoding it into a packet. - * It is mostly useful for devices and similar special muxers that use raw - * video or PCM data and will not serialize it into a byte stream. - * - * To test whether it is possible to use it with a given muxer and stream, - * use av_write_uncoded_frame_query(). - * - * The caller gives up ownership of the frame and must not access it - * afterwards. - * - * @return >=0 for success, a negative code on error - */ -int av_interleaved_write_uncoded_frame(AVFormatContext *s, int stream_index, - struct AVFrame *frame); - -/** - * Test whether a muxer supports uncoded frame. - * - * @return >=0 if an uncoded frame can be written to that muxer and stream, - * <0 if not - */ -int av_write_uncoded_frame_query(AVFormatContext *s, int stream_index); - -/** - * Write the stream trailer to an output media file and free the - * file private data. - * - * May only be called after a successful call to avformat_write_header. - * - * @param s media file handle - * @return 0 if OK, AVERROR_xxx on error - */ -int av_write_trailer(AVFormatContext *s); - -/** - * Return the output format in the list of registered output formats - * which best matches the provided parameters, or return NULL if - * there is no match. - * - * @param short_name if non-NULL checks if short_name matches with the - * names of the registered formats - * @param filename if non-NULL checks if filename terminates with the - * extensions of the registered formats - * @param mime_type if non-NULL checks if mime_type matches with the - * MIME type of the registered formats - */ -const AVOutputFormat *av_guess_format(const char *short_name, - const char *filename, - const char *mime_type); - -/** - * Guess the codec ID based upon muxer and filename. - */ -enum AVCodecID av_guess_codec(const AVOutputFormat *fmt, const char *short_name, - const char *filename, const char *mime_type, - enum AVMediaType type); - -/** - * Get timing information for the data currently output. - * The exact meaning of "currently output" depends on the format. - * It is mostly relevant for devices that have an internal buffer and/or - * work in real time. - * @param s media file handle - * @param stream stream in the media file - * @param[out] dts DTS of the last packet output for the stream, in stream - * time_base units - * @param[out] wall absolute time when that packet whas output, - * in microsecond - * @retval 0 Success - * @retval AVERROR(ENOSYS) The format does not support it - * - * @note Some formats or devices may not allow to measure dts and wall - * atomically. - */ -int av_get_output_timestamp(struct AVFormatContext *s, int stream, - int64_t *dts, int64_t *wall); - - -/** - * @} - */ - - -/** - * @defgroup lavf_misc Utility functions - * @ingroup libavf - * @{ - * - * Miscellaneous utility functions related to both muxing and demuxing - * (or neither). - */ - -/** - * Send a nice hexadecimal dump of a buffer to the specified file stream. - * - * @param f The file stream pointer where the dump should be sent to. - * @param buf buffer - * @param size buffer size - * - * @see av_hex_dump_log, av_pkt_dump2, av_pkt_dump_log2 - */ -void av_hex_dump(FILE *f, const uint8_t *buf, int size); - -/** - * Send a nice hexadecimal dump of a buffer to the log. - * - * @param avcl A pointer to an arbitrary struct of which the first field is a - * pointer to an AVClass struct. - * @param level The importance level of the message, lower values signifying - * higher importance. - * @param buf buffer - * @param size buffer size - * - * @see av_hex_dump, av_pkt_dump2, av_pkt_dump_log2 - */ -void av_hex_dump_log(void *avcl, int level, const uint8_t *buf, int size); - -/** - * Send a nice dump of a packet to the specified file stream. - * - * @param f The file stream pointer where the dump should be sent to. - * @param pkt packet to dump - * @param dump_payload True if the payload must be displayed, too. - * @param st AVStream that the packet belongs to - */ -void av_pkt_dump2(FILE *f, const AVPacket *pkt, int dump_payload, const AVStream *st); - - -/** - * Send a nice dump of a packet to the log. - * - * @param avcl A pointer to an arbitrary struct of which the first field is a - * pointer to an AVClass struct. - * @param level The importance level of the message, lower values signifying - * higher importance. - * @param pkt packet to dump - * @param dump_payload True if the payload must be displayed, too. - * @param st AVStream that the packet belongs to - */ -void av_pkt_dump_log2(void *avcl, int level, const AVPacket *pkt, int dump_payload, - const AVStream *st); - -/** - * Get the AVCodecID for the given codec tag tag. - * If no codec id is found returns AV_CODEC_ID_NONE. - * - * @param tags list of supported codec_id-codec_tag pairs, as stored - * in AVInputFormat.codec_tag and AVOutputFormat.codec_tag - * @param tag codec tag to match to a codec ID - */ -enum AVCodecID av_codec_get_id(const struct AVCodecTag * const *tags, unsigned int tag); - -/** - * Get the codec tag for the given codec id id. - * If no codec tag is found returns 0. - * - * @param tags list of supported codec_id-codec_tag pairs, as stored - * in AVInputFormat.codec_tag and AVOutputFormat.codec_tag - * @param id codec ID to match to a codec tag - */ -unsigned int av_codec_get_tag(const struct AVCodecTag * const *tags, enum AVCodecID id); - -/** - * Get the codec tag for the given codec id. - * - * @param tags list of supported codec_id - codec_tag pairs, as stored - * in AVInputFormat.codec_tag and AVOutputFormat.codec_tag - * @param id codec id that should be searched for in the list - * @param tag A pointer to the found tag - * @return 0 if id was not found in tags, > 0 if it was found - */ -int av_codec_get_tag2(const struct AVCodecTag * const *tags, enum AVCodecID id, - unsigned int *tag); - -int av_find_default_stream_index(AVFormatContext *s); - -/** - * Get the index for a specific timestamp. - * - * @param st stream that the timestamp belongs to - * @param timestamp timestamp to retrieve the index for - * @param flags if AVSEEK_FLAG_BACKWARD then the returned index will correspond - * to the timestamp which is <= the requested one, if backward - * is 0, then it will be >= - * if AVSEEK_FLAG_ANY seek to any frame, only keyframes otherwise - * @return < 0 if no such timestamp could be found - */ -int av_index_search_timestamp(AVStream *st, int64_t timestamp, int flags); - -/** - * Get the index entry count for the given AVStream. - * - * @param st stream - * @return the number of index entries in the stream - */ -int avformat_index_get_entries_count(const AVStream *st); - -/** - * Get the AVIndexEntry corresponding to the given index. - * - * @param st Stream containing the requested AVIndexEntry. - * @param idx The desired index. - * @return A pointer to the requested AVIndexEntry if it exists, NULL otherwise. - * - * @note The pointer returned by this function is only guaranteed to be valid - * until any function that takes the stream or the parent AVFormatContext - * as input argument is called. - */ -const AVIndexEntry *avformat_index_get_entry(AVStream *st, int idx); - -/** - * Get the AVIndexEntry corresponding to the given timestamp. - * - * @param st Stream containing the requested AVIndexEntry. - * @param wanted_timestamp Timestamp to retrieve the index entry for. - * @param flags If AVSEEK_FLAG_BACKWARD then the returned entry will correspond - * to the timestamp which is <= the requested one, if backward - * is 0, then it will be >= - * if AVSEEK_FLAG_ANY seek to any frame, only keyframes otherwise. - * @return A pointer to the requested AVIndexEntry if it exists, NULL otherwise. - * - * @note The pointer returned by this function is only guaranteed to be valid - * until any function that takes the stream or the parent AVFormatContext - * as input argument is called. - */ -const AVIndexEntry *avformat_index_get_entry_from_timestamp(AVStream *st, - int64_t wanted_timestamp, - int flags); -/** - * Add an index entry into a sorted list. Update the entry if the list - * already contains it. - * - * @param timestamp timestamp in the time base of the given stream - */ -int av_add_index_entry(AVStream *st, int64_t pos, int64_t timestamp, - int size, int distance, int flags); - - -/** - * Split a URL string into components. - * - * The pointers to buffers for storing individual components may be null, - * in order to ignore that component. Buffers for components not found are - * set to empty strings. If the port is not found, it is set to a negative - * value. - * - * @param proto the buffer for the protocol - * @param proto_size the size of the proto buffer - * @param authorization the buffer for the authorization - * @param authorization_size the size of the authorization buffer - * @param hostname the buffer for the host name - * @param hostname_size the size of the hostname buffer - * @param port_ptr a pointer to store the port number in - * @param path the buffer for the path - * @param path_size the size of the path buffer - * @param url the URL to split - */ -void av_url_split(char *proto, int proto_size, - char *authorization, int authorization_size, - char *hostname, int hostname_size, - int *port_ptr, - char *path, int path_size, - const char *url); - - -/** - * Print detailed information about the input or output format, such as - * duration, bitrate, streams, container, programs, metadata, side data, - * codec and time base. - * - * @param ic the context to analyze - * @param index index of the stream to dump information about - * @param url the URL to print, such as source or destination file - * @param is_output Select whether the specified context is an input(0) or output(1) - */ -void av_dump_format(AVFormatContext *ic, - int index, - const char *url, - int is_output); - - -#define AV_FRAME_FILENAME_FLAGS_MULTIPLE 1 ///< Allow multiple %d - -/** - * Return in 'buf' the path with '%d' replaced by a number. - * - * Also handles the '%0nd' format where 'n' is the total number - * of digits and '%%'. - * - * @param buf destination buffer - * @param buf_size destination buffer size - * @param path numbered sequence string - * @param number frame number - * @param flags AV_FRAME_FILENAME_FLAGS_* - * @return 0 if OK, -1 on format error - */ -int av_get_frame_filename2(char *buf, int buf_size, - const char *path, int number, int flags); - -int av_get_frame_filename(char *buf, int buf_size, - const char *path, int number); - -/** - * Check whether filename actually is a numbered sequence generator. - * - * @param filename possible numbered sequence string - * @return 1 if a valid numbered sequence string, 0 otherwise - */ -int av_filename_number_test(const char *filename); - -/** - * Generate an SDP for an RTP session. - * - * Note, this overwrites the id values of AVStreams in the muxer contexts - * for getting unique dynamic payload types. - * - * @param ac array of AVFormatContexts describing the RTP streams. If the - * array is composed by only one context, such context can contain - * multiple AVStreams (one AVStream per RTP stream). Otherwise, - * all the contexts in the array (an AVCodecContext per RTP stream) - * must contain only one AVStream. - * @param n_files number of AVCodecContexts contained in ac - * @param buf buffer where the SDP will be stored (must be allocated by - * the caller) - * @param size the size of the buffer - * @return 0 if OK, AVERROR_xxx on error - */ -int av_sdp_create(AVFormatContext *ac[], int n_files, char *buf, int size); - -/** - * Return a positive value if the given filename has one of the given - * extensions, 0 otherwise. - * - * @param filename file name to check against the given extensions - * @param extensions a comma-separated list of filename extensions - */ -int av_match_ext(const char *filename, const char *extensions); - -/** - * Test if the given container can store a codec. - * - * @param ofmt container to check for compatibility - * @param codec_id codec to potentially store in container - * @param std_compliance standards compliance level, one of FF_COMPLIANCE_* - * - * @return 1 if codec with ID codec_id can be stored in ofmt, 0 if it cannot. - * A negative number if this information is not available. - */ -int avformat_query_codec(const AVOutputFormat *ofmt, enum AVCodecID codec_id, - int std_compliance); - -/** - * @defgroup riff_fourcc RIFF FourCCs - * @{ - * Get the tables mapping RIFF FourCCs to libavcodec AVCodecIDs. The tables are - * meant to be passed to av_codec_get_id()/av_codec_get_tag() as in the - * following code: - * @code - * uint32_t tag = MKTAG('H', '2', '6', '4'); - * const struct AVCodecTag *table[] = { avformat_get_riff_video_tags(), 0 }; - * enum AVCodecID id = av_codec_get_id(table, tag); - * @endcode - */ -/** - * @return the table mapping RIFF FourCCs for video to libavcodec AVCodecID. - */ -const struct AVCodecTag *avformat_get_riff_video_tags(void); -/** - * @return the table mapping RIFF FourCCs for audio to AVCodecID. - */ -const struct AVCodecTag *avformat_get_riff_audio_tags(void); -/** - * @return the table mapping MOV FourCCs for video to libavcodec AVCodecID. - */ -const struct AVCodecTag *avformat_get_mov_video_tags(void); -/** - * @return the table mapping MOV FourCCs for audio to AVCodecID. - */ -const struct AVCodecTag *avformat_get_mov_audio_tags(void); - -/** - * @} - */ - -/** - * Guess the sample aspect ratio of a frame, based on both the stream and the - * frame aspect ratio. - * - * Since the frame aspect ratio is set by the codec but the stream aspect ratio - * is set by the demuxer, these two may not be equal. This function tries to - * return the value that you should use if you would like to display the frame. - * - * Basic logic is to use the stream aspect ratio if it is set to something sane - * otherwise use the frame aspect ratio. This way a container setting, which is - * usually easy to modify can override the coded value in the frames. - * - * @param format the format context which the stream is part of - * @param stream the stream which the frame is part of - * @param frame the frame with the aspect ratio to be determined - * @return the guessed (valid) sample_aspect_ratio, 0/1 if no idea - */ -AVRational av_guess_sample_aspect_ratio(AVFormatContext *format, AVStream *stream, - struct AVFrame *frame); - -/** - * Guess the frame rate, based on both the container and codec information. - * - * @param ctx the format context which the stream is part of - * @param stream the stream which the frame is part of - * @param frame the frame for which the frame rate should be determined, may be NULL - * @return the guessed (valid) frame rate, 0/1 if no idea - */ -AVRational av_guess_frame_rate(AVFormatContext *ctx, AVStream *stream, - struct AVFrame *frame); - -/** - * Check if the stream st contained in s is matched by the stream specifier - * spec. - * - * See the "stream specifiers" chapter in the documentation for the syntax - * of spec. - * - * @return >0 if st is matched by spec; - * 0 if st is not matched by spec; - * AVERROR code if spec is invalid - * - * @note A stream specifier can match several streams in the format. - */ -int avformat_match_stream_specifier(AVFormatContext *s, AVStream *st, - const char *spec); - -int avformat_queue_attached_pictures(AVFormatContext *s); - -enum AVTimebaseSource { - AVFMT_TBCF_AUTO = -1, - AVFMT_TBCF_DECODER, - AVFMT_TBCF_DEMUXER, -#if FF_API_R_FRAME_RATE - AVFMT_TBCF_R_FRAMERATE, -#endif -}; - -/** - * Transfer internal timing information from one stream to another. - * - * This function is useful when doing stream copy. - * - * @param ofmt target output format for ost - * @param ost output stream which needs timings copy and adjustments - * @param ist reference input stream to copy timings from - * @param copy_tb define from where the stream codec timebase needs to be imported - */ -int avformat_transfer_internal_stream_timing_info(const AVOutputFormat *ofmt, - AVStream *ost, const AVStream *ist, - enum AVTimebaseSource copy_tb); - -/** - * Get the internal codec timebase from a stream. - * - * @param st input stream to extract the timebase from - */ -AVRational av_stream_get_codec_timebase(const AVStream *st); - -/** - * @} - */ - -#endif /* AVFORMAT_AVFORMAT_H */ diff --git a/gostream/ffmpeg/include/libavformat/avio.h b/gostream/ffmpeg/include/libavformat/avio.h deleted file mode 100644 index 887a397c378..00000000000 --- a/gostream/ffmpeg/include/libavformat/avio.h +++ /dev/null @@ -1,850 +0,0 @@ -/* - * copyright (c) 2001 Fabrice Bellard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -#ifndef AVFORMAT_AVIO_H -#define AVFORMAT_AVIO_H - -/** - * @file - * @ingroup lavf_io - * Buffered I/O operations - */ - -#include -#include - -#include "libavutil/attributes.h" -#include "libavutil/dict.h" -#include "libavutil/log.h" - -#include "libavformat/version_major.h" - -/** - * Seeking works like for a local file. - */ -#define AVIO_SEEKABLE_NORMAL (1 << 0) - -/** - * Seeking by timestamp with avio_seek_time() is possible. - */ -#define AVIO_SEEKABLE_TIME (1 << 1) - -/** - * Callback for checking whether to abort blocking functions. - * AVERROR_EXIT is returned in this case by the interrupted - * function. During blocking operations, callback is called with - * opaque as parameter. If the callback returns 1, the - * blocking operation will be aborted. - * - * No members can be added to this struct without a major bump, if - * new elements have been added after this struct in AVFormatContext - * or AVIOContext. - */ -typedef struct AVIOInterruptCB { - int (*callback)(void*); - void *opaque; -} AVIOInterruptCB; - -/** - * Directory entry types. - */ -enum AVIODirEntryType { - AVIO_ENTRY_UNKNOWN, - AVIO_ENTRY_BLOCK_DEVICE, - AVIO_ENTRY_CHARACTER_DEVICE, - AVIO_ENTRY_DIRECTORY, - AVIO_ENTRY_NAMED_PIPE, - AVIO_ENTRY_SYMBOLIC_LINK, - AVIO_ENTRY_SOCKET, - AVIO_ENTRY_FILE, - AVIO_ENTRY_SERVER, - AVIO_ENTRY_SHARE, - AVIO_ENTRY_WORKGROUP, -}; - -/** - * Describes single entry of the directory. - * - * Only name and type fields are guaranteed be set. - * Rest of fields are protocol or/and platform dependent and might be unknown. - */ -typedef struct AVIODirEntry { - char *name; /**< Filename */ - int type; /**< Type of the entry */ - int utf8; /**< Set to 1 when name is encoded with UTF-8, 0 otherwise. - Name can be encoded with UTF-8 even though 0 is set. */ - int64_t size; /**< File size in bytes, -1 if unknown. */ - int64_t modification_timestamp; /**< Time of last modification in microseconds since unix - epoch, -1 if unknown. */ - int64_t access_timestamp; /**< Time of last access in microseconds since unix epoch, - -1 if unknown. */ - int64_t status_change_timestamp; /**< Time of last status change in microseconds since unix - epoch, -1 if unknown. */ - int64_t user_id; /**< User ID of owner, -1 if unknown. */ - int64_t group_id; /**< Group ID of owner, -1 if unknown. */ - int64_t filemode; /**< Unix file mode, -1 if unknown. */ -} AVIODirEntry; - -#if FF_API_AVIODIRCONTEXT -typedef struct AVIODirContext { - struct URLContext *url_context; -} AVIODirContext; -#else -typedef struct AVIODirContext AVIODirContext; -#endif - -/** - * Different data types that can be returned via the AVIO - * write_data_type callback. - */ -enum AVIODataMarkerType { - /** - * Header data; this needs to be present for the stream to be decodeable. - */ - AVIO_DATA_MARKER_HEADER, - /** - * A point in the output bytestream where a decoder can start decoding - * (i.e. a keyframe). A demuxer/decoder given the data flagged with - * AVIO_DATA_MARKER_HEADER, followed by any AVIO_DATA_MARKER_SYNC_POINT, - * should give decodeable results. - */ - AVIO_DATA_MARKER_SYNC_POINT, - /** - * A point in the output bytestream where a demuxer can start parsing - * (for non self synchronizing bytestream formats). That is, any - * non-keyframe packet start point. - */ - AVIO_DATA_MARKER_BOUNDARY_POINT, - /** - * This is any, unlabelled data. It can either be a muxer not marking - * any positions at all, it can be an actual boundary/sync point - * that the muxer chooses not to mark, or a later part of a packet/fragment - * that is cut into multiple write callbacks due to limited IO buffer size. - */ - AVIO_DATA_MARKER_UNKNOWN, - /** - * Trailer data, which doesn't contain actual content, but only for - * finalizing the output file. - */ - AVIO_DATA_MARKER_TRAILER, - /** - * A point in the output bytestream where the underlying AVIOContext might - * flush the buffer depending on latency or buffering requirements. Typically - * means the end of a packet. - */ - AVIO_DATA_MARKER_FLUSH_POINT, -}; - -/** - * Bytestream IO Context. - * New public fields can be added with minor version bumps. - * Removal, reordering and changes to existing public fields require - * a major version bump. - * sizeof(AVIOContext) must not be used outside libav*. - * - * @note None of the function pointers in AVIOContext should be called - * directly, they should only be set by the client application - * when implementing custom I/O. Normally these are set to the - * function pointers specified in avio_alloc_context() - */ -typedef struct AVIOContext { - /** - * A class for private options. - * - * If this AVIOContext is created by avio_open2(), av_class is set and - * passes the options down to protocols. - * - * If this AVIOContext is manually allocated, then av_class may be set by - * the caller. - * - * warning -- this field can be NULL, be sure to not pass this AVIOContext - * to any av_opt_* functions in that case. - */ - const AVClass *av_class; - - /* - * The following shows the relationship between buffer, buf_ptr, - * buf_ptr_max, buf_end, buf_size, and pos, when reading and when writing - * (since AVIOContext is used for both): - * - ********************************************************************************** - * READING - ********************************************************************************** - * - * | buffer_size | - * |---------------------------------------| - * | | - * - * buffer buf_ptr buf_end - * +---------------+-----------------------+ - * |/ / / / / / / /|/ / / / / / /| | - * read buffer: |/ / consumed / | to be read /| | - * |/ / / / / / / /|/ / / / / / /| | - * +---------------+-----------------------+ - * - * pos - * +-------------------------------------------+-----------------+ - * input file: | | | - * +-------------------------------------------+-----------------+ - * - * - ********************************************************************************** - * WRITING - ********************************************************************************** - * - * | buffer_size | - * |--------------------------------------| - * | | - * - * buf_ptr_max - * buffer (buf_ptr) buf_end - * +-----------------------+--------------+ - * |/ / / / / / / / / / / /| | - * write buffer: | / / to be flushed / / | | - * |/ / / / / / / / / / / /| | - * +-----------------------+--------------+ - * buf_ptr can be in this - * due to a backward seek - * - * pos - * +-------------+----------------------------------------------+ - * output file: | | | - * +-------------+----------------------------------------------+ - * - */ - unsigned char *buffer; /**< Start of the buffer. */ - int buffer_size; /**< Maximum buffer size */ - unsigned char *buf_ptr; /**< Current position in the buffer */ - unsigned char *buf_end; /**< End of the data, may be less than - buffer+buffer_size if the read function returned - less data than requested, e.g. for streams where - no more data has been received yet. */ - void *opaque; /**< A private pointer, passed to the read/write/seek/... - functions. */ - int (*read_packet)(void *opaque, uint8_t *buf, int buf_size); -#if FF_API_AVIO_WRITE_NONCONST - int (*write_packet)(void *opaque, uint8_t *buf, int buf_size); -#else - int (*write_packet)(void *opaque, const uint8_t *buf, int buf_size); -#endif - int64_t (*seek)(void *opaque, int64_t offset, int whence); - int64_t pos; /**< position in the file of the current buffer */ - int eof_reached; /**< true if was unable to read due to error or eof */ - int error; /**< contains the error code or 0 if no error happened */ - int write_flag; /**< true if open for writing */ - int max_packet_size; - int min_packet_size; /**< Try to buffer at least this amount of data - before flushing it. */ - unsigned long checksum; - unsigned char *checksum_ptr; - unsigned long (*update_checksum)(unsigned long checksum, const uint8_t *buf, unsigned int size); - /** - * Pause or resume playback for network streaming protocols - e.g. MMS. - */ - int (*read_pause)(void *opaque, int pause); - /** - * Seek to a given timestamp in stream with the specified stream_index. - * Needed for some network streaming protocols which don't support seeking - * to byte position. - */ - int64_t (*read_seek)(void *opaque, int stream_index, - int64_t timestamp, int flags); - /** - * A combination of AVIO_SEEKABLE_ flags or 0 when the stream is not seekable. - */ - int seekable; - - /** - * avio_read and avio_write should if possible be satisfied directly - * instead of going through a buffer, and avio_seek will always - * call the underlying seek function directly. - */ - int direct; - - /** - * ',' separated list of allowed protocols. - */ - const char *protocol_whitelist; - - /** - * ',' separated list of disallowed protocols. - */ - const char *protocol_blacklist; - - /** - * A callback that is used instead of write_packet. - */ -#if FF_API_AVIO_WRITE_NONCONST - int (*write_data_type)(void *opaque, uint8_t *buf, int buf_size, - enum AVIODataMarkerType type, int64_t time); -#else - int (*write_data_type)(void *opaque, const uint8_t *buf, int buf_size, - enum AVIODataMarkerType type, int64_t time); -#endif - /** - * If set, don't call write_data_type separately for AVIO_DATA_MARKER_BOUNDARY_POINT, - * but ignore them and treat them as AVIO_DATA_MARKER_UNKNOWN (to avoid needlessly - * small chunks of data returned from the callback). - */ - int ignore_boundary_point; - - /** - * Maximum reached position before a backward seek in the write buffer, - * used keeping track of already written data for a later flush. - */ - unsigned char *buf_ptr_max; - - /** - * Read-only statistic of bytes read for this AVIOContext. - */ - int64_t bytes_read; - - /** - * Read-only statistic of bytes written for this AVIOContext. - */ - int64_t bytes_written; -} AVIOContext; - -/** - * Return the name of the protocol that will handle the passed URL. - * - * NULL is returned if no protocol could be found for the given URL. - * - * @return Name of the protocol or NULL. - */ -const char *avio_find_protocol_name(const char *url); - -/** - * Return AVIO_FLAG_* access flags corresponding to the access permissions - * of the resource in url, or a negative value corresponding to an - * AVERROR code in case of failure. The returned access flags are - * masked by the value in flags. - * - * @note This function is intrinsically unsafe, in the sense that the - * checked resource may change its existence or permission status from - * one call to another. Thus you should not trust the returned value, - * unless you are sure that no other processes are accessing the - * checked resource. - */ -int avio_check(const char *url, int flags); - -/** - * Open directory for reading. - * - * @param s directory read context. Pointer to a NULL pointer must be passed. - * @param url directory to be listed. - * @param options A dictionary filled with protocol-private options. On return - * this parameter will be destroyed and replaced with a dictionary - * containing options that were not found. May be NULL. - * @return >=0 on success or negative on error. - */ -int avio_open_dir(AVIODirContext **s, const char *url, AVDictionary **options); - -/** - * Get next directory entry. - * - * Returned entry must be freed with avio_free_directory_entry(). In particular - * it may outlive AVIODirContext. - * - * @param s directory read context. - * @param[out] next next entry or NULL when no more entries. - * @return >=0 on success or negative on error. End of list is not considered an - * error. - */ -int avio_read_dir(AVIODirContext *s, AVIODirEntry **next); - -/** - * Close directory. - * - * @note Entries created using avio_read_dir() are not deleted and must be - * freeded with avio_free_directory_entry(). - * - * @param s directory read context. - * @return >=0 on success or negative on error. - */ -int avio_close_dir(AVIODirContext **s); - -/** - * Free entry allocated by avio_read_dir(). - * - * @param entry entry to be freed. - */ -void avio_free_directory_entry(AVIODirEntry **entry); - -/** - * Allocate and initialize an AVIOContext for buffered I/O. It must be later - * freed with avio_context_free(). - * - * @param buffer Memory block for input/output operations via AVIOContext. - * The buffer must be allocated with av_malloc() and friends. - * It may be freed and replaced with a new buffer by libavformat. - * AVIOContext.buffer holds the buffer currently in use, - * which must be later freed with av_free(). - * @param buffer_size The buffer size is very important for performance. - * For protocols with fixed blocksize it should be set to this blocksize. - * For others a typical size is a cache page, e.g. 4kb. - * @param write_flag Set to 1 if the buffer should be writable, 0 otherwise. - * @param opaque An opaque pointer to user-specific data. - * @param read_packet A function for refilling the buffer, may be NULL. - * For stream protocols, must never return 0 but rather - * a proper AVERROR code. - * @param write_packet A function for writing the buffer contents, may be NULL. - * The function may not change the input buffers content. - * @param seek A function for seeking to specified byte position, may be NULL. - * - * @return Allocated AVIOContext or NULL on failure. - */ -AVIOContext *avio_alloc_context( - unsigned char *buffer, - int buffer_size, - int write_flag, - void *opaque, - int (*read_packet)(void *opaque, uint8_t *buf, int buf_size), -#if FF_API_AVIO_WRITE_NONCONST - int (*write_packet)(void *opaque, uint8_t *buf, int buf_size), -#else - int (*write_packet)(void *opaque, const uint8_t *buf, int buf_size), -#endif - int64_t (*seek)(void *opaque, int64_t offset, int whence)); - -/** - * Free the supplied IO context and everything associated with it. - * - * @param s Double pointer to the IO context. This function will write NULL - * into s. - */ -void avio_context_free(AVIOContext **s); - -void avio_w8(AVIOContext *s, int b); -void avio_write(AVIOContext *s, const unsigned char *buf, int size); -void avio_wl64(AVIOContext *s, uint64_t val); -void avio_wb64(AVIOContext *s, uint64_t val); -void avio_wl32(AVIOContext *s, unsigned int val); -void avio_wb32(AVIOContext *s, unsigned int val); -void avio_wl24(AVIOContext *s, unsigned int val); -void avio_wb24(AVIOContext *s, unsigned int val); -void avio_wl16(AVIOContext *s, unsigned int val); -void avio_wb16(AVIOContext *s, unsigned int val); - -/** - * Write a NULL-terminated string. - * @return number of bytes written. - */ -int avio_put_str(AVIOContext *s, const char *str); - -/** - * Convert an UTF-8 string to UTF-16LE and write it. - * @param s the AVIOContext - * @param str NULL-terminated UTF-8 string - * - * @return number of bytes written. - */ -int avio_put_str16le(AVIOContext *s, const char *str); - -/** - * Convert an UTF-8 string to UTF-16BE and write it. - * @param s the AVIOContext - * @param str NULL-terminated UTF-8 string - * - * @return number of bytes written. - */ -int avio_put_str16be(AVIOContext *s, const char *str); - -/** - * Mark the written bytestream as a specific type. - * - * Zero-length ranges are omitted from the output. - * - * @param s the AVIOContext - * @param time the stream time the current bytestream pos corresponds to - * (in AV_TIME_BASE units), or AV_NOPTS_VALUE if unknown or not - * applicable - * @param type the kind of data written starting at the current pos - */ -void avio_write_marker(AVIOContext *s, int64_t time, enum AVIODataMarkerType type); - -/** - * ORing this as the "whence" parameter to a seek function causes it to - * return the filesize without seeking anywhere. Supporting this is optional. - * If it is not supported then the seek function will return <0. - */ -#define AVSEEK_SIZE 0x10000 - -/** - * Passing this flag as the "whence" parameter to a seek function causes it to - * seek by any means (like reopening and linear reading) or other normally unreasonable - * means that can be extremely slow. - * This may be ignored by the seek code. - */ -#define AVSEEK_FORCE 0x20000 - -/** - * fseek() equivalent for AVIOContext. - * @return new position or AVERROR. - */ -int64_t avio_seek(AVIOContext *s, int64_t offset, int whence); - -/** - * Skip given number of bytes forward - * @return new position or AVERROR. - */ -int64_t avio_skip(AVIOContext *s, int64_t offset); - -/** - * ftell() equivalent for AVIOContext. - * @return position or AVERROR. - */ -static av_always_inline int64_t avio_tell(AVIOContext *s) -{ - return avio_seek(s, 0, SEEK_CUR); -} - -/** - * Get the filesize. - * @return filesize or AVERROR - */ -int64_t avio_size(AVIOContext *s); - -/** - * Similar to feof() but also returns nonzero on read errors. - * @return non zero if and only if at end of file or a read error happened when reading. - */ -int avio_feof(AVIOContext *s); - -/** - * Writes a formatted string to the context taking a va_list. - * @return number of bytes written, < 0 on error. - */ -int avio_vprintf(AVIOContext *s, const char *fmt, va_list ap); - -/** - * Writes a formatted string to the context. - * @return number of bytes written, < 0 on error. - */ -int avio_printf(AVIOContext *s, const char *fmt, ...) av_printf_format(2, 3); - -/** - * Write a NULL terminated array of strings to the context. - * Usually you don't need to use this function directly but its macro wrapper, - * avio_print. - */ -void avio_print_string_array(AVIOContext *s, const char *strings[]); - -/** - * Write strings (const char *) to the context. - * This is a convenience macro around avio_print_string_array and it - * automatically creates the string array from the variable argument list. - * For simple string concatenations this function is more performant than using - * avio_printf since it does not need a temporary buffer. - */ -#define avio_print(s, ...) \ - avio_print_string_array(s, (const char*[]){__VA_ARGS__, NULL}) - -/** - * Force flushing of buffered data. - * - * For write streams, force the buffered data to be immediately written to the output, - * without to wait to fill the internal buffer. - * - * For read streams, discard all currently buffered data, and advance the - * reported file position to that of the underlying stream. This does not - * read new data, and does not perform any seeks. - */ -void avio_flush(AVIOContext *s); - -/** - * Read size bytes from AVIOContext into buf. - * @return number of bytes read or AVERROR - */ -int avio_read(AVIOContext *s, unsigned char *buf, int size); - -/** - * Read size bytes from AVIOContext into buf. Unlike avio_read(), this is allowed - * to read fewer bytes than requested. The missing bytes can be read in the next - * call. This always tries to read at least 1 byte. - * Useful to reduce latency in certain cases. - * @return number of bytes read or AVERROR - */ -int avio_read_partial(AVIOContext *s, unsigned char *buf, int size); - -/** - * @name Functions for reading from AVIOContext - * @{ - * - * @note return 0 if EOF, so you cannot use it if EOF handling is - * necessary - */ -int avio_r8 (AVIOContext *s); -unsigned int avio_rl16(AVIOContext *s); -unsigned int avio_rl24(AVIOContext *s); -unsigned int avio_rl32(AVIOContext *s); -uint64_t avio_rl64(AVIOContext *s); -unsigned int avio_rb16(AVIOContext *s); -unsigned int avio_rb24(AVIOContext *s); -unsigned int avio_rb32(AVIOContext *s); -uint64_t avio_rb64(AVIOContext *s); -/** - * @} - */ - -/** - * Read a string from pb into buf. The reading will terminate when either - * a NULL character was encountered, maxlen bytes have been read, or nothing - * more can be read from pb. The result is guaranteed to be NULL-terminated, it - * will be truncated if buf is too small. - * Note that the string is not interpreted or validated in any way, it - * might get truncated in the middle of a sequence for multi-byte encodings. - * - * @return number of bytes read (is always <= maxlen). - * If reading ends on EOF or error, the return value will be one more than - * bytes actually read. - */ -int avio_get_str(AVIOContext *pb, int maxlen, char *buf, int buflen); - -/** - * Read a UTF-16 string from pb and convert it to UTF-8. - * The reading will terminate when either a null or invalid character was - * encountered or maxlen bytes have been read. - * @return number of bytes read (is always <= maxlen) - */ -int avio_get_str16le(AVIOContext *pb, int maxlen, char *buf, int buflen); -int avio_get_str16be(AVIOContext *pb, int maxlen, char *buf, int buflen); - - -/** - * @name URL open modes - * The flags argument to avio_open must be one of the following - * constants, optionally ORed with other flags. - * @{ - */ -#define AVIO_FLAG_READ 1 /**< read-only */ -#define AVIO_FLAG_WRITE 2 /**< write-only */ -#define AVIO_FLAG_READ_WRITE (AVIO_FLAG_READ|AVIO_FLAG_WRITE) /**< read-write pseudo flag */ -/** - * @} - */ - -/** - * Use non-blocking mode. - * If this flag is set, operations on the context will return - * AVERROR(EAGAIN) if they can not be performed immediately. - * If this flag is not set, operations on the context will never return - * AVERROR(EAGAIN). - * Note that this flag does not affect the opening/connecting of the - * context. Connecting a protocol will always block if necessary (e.g. on - * network protocols) but never hang (e.g. on busy devices). - * Warning: non-blocking protocols is work-in-progress; this flag may be - * silently ignored. - */ -#define AVIO_FLAG_NONBLOCK 8 - -/** - * Use direct mode. - * avio_read and avio_write should if possible be satisfied directly - * instead of going through a buffer, and avio_seek will always - * call the underlying seek function directly. - */ -#define AVIO_FLAG_DIRECT 0x8000 - -/** - * Create and initialize a AVIOContext for accessing the - * resource indicated by url. - * @note When the resource indicated by url has been opened in - * read+write mode, the AVIOContext can be used only for writing. - * - * @param s Used to return the pointer to the created AVIOContext. - * In case of failure the pointed to value is set to NULL. - * @param url resource to access - * @param flags flags which control how the resource indicated by url - * is to be opened - * @return >= 0 in case of success, a negative value corresponding to an - * AVERROR code in case of failure - */ -int avio_open(AVIOContext **s, const char *url, int flags); - -/** - * Create and initialize a AVIOContext for accessing the - * resource indicated by url. - * @note When the resource indicated by url has been opened in - * read+write mode, the AVIOContext can be used only for writing. - * - * @param s Used to return the pointer to the created AVIOContext. - * In case of failure the pointed to value is set to NULL. - * @param url resource to access - * @param flags flags which control how the resource indicated by url - * is to be opened - * @param int_cb an interrupt callback to be used at the protocols level - * @param options A dictionary filled with protocol-private options. On return - * this parameter will be destroyed and replaced with a dict containing options - * that were not found. May be NULL. - * @return >= 0 in case of success, a negative value corresponding to an - * AVERROR code in case of failure - */ -int avio_open2(AVIOContext **s, const char *url, int flags, - const AVIOInterruptCB *int_cb, AVDictionary **options); - -/** - * Close the resource accessed by the AVIOContext s and free it. - * This function can only be used if s was opened by avio_open(). - * - * The internal buffer is automatically flushed before closing the - * resource. - * - * @return 0 on success, an AVERROR < 0 on error. - * @see avio_closep - */ -int avio_close(AVIOContext *s); - -/** - * Close the resource accessed by the AVIOContext *s, free it - * and set the pointer pointing to it to NULL. - * This function can only be used if s was opened by avio_open(). - * - * The internal buffer is automatically flushed before closing the - * resource. - * - * @return 0 on success, an AVERROR < 0 on error. - * @see avio_close - */ -int avio_closep(AVIOContext **s); - - -/** - * Open a write only memory stream. - * - * @param s new IO context - * @return zero if no error. - */ -int avio_open_dyn_buf(AVIOContext **s); - -/** - * Return the written size and a pointer to the buffer. - * The AVIOContext stream is left intact. - * The buffer must NOT be freed. - * No padding is added to the buffer. - * - * @param s IO context - * @param pbuffer pointer to a byte buffer - * @return the length of the byte buffer - */ -int avio_get_dyn_buf(AVIOContext *s, uint8_t **pbuffer); - -/** - * Return the written size and a pointer to the buffer. The buffer - * must be freed with av_free(). - * Padding of AV_INPUT_BUFFER_PADDING_SIZE is added to the buffer. - * - * @param s IO context - * @param pbuffer pointer to a byte buffer - * @return the length of the byte buffer - */ -int avio_close_dyn_buf(AVIOContext *s, uint8_t **pbuffer); - -/** - * Iterate through names of available protocols. - * - * @param opaque A private pointer representing current protocol. - * It must be a pointer to NULL on first iteration and will - * be updated by successive calls to avio_enum_protocols. - * @param output If set to 1, iterate over output protocols, - * otherwise over input protocols. - * - * @return A static string containing the name of current protocol or NULL - */ -const char *avio_enum_protocols(void **opaque, int output); - -/** - * Get AVClass by names of available protocols. - * - * @return A AVClass of input protocol name or NULL - */ -const AVClass *avio_protocol_get_class(const char *name); - -/** - * Pause and resume playing - only meaningful if using a network streaming - * protocol (e.g. MMS). - * - * @param h IO context from which to call the read_pause function pointer - * @param pause 1 for pause, 0 for resume - */ -int avio_pause(AVIOContext *h, int pause); - -/** - * Seek to a given timestamp relative to some component stream. - * Only meaningful if using a network streaming protocol (e.g. MMS.). - * - * @param h IO context from which to call the seek function pointers - * @param stream_index The stream index that the timestamp is relative to. - * If stream_index is (-1) the timestamp should be in AV_TIME_BASE - * units from the beginning of the presentation. - * If a stream_index >= 0 is used and the protocol does not support - * seeking based on component streams, the call will fail. - * @param timestamp timestamp in AVStream.time_base units - * or if there is no stream specified then in AV_TIME_BASE units. - * @param flags Optional combination of AVSEEK_FLAG_BACKWARD, AVSEEK_FLAG_BYTE - * and AVSEEK_FLAG_ANY. The protocol may silently ignore - * AVSEEK_FLAG_BACKWARD and AVSEEK_FLAG_ANY, but AVSEEK_FLAG_BYTE will - * fail if used and not supported. - * @return >= 0 on success - * @see AVInputFormat::read_seek - */ -int64_t avio_seek_time(AVIOContext *h, int stream_index, - int64_t timestamp, int flags); - -/* Avoid a warning. The header can not be included because it breaks c++. */ -struct AVBPrint; - -/** - * Read contents of h into print buffer, up to max_size bytes, or up to EOF. - * - * @return 0 for success (max_size bytes read or EOF reached), negative error - * code otherwise - */ -int avio_read_to_bprint(AVIOContext *h, struct AVBPrint *pb, size_t max_size); - -/** - * Accept and allocate a client context on a server context. - * @param s the server context - * @param c the client context, must be unallocated - * @return >= 0 on success or a negative value corresponding - * to an AVERROR on failure - */ -int avio_accept(AVIOContext *s, AVIOContext **c); - -/** - * Perform one step of the protocol handshake to accept a new client. - * This function must be called on a client returned by avio_accept() before - * using it as a read/write context. - * It is separate from avio_accept() because it may block. - * A step of the handshake is defined by places where the application may - * decide to change the proceedings. - * For example, on a protocol with a request header and a reply header, each - * one can constitute a step because the application may use the parameters - * from the request to change parameters in the reply; or each individual - * chunk of the request can constitute a step. - * If the handshake is already finished, avio_handshake() does nothing and - * returns 0 immediately. - * - * @param c the client context to perform the handshake on - * @return 0 on a complete and successful handshake - * > 0 if the handshake progressed, but is not complete - * < 0 for an AVERROR code - */ -int avio_handshake(AVIOContext *c); -#endif /* AVFORMAT_AVIO_H */ diff --git a/gostream/ffmpeg/include/libavformat/version.h b/gostream/ffmpeg/include/libavformat/version.h deleted file mode 100644 index 9e1f484db4c..00000000000 --- a/gostream/ffmpeg/include/libavformat/version.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Version macros. - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFORMAT_VERSION_H -#define AVFORMAT_VERSION_H - -/** - * @file - * @ingroup libavf - * Libavformat version macros - */ - -#include "libavutil/version.h" - -#include "version_major.h" - -#define LIBAVFORMAT_VERSION_MINOR 16 -#define LIBAVFORMAT_VERSION_MICRO 100 - -#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \ - LIBAVFORMAT_VERSION_MINOR, \ - LIBAVFORMAT_VERSION_MICRO) -#define LIBAVFORMAT_VERSION AV_VERSION(LIBAVFORMAT_VERSION_MAJOR, \ - LIBAVFORMAT_VERSION_MINOR, \ - LIBAVFORMAT_VERSION_MICRO) -#define LIBAVFORMAT_BUILD LIBAVFORMAT_VERSION_INT - -#define LIBAVFORMAT_IDENT "Lavf" AV_STRINGIFY(LIBAVFORMAT_VERSION) - -#endif /* AVFORMAT_VERSION_H */ diff --git a/gostream/ffmpeg/include/libavformat/version_major.h b/gostream/ffmpeg/include/libavformat/version_major.h deleted file mode 100644 index 224fdacf235..00000000000 --- a/gostream/ffmpeg/include/libavformat/version_major.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Version macros. - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVFORMAT_VERSION_MAJOR_H -#define AVFORMAT_VERSION_MAJOR_H - -/** - * @file - * @ingroup libavf - * Libavformat version macros - */ - -// Major bumping may affect Ticket5467, 5421, 5451(compatibility with Chromium) -// Also please add any ticket numbers that you believe might be affected here -#define LIBAVFORMAT_VERSION_MAJOR 60 - -/** - * FF_API_* defines may be placed below to indicate public API that will be - * dropped at a future version bump. The defines themselves are not part of - * the public API and may change, break or disappear at any time. - * - * @note, when bumping the major version it is recommended to manually - * disable each FF_API_* in its own commit instead of disabling them all - * at once through the bump. This improves the git bisect-ability of the change. - * - */ -#define FF_API_COMPUTE_PKT_FIELDS2 (LIBAVFORMAT_VERSION_MAJOR < 61) -#define FF_API_GET_END_PTS (LIBAVFORMAT_VERSION_MAJOR < 61) -#define FF_API_AVIODIRCONTEXT (LIBAVFORMAT_VERSION_MAJOR < 61) -#define FF_API_AVFORMAT_IO_CLOSE (LIBAVFORMAT_VERSION_MAJOR < 61) -#define FF_API_AVIO_WRITE_NONCONST (LIBAVFORMAT_VERSION_MAJOR < 61) -#define FF_API_LAVF_SHORTEST (LIBAVFORMAT_VERSION_MAJOR < 61) -#define FF_API_ALLOW_FLUSH (LIBAVFORMAT_VERSION_MAJOR < 61) -#define FF_API_AVSTREAM_SIDE_DATA (LIBAVFORMAT_VERSION_MAJOR < 61) - - -#define FF_API_R_FRAME_RATE 1 - -#endif /* AVFORMAT_VERSION_MAJOR_H */ diff --git a/gostream/ffmpeg/include/libavutil/adler32.h b/gostream/ffmpeg/include/libavutil/adler32.h deleted file mode 100644 index 232d07f5fe8..00000000000 --- a/gostream/ffmpeg/include/libavutil/adler32.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - * copyright (c) 2006 Mans Rullgard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_adler32 - * Public header for Adler-32 hash function implementation. - */ - -#ifndef AVUTIL_ADLER32_H -#define AVUTIL_ADLER32_H - -#include -#include -#include "attributes.h" - -/** - * @defgroup lavu_adler32 Adler-32 - * @ingroup lavu_hash - * Adler-32 hash function implementation. - * - * @{ - */ - -typedef uint32_t AVAdler; - -/** - * Calculate the Adler32 checksum of a buffer. - * - * Passing the return value to a subsequent av_adler32_update() call - * allows the checksum of multiple buffers to be calculated as though - * they were concatenated. - * - * @param adler initial checksum value - * @param buf pointer to input buffer - * @param len size of input buffer - * @return updated checksum - */ -AVAdler av_adler32_update(AVAdler adler, const uint8_t *buf, - size_t len) av_pure; - -/** - * @} - */ - -#endif /* AVUTIL_ADLER32_H */ diff --git a/gostream/ffmpeg/include/libavutil/aes.h b/gostream/ffmpeg/include/libavutil/aes.h deleted file mode 100644 index 4e73473688f..00000000000 --- a/gostream/ffmpeg/include/libavutil/aes.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * copyright (c) 2007 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_AES_H -#define AVUTIL_AES_H - -#include - -#include "attributes.h" - -/** - * @defgroup lavu_aes AES - * @ingroup lavu_crypto - * @{ - */ - -extern const int av_aes_size; - -struct AVAES; - -/** - * Allocate an AVAES context. - */ -struct AVAES *av_aes_alloc(void); - -/** - * Initialize an AVAES context. - * - * @param a The AVAES context - * @param key Pointer to the key - * @param key_bits 128, 192 or 256 - * @param decrypt 0 for encryption, 1 for decryption - */ -int av_aes_init(struct AVAES *a, const uint8_t *key, int key_bits, int decrypt); - -/** - * Encrypt or decrypt a buffer using a previously initialized context. - * - * @param a The AVAES context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 16 byte blocks - * @param iv initialization vector for CBC mode, if NULL then ECB will be used - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_aes_crypt(struct AVAES *a, uint8_t *dst, const uint8_t *src, int count, uint8_t *iv, int decrypt); - -/** - * @} - */ - -#endif /* AVUTIL_AES_H */ diff --git a/gostream/ffmpeg/include/libavutil/aes_ctr.h b/gostream/ffmpeg/include/libavutil/aes_ctr.h deleted file mode 100644 index d98c0712276..00000000000 --- a/gostream/ffmpeg/include/libavutil/aes_ctr.h +++ /dev/null @@ -1,99 +0,0 @@ -/* - * AES-CTR cipher - * Copyright (c) 2015 Eran Kornblau - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_AES_CTR_H -#define AVUTIL_AES_CTR_H - -/** - * @defgroup lavu_aes_ctr AES-CTR - * @ingroup lavu_crypto - * @{ - */ - -#include - -#include "attributes.h" - -#define AES_CTR_KEY_SIZE (16) -#define AES_CTR_IV_SIZE (8) - -struct AVAESCTR; - -/** - * Allocate an AVAESCTR context. - */ -struct AVAESCTR *av_aes_ctr_alloc(void); - -/** - * Initialize an AVAESCTR context. - * - * @param a The AVAESCTR context to initialize - * @param key encryption key, must have a length of AES_CTR_KEY_SIZE - */ -int av_aes_ctr_init(struct AVAESCTR *a, const uint8_t *key); - -/** - * Release an AVAESCTR context. - * - * @param a The AVAESCTR context - */ -void av_aes_ctr_free(struct AVAESCTR *a); - -/** - * Process a buffer using a previously initialized context. - * - * @param a The AVAESCTR context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param size the size of src and dst - */ -void av_aes_ctr_crypt(struct AVAESCTR *a, uint8_t *dst, const uint8_t *src, int size); - -/** - * Get the current iv - */ -const uint8_t* av_aes_ctr_get_iv(struct AVAESCTR *a); - -/** - * Generate a random iv - */ -void av_aes_ctr_set_random_iv(struct AVAESCTR *a); - -/** - * Forcefully change the 8-byte iv - */ -void av_aes_ctr_set_iv(struct AVAESCTR *a, const uint8_t* iv); - -/** - * Forcefully change the "full" 16-byte iv, including the counter - */ -void av_aes_ctr_set_full_iv(struct AVAESCTR *a, const uint8_t* iv); - -/** - * Increment the top 64 bit of the iv (performed after each frame) - */ -void av_aes_ctr_increment_iv(struct AVAESCTR *a); - -/** - * @} - */ - -#endif /* AVUTIL_AES_CTR_H */ diff --git a/gostream/ffmpeg/include/libavutil/ambient_viewing_environment.h b/gostream/ffmpeg/include/libavutil/ambient_viewing_environment.h deleted file mode 100644 index e5e4ac21732..00000000000 --- a/gostream/ffmpeg/include/libavutil/ambient_viewing_environment.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2023 Jan Ekström - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_AMBIENT_VIEWING_ENVIRONMENT_H -#define AVUTIL_AMBIENT_VIEWING_ENVIRONMENT_H - -#include -#include "frame.h" -#include "rational.h" - -/** - * Ambient viewing environment metadata as defined by H.274. The values are - * saved in AVRationals so that they keep their exactness, while allowing for - * easy access to a double value with f.ex. av_q2d. - * - * @note sizeof(AVAmbientViewingEnvironment) is not part of the public ABI, and - * it must be allocated using av_ambient_viewing_environment_alloc. - */ -typedef struct AVAmbientViewingEnvironment { - /** - * Environmental illuminance of the ambient viewing environment in lux. - */ - AVRational ambient_illuminance; - - /** - * Normalized x chromaticity coordinate of the environmental ambient light - * in the nominal viewing environment according to the CIE 1931 definition - * of x and y as specified in ISO/CIE 11664-1. - */ - AVRational ambient_light_x; - - /** - * Normalized y chromaticity coordinate of the environmental ambient light - * in the nominal viewing environment according to the CIE 1931 definition - * of x and y as specified in ISO/CIE 11664-1. - */ - AVRational ambient_light_y; -} AVAmbientViewingEnvironment; - -/** - * Allocate an AVAmbientViewingEnvironment structure. - * - * @return the newly allocated struct or NULL on failure - */ -AVAmbientViewingEnvironment *av_ambient_viewing_environment_alloc(size_t *size); - -/** - * Allocate and add an AVAmbientViewingEnvironment structure to an existing - * AVFrame as side data. - * - * @return the newly allocated struct, or NULL on failure - */ -AVAmbientViewingEnvironment *av_ambient_viewing_environment_create_side_data(AVFrame *frame); - -#endif /* AVUTIL_AMBIENT_VIEWING_ENVIRONMENT_H */ diff --git a/gostream/ffmpeg/include/libavutil/attributes.h b/gostream/ffmpeg/include/libavutil/attributes.h deleted file mode 100644 index 04c615c952c..00000000000 --- a/gostream/ffmpeg/include/libavutil/attributes.h +++ /dev/null @@ -1,173 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * Macro definitions for various function/variable attributes - */ - -#ifndef AVUTIL_ATTRIBUTES_H -#define AVUTIL_ATTRIBUTES_H - -#ifdef __GNUC__ -# define AV_GCC_VERSION_AT_LEAST(x,y) (__GNUC__ > (x) || __GNUC__ == (x) && __GNUC_MINOR__ >= (y)) -# define AV_GCC_VERSION_AT_MOST(x,y) (__GNUC__ < (x) || __GNUC__ == (x) && __GNUC_MINOR__ <= (y)) -#else -# define AV_GCC_VERSION_AT_LEAST(x,y) 0 -# define AV_GCC_VERSION_AT_MOST(x,y) 0 -#endif - -#ifdef __has_builtin -# define AV_HAS_BUILTIN(x) __has_builtin(x) -#else -# define AV_HAS_BUILTIN(x) 0 -#endif - -#ifndef av_always_inline -#if AV_GCC_VERSION_AT_LEAST(3,1) -# define av_always_inline __attribute__((always_inline)) inline -#elif defined(_MSC_VER) -# define av_always_inline __forceinline -#else -# define av_always_inline inline -#endif -#endif - -#ifndef av_extern_inline -#if defined(__ICL) && __ICL >= 1210 || defined(__GNUC_STDC_INLINE__) -# define av_extern_inline extern inline -#else -# define av_extern_inline inline -#endif -#endif - -#if AV_GCC_VERSION_AT_LEAST(3,4) -# define av_warn_unused_result __attribute__((warn_unused_result)) -#else -# define av_warn_unused_result -#endif - -#if AV_GCC_VERSION_AT_LEAST(3,1) -# define av_noinline __attribute__((noinline)) -#elif defined(_MSC_VER) -# define av_noinline __declspec(noinline) -#else -# define av_noinline -#endif - -#if AV_GCC_VERSION_AT_LEAST(3,1) || defined(__clang__) -# define av_pure __attribute__((pure)) -#else -# define av_pure -#endif - -#if AV_GCC_VERSION_AT_LEAST(2,6) || defined(__clang__) -# define av_const __attribute__((const)) -#else -# define av_const -#endif - -#if AV_GCC_VERSION_AT_LEAST(4,3) || defined(__clang__) -# define av_cold __attribute__((cold)) -#else -# define av_cold -#endif - -#if AV_GCC_VERSION_AT_LEAST(4,1) && !defined(__llvm__) -# define av_flatten __attribute__((flatten)) -#else -# define av_flatten -#endif - -#if AV_GCC_VERSION_AT_LEAST(3,1) -# define attribute_deprecated __attribute__((deprecated)) -#elif defined(_MSC_VER) -# define attribute_deprecated __declspec(deprecated) -#else -# define attribute_deprecated -#endif - -/** - * Disable warnings about deprecated features - * This is useful for sections of code kept for backward compatibility and - * scheduled for removal. - */ -#ifndef AV_NOWARN_DEPRECATED -#if AV_GCC_VERSION_AT_LEAST(4,6) || defined(__clang__) -# define AV_NOWARN_DEPRECATED(code) \ - _Pragma("GCC diagnostic push") \ - _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") \ - code \ - _Pragma("GCC diagnostic pop") -#elif defined(_MSC_VER) -# define AV_NOWARN_DEPRECATED(code) \ - __pragma(warning(push)) \ - __pragma(warning(disable : 4996)) \ - code; \ - __pragma(warning(pop)) -#else -# define AV_NOWARN_DEPRECATED(code) code -#endif -#endif - -#if defined(__GNUC__) || defined(__clang__) -# define av_unused __attribute__((unused)) -#else -# define av_unused -#endif - -/** - * Mark a variable as used and prevent the compiler from optimizing it - * away. This is useful for variables accessed only from inline - * assembler without the compiler being aware. - */ -#if AV_GCC_VERSION_AT_LEAST(3,1) || defined(__clang__) -# define av_used __attribute__((used)) -#else -# define av_used -#endif - -#if AV_GCC_VERSION_AT_LEAST(3,3) || defined(__clang__) -# define av_alias __attribute__((may_alias)) -#else -# define av_alias -#endif - -#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER) -# define av_uninit(x) x=x -#else -# define av_uninit(x) x -#endif - -#if defined(__GNUC__) || defined(__clang__) -# define av_builtin_constant_p __builtin_constant_p -# define av_printf_format(fmtpos, attrpos) __attribute__((__format__(__printf__, fmtpos, attrpos))) -#else -# define av_builtin_constant_p(x) 0 -# define av_printf_format(fmtpos, attrpos) -#endif - -#if AV_GCC_VERSION_AT_LEAST(2,5) || defined(__clang__) -# define av_noreturn __attribute__((noreturn)) -#else -# define av_noreturn -#endif - -#endif /* AVUTIL_ATTRIBUTES_H */ diff --git a/gostream/ffmpeg/include/libavutil/audio_fifo.h b/gostream/ffmpeg/include/libavutil/audio_fifo.h deleted file mode 100644 index fa5f59a2bef..00000000000 --- a/gostream/ffmpeg/include/libavutil/audio_fifo.h +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Audio FIFO - * Copyright (c) 2012 Justin Ruggles - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * Audio FIFO Buffer - */ - -#ifndef AVUTIL_AUDIO_FIFO_H -#define AVUTIL_AUDIO_FIFO_H - -#include "attributes.h" -#include "samplefmt.h" - -/** - * @addtogroup lavu_audio - * @{ - * - * @defgroup lavu_audiofifo Audio FIFO Buffer - * @{ - */ - -/** - * Context for an Audio FIFO Buffer. - * - * - Operates at the sample level rather than the byte level. - * - Supports multiple channels with either planar or packed sample format. - * - Automatic reallocation when writing to a full buffer. - */ -typedef struct AVAudioFifo AVAudioFifo; - -/** - * Free an AVAudioFifo. - * - * @param af AVAudioFifo to free - */ -void av_audio_fifo_free(AVAudioFifo *af); - -/** - * Allocate an AVAudioFifo. - * - * @param sample_fmt sample format - * @param channels number of channels - * @param nb_samples initial allocation size, in samples - * @return newly allocated AVAudioFifo, or NULL on error - */ -AVAudioFifo *av_audio_fifo_alloc(enum AVSampleFormat sample_fmt, int channels, - int nb_samples); - -/** - * Reallocate an AVAudioFifo. - * - * @param af AVAudioFifo to reallocate - * @param nb_samples new allocation size, in samples - * @return 0 if OK, or negative AVERROR code on failure - */ -av_warn_unused_result -int av_audio_fifo_realloc(AVAudioFifo *af, int nb_samples); - -/** - * Write data to an AVAudioFifo. - * - * The AVAudioFifo will be reallocated automatically if the available space - * is less than nb_samples. - * - * @see enum AVSampleFormat - * The documentation for AVSampleFormat describes the data layout. - * - * @param af AVAudioFifo to write to - * @param data audio data plane pointers - * @param nb_samples number of samples to write - * @return number of samples actually written, or negative AVERROR - * code on failure. If successful, the number of samples - * actually written will always be nb_samples. - */ -int av_audio_fifo_write(AVAudioFifo *af, void * const *data, int nb_samples); - -/** - * Peek data from an AVAudioFifo. - * - * @see enum AVSampleFormat - * The documentation for AVSampleFormat describes the data layout. - * - * @param af AVAudioFifo to read from - * @param data audio data plane pointers - * @param nb_samples number of samples to peek - * @return number of samples actually peek, or negative AVERROR code - * on failure. The number of samples actually peek will not - * be greater than nb_samples, and will only be less than - * nb_samples if av_audio_fifo_size is less than nb_samples. - */ -int av_audio_fifo_peek(const AVAudioFifo *af, void * const *data, int nb_samples); - -/** - * Peek data from an AVAudioFifo. - * - * @see enum AVSampleFormat - * The documentation for AVSampleFormat describes the data layout. - * - * @param af AVAudioFifo to read from - * @param data audio data plane pointers - * @param nb_samples number of samples to peek - * @param offset offset from current read position - * @return number of samples actually peek, or negative AVERROR code - * on failure. The number of samples actually peek will not - * be greater than nb_samples, and will only be less than - * nb_samples if av_audio_fifo_size is less than nb_samples. - */ -int av_audio_fifo_peek_at(const AVAudioFifo *af, void * const *data, - int nb_samples, int offset); - -/** - * Read data from an AVAudioFifo. - * - * @see enum AVSampleFormat - * The documentation for AVSampleFormat describes the data layout. - * - * @param af AVAudioFifo to read from - * @param data audio data plane pointers - * @param nb_samples number of samples to read - * @return number of samples actually read, or negative AVERROR code - * on failure. The number of samples actually read will not - * be greater than nb_samples, and will only be less than - * nb_samples if av_audio_fifo_size is less than nb_samples. - */ -int av_audio_fifo_read(AVAudioFifo *af, void * const *data, int nb_samples); - -/** - * Drain data from an AVAudioFifo. - * - * Removes the data without reading it. - * - * @param af AVAudioFifo to drain - * @param nb_samples number of samples to drain - * @return 0 if OK, or negative AVERROR code on failure - */ -int av_audio_fifo_drain(AVAudioFifo *af, int nb_samples); - -/** - * Reset the AVAudioFifo buffer. - * - * This empties all data in the buffer. - * - * @param af AVAudioFifo to reset - */ -void av_audio_fifo_reset(AVAudioFifo *af); - -/** - * Get the current number of samples in the AVAudioFifo available for reading. - * - * @param af the AVAudioFifo to query - * @return number of samples available for reading - */ -int av_audio_fifo_size(AVAudioFifo *af); - -/** - * Get the current number of samples in the AVAudioFifo available for writing. - * - * @param af the AVAudioFifo to query - * @return number of samples available for writing - */ -int av_audio_fifo_space(AVAudioFifo *af); - -/** - * @} - * @} - */ - -#endif /* AVUTIL_AUDIO_FIFO_H */ diff --git a/gostream/ffmpeg/include/libavutil/avassert.h b/gostream/ffmpeg/include/libavutil/avassert.h deleted file mode 100644 index 1895fb75513..00000000000 --- a/gostream/ffmpeg/include/libavutil/avassert.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * copyright (c) 2010 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * simple assert() macros that are a bit more flexible than ISO C assert(). - * @author Michael Niedermayer - */ - -#ifndef AVUTIL_AVASSERT_H -#define AVUTIL_AVASSERT_H - -#include -#ifdef HAVE_AV_CONFIG_H -# include "config.h" -#endif -#include "log.h" -#include "macros.h" - -/** - * assert() equivalent, that is always enabled. - */ -#define av_assert0(cond) do { \ - if (!(cond)) { \ - av_log(NULL, AV_LOG_PANIC, "Assertion %s failed at %s:%d\n", \ - AV_STRINGIFY(cond), __FILE__, __LINE__); \ - abort(); \ - } \ -} while (0) - - -/** - * assert() equivalent, that does not lie in speed critical code. - * These asserts() thus can be enabled without fearing speed loss. - */ -#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 0 -#define av_assert1(cond) av_assert0(cond) -#else -#define av_assert1(cond) ((void)0) -#endif - - -/** - * assert() equivalent, that does lie in speed critical code. - */ -#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 1 -#define av_assert2(cond) av_assert0(cond) -#define av_assert2_fpu() av_assert0_fpu() -#else -#define av_assert2(cond) ((void)0) -#define av_assert2_fpu() ((void)0) -#endif - -/** - * Assert that floating point operations can be executed. - * - * This will av_assert0() that the cpu is not in MMX state on X86 - */ -void av_assert0_fpu(void); - -#endif /* AVUTIL_AVASSERT_H */ diff --git a/gostream/ffmpeg/include/libavutil/avconfig.h b/gostream/ffmpeg/include/libavutil/avconfig.h deleted file mode 100644 index c289fbb551c..00000000000 --- a/gostream/ffmpeg/include/libavutil/avconfig.h +++ /dev/null @@ -1,6 +0,0 @@ -/* Generated by ffmpeg configure */ -#ifndef AVUTIL_AVCONFIG_H -#define AVUTIL_AVCONFIG_H -#define AV_HAVE_BIGENDIAN 0 -#define AV_HAVE_FAST_UNALIGNED 1 -#endif /* AVUTIL_AVCONFIG_H */ diff --git a/gostream/ffmpeg/include/libavutil/avstring.h b/gostream/ffmpeg/include/libavutil/avstring.h deleted file mode 100644 index fc095349d26..00000000000 --- a/gostream/ffmpeg/include/libavutil/avstring.h +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright (c) 2007 Mans Rullgard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_AVSTRING_H -#define AVUTIL_AVSTRING_H - -#include -#include -#include "attributes.h" - -/** - * @addtogroup lavu_string - * @{ - */ - -/** - * Return non-zero if pfx is a prefix of str. If it is, *ptr is set to - * the address of the first character in str after the prefix. - * - * @param str input string - * @param pfx prefix to test - * @param ptr updated if the prefix is matched inside str - * @return non-zero if the prefix matches, zero otherwise - */ -int av_strstart(const char *str, const char *pfx, const char **ptr); - -/** - * Return non-zero if pfx is a prefix of str independent of case. If - * it is, *ptr is set to the address of the first character in str - * after the prefix. - * - * @param str input string - * @param pfx prefix to test - * @param ptr updated if the prefix is matched inside str - * @return non-zero if the prefix matches, zero otherwise - */ -int av_stristart(const char *str, const char *pfx, const char **ptr); - -/** - * Locate the first case-independent occurrence in the string haystack - * of the string needle. A zero-length string needle is considered to - * match at the start of haystack. - * - * This function is a case-insensitive version of the standard strstr(). - * - * @param haystack string to search in - * @param needle string to search for - * @return pointer to the located match within haystack - * or a null pointer if no match - */ -char *av_stristr(const char *haystack, const char *needle); - -/** - * Locate the first occurrence of the string needle in the string haystack - * where not more than hay_length characters are searched. A zero-length - * string needle is considered to match at the start of haystack. - * - * This function is a length-limited version of the standard strstr(). - * - * @param haystack string to search in - * @param needle string to search for - * @param hay_length length of string to search in - * @return pointer to the located match within haystack - * or a null pointer if no match - */ -char *av_strnstr(const char *haystack, const char *needle, size_t hay_length); - -/** - * Copy the string src to dst, but no more than size - 1 bytes, and - * null-terminate dst. - * - * This function is the same as BSD strlcpy(). - * - * @param dst destination buffer - * @param src source string - * @param size size of destination buffer - * @return the length of src - * - * @warning since the return value is the length of src, src absolutely - * _must_ be a properly 0-terminated string, otherwise this will read beyond - * the end of the buffer and possibly crash. - */ -size_t av_strlcpy(char *dst, const char *src, size_t size); - -/** - * Append the string src to the string dst, but to a total length of - * no more than size - 1 bytes, and null-terminate dst. - * - * This function is similar to BSD strlcat(), but differs when - * size <= strlen(dst). - * - * @param dst destination buffer - * @param src source string - * @param size size of destination buffer - * @return the total length of src and dst - * - * @warning since the return value use the length of src and dst, these - * absolutely _must_ be a properly 0-terminated strings, otherwise this - * will read beyond the end of the buffer and possibly crash. - */ -size_t av_strlcat(char *dst, const char *src, size_t size); - -/** - * Append output to a string, according to a format. Never write out of - * the destination buffer, and always put a terminating 0 within - * the buffer. - * @param dst destination buffer (string to which the output is - * appended) - * @param size total size of the destination buffer - * @param fmt printf-compatible format string, specifying how the - * following parameters are used - * @return the length of the string that would have been generated - * if enough space had been available - */ -size_t av_strlcatf(char *dst, size_t size, const char *fmt, ...) av_printf_format(3, 4); - -/** - * Get the count of continuous non zero chars starting from the beginning. - * - * @param s the string whose length to count - * @param len maximum number of characters to check in the string, that - * is the maximum value which is returned by the function - */ -static inline size_t av_strnlen(const char *s, size_t len) -{ - size_t i; - for (i = 0; i < len && s[i]; i++) - ; - return i; -} - -/** - * Print arguments following specified format into a large enough auto - * allocated buffer. It is similar to GNU asprintf(). - * @param fmt printf-compatible format string, specifying how the - * following parameters are used. - * @return the allocated string - * @note You have to free the string yourself with av_free(). - */ -char *av_asprintf(const char *fmt, ...) av_printf_format(1, 2); - -/** - * Unescape the given string until a non escaped terminating char, - * and return the token corresponding to the unescaped string. - * - * The normal \ and ' escaping is supported. Leading and trailing - * whitespaces are removed, unless they are escaped with '\' or are - * enclosed between ''. - * - * @param buf the buffer to parse, buf will be updated to point to the - * terminating char - * @param term a 0-terminated list of terminating chars - * @return the malloced unescaped string, which must be av_freed by - * the user, NULL in case of allocation failure - */ -char *av_get_token(const char **buf, const char *term); - -/** - * Split the string into several tokens which can be accessed by - * successive calls to av_strtok(). - * - * A token is defined as a sequence of characters not belonging to the - * set specified in delim. - * - * On the first call to av_strtok(), s should point to the string to - * parse, and the value of saveptr is ignored. In subsequent calls, s - * should be NULL, and saveptr should be unchanged since the previous - * call. - * - * This function is similar to strtok_r() defined in POSIX.1. - * - * @param s the string to parse, may be NULL - * @param delim 0-terminated list of token delimiters, must be non-NULL - * @param saveptr user-provided pointer which points to stored - * information necessary for av_strtok() to continue scanning the same - * string. saveptr is updated to point to the next character after the - * first delimiter found, or to NULL if the string was terminated - * @return the found token, or NULL when no token is found - */ -char *av_strtok(char *s, const char *delim, char **saveptr); - -/** - * Locale-independent conversion of ASCII isdigit. - */ -static inline av_const int av_isdigit(int c) -{ - return c >= '0' && c <= '9'; -} - -/** - * Locale-independent conversion of ASCII isgraph. - */ -static inline av_const int av_isgraph(int c) -{ - return c > 32 && c < 127; -} - -/** - * Locale-independent conversion of ASCII isspace. - */ -static inline av_const int av_isspace(int c) -{ - return c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || - c == '\v'; -} - -/** - * Locale-independent conversion of ASCII characters to uppercase. - */ -static inline av_const int av_toupper(int c) -{ - if (c >= 'a' && c <= 'z') - c ^= 0x20; - return c; -} - -/** - * Locale-independent conversion of ASCII characters to lowercase. - */ -static inline av_const int av_tolower(int c) -{ - if (c >= 'A' && c <= 'Z') - c ^= 0x20; - return c; -} - -/** - * Locale-independent conversion of ASCII isxdigit. - */ -static inline av_const int av_isxdigit(int c) -{ - c = av_tolower(c); - return av_isdigit(c) || (c >= 'a' && c <= 'f'); -} - -/** - * Locale-independent case-insensitive compare. - * @note This means only ASCII-range characters are case-insensitive - */ -int av_strcasecmp(const char *a, const char *b); - -/** - * Locale-independent case-insensitive compare. - * @note This means only ASCII-range characters are case-insensitive - */ -int av_strncasecmp(const char *a, const char *b, size_t n); - -/** - * Locale-independent strings replace. - * @note This means only ASCII-range characters are replaced. - */ -char *av_strireplace(const char *str, const char *from, const char *to); - -/** - * Thread safe basename. - * @param path the string to parse, on DOS both \ and / are considered separators. - * @return pointer to the basename substring. - * If path does not contain a slash, the function returns a copy of path. - * If path is a NULL pointer or points to an empty string, a pointer - * to a string "." is returned. - */ -const char *av_basename(const char *path); - -/** - * Thread safe dirname. - * @param path the string to parse, on DOS both \ and / are considered separators. - * @return A pointer to a string that's the parent directory of path. - * If path is a NULL pointer or points to an empty string, a pointer - * to a string "." is returned. - * @note the function may modify the contents of the path, so copies should be passed. - */ -const char *av_dirname(char *path); - -/** - * Match instances of a name in a comma-separated list of names. - * List entries are checked from the start to the end of the names list, - * the first match ends further processing. If an entry prefixed with '-' - * matches, then 0 is returned. The "ALL" list entry is considered to - * match all names. - * - * @param name Name to look for. - * @param names List of names. - * @return 1 on match, 0 otherwise. - */ -int av_match_name(const char *name, const char *names); - -/** - * Append path component to the existing path. - * Path separator '/' is placed between when needed. - * Resulting string have to be freed with av_free(). - * @param path base path - * @param component component to be appended - * @return new path or NULL on error. - */ -char *av_append_path_component(const char *path, const char *component); - -enum AVEscapeMode { - AV_ESCAPE_MODE_AUTO, ///< Use auto-selected escaping mode. - AV_ESCAPE_MODE_BACKSLASH, ///< Use backslash escaping. - AV_ESCAPE_MODE_QUOTE, ///< Use single-quote escaping. - AV_ESCAPE_MODE_XML, ///< Use XML non-markup character data escaping. -}; - -/** - * Consider spaces special and escape them even in the middle of the - * string. - * - * This is equivalent to adding the whitespace characters to the special - * characters lists, except it is guaranteed to use the exact same list - * of whitespace characters as the rest of libavutil. - */ -#define AV_ESCAPE_FLAG_WHITESPACE (1 << 0) - -/** - * Escape only specified special characters. - * Without this flag, escape also any characters that may be considered - * special by av_get_token(), such as the single quote. - */ -#define AV_ESCAPE_FLAG_STRICT (1 << 1) - -/** - * Within AV_ESCAPE_MODE_XML, additionally escape single quotes for single - * quoted attributes. - */ -#define AV_ESCAPE_FLAG_XML_SINGLE_QUOTES (1 << 2) - -/** - * Within AV_ESCAPE_MODE_XML, additionally escape double quotes for double - * quoted attributes. - */ -#define AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES (1 << 3) - - -/** - * Escape string in src, and put the escaped string in an allocated - * string in *dst, which must be freed with av_free(). - * - * @param dst pointer where an allocated string is put - * @param src string to escape, must be non-NULL - * @param special_chars string containing the special characters which - * need to be escaped, can be NULL - * @param mode escape mode to employ, see AV_ESCAPE_MODE_* macros. - * Any unknown value for mode will be considered equivalent to - * AV_ESCAPE_MODE_BACKSLASH, but this behaviour can change without - * notice. - * @param flags flags which control how to escape, see AV_ESCAPE_FLAG_ macros - * @return the length of the allocated string, or a negative error code in case of error - * @see av_bprint_escape() - */ -av_warn_unused_result -int av_escape(char **dst, const char *src, const char *special_chars, - enum AVEscapeMode mode, int flags); - -#define AV_UTF8_FLAG_ACCEPT_INVALID_BIG_CODES 1 ///< accept codepoints over 0x10FFFF -#define AV_UTF8_FLAG_ACCEPT_NON_CHARACTERS 2 ///< accept non-characters - 0xFFFE and 0xFFFF -#define AV_UTF8_FLAG_ACCEPT_SURROGATES 4 ///< accept UTF-16 surrogates codes -#define AV_UTF8_FLAG_EXCLUDE_XML_INVALID_CONTROL_CODES 8 ///< exclude control codes not accepted by XML - -#define AV_UTF8_FLAG_ACCEPT_ALL \ - AV_UTF8_FLAG_ACCEPT_INVALID_BIG_CODES|AV_UTF8_FLAG_ACCEPT_NON_CHARACTERS|AV_UTF8_FLAG_ACCEPT_SURROGATES - -/** - * Read and decode a single UTF-8 code point (character) from the - * buffer in *buf, and update *buf to point to the next byte to - * decode. - * - * In case of an invalid byte sequence, the pointer will be updated to - * the next byte after the invalid sequence and the function will - * return an error code. - * - * Depending on the specified flags, the function will also fail in - * case the decoded code point does not belong to a valid range. - * - * @note For speed-relevant code a carefully implemented use of - * GET_UTF8() may be preferred. - * - * @param codep pointer used to return the parsed code in case of success. - * The value in *codep is set even in case the range check fails. - * @param bufp pointer to the address the first byte of the sequence - * to decode, updated by the function to point to the - * byte next after the decoded sequence - * @param buf_end pointer to the end of the buffer, points to the next - * byte past the last in the buffer. This is used to - * avoid buffer overreads (in case of an unfinished - * UTF-8 sequence towards the end of the buffer). - * @param flags a collection of AV_UTF8_FLAG_* flags - * @return >= 0 in case a sequence was successfully read, a negative - * value in case of invalid sequence - */ -av_warn_unused_result -int av_utf8_decode(int32_t *codep, const uint8_t **bufp, const uint8_t *buf_end, - unsigned int flags); - -/** - * Check if a name is in a list. - * @returns 0 if not found, or the 1 based index where it has been found in the - * list. - */ -int av_match_list(const char *name, const char *list, char separator); - -/** - * See libc sscanf manual for more information. - * Locale-independent sscanf implementation. - */ -int av_sscanf(const char *string, const char *format, ...); - -/** - * @} - */ - -#endif /* AVUTIL_AVSTRING_H */ diff --git a/gostream/ffmpeg/include/libavutil/avutil.h b/gostream/ffmpeg/include/libavutil/avutil.h deleted file mode 100644 index a362c8baa83..00000000000 --- a/gostream/ffmpeg/include/libavutil/avutil.h +++ /dev/null @@ -1,375 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_AVUTIL_H -#define AVUTIL_AVUTIL_H - -/** - * @file - * @ingroup lavu - * Convenience header that includes @ref lavu "libavutil"'s core. - */ - -/** - * @mainpage - * - * @section ffmpeg_intro Introduction - * - * This document describes the usage of the different libraries - * provided by FFmpeg. - * - * @li @ref libavc "libavcodec" encoding/decoding library - * @li @ref lavfi "libavfilter" graph-based frame editing library - * @li @ref libavf "libavformat" I/O and muxing/demuxing library - * @li @ref lavd "libavdevice" special devices muxing/demuxing library - * @li @ref lavu "libavutil" common utility library - * @li @ref lswr "libswresample" audio resampling, format conversion and mixing - * @li @ref lpp "libpostproc" post processing library - * @li @ref libsws "libswscale" color conversion and scaling library - * - * @section ffmpeg_versioning Versioning and compatibility - * - * Each of the FFmpeg libraries contains a version.h header, which defines a - * major, minor and micro version number with the - * LIBRARYNAME_VERSION_{MAJOR,MINOR,MICRO} macros. The major version - * number is incremented with backward incompatible changes - e.g. removing - * parts of the public API, reordering public struct members, etc. The minor - * version number is incremented for backward compatible API changes or major - * new features - e.g. adding a new public function or a new decoder. The micro - * version number is incremented for smaller changes that a calling program - * might still want to check for - e.g. changing behavior in a previously - * unspecified situation. - * - * FFmpeg guarantees backward API and ABI compatibility for each library as long - * as its major version number is unchanged. This means that no public symbols - * will be removed or renamed. Types and names of the public struct members and - * values of public macros and enums will remain the same (unless they were - * explicitly declared as not part of the public API). Documented behavior will - * not change. - * - * In other words, any correct program that works with a given FFmpeg snapshot - * should work just as well without any changes with any later snapshot with the - * same major versions. This applies to both rebuilding the program against new - * FFmpeg versions or to replacing the dynamic FFmpeg libraries that a program - * links against. - * - * However, new public symbols may be added and new members may be appended to - * public structs whose size is not part of public ABI (most public structs in - * FFmpeg). New macros and enum values may be added. Behavior in undocumented - * situations may change slightly (and be documented). All those are accompanied - * by an entry in doc/APIchanges and incrementing either the minor or micro - * version number. - */ - -/** - * @defgroup lavu libavutil - * Common code shared across all FFmpeg libraries. - * - * @note - * libavutil is designed to be modular. In most cases, in order to use the - * functions provided by one component of libavutil you must explicitly include - * the specific header containing that feature. If you are only using - * media-related components, you could simply include libavutil/avutil.h, which - * brings in most of the "core" components. - * - * @{ - * - * @defgroup lavu_crypto Crypto and Hashing - * - * @{ - * @} - * - * @defgroup lavu_math Mathematics - * @{ - * - * @} - * - * @defgroup lavu_string String Manipulation - * - * @{ - * - * @} - * - * @defgroup lavu_mem Memory Management - * - * @{ - * - * @} - * - * @defgroup lavu_data Data Structures - * @{ - * - * @} - * - * @defgroup lavu_video Video related - * - * @{ - * - * @} - * - * @defgroup lavu_audio Audio related - * - * @{ - * - * @} - * - * @defgroup lavu_error Error Codes - * - * @{ - * - * @} - * - * @defgroup lavu_log Logging Facility - * - * @{ - * - * @} - * - * @defgroup lavu_misc Other - * - * @{ - * - * @defgroup preproc_misc Preprocessor String Macros - * - * @{ - * - * @} - * - * @defgroup version_utils Library Version Macros - * - * @{ - * - * @} - */ - - -/** - * @addtogroup lavu_ver - * @{ - */ - -/** - * Return the LIBAVUTIL_VERSION_INT constant. - */ -unsigned avutil_version(void); - -/** - * Return an informative version string. This usually is the actual release - * version number or a git commit description. This string has no fixed format - * and can change any time. It should never be parsed by code. - */ -const char *av_version_info(void); - -/** - * Return the libavutil build-time configuration. - */ -const char *avutil_configuration(void); - -/** - * Return the libavutil license. - */ -const char *avutil_license(void); - -/** - * @} - */ - -/** - * @addtogroup lavu_media Media Type - * @brief Media Type - */ - -enum AVMediaType { - AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA - AVMEDIA_TYPE_VIDEO, - AVMEDIA_TYPE_AUDIO, - AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous - AVMEDIA_TYPE_SUBTITLE, - AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse - AVMEDIA_TYPE_NB -}; - -/** - * Return a string describing the media_type enum, NULL if media_type - * is unknown. - */ -const char *av_get_media_type_string(enum AVMediaType media_type); - -/** - * @defgroup lavu_const Constants - * @{ - * - * @defgroup lavu_enc Encoding specific - * - * @note those definition should move to avcodec - * @{ - */ - -#define FF_LAMBDA_SHIFT 7 -#define FF_LAMBDA_SCALE (1< - -/** - * @defgroup lavu_base64 Base64 - * @ingroup lavu_crypto - * @{ - */ - -/** - * Decode a base64-encoded string. - * - * @param out buffer for decoded data - * @param in null-terminated input string - * @param out_size size in bytes of the out buffer, must be at - * least 3/4 of the length of in, that is AV_BASE64_DECODE_SIZE(strlen(in)) - * @return number of bytes written, or a negative value in case of - * invalid input - */ -int av_base64_decode(uint8_t *out, const char *in, int out_size); - -/** - * Calculate the output size in bytes needed to decode a base64 string - * with length x to a data buffer. - */ -#define AV_BASE64_DECODE_SIZE(x) ((x) * 3LL / 4) - -/** - * Encode data to base64 and null-terminate. - * - * @param out buffer for encoded data - * @param out_size size in bytes of the out buffer (including the - * null terminator), must be at least AV_BASE64_SIZE(in_size) - * @param in input buffer containing the data to encode - * @param in_size size in bytes of the in buffer - * @return out or NULL in case of error - */ -char *av_base64_encode(char *out, int out_size, const uint8_t *in, int in_size); - -/** - * Calculate the output size needed to base64-encode x bytes to a - * null-terminated string. - */ -#define AV_BASE64_SIZE(x) (((x)+2) / 3 * 4 + 1) - - /** - * @} - */ - -#endif /* AVUTIL_BASE64_H */ diff --git a/gostream/ffmpeg/include/libavutil/blowfish.h b/gostream/ffmpeg/include/libavutil/blowfish.h deleted file mode 100644 index 9e289a40dab..00000000000 --- a/gostream/ffmpeg/include/libavutil/blowfish.h +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Blowfish algorithm - * Copyright (c) 2012 Samuel Pitoiset - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_BLOWFISH_H -#define AVUTIL_BLOWFISH_H - -#include - -/** - * @defgroup lavu_blowfish Blowfish - * @ingroup lavu_crypto - * @{ - */ - -#define AV_BF_ROUNDS 16 - -typedef struct AVBlowfish { - uint32_t p[AV_BF_ROUNDS + 2]; - uint32_t s[4][256]; -} AVBlowfish; - -/** - * Allocate an AVBlowfish context. - */ -AVBlowfish *av_blowfish_alloc(void); - -/** - * Initialize an AVBlowfish context. - * - * @param ctx an AVBlowfish context - * @param key a key - * @param key_len length of the key - */ -void av_blowfish_init(struct AVBlowfish *ctx, const uint8_t *key, int key_len); - -/** - * Encrypt or decrypt a buffer using a previously initialized context. - * - * @param ctx an AVBlowfish context - * @param xl left four bytes halves of input to be encrypted - * @param xr right four bytes halves of input to be encrypted - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_blowfish_crypt_ecb(struct AVBlowfish *ctx, uint32_t *xl, uint32_t *xr, - int decrypt); - -/** - * Encrypt or decrypt a buffer using a previously initialized context. - * - * @param ctx an AVBlowfish context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 8 byte blocks - * @param iv initialization vector for CBC mode, if NULL ECB will be used - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_blowfish_crypt(struct AVBlowfish *ctx, uint8_t *dst, const uint8_t *src, - int count, uint8_t *iv, int decrypt); - -/** - * @} - */ - -#endif /* AVUTIL_BLOWFISH_H */ diff --git a/gostream/ffmpeg/include/libavutil/bprint.h b/gostream/ffmpeg/include/libavutil/bprint.h deleted file mode 100644 index 85597454788..00000000000 --- a/gostream/ffmpeg/include/libavutil/bprint.h +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (c) 2012 Nicolas George - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_avbprint - * AVBPrint public header - */ - -#ifndef AVUTIL_BPRINT_H -#define AVUTIL_BPRINT_H - -#include - -#include "attributes.h" -#include "avstring.h" - -/** - * @defgroup lavu_avbprint AVBPrint - * @ingroup lavu_data - * - * A buffer to print data progressively - * @{ - */ - -/** - * Define a structure with extra padding to a fixed size - * This helps ensuring binary compatibility with future versions. - */ - -#define FF_PAD_STRUCTURE(name, size, ...) \ -struct ff_pad_helper_##name { __VA_ARGS__ }; \ -typedef struct name { \ - __VA_ARGS__ \ - char reserved_padding[size - sizeof(struct ff_pad_helper_##name)]; \ -} name; - -/** - * Buffer to print data progressively - * - * The string buffer grows as necessary and is always 0-terminated. - * The content of the string is never accessed, and thus is - * encoding-agnostic and can even hold binary data. - * - * Small buffers are kept in the structure itself, and thus require no - * memory allocation at all (unless the contents of the buffer is needed - * after the structure goes out of scope). This is almost as lightweight as - * declaring a local `char buf[512]`. - * - * The length of the string can go beyond the allocated size: the buffer is - * then truncated, but the functions still keep account of the actual total - * length. - * - * In other words, AVBPrint.len can be greater than AVBPrint.size and records - * the total length of what would have been to the buffer if there had been - * enough memory. - * - * Append operations do not need to be tested for failure: if a memory - * allocation fails, data stop being appended to the buffer, but the length - * is still updated. This situation can be tested with - * av_bprint_is_complete(). - * - * The AVBPrint.size_max field determines several possible behaviours: - * - `size_max = -1` (= `UINT_MAX`) or any large value will let the buffer be - * reallocated as necessary, with an amortized linear cost. - * - `size_max = 0` prevents writing anything to the buffer: only the total - * length is computed. The write operations can then possibly be repeated in - * a buffer with exactly the necessary size - * (using `size_init = size_max = len + 1`). - * - `size_max = 1` is automatically replaced by the exact size available in the - * structure itself, thus ensuring no dynamic memory allocation. The - * internal buffer is large enough to hold a reasonable paragraph of text, - * such as the current paragraph. - */ - -FF_PAD_STRUCTURE(AVBPrint, 1024, - char *str; /**< string so far */ - unsigned len; /**< length so far */ - unsigned size; /**< allocated memory */ - unsigned size_max; /**< maximum allocated memory */ - char reserved_internal_buffer[1]; -) - -/** - * @name Max size special values - * Convenience macros for special values for av_bprint_init() size_max - * parameter. - * @{ - */ - -/** - * Buffer will be reallocated as necessary, with an amortized linear cost. - */ -#define AV_BPRINT_SIZE_UNLIMITED ((unsigned)-1) -/** - * Use the exact size available in the AVBPrint structure itself. - * - * Thus ensuring no dynamic memory allocation. The internal buffer is large - * enough to hold a reasonable paragraph of text, such as the current paragraph. - */ -#define AV_BPRINT_SIZE_AUTOMATIC 1 -/** - * Do not write anything to the buffer, only calculate the total length. - * - * The write operations can then possibly be repeated in a buffer with - * exactly the necessary size (using `size_init = size_max = AVBPrint.len + 1`). - */ -#define AV_BPRINT_SIZE_COUNT_ONLY 0 -/** @} */ - -/** - * Init a print buffer. - * - * @param buf buffer to init - * @param size_init initial size (including the final 0) - * @param size_max maximum size; - * - `0` means do not write anything, just count the length - * - `1` is replaced by the maximum value for automatic storage - * any large value means that the internal buffer will be - * reallocated as needed up to that limit - * - `-1` is converted to `UINT_MAX`, the largest limit possible. - * Check also `AV_BPRINT_SIZE_*` macros. - */ -void av_bprint_init(AVBPrint *buf, unsigned size_init, unsigned size_max); - -/** - * Init a print buffer using a pre-existing buffer. - * - * The buffer will not be reallocated. - * In case size equals zero, the AVBPrint will be initialized to use - * the internal buffer as if using AV_BPRINT_SIZE_COUNT_ONLY with - * av_bprint_init(). - * - * @param buf buffer structure to init - * @param buffer byte buffer to use for the string data - * @param size size of buffer - */ -void av_bprint_init_for_buffer(AVBPrint *buf, char *buffer, unsigned size); - -/** - * Append a formatted string to a print buffer. - */ -void av_bprintf(AVBPrint *buf, const char *fmt, ...) av_printf_format(2, 3); - -/** - * Append a formatted string to a print buffer. - */ -void av_vbprintf(AVBPrint *buf, const char *fmt, va_list vl_arg); - -/** - * Append char c n times to a print buffer. - */ -void av_bprint_chars(AVBPrint *buf, char c, unsigned n); - -/** - * Append data to a print buffer. - * - * param buf bprint buffer to use - * param data pointer to data - * param size size of data - */ -void av_bprint_append_data(AVBPrint *buf, const char *data, unsigned size); - -struct tm; -/** - * Append a formatted date and time to a print buffer. - * - * param buf bprint buffer to use - * param fmt date and time format string, see strftime() - * param tm broken-down time structure to translate - * - * @note due to poor design of the standard strftime function, it may - * produce poor results if the format string expands to a very long text and - * the bprint buffer is near the limit stated by the size_max option. - */ -void av_bprint_strftime(AVBPrint *buf, const char *fmt, const struct tm *tm); - -/** - * Allocate bytes in the buffer for external use. - * - * @param[in] buf buffer structure - * @param[in] size required size - * @param[out] mem pointer to the memory area - * @param[out] actual_size size of the memory area after allocation; - * can be larger or smaller than size - */ -void av_bprint_get_buffer(AVBPrint *buf, unsigned size, - unsigned char **mem, unsigned *actual_size); - -/** - * Reset the string to "" but keep internal allocated data. - */ -void av_bprint_clear(AVBPrint *buf); - -/** - * Test if the print buffer is complete (not truncated). - * - * It may have been truncated due to a memory allocation failure - * or the size_max limit (compare size and size_max if necessary). - */ -static inline int av_bprint_is_complete(const AVBPrint *buf) -{ - return buf->len < buf->size; -} - -/** - * Finalize a print buffer. - * - * The print buffer can no longer be used afterwards, - * but the len and size fields are still valid. - * - * @arg[out] ret_str if not NULL, used to return a permanent copy of the - * buffer contents, or NULL if memory allocation fails; - * if NULL, the buffer is discarded and freed - * @return 0 for success or error code (probably AVERROR(ENOMEM)) - */ -int av_bprint_finalize(AVBPrint *buf, char **ret_str); - -/** - * Escape the content in src and append it to dstbuf. - * - * @param dstbuf already inited destination bprint buffer - * @param src string containing the text to escape - * @param special_chars string containing the special characters which - * need to be escaped, can be NULL - * @param mode escape mode to employ, see AV_ESCAPE_MODE_* macros. - * Any unknown value for mode will be considered equivalent to - * AV_ESCAPE_MODE_BACKSLASH, but this behaviour can change without - * notice. - * @param flags flags which control how to escape, see AV_ESCAPE_FLAG_* macros - */ -void av_bprint_escape(AVBPrint *dstbuf, const char *src, const char *special_chars, - enum AVEscapeMode mode, int flags); - -/** @} */ - -#endif /* AVUTIL_BPRINT_H */ diff --git a/gostream/ffmpeg/include/libavutil/bswap.h b/gostream/ffmpeg/include/libavutil/bswap.h deleted file mode 100644 index 4840ab433f2..00000000000 --- a/gostream/ffmpeg/include/libavutil/bswap.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * byte swapping routines - */ - -#ifndef AVUTIL_BSWAP_H -#define AVUTIL_BSWAP_H - -#include -#include "libavutil/avconfig.h" -#include "attributes.h" - -#ifdef HAVE_AV_CONFIG_H - -#include "config.h" - -#if ARCH_AARCH64 -# include "aarch64/bswap.h" -#elif ARCH_ARM -# include "arm/bswap.h" -#elif ARCH_AVR32 -# include "avr32/bswap.h" -#elif ARCH_RISCV -# include "riscv/bswap.h" -#elif ARCH_SH4 -# include "sh4/bswap.h" -#elif ARCH_X86 -# include "x86/bswap.h" -#endif - -#endif /* HAVE_AV_CONFIG_H */ - -#define AV_BSWAP16C(x) (((x) << 8 & 0xff00) | ((x) >> 8 & 0x00ff)) -#define AV_BSWAP32C(x) (AV_BSWAP16C(x) << 16 | AV_BSWAP16C((x) >> 16)) -#define AV_BSWAP64C(x) (AV_BSWAP32C(x) << 32 | AV_BSWAP32C((x) >> 32)) - -#define AV_BSWAPC(s, x) AV_BSWAP##s##C(x) - -#ifndef av_bswap16 -static av_always_inline av_const uint16_t av_bswap16(uint16_t x) -{ - x= (x>>8) | (x<<8); - return x; -} -#endif - -#ifndef av_bswap32 -static av_always_inline av_const uint32_t av_bswap32(uint32_t x) -{ - return AV_BSWAP32C(x); -} -#endif - -#ifndef av_bswap64 -static inline uint64_t av_const av_bswap64(uint64_t x) -{ - return (uint64_t)av_bswap32(x) << 32 | av_bswap32(x >> 32); -} -#endif - -// be2ne ... big-endian to native-endian -// le2ne ... little-endian to native-endian - -#if AV_HAVE_BIGENDIAN -#define av_be2ne16(x) (x) -#define av_be2ne32(x) (x) -#define av_be2ne64(x) (x) -#define av_le2ne16(x) av_bswap16(x) -#define av_le2ne32(x) av_bswap32(x) -#define av_le2ne64(x) av_bswap64(x) -#define AV_BE2NEC(s, x) (x) -#define AV_LE2NEC(s, x) AV_BSWAPC(s, x) -#else -#define av_be2ne16(x) av_bswap16(x) -#define av_be2ne32(x) av_bswap32(x) -#define av_be2ne64(x) av_bswap64(x) -#define av_le2ne16(x) (x) -#define av_le2ne32(x) (x) -#define av_le2ne64(x) (x) -#define AV_BE2NEC(s, x) AV_BSWAPC(s, x) -#define AV_LE2NEC(s, x) (x) -#endif - -#define AV_BE2NE16C(x) AV_BE2NEC(16, x) -#define AV_BE2NE32C(x) AV_BE2NEC(32, x) -#define AV_BE2NE64C(x) AV_BE2NEC(64, x) -#define AV_LE2NE16C(x) AV_LE2NEC(16, x) -#define AV_LE2NE32C(x) AV_LE2NEC(32, x) -#define AV_LE2NE64C(x) AV_LE2NEC(64, x) - -#endif /* AVUTIL_BSWAP_H */ diff --git a/gostream/ffmpeg/include/libavutil/buffer.h b/gostream/ffmpeg/include/libavutil/buffer.h deleted file mode 100644 index e1ef5b7f07f..00000000000 --- a/gostream/ffmpeg/include/libavutil/buffer.h +++ /dev/null @@ -1,322 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_buffer - * refcounted data buffer API - */ - -#ifndef AVUTIL_BUFFER_H -#define AVUTIL_BUFFER_H - -#include -#include - -/** - * @defgroup lavu_buffer AVBuffer - * @ingroup lavu_data - * - * @{ - * AVBuffer is an API for reference-counted data buffers. - * - * There are two core objects in this API -- AVBuffer and AVBufferRef. AVBuffer - * represents the data buffer itself; it is opaque and not meant to be accessed - * by the caller directly, but only through AVBufferRef. However, the caller may - * e.g. compare two AVBuffer pointers to check whether two different references - * are describing the same data buffer. AVBufferRef represents a single - * reference to an AVBuffer and it is the object that may be manipulated by the - * caller directly. - * - * There are two functions provided for creating a new AVBuffer with a single - * reference -- av_buffer_alloc() to just allocate a new buffer, and - * av_buffer_create() to wrap an existing array in an AVBuffer. From an existing - * reference, additional references may be created with av_buffer_ref(). - * Use av_buffer_unref() to free a reference (this will automatically free the - * data once all the references are freed). - * - * The convention throughout this API and the rest of FFmpeg is such that the - * buffer is considered writable if there exists only one reference to it (and - * it has not been marked as read-only). The av_buffer_is_writable() function is - * provided to check whether this is true and av_buffer_make_writable() will - * automatically create a new writable buffer when necessary. - * Of course nothing prevents the calling code from violating this convention, - * however that is safe only when all the existing references are under its - * control. - * - * @note Referencing and unreferencing the buffers is thread-safe and thus - * may be done from multiple threads simultaneously without any need for - * additional locking. - * - * @note Two different references to the same buffer can point to different - * parts of the buffer (i.e. their AVBufferRef.data will not be equal). - */ - -/** - * A reference counted buffer type. It is opaque and is meant to be used through - * references (AVBufferRef). - */ -typedef struct AVBuffer AVBuffer; - -/** - * A reference to a data buffer. - * - * The size of this struct is not a part of the public ABI and it is not meant - * to be allocated directly. - */ -typedef struct AVBufferRef { - AVBuffer *buffer; - - /** - * The data buffer. It is considered writable if and only if - * this is the only reference to the buffer, in which case - * av_buffer_is_writable() returns 1. - */ - uint8_t *data; - /** - * Size of data in bytes. - */ - size_t size; -} AVBufferRef; - -/** - * Allocate an AVBuffer of the given size using av_malloc(). - * - * @return an AVBufferRef of given size or NULL when out of memory - */ -AVBufferRef *av_buffer_alloc(size_t size); - -/** - * Same as av_buffer_alloc(), except the returned buffer will be initialized - * to zero. - */ -AVBufferRef *av_buffer_allocz(size_t size); - -/** - * Always treat the buffer as read-only, even when it has only one - * reference. - */ -#define AV_BUFFER_FLAG_READONLY (1 << 0) - -/** - * Create an AVBuffer from an existing array. - * - * If this function is successful, data is owned by the AVBuffer. The caller may - * only access data through the returned AVBufferRef and references derived from - * it. - * If this function fails, data is left untouched. - * @param data data array - * @param size size of data in bytes - * @param free a callback for freeing this buffer's data - * @param opaque parameter to be got for processing or passed to free - * @param flags a combination of AV_BUFFER_FLAG_* - * - * @return an AVBufferRef referring to data on success, NULL on failure. - */ -AVBufferRef *av_buffer_create(uint8_t *data, size_t size, - void (*free)(void *opaque, uint8_t *data), - void *opaque, int flags); - -/** - * Default free callback, which calls av_free() on the buffer data. - * This function is meant to be passed to av_buffer_create(), not called - * directly. - */ -void av_buffer_default_free(void *opaque, uint8_t *data); - -/** - * Create a new reference to an AVBuffer. - * - * @return a new AVBufferRef referring to the same AVBuffer as buf or NULL on - * failure. - */ -AVBufferRef *av_buffer_ref(const AVBufferRef *buf); - -/** - * Free a given reference and automatically free the buffer if there are no more - * references to it. - * - * @param buf the reference to be freed. The pointer is set to NULL on return. - */ -void av_buffer_unref(AVBufferRef **buf); - -/** - * @return 1 if the caller may write to the data referred to by buf (which is - * true if and only if buf is the only reference to the underlying AVBuffer). - * Return 0 otherwise. - * A positive answer is valid until av_buffer_ref() is called on buf. - */ -int av_buffer_is_writable(const AVBufferRef *buf); - -/** - * @return the opaque parameter set by av_buffer_create. - */ -void *av_buffer_get_opaque(const AVBufferRef *buf); - -int av_buffer_get_ref_count(const AVBufferRef *buf); - -/** - * Create a writable reference from a given buffer reference, avoiding data copy - * if possible. - * - * @param buf buffer reference to make writable. On success, buf is either left - * untouched, or it is unreferenced and a new writable AVBufferRef is - * written in its place. On failure, buf is left untouched. - * @return 0 on success, a negative AVERROR on failure. - */ -int av_buffer_make_writable(AVBufferRef **buf); - -/** - * Reallocate a given buffer. - * - * @param buf a buffer reference to reallocate. On success, buf will be - * unreferenced and a new reference with the required size will be - * written in its place. On failure buf will be left untouched. *buf - * may be NULL, then a new buffer is allocated. - * @param size required new buffer size. - * @return 0 on success, a negative AVERROR on failure. - * - * @note the buffer is actually reallocated with av_realloc() only if it was - * initially allocated through av_buffer_realloc(NULL) and there is only one - * reference to it (i.e. the one passed to this function). In all other cases - * a new buffer is allocated and the data is copied. - */ -int av_buffer_realloc(AVBufferRef **buf, size_t size); - -/** - * Ensure dst refers to the same data as src. - * - * When *dst is already equivalent to src, do nothing. Otherwise unreference dst - * and replace it with a new reference to src. - * - * @param dst Pointer to either a valid buffer reference or NULL. On success, - * this will point to a buffer reference equivalent to src. On - * failure, dst will be left untouched. - * @param src A buffer reference to replace dst with. May be NULL, then this - * function is equivalent to av_buffer_unref(dst). - * @return 0 on success - * AVERROR(ENOMEM) on memory allocation failure. - */ -int av_buffer_replace(AVBufferRef **dst, const AVBufferRef *src); - -/** - * @} - */ - -/** - * @defgroup lavu_bufferpool AVBufferPool - * @ingroup lavu_data - * - * @{ - * AVBufferPool is an API for a lock-free thread-safe pool of AVBuffers. - * - * Frequently allocating and freeing large buffers may be slow. AVBufferPool is - * meant to solve this in cases when the caller needs a set of buffers of the - * same size (the most obvious use case being buffers for raw video or audio - * frames). - * - * At the beginning, the user must call av_buffer_pool_init() to create the - * buffer pool. Then whenever a buffer is needed, call av_buffer_pool_get() to - * get a reference to a new buffer, similar to av_buffer_alloc(). This new - * reference works in all aspects the same way as the one created by - * av_buffer_alloc(). However, when the last reference to this buffer is - * unreferenced, it is returned to the pool instead of being freed and will be - * reused for subsequent av_buffer_pool_get() calls. - * - * When the caller is done with the pool and no longer needs to allocate any new - * buffers, av_buffer_pool_uninit() must be called to mark the pool as freeable. - * Once all the buffers are released, it will automatically be freed. - * - * Allocating and releasing buffers with this API is thread-safe as long as - * either the default alloc callback is used, or the user-supplied one is - * thread-safe. - */ - -/** - * The buffer pool. This structure is opaque and not meant to be accessed - * directly. It is allocated with av_buffer_pool_init() and freed with - * av_buffer_pool_uninit(). - */ -typedef struct AVBufferPool AVBufferPool; - -/** - * Allocate and initialize a buffer pool. - * - * @param size size of each buffer in this pool - * @param alloc a function that will be used to allocate new buffers when the - * pool is empty. May be NULL, then the default allocator will be used - * (av_buffer_alloc()). - * @return newly created buffer pool on success, NULL on error. - */ -AVBufferPool *av_buffer_pool_init(size_t size, AVBufferRef* (*alloc)(size_t size)); - -/** - * Allocate and initialize a buffer pool with a more complex allocator. - * - * @param size size of each buffer in this pool - * @param opaque arbitrary user data used by the allocator - * @param alloc a function that will be used to allocate new buffers when the - * pool is empty. May be NULL, then the default allocator will be - * used (av_buffer_alloc()). - * @param pool_free a function that will be called immediately before the pool - * is freed. I.e. after av_buffer_pool_uninit() is called - * by the caller and all the frames are returned to the pool - * and freed. It is intended to uninitialize the user opaque - * data. May be NULL. - * @return newly created buffer pool on success, NULL on error. - */ -AVBufferPool *av_buffer_pool_init2(size_t size, void *opaque, - AVBufferRef* (*alloc)(void *opaque, size_t size), - void (*pool_free)(void *opaque)); - -/** - * Mark the pool as being available for freeing. It will actually be freed only - * once all the allocated buffers associated with the pool are released. Thus it - * is safe to call this function while some of the allocated buffers are still - * in use. - * - * @param pool pointer to the pool to be freed. It will be set to NULL. - */ -void av_buffer_pool_uninit(AVBufferPool **pool); - -/** - * Allocate a new AVBuffer, reusing an old buffer from the pool when available. - * This function may be called simultaneously from multiple threads. - * - * @return a reference to the new buffer on success, NULL on error. - */ -AVBufferRef *av_buffer_pool_get(AVBufferPool *pool); - -/** - * Query the original opaque parameter of an allocated buffer in the pool. - * - * @param ref a buffer reference to a buffer returned by av_buffer_pool_get. - * @return the opaque parameter set by the buffer allocator function of the - * buffer pool. - * - * @note the opaque parameter of ref is used by the buffer pool implementation, - * therefore you have to use this function to access the original opaque - * parameter of an allocated buffer. - */ -void *av_buffer_pool_buffer_get_opaque(const AVBufferRef *ref); - -/** - * @} - */ - -#endif /* AVUTIL_BUFFER_H */ diff --git a/gostream/ffmpeg/include/libavutil/camellia.h b/gostream/ffmpeg/include/libavutil/camellia.h deleted file mode 100644 index 96787102e2a..00000000000 --- a/gostream/ffmpeg/include/libavutil/camellia.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - * An implementation of the CAMELLIA algorithm as mentioned in RFC3713 - * Copyright (c) 2014 Supraja Meedinti - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_CAMELLIA_H -#define AVUTIL_CAMELLIA_H - -#include - - -/** - * @file - * @brief Public header for libavutil CAMELLIA algorithm - * @defgroup lavu_camellia CAMELLIA - * @ingroup lavu_crypto - * @{ - */ - -extern const int av_camellia_size; - -struct AVCAMELLIA; - -/** - * Allocate an AVCAMELLIA context - * To free the struct: av_free(ptr) - */ -struct AVCAMELLIA *av_camellia_alloc(void); - -/** - * Initialize an AVCAMELLIA context. - * - * @param ctx an AVCAMELLIA context - * @param key a key of 16, 24, 32 bytes used for encryption/decryption - * @param key_bits number of keybits: possible are 128, 192, 256 - */ -int av_camellia_init(struct AVCAMELLIA *ctx, const uint8_t *key, int key_bits); - -/** - * Encrypt or decrypt a buffer using a previously initialized context - * - * @param ctx an AVCAMELLIA context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 16 byte blocks - * @param iv initialization vector for CBC mode, NULL for ECB mode - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_camellia_crypt(struct AVCAMELLIA *ctx, uint8_t *dst, const uint8_t *src, int count, uint8_t* iv, int decrypt); - -/** - * @} - */ -#endif /* AVUTIL_CAMELLIA_H */ diff --git a/gostream/ffmpeg/include/libavutil/cast5.h b/gostream/ffmpeg/include/libavutil/cast5.h deleted file mode 100644 index ad5b347e685..00000000000 --- a/gostream/ffmpeg/include/libavutil/cast5.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * An implementation of the CAST128 algorithm as mentioned in RFC2144 - * Copyright (c) 2014 Supraja Meedinti - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_CAST5_H -#define AVUTIL_CAST5_H - -#include - - -/** - * @file - * @brief Public header for libavutil CAST5 algorithm - * @defgroup lavu_cast5 CAST5 - * @ingroup lavu_crypto - * @{ - */ - -extern const int av_cast5_size; - -struct AVCAST5; - -/** - * Allocate an AVCAST5 context - * To free the struct: av_free(ptr) - */ -struct AVCAST5 *av_cast5_alloc(void); -/** - * Initialize an AVCAST5 context. - * - * @param ctx an AVCAST5 context - * @param key a key of 5,6,...16 bytes used for encryption/decryption - * @param key_bits number of keybits: possible are 40,48,...,128 - * @return 0 on success, less than 0 on failure - */ -int av_cast5_init(struct AVCAST5 *ctx, const uint8_t *key, int key_bits); - -/** - * Encrypt or decrypt a buffer using a previously initialized context, ECB mode only - * - * @param ctx an AVCAST5 context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 8 byte blocks - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_cast5_crypt(struct AVCAST5 *ctx, uint8_t *dst, const uint8_t *src, int count, int decrypt); - -/** - * Encrypt or decrypt a buffer using a previously initialized context - * - * @param ctx an AVCAST5 context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 8 byte blocks - * @param iv initialization vector for CBC mode, NULL for ECB mode - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_cast5_crypt2(struct AVCAST5 *ctx, uint8_t *dst, const uint8_t *src, int count, uint8_t *iv, int decrypt); -/** - * @} - */ -#endif /* AVUTIL_CAST5_H */ diff --git a/gostream/ffmpeg/include/libavutil/channel_layout.h b/gostream/ffmpeg/include/libavutil/channel_layout.h deleted file mode 100644 index c9c404ef185..00000000000 --- a/gostream/ffmpeg/include/libavutil/channel_layout.h +++ /dev/null @@ -1,807 +0,0 @@ -/* - * Copyright (c) 2006 Michael Niedermayer - * Copyright (c) 2008 Peter Ross - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_CHANNEL_LAYOUT_H -#define AVUTIL_CHANNEL_LAYOUT_H - -#include -#include - -#include "version.h" -#include "attributes.h" - -/** - * @file - * @ingroup lavu_audio_channels - * Public libavutil channel layout APIs header. - */ - - -/** - * @defgroup lavu_audio_channels Audio channels - * @ingroup lavu_audio - * - * Audio channel layout utility functions - * - * @{ - */ - -enum AVChannel { - ///< Invalid channel index - AV_CHAN_NONE = -1, - AV_CHAN_FRONT_LEFT, - AV_CHAN_FRONT_RIGHT, - AV_CHAN_FRONT_CENTER, - AV_CHAN_LOW_FREQUENCY, - AV_CHAN_BACK_LEFT, - AV_CHAN_BACK_RIGHT, - AV_CHAN_FRONT_LEFT_OF_CENTER, - AV_CHAN_FRONT_RIGHT_OF_CENTER, - AV_CHAN_BACK_CENTER, - AV_CHAN_SIDE_LEFT, - AV_CHAN_SIDE_RIGHT, - AV_CHAN_TOP_CENTER, - AV_CHAN_TOP_FRONT_LEFT, - AV_CHAN_TOP_FRONT_CENTER, - AV_CHAN_TOP_FRONT_RIGHT, - AV_CHAN_TOP_BACK_LEFT, - AV_CHAN_TOP_BACK_CENTER, - AV_CHAN_TOP_BACK_RIGHT, - /** Stereo downmix. */ - AV_CHAN_STEREO_LEFT = 29, - /** See above. */ - AV_CHAN_STEREO_RIGHT, - AV_CHAN_WIDE_LEFT, - AV_CHAN_WIDE_RIGHT, - AV_CHAN_SURROUND_DIRECT_LEFT, - AV_CHAN_SURROUND_DIRECT_RIGHT, - AV_CHAN_LOW_FREQUENCY_2, - AV_CHAN_TOP_SIDE_LEFT, - AV_CHAN_TOP_SIDE_RIGHT, - AV_CHAN_BOTTOM_FRONT_CENTER, - AV_CHAN_BOTTOM_FRONT_LEFT, - AV_CHAN_BOTTOM_FRONT_RIGHT, - - /** Channel is empty can be safely skipped. */ - AV_CHAN_UNUSED = 0x200, - - /** Channel contains data, but its position is unknown. */ - AV_CHAN_UNKNOWN = 0x300, - - /** - * Range of channels between AV_CHAN_AMBISONIC_BASE and - * AV_CHAN_AMBISONIC_END represent Ambisonic components using the ACN system. - * - * Given a channel id `` between AV_CHAN_AMBISONIC_BASE and - * AV_CHAN_AMBISONIC_END (inclusive), the ACN index of the channel `` is - * ` = - AV_CHAN_AMBISONIC_BASE`. - * - * @note these values are only used for AV_CHANNEL_ORDER_CUSTOM channel - * orderings, the AV_CHANNEL_ORDER_AMBISONIC ordering orders the channels - * implicitly by their position in the stream. - */ - AV_CHAN_AMBISONIC_BASE = 0x400, - // leave space for 1024 ids, which correspond to maximum order-32 harmonics, - // which should be enough for the foreseeable use cases - AV_CHAN_AMBISONIC_END = 0x7ff, -}; - -enum AVChannelOrder { - /** - * Only the channel count is specified, without any further information - * about the channel order. - */ - AV_CHANNEL_ORDER_UNSPEC, - /** - * The native channel order, i.e. the channels are in the same order in - * which they are defined in the AVChannel enum. This supports up to 63 - * different channels. - */ - AV_CHANNEL_ORDER_NATIVE, - /** - * The channel order does not correspond to any other predefined order and - * is stored as an explicit map. For example, this could be used to support - * layouts with 64 or more channels, or with empty/skipped (AV_CHAN_SILENCE) - * channels at arbitrary positions. - */ - AV_CHANNEL_ORDER_CUSTOM, - /** - * The audio is represented as the decomposition of the sound field into - * spherical harmonics. Each channel corresponds to a single expansion - * component. Channels are ordered according to ACN (Ambisonic Channel - * Number). - * - * The channel with the index n in the stream contains the spherical - * harmonic of degree l and order m given by - * @code{.unparsed} - * l = floor(sqrt(n)), - * m = n - l * (l + 1). - * @endcode - * - * Conversely given a spherical harmonic of degree l and order m, the - * corresponding channel index n is given by - * @code{.unparsed} - * n = l * (l + 1) + m. - * @endcode - * - * Normalization is assumed to be SN3D (Schmidt Semi-Normalization) - * as defined in AmbiX format $ 2.1. - */ - AV_CHANNEL_ORDER_AMBISONIC, -}; - - -/** - * @defgroup channel_masks Audio channel masks - * - * A channel layout is a 64-bits integer with a bit set for every channel. - * The number of bits set must be equal to the number of channels. - * The value 0 means that the channel layout is not known. - * @note this data structure is not powerful enough to handle channels - * combinations that have the same channel multiple times, such as - * dual-mono. - * - * @{ - */ -#define AV_CH_FRONT_LEFT (1ULL << AV_CHAN_FRONT_LEFT ) -#define AV_CH_FRONT_RIGHT (1ULL << AV_CHAN_FRONT_RIGHT ) -#define AV_CH_FRONT_CENTER (1ULL << AV_CHAN_FRONT_CENTER ) -#define AV_CH_LOW_FREQUENCY (1ULL << AV_CHAN_LOW_FREQUENCY ) -#define AV_CH_BACK_LEFT (1ULL << AV_CHAN_BACK_LEFT ) -#define AV_CH_BACK_RIGHT (1ULL << AV_CHAN_BACK_RIGHT ) -#define AV_CH_FRONT_LEFT_OF_CENTER (1ULL << AV_CHAN_FRONT_LEFT_OF_CENTER ) -#define AV_CH_FRONT_RIGHT_OF_CENTER (1ULL << AV_CHAN_FRONT_RIGHT_OF_CENTER) -#define AV_CH_BACK_CENTER (1ULL << AV_CHAN_BACK_CENTER ) -#define AV_CH_SIDE_LEFT (1ULL << AV_CHAN_SIDE_LEFT ) -#define AV_CH_SIDE_RIGHT (1ULL << AV_CHAN_SIDE_RIGHT ) -#define AV_CH_TOP_CENTER (1ULL << AV_CHAN_TOP_CENTER ) -#define AV_CH_TOP_FRONT_LEFT (1ULL << AV_CHAN_TOP_FRONT_LEFT ) -#define AV_CH_TOP_FRONT_CENTER (1ULL << AV_CHAN_TOP_FRONT_CENTER ) -#define AV_CH_TOP_FRONT_RIGHT (1ULL << AV_CHAN_TOP_FRONT_RIGHT ) -#define AV_CH_TOP_BACK_LEFT (1ULL << AV_CHAN_TOP_BACK_LEFT ) -#define AV_CH_TOP_BACK_CENTER (1ULL << AV_CHAN_TOP_BACK_CENTER ) -#define AV_CH_TOP_BACK_RIGHT (1ULL << AV_CHAN_TOP_BACK_RIGHT ) -#define AV_CH_STEREO_LEFT (1ULL << AV_CHAN_STEREO_LEFT ) -#define AV_CH_STEREO_RIGHT (1ULL << AV_CHAN_STEREO_RIGHT ) -#define AV_CH_WIDE_LEFT (1ULL << AV_CHAN_WIDE_LEFT ) -#define AV_CH_WIDE_RIGHT (1ULL << AV_CHAN_WIDE_RIGHT ) -#define AV_CH_SURROUND_DIRECT_LEFT (1ULL << AV_CHAN_SURROUND_DIRECT_LEFT ) -#define AV_CH_SURROUND_DIRECT_RIGHT (1ULL << AV_CHAN_SURROUND_DIRECT_RIGHT) -#define AV_CH_LOW_FREQUENCY_2 (1ULL << AV_CHAN_LOW_FREQUENCY_2 ) -#define AV_CH_TOP_SIDE_LEFT (1ULL << AV_CHAN_TOP_SIDE_LEFT ) -#define AV_CH_TOP_SIDE_RIGHT (1ULL << AV_CHAN_TOP_SIDE_RIGHT ) -#define AV_CH_BOTTOM_FRONT_CENTER (1ULL << AV_CHAN_BOTTOM_FRONT_CENTER ) -#define AV_CH_BOTTOM_FRONT_LEFT (1ULL << AV_CHAN_BOTTOM_FRONT_LEFT ) -#define AV_CH_BOTTOM_FRONT_RIGHT (1ULL << AV_CHAN_BOTTOM_FRONT_RIGHT ) - -#if FF_API_OLD_CHANNEL_LAYOUT -/** Channel mask value used for AVCodecContext.request_channel_layout - to indicate that the user requests the channel order of the decoder output - to be the native codec channel order. - @deprecated channel order is now indicated in a special field in - AVChannelLayout - */ -#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL -#endif - -/** - * @} - * @defgroup channel_mask_c Audio channel layouts - * @{ - * */ -#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER) -#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT) -#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY) -#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER) -#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER) -#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY) -#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER) -#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY) -#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT) -#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) -#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT) -#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY) -#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) -#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY) -#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER) -#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) -#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER) -#define AV_CH_LAYOUT_3POINT1POINT2 (AV_CH_LAYOUT_3POINT1|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT) -#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER) -#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER) -#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY) -#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) -#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) -#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT) -#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) -#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER) -#define AV_CH_LAYOUT_5POINT1POINT2_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT) -#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT) -#define AV_CH_LAYOUT_CUBE (AV_CH_LAYOUT_QUAD|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT) -#define AV_CH_LAYOUT_5POINT1POINT4_BACK (AV_CH_LAYOUT_5POINT1POINT2_BACK|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT) -#define AV_CH_LAYOUT_7POINT1POINT2 (AV_CH_LAYOUT_7POINT1|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT) -#define AV_CH_LAYOUT_7POINT1POINT4_BACK (AV_CH_LAYOUT_7POINT1POINT2|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT) -#define AV_CH_LAYOUT_HEXADECAGONAL (AV_CH_LAYOUT_OCTAGONAL|AV_CH_WIDE_LEFT|AV_CH_WIDE_RIGHT|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT|AV_CH_TOP_BACK_CENTER|AV_CH_TOP_FRONT_CENTER|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT) -#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT) -#define AV_CH_LAYOUT_22POINT2 (AV_CH_LAYOUT_7POINT1POINT4_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER|AV_CH_BACK_CENTER|AV_CH_LOW_FREQUENCY_2|AV_CH_TOP_FRONT_CENTER|AV_CH_TOP_CENTER|AV_CH_TOP_SIDE_LEFT|AV_CH_TOP_SIDE_RIGHT|AV_CH_TOP_BACK_CENTER|AV_CH_BOTTOM_FRONT_CENTER|AV_CH_BOTTOM_FRONT_LEFT|AV_CH_BOTTOM_FRONT_RIGHT) - -#define AV_CH_LAYOUT_7POINT1_TOP_BACK AV_CH_LAYOUT_5POINT1POINT2_BACK - -enum AVMatrixEncoding { - AV_MATRIX_ENCODING_NONE, - AV_MATRIX_ENCODING_DOLBY, - AV_MATRIX_ENCODING_DPLII, - AV_MATRIX_ENCODING_DPLIIX, - AV_MATRIX_ENCODING_DPLIIZ, - AV_MATRIX_ENCODING_DOLBYEX, - AV_MATRIX_ENCODING_DOLBYHEADPHONE, - AV_MATRIX_ENCODING_NB -}; - -/** - * @} - */ - -/** - * An AVChannelCustom defines a single channel within a custom order layout - * - * Unlike most structures in FFmpeg, sizeof(AVChannelCustom) is a part of the - * public ABI. - * - * No new fields may be added to it without a major version bump. - */ -typedef struct AVChannelCustom { - enum AVChannel id; - char name[16]; - void *opaque; -} AVChannelCustom; - -/** - * An AVChannelLayout holds information about the channel layout of audio data. - * - * A channel layout here is defined as a set of channels ordered in a specific - * way (unless the channel order is AV_CHANNEL_ORDER_UNSPEC, in which case an - * AVChannelLayout carries only the channel count). - * All orders may be treated as if they were AV_CHANNEL_ORDER_UNSPEC by - * ignoring everything but the channel count, as long as av_channel_layout_check() - * considers they are valid. - * - * Unlike most structures in FFmpeg, sizeof(AVChannelLayout) is a part of the - * public ABI and may be used by the caller. E.g. it may be allocated on stack - * or embedded in caller-defined structs. - * - * AVChannelLayout can be initialized as follows: - * - default initialization with {0}, followed by setting all used fields - * correctly; - * - by assigning one of the predefined AV_CHANNEL_LAYOUT_* initializers; - * - with a constructor function, such as av_channel_layout_default(), - * av_channel_layout_from_mask() or av_channel_layout_from_string(). - * - * The channel layout must be unitialized with av_channel_layout_uninit() - * - * Copying an AVChannelLayout via assigning is forbidden, - * av_channel_layout_copy() must be used instead (and its return value should - * be checked) - * - * No new fields may be added to it without a major version bump, except for - * new elements of the union fitting in sizeof(uint64_t). - */ -typedef struct AVChannelLayout { - /** - * Channel order used in this layout. - * This is a mandatory field. - */ - enum AVChannelOrder order; - - /** - * Number of channels in this layout. Mandatory field. - */ - int nb_channels; - - /** - * Details about which channels are present in this layout. - * For AV_CHANNEL_ORDER_UNSPEC, this field is undefined and must not be - * used. - */ - union { - /** - * This member must be used for AV_CHANNEL_ORDER_NATIVE, and may be used - * for AV_CHANNEL_ORDER_AMBISONIC to signal non-diegetic channels. - * It is a bitmask, where the position of each set bit means that the - * AVChannel with the corresponding value is present. - * - * I.e. when (mask & (1 << AV_CHAN_FOO)) is non-zero, then AV_CHAN_FOO - * is present in the layout. Otherwise it is not present. - * - * @note when a channel layout using a bitmask is constructed or - * modified manually (i.e. not using any of the av_channel_layout_* - * functions), the code doing it must ensure that the number of set bits - * is equal to nb_channels. - */ - uint64_t mask; - /** - * This member must be used when the channel order is - * AV_CHANNEL_ORDER_CUSTOM. It is a nb_channels-sized array, with each - * element signalling the presence of the AVChannel with the - * corresponding value in map[i].id. - * - * I.e. when map[i].id is equal to AV_CHAN_FOO, then AV_CH_FOO is the - * i-th channel in the audio data. - * - * When map[i].id is in the range between AV_CHAN_AMBISONIC_BASE and - * AV_CHAN_AMBISONIC_END (inclusive), the channel contains an ambisonic - * component with ACN index (as defined above) - * n = map[i].id - AV_CHAN_AMBISONIC_BASE. - * - * map[i].name may be filled with a 0-terminated string, in which case - * it will be used for the purpose of identifying the channel with the - * convenience functions below. Otherise it must be zeroed. - */ - AVChannelCustom *map; - } u; - - /** - * For some private data of the user. - */ - void *opaque; -} AVChannelLayout; - -/** - * Macro to define native channel layouts - * - * @note This doesn't use designated initializers for compatibility with C++ 17 and older. - */ -#define AV_CHANNEL_LAYOUT_MASK(nb, m) \ - { /* .order */ AV_CHANNEL_ORDER_NATIVE, \ - /* .nb_channels */ (nb), \ - /* .u.mask */ { m }, \ - /* .opaque */ NULL } - -/** - * @name Common pre-defined channel layouts - * @{ - */ -#define AV_CHANNEL_LAYOUT_MONO AV_CHANNEL_LAYOUT_MASK(1, AV_CH_LAYOUT_MONO) -#define AV_CHANNEL_LAYOUT_STEREO AV_CHANNEL_LAYOUT_MASK(2, AV_CH_LAYOUT_STEREO) -#define AV_CHANNEL_LAYOUT_2POINT1 AV_CHANNEL_LAYOUT_MASK(3, AV_CH_LAYOUT_2POINT1) -#define AV_CHANNEL_LAYOUT_2_1 AV_CHANNEL_LAYOUT_MASK(3, AV_CH_LAYOUT_2_1) -#define AV_CHANNEL_LAYOUT_SURROUND AV_CHANNEL_LAYOUT_MASK(3, AV_CH_LAYOUT_SURROUND) -#define AV_CHANNEL_LAYOUT_3POINT1 AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_3POINT1) -#define AV_CHANNEL_LAYOUT_4POINT0 AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_4POINT0) -#define AV_CHANNEL_LAYOUT_4POINT1 AV_CHANNEL_LAYOUT_MASK(5, AV_CH_LAYOUT_4POINT1) -#define AV_CHANNEL_LAYOUT_2_2 AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_2_2) -#define AV_CHANNEL_LAYOUT_QUAD AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_QUAD) -#define AV_CHANNEL_LAYOUT_5POINT0 AV_CHANNEL_LAYOUT_MASK(5, AV_CH_LAYOUT_5POINT0) -#define AV_CHANNEL_LAYOUT_5POINT1 AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_5POINT1) -#define AV_CHANNEL_LAYOUT_5POINT0_BACK AV_CHANNEL_LAYOUT_MASK(5, AV_CH_LAYOUT_5POINT0_BACK) -#define AV_CHANNEL_LAYOUT_5POINT1_BACK AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_5POINT1_BACK) -#define AV_CHANNEL_LAYOUT_6POINT0 AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_6POINT0) -#define AV_CHANNEL_LAYOUT_6POINT0_FRONT AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_6POINT0_FRONT) -#define AV_CHANNEL_LAYOUT_3POINT1POINT2 AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_3POINT1POINT2) -#define AV_CHANNEL_LAYOUT_HEXAGONAL AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_HEXAGONAL) -#define AV_CHANNEL_LAYOUT_6POINT1 AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_6POINT1) -#define AV_CHANNEL_LAYOUT_6POINT1_BACK AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_6POINT1_BACK) -#define AV_CHANNEL_LAYOUT_6POINT1_FRONT AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_6POINT1_FRONT) -#define AV_CHANNEL_LAYOUT_7POINT0 AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_7POINT0) -#define AV_CHANNEL_LAYOUT_7POINT0_FRONT AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_7POINT0_FRONT) -#define AV_CHANNEL_LAYOUT_7POINT1 AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_7POINT1) -#define AV_CHANNEL_LAYOUT_7POINT1_WIDE AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_7POINT1_WIDE) -#define AV_CHANNEL_LAYOUT_7POINT1_WIDE_BACK AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_7POINT1_WIDE_BACK) -#define AV_CHANNEL_LAYOUT_5POINT1POINT2_BACK AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_5POINT1POINT2_BACK) -#define AV_CHANNEL_LAYOUT_OCTAGONAL AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_OCTAGONAL) -#define AV_CHANNEL_LAYOUT_CUBE AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_CUBE) -#define AV_CHANNEL_LAYOUT_5POINT1POINT4_BACK AV_CHANNEL_LAYOUT_MASK(10, AV_CH_LAYOUT_5POINT1POINT4_BACK) -#define AV_CHANNEL_LAYOUT_7POINT1POINT2 AV_CHANNEL_LAYOUT_MASK(10, AV_CH_LAYOUT_7POINT1POINT2) -#define AV_CHANNEL_LAYOUT_7POINT1POINT4_BACK AV_CHANNEL_LAYOUT_MASK(12, AV_CH_LAYOUT_7POINT1POINT4_BACK) -#define AV_CHANNEL_LAYOUT_HEXADECAGONAL AV_CHANNEL_LAYOUT_MASK(16, AV_CH_LAYOUT_HEXADECAGONAL) -#define AV_CHANNEL_LAYOUT_STEREO_DOWNMIX AV_CHANNEL_LAYOUT_MASK(2, AV_CH_LAYOUT_STEREO_DOWNMIX) -#define AV_CHANNEL_LAYOUT_22POINT2 AV_CHANNEL_LAYOUT_MASK(24, AV_CH_LAYOUT_22POINT2) - -#define AV_CHANNEL_LAYOUT_7POINT1_TOP_BACK AV_CHANNEL_LAYOUT_5POINT1POINT2_BACK - -#define AV_CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER \ - { /* .order */ AV_CHANNEL_ORDER_AMBISONIC, \ - /* .nb_channels */ 4, \ - /* .u.mask */ { 0 }, \ - /* .opaque */ NULL } -/** @} */ - -struct AVBPrint; - -#if FF_API_OLD_CHANNEL_LAYOUT -/** - * @name Deprecated Functions - * @{ - */ - -/** - * Return a channel layout id that matches name, or 0 if no match is found. - * - * name can be one or several of the following notations, - * separated by '+' or '|': - * - the name of an usual channel layout (mono, stereo, 4.0, quad, 5.0, - * 5.0(side), 5.1, 5.1(side), 7.1, 7.1(wide), downmix); - * - the name of a single channel (FL, FR, FC, LFE, BL, BR, FLC, FRC, BC, - * SL, SR, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR); - * - a number of channels, in decimal, followed by 'c', yielding - * the default channel layout for that number of channels (@see - * av_get_default_channel_layout); - * - a channel layout mask, in hexadecimal starting with "0x" (see the - * AV_CH_* macros). - * - * Example: "stereo+FC" = "2c+FC" = "2c+1c" = "0x7" - * - * @deprecated use av_channel_layout_from_string() - */ -attribute_deprecated -uint64_t av_get_channel_layout(const char *name); - -/** - * Return a channel layout and the number of channels based on the specified name. - * - * This function is similar to (@see av_get_channel_layout), but can also parse - * unknown channel layout specifications. - * - * @param[in] name channel layout specification string - * @param[out] channel_layout parsed channel layout (0 if unknown) - * @param[out] nb_channels number of channels - * - * @return 0 on success, AVERROR(EINVAL) if the parsing fails. - * @deprecated use av_channel_layout_from_string() - */ -attribute_deprecated -int av_get_extended_channel_layout(const char *name, uint64_t* channel_layout, int* nb_channels); - -/** - * Return a description of a channel layout. - * If nb_channels is <= 0, it is guessed from the channel_layout. - * - * @param buf put here the string containing the channel layout - * @param buf_size size in bytes of the buffer - * @param nb_channels number of channels - * @param channel_layout channel layout bitset - * @deprecated use av_channel_layout_describe() - */ -attribute_deprecated -void av_get_channel_layout_string(char *buf, int buf_size, int nb_channels, uint64_t channel_layout); - -/** - * Append a description of a channel layout to a bprint buffer. - * @deprecated use av_channel_layout_describe() - */ -attribute_deprecated -void av_bprint_channel_layout(struct AVBPrint *bp, int nb_channels, uint64_t channel_layout); - -/** - * Return the number of channels in the channel layout. - * @deprecated use AVChannelLayout.nb_channels - */ -attribute_deprecated -int av_get_channel_layout_nb_channels(uint64_t channel_layout); - -/** - * Return default channel layout for a given number of channels. - * - * @deprecated use av_channel_layout_default() - */ -attribute_deprecated -int64_t av_get_default_channel_layout(int nb_channels); - -/** - * Get the index of a channel in channel_layout. - * - * @param channel_layout channel layout bitset - * @param channel a channel layout describing exactly one channel which must be - * present in channel_layout. - * - * @return index of channel in channel_layout on success, a negative AVERROR - * on error. - * - * @deprecated use av_channel_layout_index_from_channel() - */ -attribute_deprecated -int av_get_channel_layout_channel_index(uint64_t channel_layout, - uint64_t channel); - -/** - * Get the channel with the given index in channel_layout. - * @deprecated use av_channel_layout_channel_from_index() - */ -attribute_deprecated -uint64_t av_channel_layout_extract_channel(uint64_t channel_layout, int index); - -/** - * Get the name of a given channel. - * - * @return channel name on success, NULL on error. - * - * @deprecated use av_channel_name() - */ -attribute_deprecated -const char *av_get_channel_name(uint64_t channel); - -/** - * Get the description of a given channel. - * - * @param channel a channel layout with a single channel - * @return channel description on success, NULL on error - * @deprecated use av_channel_description() - */ -attribute_deprecated -const char *av_get_channel_description(uint64_t channel); - -/** - * Get the value and name of a standard channel layout. - * - * @param[in] index index in an internal list, starting at 0 - * @param[out] layout channel layout mask - * @param[out] name name of the layout - * @return 0 if the layout exists, - * <0 if index is beyond the limits - * @deprecated use av_channel_layout_standard() - */ -attribute_deprecated -int av_get_standard_channel_layout(unsigned index, uint64_t *layout, - const char **name); -/** - * @} - */ -#endif - -/** - * Get a human readable string in an abbreviated form describing a given channel. - * This is the inverse function of @ref av_channel_from_string(). - * - * @param buf pre-allocated buffer where to put the generated string - * @param buf_size size in bytes of the buffer. - * @param channel the AVChannel whose name to get - * @return amount of bytes needed to hold the output string, or a negative AVERROR - * on failure. If the returned value is bigger than buf_size, then the - * string was truncated. - */ -int av_channel_name(char *buf, size_t buf_size, enum AVChannel channel); - -/** - * bprint variant of av_channel_name(). - * - * @note the string will be appended to the bprint buffer. - */ -void av_channel_name_bprint(struct AVBPrint *bp, enum AVChannel channel_id); - -/** - * Get a human readable string describing a given channel. - * - * @param buf pre-allocated buffer where to put the generated string - * @param buf_size size in bytes of the buffer. - * @param channel the AVChannel whose description to get - * @return amount of bytes needed to hold the output string, or a negative AVERROR - * on failure. If the returned value is bigger than buf_size, then the - * string was truncated. - */ -int av_channel_description(char *buf, size_t buf_size, enum AVChannel channel); - -/** - * bprint variant of av_channel_description(). - * - * @note the string will be appended to the bprint buffer. - */ -void av_channel_description_bprint(struct AVBPrint *bp, enum AVChannel channel_id); - -/** - * This is the inverse function of @ref av_channel_name(). - * - * @return the channel with the given name - * AV_CHAN_NONE when name does not identify a known channel - */ -enum AVChannel av_channel_from_string(const char *name); - -/** - * Initialize a native channel layout from a bitmask indicating which channels - * are present. - * - * @param channel_layout the layout structure to be initialized - * @param mask bitmask describing the channel layout - * - * @return 0 on success - * AVERROR(EINVAL) for invalid mask values - */ -int av_channel_layout_from_mask(AVChannelLayout *channel_layout, uint64_t mask); - -/** - * Initialize a channel layout from a given string description. - * The input string can be represented by: - * - the formal channel layout name (returned by av_channel_layout_describe()) - * - single or multiple channel names (returned by av_channel_name(), eg. "FL", - * or concatenated with "+", each optionally containing a custom name after - * a "@", eg. "FL@Left+FR@Right+LFE") - * - a decimal or hexadecimal value of a native channel layout (eg. "4" or "0x4") - * - the number of channels with default layout (eg. "4c") - * - the number of unordered channels (eg. "4C" or "4 channels") - * - the ambisonic order followed by optional non-diegetic channels (eg. - * "ambisonic 2+stereo") - * - * @param channel_layout input channel layout - * @param str string describing the channel layout - * @return 0 channel layout was detected, AVERROR_INVALIDATATA otherwise - */ -int av_channel_layout_from_string(AVChannelLayout *channel_layout, - const char *str); - -/** - * Get the default channel layout for a given number of channels. - * - * @param ch_layout the layout structure to be initialized - * @param nb_channels number of channels - */ -void av_channel_layout_default(AVChannelLayout *ch_layout, int nb_channels); - -/** - * Iterate over all standard channel layouts. - * - * @param opaque a pointer where libavutil will store the iteration state. Must - * point to NULL to start the iteration. - * - * @return the standard channel layout or NULL when the iteration is - * finished - */ -const AVChannelLayout *av_channel_layout_standard(void **opaque); - -/** - * Free any allocated data in the channel layout and reset the channel - * count to 0. - * - * @param channel_layout the layout structure to be uninitialized - */ -void av_channel_layout_uninit(AVChannelLayout *channel_layout); - -/** - * Make a copy of a channel layout. This differs from just assigning src to dst - * in that it allocates and copies the map for AV_CHANNEL_ORDER_CUSTOM. - * - * @note the destination channel_layout will be always uninitialized before copy. - * - * @param dst destination channel layout - * @param src source channel layout - * @return 0 on success, a negative AVERROR on error. - */ -int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src); - -/** - * Get a human-readable string describing the channel layout properties. - * The string will be in the same format that is accepted by - * @ref av_channel_layout_from_string(), allowing to rebuild the same - * channel layout, except for opaque pointers. - * - * @param channel_layout channel layout to be described - * @param buf pre-allocated buffer where to put the generated string - * @param buf_size size in bytes of the buffer. - * @return amount of bytes needed to hold the output string, or a negative AVERROR - * on failure. If the returned value is bigger than buf_size, then the - * string was truncated. - */ -int av_channel_layout_describe(const AVChannelLayout *channel_layout, - char *buf, size_t buf_size); - -/** - * bprint variant of av_channel_layout_describe(). - * - * @note the string will be appended to the bprint buffer. - * @return 0 on success, or a negative AVERROR value on failure. - */ -int av_channel_layout_describe_bprint(const AVChannelLayout *channel_layout, - struct AVBPrint *bp); - -/** - * Get the channel with the given index in a channel layout. - * - * @param channel_layout input channel layout - * @param idx index of the channel - * @return channel with the index idx in channel_layout on success or - * AV_CHAN_NONE on failure (if idx is not valid or the channel order is - * unspecified) - */ -enum AVChannel -av_channel_layout_channel_from_index(const AVChannelLayout *channel_layout, unsigned int idx); - -/** - * Get the index of a given channel in a channel layout. In case multiple - * channels are found, only the first match will be returned. - * - * @param channel_layout input channel layout - * @param channel the channel whose index to obtain - * @return index of channel in channel_layout on success or a negative number if - * channel is not present in channel_layout. - */ -int av_channel_layout_index_from_channel(const AVChannelLayout *channel_layout, - enum AVChannel channel); - -/** - * Get the index in a channel layout of a channel described by the given string. - * In case multiple channels are found, only the first match will be returned. - * - * This function accepts channel names in the same format as - * @ref av_channel_from_string(). - * - * @param channel_layout input channel layout - * @param name string describing the channel whose index to obtain - * @return a channel index described by the given string, or a negative AVERROR - * value. - */ -int av_channel_layout_index_from_string(const AVChannelLayout *channel_layout, - const char *name); - -/** - * Get a channel described by the given string. - * - * This function accepts channel names in the same format as - * @ref av_channel_from_string(). - * - * @param channel_layout input channel layout - * @param name string describing the channel to obtain - * @return a channel described by the given string in channel_layout on success - * or AV_CHAN_NONE on failure (if the string is not valid or the channel - * order is unspecified) - */ -enum AVChannel -av_channel_layout_channel_from_string(const AVChannelLayout *channel_layout, - const char *name); - -/** - * Find out what channels from a given set are present in a channel layout, - * without regard for their positions. - * - * @param channel_layout input channel layout - * @param mask a combination of AV_CH_* representing a set of channels - * @return a bitfield representing all the channels from mask that are present - * in channel_layout - */ -uint64_t av_channel_layout_subset(const AVChannelLayout *channel_layout, - uint64_t mask); - -/** - * Check whether a channel layout is valid, i.e. can possibly describe audio - * data. - * - * @param channel_layout input channel layout - * @return 1 if channel_layout is valid, 0 otherwise. - */ -int av_channel_layout_check(const AVChannelLayout *channel_layout); - -/** - * Check whether two channel layouts are semantically the same, i.e. the same - * channels are present on the same positions in both. - * - * If one of the channel layouts is AV_CHANNEL_ORDER_UNSPEC, while the other is - * not, they are considered to be unequal. If both are AV_CHANNEL_ORDER_UNSPEC, - * they are considered equal iff the channel counts are the same in both. - * - * @param chl input channel layout - * @param chl1 input channel layout - * @return 0 if chl and chl1 are equal, 1 if they are not equal. A negative - * AVERROR code if one or both are invalid. - */ -int av_channel_layout_compare(const AVChannelLayout *chl, const AVChannelLayout *chl1); - -/** - * @} - */ - -#endif /* AVUTIL_CHANNEL_LAYOUT_H */ diff --git a/gostream/ffmpeg/include/libavutil/common.h b/gostream/ffmpeg/include/libavutil/common.h deleted file mode 100644 index de2140a6786..00000000000 --- a/gostream/ffmpeg/include/libavutil/common.h +++ /dev/null @@ -1,579 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * common internal and external API header - */ - -#ifndef AVUTIL_COMMON_H -#define AVUTIL_COMMON_H - -#if defined(__cplusplus) && !defined(__STDC_CONSTANT_MACROS) && !defined(UINT64_C) -#error missing -D__STDC_CONSTANT_MACROS / #define __STDC_CONSTANT_MACROS -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "attributes.h" -#include "error.h" -#include "macros.h" - -//rounded division & shift -#define RSHIFT(a,b) ((a) > 0 ? ((a) + ((1<<(b))>>1))>>(b) : ((a) + ((1<<(b))>>1)-1)>>(b)) -/* assume b>0 */ -#define ROUNDED_DIV(a,b) (((a)>=0 ? (a) + ((b)>>1) : (a) - ((b)>>1))/(b)) -/* Fast a/(1<=0 and b>=0 */ -#define AV_CEIL_RSHIFT(a,b) (!av_builtin_constant_p(b) ? -((-(a)) >> (b)) \ - : ((a) + (1<<(b)) - 1) >> (b)) -/* Backwards compat. */ -#define FF_CEIL_RSHIFT AV_CEIL_RSHIFT - -#define FFUDIV(a,b) (((a)>0 ?(a):(a)-(b)+1) / (b)) -#define FFUMOD(a,b) ((a)-(b)*FFUDIV(a,b)) - -/** - * Absolute value, Note, INT_MIN / INT64_MIN result in undefined behavior as they - * are not representable as absolute values of their type. This is the same - * as with *abs() - * @see FFNABS() - */ -#define FFABS(a) ((a) >= 0 ? (a) : (-(a))) -#define FFSIGN(a) ((a) > 0 ? 1 : -1) - -/** - * Negative Absolute value. - * this works for all integers of all types. - * As with many macros, this evaluates its argument twice, it thus must not have - * a sideeffect, that is FFNABS(x++) has undefined behavior. - */ -#define FFNABS(a) ((a) <= 0 ? (a) : (-(a))) - -/** - * Unsigned Absolute value. - * This takes the absolute value of a signed int and returns it as a unsigned. - * This also works with INT_MIN which would otherwise not be representable - * As with many macros, this evaluates its argument twice. - */ -#define FFABSU(a) ((a) <= 0 ? -(unsigned)(a) : (unsigned)(a)) -#define FFABS64U(a) ((a) <= 0 ? -(uint64_t)(a) : (uint64_t)(a)) - -/* misc math functions */ - -#ifdef HAVE_AV_CONFIG_H -# include "config.h" -# include "intmath.h" -#endif - -#ifndef av_ceil_log2 -# define av_ceil_log2 av_ceil_log2_c -#endif -#ifndef av_clip -# define av_clip av_clip_c -#endif -#ifndef av_clip64 -# define av_clip64 av_clip64_c -#endif -#ifndef av_clip_uint8 -# define av_clip_uint8 av_clip_uint8_c -#endif -#ifndef av_clip_int8 -# define av_clip_int8 av_clip_int8_c -#endif -#ifndef av_clip_uint16 -# define av_clip_uint16 av_clip_uint16_c -#endif -#ifndef av_clip_int16 -# define av_clip_int16 av_clip_int16_c -#endif -#ifndef av_clipl_int32 -# define av_clipl_int32 av_clipl_int32_c -#endif -#ifndef av_clip_intp2 -# define av_clip_intp2 av_clip_intp2_c -#endif -#ifndef av_clip_uintp2 -# define av_clip_uintp2 av_clip_uintp2_c -#endif -#ifndef av_mod_uintp2 -# define av_mod_uintp2 av_mod_uintp2_c -#endif -#ifndef av_sat_add32 -# define av_sat_add32 av_sat_add32_c -#endif -#ifndef av_sat_dadd32 -# define av_sat_dadd32 av_sat_dadd32_c -#endif -#ifndef av_sat_sub32 -# define av_sat_sub32 av_sat_sub32_c -#endif -#ifndef av_sat_dsub32 -# define av_sat_dsub32 av_sat_dsub32_c -#endif -#ifndef av_sat_add64 -# define av_sat_add64 av_sat_add64_c -#endif -#ifndef av_sat_sub64 -# define av_sat_sub64 av_sat_sub64_c -#endif -#ifndef av_clipf -# define av_clipf av_clipf_c -#endif -#ifndef av_clipd -# define av_clipd av_clipd_c -#endif -#ifndef av_popcount -# define av_popcount av_popcount_c -#endif -#ifndef av_popcount64 -# define av_popcount64 av_popcount64_c -#endif -#ifndef av_parity -# define av_parity av_parity_c -#endif - -#ifndef av_log2 -av_const int av_log2(unsigned v); -#endif - -#ifndef av_log2_16bit -av_const int av_log2_16bit(unsigned v); -#endif - -/** - * Clip a signed integer value into the amin-amax range. - * @param a value to clip - * @param amin minimum value of the clip range - * @param amax maximum value of the clip range - * @return clipped value - */ -static av_always_inline av_const int av_clip_c(int a, int amin, int amax) -{ -#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2 - if (amin > amax) abort(); -#endif - if (a < amin) return amin; - else if (a > amax) return amax; - else return a; -} - -/** - * Clip a signed 64bit integer value into the amin-amax range. - * @param a value to clip - * @param amin minimum value of the clip range - * @param amax maximum value of the clip range - * @return clipped value - */ -static av_always_inline av_const int64_t av_clip64_c(int64_t a, int64_t amin, int64_t amax) -{ -#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2 - if (amin > amax) abort(); -#endif - if (a < amin) return amin; - else if (a > amax) return amax; - else return a; -} - -/** - * Clip a signed integer value into the 0-255 range. - * @param a value to clip - * @return clipped value - */ -static av_always_inline av_const uint8_t av_clip_uint8_c(int a) -{ - if (a&(~0xFF)) return (~a)>>31; - else return a; -} - -/** - * Clip a signed integer value into the -128,127 range. - * @param a value to clip - * @return clipped value - */ -static av_always_inline av_const int8_t av_clip_int8_c(int a) -{ - if ((a+0x80U) & ~0xFF) return (a>>31) ^ 0x7F; - else return a; -} - -/** - * Clip a signed integer value into the 0-65535 range. - * @param a value to clip - * @return clipped value - */ -static av_always_inline av_const uint16_t av_clip_uint16_c(int a) -{ - if (a&(~0xFFFF)) return (~a)>>31; - else return a; -} - -/** - * Clip a signed integer value into the -32768,32767 range. - * @param a value to clip - * @return clipped value - */ -static av_always_inline av_const int16_t av_clip_int16_c(int a) -{ - if ((a+0x8000U) & ~0xFFFF) return (a>>31) ^ 0x7FFF; - else return a; -} - -/** - * Clip a signed 64-bit integer value into the -2147483648,2147483647 range. - * @param a value to clip - * @return clipped value - */ -static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a) -{ - if ((a+0x80000000u) & ~UINT64_C(0xFFFFFFFF)) return (int32_t)((a>>63) ^ 0x7FFFFFFF); - else return (int32_t)a; -} - -/** - * Clip a signed integer into the -(2^p),(2^p-1) range. - * @param a value to clip - * @param p bit position to clip at - * @return clipped value - */ -static av_always_inline av_const int av_clip_intp2_c(int a, int p) -{ - if (((unsigned)a + (1 << p)) & ~((2 << p) - 1)) - return (a >> 31) ^ ((1 << p) - 1); - else - return a; -} - -/** - * Clip a signed integer to an unsigned power of two range. - * @param a value to clip - * @param p bit position to clip at - * @return clipped value - */ -static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p) -{ - if (a & ~((1<> 31 & ((1<= 0) - return INT64_MAX ^ (b >> 63); - return s; -#endif -} - -/** - * Subtract two signed 64-bit values with saturation. - * - * @param a one value - * @param b another value - * @return difference with signed saturation - */ -static av_always_inline int64_t av_sat_sub64_c(int64_t a, int64_t b) { -#if (!defined(__INTEL_COMPILER) && AV_GCC_VERSION_AT_LEAST(5,1)) || AV_HAS_BUILTIN(__builtin_sub_overflow) - int64_t tmp; - return !__builtin_sub_overflow(a, b, &tmp) ? tmp : (tmp < 0 ? INT64_MAX : INT64_MIN); -#else - if (b <= 0 && a >= INT64_MAX + b) - return INT64_MAX; - if (b >= 0 && a <= INT64_MIN + b) - return INT64_MIN; - return a - b; -#endif -} - -/** - * Clip a float value into the amin-amax range. - * If a is nan or -inf amin will be returned. - * If a is +inf amax will be returned. - * @param a value to clip - * @param amin minimum value of the clip range - * @param amax maximum value of the clip range - * @return clipped value - */ -static av_always_inline av_const float av_clipf_c(float a, float amin, float amax) -{ -#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2 - if (amin > amax) abort(); -#endif - return FFMIN(FFMAX(a, amin), amax); -} - -/** - * Clip a double value into the amin-amax range. - * If a is nan or -inf amin will be returned. - * If a is +inf amax will be returned. - * @param a value to clip - * @param amin minimum value of the clip range - * @param amax maximum value of the clip range - * @return clipped value - */ -static av_always_inline av_const double av_clipd_c(double a, double amin, double amax) -{ -#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2 - if (amin > amax) abort(); -#endif - return FFMIN(FFMAX(a, amin), amax); -} - -/** Compute ceil(log2(x)). - * @param x value used to compute ceil(log2(x)) - * @return computed ceiling of log2(x) - */ -static av_always_inline av_const int av_ceil_log2_c(int x) -{ - return av_log2((x - 1U) << 1); -} - -/** - * Count number of bits set to one in x - * @param x value to count bits of - * @return the number of bits set to one in x - */ -static av_always_inline av_const int av_popcount_c(uint32_t x) -{ - x -= (x >> 1) & 0x55555555; - x = (x & 0x33333333) + ((x >> 2) & 0x33333333); - x = (x + (x >> 4)) & 0x0F0F0F0F; - x += x >> 8; - return (x + (x >> 16)) & 0x3F; -} - -/** - * Count number of bits set to one in x - * @param x value to count bits of - * @return the number of bits set to one in x - */ -static av_always_inline av_const int av_popcount64_c(uint64_t x) -{ - return av_popcount((uint32_t)x) + av_popcount((uint32_t)(x >> 32)); -} - -static av_always_inline av_const int av_parity_c(uint32_t v) -{ - return av_popcount(v) & 1; -} - -/** - * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form. - * - * @param val Output value, must be an lvalue of type uint32_t. - * @param GET_BYTE Expression reading one byte from the input. - * Evaluated up to 7 times (4 for the currently - * assigned Unicode range). With a memory buffer - * input, this could be *ptr++, or if you want to make sure - * that *ptr stops at the end of a NULL terminated string then - * *ptr ? *ptr++ : 0 - * @param ERROR Expression to be evaluated on invalid input, - * typically a goto statement. - * - * @warning ERROR should not contain a loop control statement which - * could interact with the internal while loop, and should force an - * exit from the macro code (e.g. through a goto or a return) in order - * to prevent undefined results. - */ -#define GET_UTF8(val, GET_BYTE, ERROR)\ - val= (GET_BYTE);\ - {\ - uint32_t top = (val & 128) >> 1;\ - if ((val & 0xc0) == 0x80 || val >= 0xFE)\ - {ERROR}\ - while (val & top) {\ - unsigned int tmp = (GET_BYTE) - 128;\ - if(tmp>>6)\ - {ERROR}\ - val= (val<<6) + tmp;\ - top <<= 5;\ - }\ - val &= (top << 1) - 1;\ - } - -/** - * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form. - * - * @param val Output value, must be an lvalue of type uint32_t. - * @param GET_16BIT Expression returning two bytes of UTF-16 data converted - * to native byte order. Evaluated one or two times. - * @param ERROR Expression to be evaluated on invalid input, - * typically a goto statement. - */ -#define GET_UTF16(val, GET_16BIT, ERROR)\ - val = (GET_16BIT);\ - {\ - unsigned int hi = val - 0xD800;\ - if (hi < 0x800) {\ - val = (GET_16BIT) - 0xDC00;\ - if (val > 0x3FFU || hi > 0x3FFU)\ - {ERROR}\ - val += (hi<<10) + 0x10000;\ - }\ - }\ - -/** - * @def PUT_UTF8(val, tmp, PUT_BYTE) - * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes long). - * @param val is an input-only argument and should be of type uint32_t. It holds - * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If - * val is given as a function it is executed only once. - * @param tmp is a temporary variable and should be of type uint8_t. It - * represents an intermediate value during conversion that is to be - * output by PUT_BYTE. - * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination. - * It could be a function or a statement, and uses tmp as the input byte. - * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be - * executed up to 4 times for values in the valid UTF-8 range and up to - * 7 times in the general case, depending on the length of the converted - * Unicode character. - */ -#define PUT_UTF8(val, tmp, PUT_BYTE)\ - {\ - int bytes, shift;\ - uint32_t in = val;\ - if (in < 0x80) {\ - tmp = in;\ - PUT_BYTE\ - } else {\ - bytes = (av_log2(in) + 4) / 5;\ - shift = (bytes - 1) * 6;\ - tmp = (256 - (256 >> bytes)) | (in >> shift);\ - PUT_BYTE\ - while (shift >= 6) {\ - shift -= 6;\ - tmp = 0x80 | ((in >> shift) & 0x3f);\ - PUT_BYTE\ - }\ - }\ - } - -/** - * @def PUT_UTF16(val, tmp, PUT_16BIT) - * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes). - * @param val is an input-only argument and should be of type uint32_t. It holds - * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If - * val is given as a function it is executed only once. - * @param tmp is a temporary variable and should be of type uint16_t. It - * represents an intermediate value during conversion that is to be - * output by PUT_16BIT. - * @param PUT_16BIT writes the converted UTF-16 data to any proper destination - * in desired endianness. It could be a function or a statement, and uses tmp - * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;" - * PUT_BYTE will be executed 1 or 2 times depending on input character. - */ -#define PUT_UTF16(val, tmp, PUT_16BIT)\ - {\ - uint32_t in = val;\ - if (in < 0x10000) {\ - tmp = in;\ - PUT_16BIT\ - } else {\ - tmp = 0xD800 | ((in - 0x10000) >> 10);\ - PUT_16BIT\ - tmp = 0xDC00 | ((in - 0x10000) & 0x3FF);\ - PUT_16BIT\ - }\ - }\ - - - -#include "mem.h" - -#ifdef HAVE_AV_CONFIG_H -# include "internal.h" -#endif /* HAVE_AV_CONFIG_H */ - -#endif /* AVUTIL_COMMON_H */ diff --git a/gostream/ffmpeg/include/libavutil/cpu.h b/gostream/ffmpeg/include/libavutil/cpu.h deleted file mode 100644 index 8dff3418864..00000000000 --- a/gostream/ffmpeg/include/libavutil/cpu.h +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) 2000, 2001, 2002 Fabrice Bellard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_CPU_H -#define AVUTIL_CPU_H - -#include - -#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */ - - /* lower 16 bits - CPU features */ -#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX -#define AV_CPU_FLAG_MMXEXT 0x0002 ///< SSE integer functions or AMD MMX ext -#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext -#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW -#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions -#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions -#define AV_CPU_FLAG_SSE2SLOW 0x40000000 ///< SSE2 supported, but usually not faster - ///< than regular MMX/SSE (e.g. Core1) -#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt -#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions -#define AV_CPU_FLAG_SSE3SLOW 0x20000000 ///< SSE3 supported, but usually not faster - ///< than regular MMX/SSE (e.g. Core1) -#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions -#define AV_CPU_FLAG_SSSE3SLOW 0x4000000 ///< SSSE3 supported, but usually not faster -#define AV_CPU_FLAG_ATOM 0x10000000 ///< Atom processor, some SSSE3 instructions are slower -#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions -#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions -#define AV_CPU_FLAG_AESNI 0x80000 ///< Advanced Encryption Standard functions -#define AV_CPU_FLAG_AVX 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't used -#define AV_CPU_FLAG_AVXSLOW 0x8000000 ///< AVX supported, but slow when using YMM registers (e.g. Bulldozer) -#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions -#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions -#define AV_CPU_FLAG_CMOV 0x1000 ///< supports cmov instruction -#define AV_CPU_FLAG_AVX2 0x8000 ///< AVX2 functions: requires OS support even if YMM registers aren't used -#define AV_CPU_FLAG_FMA3 0x10000 ///< Haswell FMA3 functions -#define AV_CPU_FLAG_BMI1 0x20000 ///< Bit Manipulation Instruction Set 1 -#define AV_CPU_FLAG_BMI2 0x40000 ///< Bit Manipulation Instruction Set 2 -#define AV_CPU_FLAG_AVX512 0x100000 ///< AVX-512 functions: requires OS support even if YMM/ZMM registers aren't used -#define AV_CPU_FLAG_AVX512ICL 0x200000 ///< F/CD/BW/DQ/VL/VNNI/IFMA/VBMI/VBMI2/VPOPCNTDQ/BITALG/GFNI/VAES/VPCLMULQDQ -#define AV_CPU_FLAG_SLOW_GATHER 0x2000000 ///< CPU has slow gathers. - -#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard -#define AV_CPU_FLAG_VSX 0x0002 ///< ISA 2.06 -#define AV_CPU_FLAG_POWER8 0x0004 ///< ISA 2.07 - -#define AV_CPU_FLAG_ARMV5TE (1 << 0) -#define AV_CPU_FLAG_ARMV6 (1 << 1) -#define AV_CPU_FLAG_ARMV6T2 (1 << 2) -#define AV_CPU_FLAG_VFP (1 << 3) -#define AV_CPU_FLAG_VFPV3 (1 << 4) -#define AV_CPU_FLAG_NEON (1 << 5) -#define AV_CPU_FLAG_ARMV8 (1 << 6) -#define AV_CPU_FLAG_VFP_VM (1 << 7) ///< VFPv2 vector mode, deprecated in ARMv7-A and unavailable in various CPUs implementations -#define AV_CPU_FLAG_DOTPROD (1 << 8) -#define AV_CPU_FLAG_I8MM (1 << 9) -#define AV_CPU_FLAG_SETEND (1 <<16) - -#define AV_CPU_FLAG_MMI (1 << 0) -#define AV_CPU_FLAG_MSA (1 << 1) - -//Loongarch SIMD extension. -#define AV_CPU_FLAG_LSX (1 << 0) -#define AV_CPU_FLAG_LASX (1 << 1) - -// RISC-V extensions -#define AV_CPU_FLAG_RVI (1 << 0) ///< I (full GPR bank) -#define AV_CPU_FLAG_RVF (1 << 1) ///< F (single precision FP) -#define AV_CPU_FLAG_RVD (1 << 2) ///< D (double precision FP) -#define AV_CPU_FLAG_RVV_I32 (1 << 3) ///< Vectors of 8/16/32-bit int's */ -#define AV_CPU_FLAG_RVV_F32 (1 << 4) ///< Vectors of float's */ -#define AV_CPU_FLAG_RVV_I64 (1 << 5) ///< Vectors of 64-bit int's */ -#define AV_CPU_FLAG_RVV_F64 (1 << 6) ///< Vectors of double's -#define AV_CPU_FLAG_RVB_BASIC (1 << 7) ///< Basic bit-manipulations -#define AV_CPU_FLAG_RVB_ADDR (1 << 8) ///< Address bit-manipulations - -/** - * Return the flags which specify extensions supported by the CPU. - * The returned value is affected by av_force_cpu_flags() if that was used - * before. So av_get_cpu_flags() can easily be used in an application to - * detect the enabled cpu flags. - */ -int av_get_cpu_flags(void); - -/** - * Disables cpu detection and forces the specified flags. - * -1 is a special case that disables forcing of specific flags. - */ -void av_force_cpu_flags(int flags); - -/** - * Parse CPU caps from a string and update the given AV_CPU_* flags based on that. - * - * @return negative on error. - */ -int av_parse_cpu_caps(unsigned *flags, const char *s); - -/** - * @return the number of logical CPU cores present. - */ -int av_cpu_count(void); - -/** - * Overrides cpu count detection and forces the specified count. - * Count < 1 disables forcing of specific count. - */ -void av_cpu_force_count(int count); - -/** - * Get the maximum data alignment that may be required by FFmpeg. - * - * Note that this is affected by the build configuration and the CPU flags mask, - * so e.g. if the CPU supports AVX, but libavutil has been built with - * --disable-avx or the AV_CPU_FLAG_AVX flag has been disabled through - * av_set_cpu_flags_mask(), then this function will behave as if AVX is not - * present. - */ -size_t av_cpu_max_align(void); - -#endif /* AVUTIL_CPU_H */ diff --git a/gostream/ffmpeg/include/libavutil/crc.h b/gostream/ffmpeg/include/libavutil/crc.h deleted file mode 100644 index 7f59812a18c..00000000000 --- a/gostream/ffmpeg/include/libavutil/crc.h +++ /dev/null @@ -1,102 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_crc32 - * Public header for CRC hash function implementation. - */ - -#ifndef AVUTIL_CRC_H -#define AVUTIL_CRC_H - -#include -#include -#include "attributes.h" - -/** - * @defgroup lavu_crc32 CRC - * @ingroup lavu_hash - * CRC (Cyclic Redundancy Check) hash function implementation. - * - * This module supports numerous CRC polynomials, in addition to the most - * widely used CRC-32-IEEE. See @ref AVCRCId for a list of available - * polynomials. - * - * @{ - */ - -typedef uint32_t AVCRC; - -typedef enum { - AV_CRC_8_ATM, - AV_CRC_16_ANSI, - AV_CRC_16_CCITT, - AV_CRC_32_IEEE, - AV_CRC_32_IEEE_LE, /*< reversed bitorder version of AV_CRC_32_IEEE */ - AV_CRC_16_ANSI_LE, /*< reversed bitorder version of AV_CRC_16_ANSI */ - AV_CRC_24_IEEE, - AV_CRC_8_EBU, - AV_CRC_MAX, /*< Not part of public API! Do not use outside libavutil. */ -}AVCRCId; - -/** - * Initialize a CRC table. - * @param ctx must be an array of size sizeof(AVCRC)*257 or sizeof(AVCRC)*1024 - * @param le If 1, the lowest bit represents the coefficient for the highest - * exponent of the corresponding polynomial (both for poly and - * actual CRC). - * If 0, you must swap the CRC parameter and the result of av_crc - * if you need the standard representation (can be simplified in - * most cases to e.g. bswap16): - * av_bswap32(crc << (32-bits)) - * @param bits number of bits for the CRC - * @param poly generator polynomial without the x**bits coefficient, in the - * representation as specified by le - * @param ctx_size size of ctx in bytes - * @return <0 on failure - */ -int av_crc_init(AVCRC *ctx, int le, int bits, uint32_t poly, int ctx_size); - -/** - * Get an initialized standard CRC table. - * @param crc_id ID of a standard CRC - * @return a pointer to the CRC table or NULL on failure - */ -const AVCRC *av_crc_get_table(AVCRCId crc_id); - -/** - * Calculate the CRC of a block. - * @param ctx initialized AVCRC array (see av_crc_init()) - * @param crc CRC of previous blocks if any or initial value for CRC - * @param buffer buffer whose CRC to calculate - * @param length length of the buffer - * @return CRC updated with the data from the given block - * - * @see av_crc_init() "le" parameter - */ -uint32_t av_crc(const AVCRC *ctx, uint32_t crc, - const uint8_t *buffer, size_t length) av_pure; - -/** - * @} - */ - -#endif /* AVUTIL_CRC_H */ diff --git a/gostream/ffmpeg/include/libavutil/csp.h b/gostream/ffmpeg/include/libavutil/csp.h deleted file mode 100644 index 73bce52bc0c..00000000000 --- a/gostream/ffmpeg/include/libavutil/csp.h +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2015 Kevin Wheatley - * Copyright (c) 2016 Ronald S. Bultje - * Copyright (c) 2023 Leo Izen - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_CSP_H -#define AVUTIL_CSP_H - -#include "pixfmt.h" -#include "rational.h" - -/** - * @file - * Colorspace value utility functions for libavutil. - * @ingroup lavu_math_csp - * @author Ronald S. Bultje - * @author Leo Izen - * @author Kevin Wheatley - */ - -/** - * @defgroup lavu_math_csp Colorspace Utility - * @ingroup lavu_math - * @{ - */ - -/** - * Struct containing luma coefficients to be used for RGB to YUV/YCoCg, or similar - * calculations. - */ -typedef struct AVLumaCoefficients { - AVRational cr, cg, cb; -} AVLumaCoefficients; - -/** - * Struct containing chromaticity x and y values for the standard CIE 1931 - * chromaticity definition. - */ -typedef struct AVCIExy { - AVRational x, y; -} AVCIExy; - -/** - * Struct defining the red, green, and blue primary locations in terms of CIE - * 1931 chromaticity x and y. - */ -typedef struct AVPrimaryCoefficients { - AVCIExy r, g, b; -} AVPrimaryCoefficients; - -/** - * Struct defining white point location in terms of CIE 1931 chromaticity x - * and y. - */ -typedef AVCIExy AVWhitepointCoefficients; - -/** - * Struct that contains both white point location and primaries location, providing - * the complete description of a color gamut. - */ -typedef struct AVColorPrimariesDesc { - AVWhitepointCoefficients wp; - AVPrimaryCoefficients prim; -} AVColorPrimariesDesc; - -/** - * Function pointer representing a double -> double transfer function that performs - * an EOTF transfer inversion. This function outputs linear light. - */ -typedef double (*av_csp_trc_function)(double); - -/** - * Retrieves the Luma coefficients necessary to construct a conversion matrix - * from an enum constant describing the colorspace. - * @param csp An enum constant indicating YUV or similar colorspace. - * @return The Luma coefficients associated with that colorspace, or NULL - * if the constant is unknown to libavutil. - */ -const AVLumaCoefficients *av_csp_luma_coeffs_from_avcsp(enum AVColorSpace csp); - -/** - * Retrieves a complete gamut description from an enum constant describing the - * color primaries. - * @param prm An enum constant indicating primaries - * @return A description of the colorspace gamut associated with that enum - * constant, or NULL if the constant is unknown to libavutil. - */ -const AVColorPrimariesDesc *av_csp_primaries_desc_from_id(enum AVColorPrimaries prm); - -/** - * Detects which enum AVColorPrimaries constant corresponds to the given complete - * gamut description. - * @see enum AVColorPrimaries - * @param prm A description of the colorspace gamut - * @return The enum constant associated with this gamut, or - * AVCOL_PRI_UNSPECIFIED if no clear match can be idenitified. - */ -enum AVColorPrimaries av_csp_primaries_id_from_desc(const AVColorPrimariesDesc *prm); - -/** - * Determine a suitable 'gamma' value to match the supplied - * AVColorTransferCharacteristic. - * - * See Apple Technical Note TN2257 (https://developer.apple.com/library/mac/technotes/tn2257/_index.html) - * - * This function returns the gamma exponent for the OETF. For example, sRGB is approximated - * by gamma 2.2, not by gamma 0.45455. - * - * @return Will return an approximation to the simple gamma function matching - * the supplied Transfer Characteristic, Will return 0.0 for any - * we cannot reasonably match against. - */ -double av_csp_approximate_trc_gamma(enum AVColorTransferCharacteristic trc); - -/** - * Determine the function needed to apply the given - * AVColorTransferCharacteristic to linear input. - * - * The function returned should expect a nominal domain and range of [0.0-1.0] - * values outside of this range maybe valid depending on the chosen - * characteristic function. - * - * @return Will return pointer to the function matching the - * supplied Transfer Characteristic. If unspecified will - * return NULL: - */ -av_csp_trc_function av_csp_trc_func_from_id(enum AVColorTransferCharacteristic trc); - -/** - * @} - */ - -#endif /* AVUTIL_CSP_H */ diff --git a/gostream/ffmpeg/include/libavutil/des.h b/gostream/ffmpeg/include/libavutil/des.h deleted file mode 100644 index 3a3e6fa47ce..00000000000 --- a/gostream/ffmpeg/include/libavutil/des.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * DES encryption/decryption - * Copyright (c) 2007 Reimar Doeffinger - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_DES_H -#define AVUTIL_DES_H - -#include - -/** - * @defgroup lavu_des DES - * @ingroup lavu_crypto - * @{ - */ - -typedef struct AVDES { - uint64_t round_keys[3][16]; - int triple_des; -} AVDES; - -/** - * Allocate an AVDES context. - */ -AVDES *av_des_alloc(void); - -/** - * @brief Initializes an AVDES context. - * - * @param d pointer to a AVDES structure to initialize - * @param key pointer to the key to use - * @param key_bits must be 64 or 192 - * @param decrypt 0 for encryption/CBC-MAC, 1 for decryption - * @return zero on success, negative value otherwise - */ -int av_des_init(struct AVDES *d, const uint8_t *key, int key_bits, int decrypt); - -/** - * @brief Encrypts / decrypts using the DES algorithm. - * - * @param d pointer to the AVDES structure - * @param dst destination array, can be equal to src, must be 8-byte aligned - * @param src source array, can be equal to dst, must be 8-byte aligned, may be NULL - * @param count number of 8 byte blocks - * @param iv initialization vector for CBC mode, if NULL then ECB will be used, - * must be 8-byte aligned - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_des_crypt(struct AVDES *d, uint8_t *dst, const uint8_t *src, int count, uint8_t *iv, int decrypt); - -/** - * @brief Calculates CBC-MAC using the DES algorithm. - * - * @param d pointer to the AVDES structure - * @param dst destination array, can be equal to src, must be 8-byte aligned - * @param src source array, can be equal to dst, must be 8-byte aligned, may be NULL - * @param count number of 8 byte blocks - */ -void av_des_mac(struct AVDES *d, uint8_t *dst, const uint8_t *src, int count); - -/** - * @} - */ - -#endif /* AVUTIL_DES_H */ diff --git a/gostream/ffmpeg/include/libavutil/detection_bbox.h b/gostream/ffmpeg/include/libavutil/detection_bbox.h deleted file mode 100644 index 011988052c2..00000000000 --- a/gostream/ffmpeg/include/libavutil/detection_bbox.h +++ /dev/null @@ -1,108 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_DETECTION_BBOX_H -#define AVUTIL_DETECTION_BBOX_H - -#include "rational.h" -#include "avassert.h" -#include "frame.h" - -typedef struct AVDetectionBBox { - /** - * Distance in pixels from the left/top edge of the frame, - * together with width and height, defining the bounding box. - */ - int x; - int y; - int w; - int h; - -#define AV_DETECTION_BBOX_LABEL_NAME_MAX_SIZE 64 - - /** - * Detect result with confidence - */ - char detect_label[AV_DETECTION_BBOX_LABEL_NAME_MAX_SIZE]; - AVRational detect_confidence; - - /** - * At most 4 classifications based on the detected bounding box. - * For example, we can get max 4 different attributes with 4 different - * DNN models on one bounding box. - * classify_count is zero if no classification. - */ -#define AV_NUM_DETECTION_BBOX_CLASSIFY 4 - uint32_t classify_count; - char classify_labels[AV_NUM_DETECTION_BBOX_CLASSIFY][AV_DETECTION_BBOX_LABEL_NAME_MAX_SIZE]; - AVRational classify_confidences[AV_NUM_DETECTION_BBOX_CLASSIFY]; -} AVDetectionBBox; - -typedef struct AVDetectionBBoxHeader { - /** - * Information about how the bounding box is generated. - * for example, the DNN model name. - */ - char source[256]; - - /** - * Number of bounding boxes in the array. - */ - uint32_t nb_bboxes; - - /** - * Offset in bytes from the beginning of this structure at which - * the array of bounding boxes starts. - */ - size_t bboxes_offset; - - /** - * Size of each bounding box in bytes. - */ - size_t bbox_size; -} AVDetectionBBoxHeader; - -/* - * Get the bounding box at the specified {@code idx}. Must be between 0 and nb_bboxes. - */ -static av_always_inline AVDetectionBBox * -av_get_detection_bbox(const AVDetectionBBoxHeader *header, unsigned int idx) -{ - av_assert0(idx < header->nb_bboxes); - return (AVDetectionBBox *)((uint8_t *)header + header->bboxes_offset + - idx * header->bbox_size); -} - -/** - * Allocates memory for AVDetectionBBoxHeader, plus an array of {@code nb_bboxes} - * AVDetectionBBox, and initializes the variables. - * Can be freed with a normal av_free() call. - * - * @param nb_bboxes number of AVDetectionBBox structures to allocate - * @param out_size if non-NULL, the size in bytes of the resulting data array is - * written here. - */ -AVDetectionBBoxHeader *av_detection_bbox_alloc(uint32_t nb_bboxes, size_t *out_size); - -/** - * Allocates memory for AVDetectionBBoxHeader, plus an array of {@code nb_bboxes} - * AVDetectionBBox, in the given AVFrame {@code frame} as AVFrameSideData of type - * AV_FRAME_DATA_DETECTION_BBOXES and initializes the variables. - */ -AVDetectionBBoxHeader *av_detection_bbox_create_side_data(AVFrame *frame, uint32_t nb_bboxes); -#endif diff --git a/gostream/ffmpeg/include/libavutil/dict.h b/gostream/ffmpeg/include/libavutil/dict.h deleted file mode 100644 index 713c9e361af..00000000000 --- a/gostream/ffmpeg/include/libavutil/dict.h +++ /dev/null @@ -1,241 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * Public dictionary API. - * @deprecated - * AVDictionary is provided for compatibility with libav. It is both in - * implementation as well as API inefficient. It does not scale and is - * extremely slow with large dictionaries. - * It is recommended that new code uses our tree container from tree.c/h - * where applicable, which uses AVL trees to achieve O(log n) performance. - */ - -#ifndef AVUTIL_DICT_H -#define AVUTIL_DICT_H - -#include - -/** - * @addtogroup lavu_dict AVDictionary - * @ingroup lavu_data - * - * @brief Simple key:value store - * - * @{ - * Dictionaries are used for storing key-value pairs. - * - * - To **create an AVDictionary**, simply pass an address of a NULL - * pointer to av_dict_set(). NULL can be used as an empty dictionary - * wherever a pointer to an AVDictionary is required. - * - To **insert an entry**, use av_dict_set(). - * - Use av_dict_get() to **retrieve an entry**. - * - To **iterate over all entries**, use av_dict_iterate(). - * - In order to **free the dictionary and all its contents**, use av_dict_free(). - * - @code - AVDictionary *d = NULL; // "create" an empty dictionary - AVDictionaryEntry *t = NULL; - - av_dict_set(&d, "foo", "bar", 0); // add an entry - - char *k = av_strdup("key"); // if your strings are already allocated, - char *v = av_strdup("value"); // you can avoid copying them like this - av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL); - - while ((t = av_dict_iterate(d, t))) { - <....> // iterate over all entries in d - } - av_dict_free(&d); - @endcode - */ - -/** - * @name AVDictionary Flags - * Flags that influence behavior of the matching of keys or insertion to the dictionary. - * @{ - */ -#define AV_DICT_MATCH_CASE 1 /**< Only get an entry with exact-case key match. Only relevant in av_dict_get(). */ -#define AV_DICT_IGNORE_SUFFIX 2 /**< Return first entry in a dictionary whose first part corresponds to the search key, - ignoring the suffix of the found key string. Only relevant in av_dict_get(). */ -#define AV_DICT_DONT_STRDUP_KEY 4 /**< Take ownership of a key that's been - allocated with av_malloc() or another memory allocation function. */ -#define AV_DICT_DONT_STRDUP_VAL 8 /**< Take ownership of a value that's been - allocated with av_malloc() or another memory allocation function. */ -#define AV_DICT_DONT_OVERWRITE 16 /**< Don't overwrite existing entries. */ -#define AV_DICT_APPEND 32 /**< If the entry already exists, append to it. Note that no - delimiter is added, the strings are simply concatenated. */ -#define AV_DICT_MULTIKEY 64 /**< Allow to store several equal keys in the dictionary */ -/** - * @} - */ - -typedef struct AVDictionaryEntry { - char *key; - char *value; -} AVDictionaryEntry; - -typedef struct AVDictionary AVDictionary; - -/** - * Get a dictionary entry with matching key. - * - * The returned entry key or value must not be changed, or it will - * cause undefined behavior. - * - * @param prev Set to the previous matching element to find the next. - * If set to NULL the first matching element is returned. - * @param key Matching key - * @param flags A collection of AV_DICT_* flags controlling how the - * entry is retrieved - * - * @return Found entry or NULL in case no matching entry was found in the dictionary - */ -AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key, - const AVDictionaryEntry *prev, int flags); - -/** - * Iterate over a dictionary - * - * Iterates through all entries in the dictionary. - * - * @warning The returned AVDictionaryEntry key/value must not be changed. - * - * @warning As av_dict_set() invalidates all previous entries returned - * by this function, it must not be called while iterating over the dict. - * - * Typical usage: - * @code - * const AVDictionaryEntry *e = NULL; - * while ((e = av_dict_iterate(m, e))) { - * // ... - * } - * @endcode - * - * @param m The dictionary to iterate over - * @param prev Pointer to the previous AVDictionaryEntry, NULL initially - * - * @retval AVDictionaryEntry* The next element in the dictionary - * @retval NULL No more elements in the dictionary - */ -const AVDictionaryEntry *av_dict_iterate(const AVDictionary *m, - const AVDictionaryEntry *prev); - -/** - * Get number of entries in dictionary. - * - * @param m dictionary - * @return number of entries in dictionary - */ -int av_dict_count(const AVDictionary *m); - -/** - * Set the given entry in *pm, overwriting an existing entry. - * - * Note: If AV_DICT_DONT_STRDUP_KEY or AV_DICT_DONT_STRDUP_VAL is set, - * these arguments will be freed on error. - * - * @warning Adding a new entry to a dictionary invalidates all existing entries - * previously returned with av_dict_get() or av_dict_iterate(). - * - * @param pm Pointer to a pointer to a dictionary struct. If *pm is NULL - * a dictionary struct is allocated and put in *pm. - * @param key Entry key to add to *pm (will either be av_strduped or added as a new key depending on flags) - * @param value Entry value to add to *pm (will be av_strduped or added as a new key depending on flags). - * Passing a NULL value will cause an existing entry to be deleted. - * - * @return >= 0 on success otherwise an error code <0 - */ -int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags); - -/** - * Convenience wrapper for av_dict_set() that converts the value to a string - * and stores it. - * - * Note: If ::AV_DICT_DONT_STRDUP_KEY is set, key will be freed on error. - */ -int av_dict_set_int(AVDictionary **pm, const char *key, int64_t value, int flags); - -/** - * Parse the key/value pairs list and add the parsed entries to a dictionary. - * - * In case of failure, all the successfully set entries are stored in - * *pm. You may need to manually free the created dictionary. - * - * @param key_val_sep A 0-terminated list of characters used to separate - * key from value - * @param pairs_sep A 0-terminated list of characters used to separate - * two pairs from each other - * @param flags Flags to use when adding to the dictionary. - * ::AV_DICT_DONT_STRDUP_KEY and ::AV_DICT_DONT_STRDUP_VAL - * are ignored since the key/value tokens will always - * be duplicated. - * - * @return 0 on success, negative AVERROR code on failure - */ -int av_dict_parse_string(AVDictionary **pm, const char *str, - const char *key_val_sep, const char *pairs_sep, - int flags); - -/** - * Copy entries from one AVDictionary struct into another. - * - * @note Metadata is read using the ::AV_DICT_IGNORE_SUFFIX flag - * - * @param dst Pointer to a pointer to a AVDictionary struct to copy into. If *dst is NULL, - * this function will allocate a struct for you and put it in *dst - * @param src Pointer to the source AVDictionary struct to copy items from. - * @param flags Flags to use when setting entries in *dst - * - * @return 0 on success, negative AVERROR code on failure. If dst was allocated - * by this function, callers should free the associated memory. - */ -int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags); - -/** - * Free all the memory allocated for an AVDictionary struct - * and all keys and values. - */ -void av_dict_free(AVDictionary **m); - -/** - * Get dictionary entries as a string. - * - * Create a string containing dictionary's entries. - * Such string may be passed back to av_dict_parse_string(). - * @note String is escaped with backslashes ('\'). - * - * @warning Separators cannot be neither '\\' nor '\0'. They also cannot be the same. - * - * @param[in] m The dictionary - * @param[out] buffer Pointer to buffer that will be allocated with string containg entries. - * Buffer must be freed by the caller when is no longer needed. - * @param[in] key_val_sep Character used to separate key from value - * @param[in] pairs_sep Character used to separate two pairs from each other - * - * @return >= 0 on success, negative on error - */ -int av_dict_get_string(const AVDictionary *m, char **buffer, - const char key_val_sep, const char pairs_sep); - -/** - * @} - */ - -#endif /* AVUTIL_DICT_H */ diff --git a/gostream/ffmpeg/include/libavutil/display.h b/gostream/ffmpeg/include/libavutil/display.h deleted file mode 100644 index 50f2b44caf5..00000000000 --- a/gostream/ffmpeg/include/libavutil/display.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2014 Vittorio Giovara - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_video_display - * Display matrix - */ - -#ifndef AVUTIL_DISPLAY_H -#define AVUTIL_DISPLAY_H - -#include - -/** - * @defgroup lavu_video_display Display transformation matrix functions - * @ingroup lavu_video - * - * The display transformation matrix specifies an affine transformation that - * should be applied to video frames for correct presentation. It is compatible - * with the matrices stored in the ISO/IEC 14496-12 container format. - * - * The data is a 3x3 matrix represented as a 9-element array: - * - * @code{.unparsed} - * | a b u | - * (a, b, u, c, d, v, x, y, w) -> | c d v | - * | x y w | - * @endcode - * - * All numbers are stored in native endianness, as 16.16 fixed-point values, - * except for u, v and w, which are stored as 2.30 fixed-point values. - * - * The transformation maps a point (p, q) in the source (pre-transformation) - * frame to the point (p', q') in the destination (post-transformation) frame as - * follows: - * - * @code{.unparsed} - * | a b u | - * (p, q, 1) . | c d v | = z * (p', q', 1) - * | x y w | - * @endcode - * - * The transformation can also be more explicitly written in components as - * follows: - * - * @code{.unparsed} - * p' = (a * p + c * q + x) / z; - * q' = (b * p + d * q + y) / z; - * z = u * p + v * q + w - * @endcode - * - * @{ - */ - -/** - * Extract the rotation component of the transformation matrix. - * - * @param matrix the transformation matrix - * @return the angle (in degrees) by which the transformation rotates the frame - * counterclockwise. The angle will be in range [-180.0, 180.0], - * or NaN if the matrix is singular. - * - * @note floating point numbers are inherently inexact, so callers are - * recommended to round the return value to nearest integer before use. - */ -double av_display_rotation_get(const int32_t matrix[9]); - -/** - * Initialize a transformation matrix describing a pure clockwise - * rotation by the specified angle (in degrees). - * - * @param[out] matrix a transformation matrix (will be fully overwritten - * by this function) - * @param angle rotation angle in degrees. - */ -void av_display_rotation_set(int32_t matrix[9], double angle); - -/** - * Flip the input matrix horizontally and/or vertically. - * - * @param[in,out] matrix a transformation matrix - * @param hflip whether the matrix should be flipped horizontally - * @param vflip whether the matrix should be flipped vertically - */ -void av_display_matrix_flip(int32_t matrix[9], int hflip, int vflip); - -/** - * @} - */ - -#endif /* AVUTIL_DISPLAY_H */ diff --git a/gostream/ffmpeg/include/libavutil/dovi_meta.h b/gostream/ffmpeg/include/libavutil/dovi_meta.h deleted file mode 100644 index 3d11e02bffc..00000000000 --- a/gostream/ffmpeg/include/libavutil/dovi_meta.h +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2020 Vacing Fang - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * DOVI configuration - */ - - -#ifndef AVUTIL_DOVI_META_H -#define AVUTIL_DOVI_META_H - -#include -#include -#include "rational.h" - -/* - * DOVI configuration - * ref: dolby-vision-bitstreams-within-the-iso-base-media-file-format-v2.1.2 - dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2 - * @code - * uint8_t dv_version_major, the major version number that the stream complies with - * uint8_t dv_version_minor, the minor version number that the stream complies with - * uint8_t dv_profile, the Dolby Vision profile - * uint8_t dv_level, the Dolby Vision level - * uint8_t rpu_present_flag - * uint8_t el_present_flag - * uint8_t bl_present_flag - * uint8_t dv_bl_signal_compatibility_id - * @endcode - * - * @note The struct must be allocated with av_dovi_alloc() and - * its size is not a part of the public ABI. - */ -typedef struct AVDOVIDecoderConfigurationRecord { - uint8_t dv_version_major; - uint8_t dv_version_minor; - uint8_t dv_profile; - uint8_t dv_level; - uint8_t rpu_present_flag; - uint8_t el_present_flag; - uint8_t bl_present_flag; - uint8_t dv_bl_signal_compatibility_id; -} AVDOVIDecoderConfigurationRecord; - -/** - * Allocate a AVDOVIDecoderConfigurationRecord structure and initialize its - * fields to default values. - * - * @return the newly allocated struct or NULL on failure - */ -AVDOVIDecoderConfigurationRecord *av_dovi_alloc(size_t *size); - -/** - * Dolby Vision RPU data header. - * - * @note sizeof(AVDOVIRpuDataHeader) is not part of the public ABI. - */ -typedef struct AVDOVIRpuDataHeader { - uint8_t rpu_type; - uint16_t rpu_format; - uint8_t vdr_rpu_profile; - uint8_t vdr_rpu_level; - uint8_t chroma_resampling_explicit_filter_flag; - uint8_t coef_data_type; /* informative, lavc always converts to fixed */ - uint8_t coef_log2_denom; - uint8_t vdr_rpu_normalized_idc; - uint8_t bl_video_full_range_flag; - uint8_t bl_bit_depth; /* [8, 16] */ - uint8_t el_bit_depth; /* [8, 16] */ - uint8_t vdr_bit_depth; /* [8, 16] */ - uint8_t spatial_resampling_filter_flag; - uint8_t el_spatial_resampling_filter_flag; - uint8_t disable_residual_flag; -} AVDOVIRpuDataHeader; - -enum AVDOVIMappingMethod { - AV_DOVI_MAPPING_POLYNOMIAL = 0, - AV_DOVI_MAPPING_MMR = 1, -}; - -/** - * Coefficients of a piece-wise function. The pieces of the function span the - * value ranges between two adjacent pivot values. - */ -#define AV_DOVI_MAX_PIECES 8 -typedef struct AVDOVIReshapingCurve { - uint8_t num_pivots; /* [2, 9] */ - uint16_t pivots[AV_DOVI_MAX_PIECES + 1]; /* sorted ascending */ - enum AVDOVIMappingMethod mapping_idc[AV_DOVI_MAX_PIECES]; - /* AV_DOVI_MAPPING_POLYNOMIAL */ - uint8_t poly_order[AV_DOVI_MAX_PIECES]; /* [1, 2] */ - int64_t poly_coef[AV_DOVI_MAX_PIECES][3]; /* x^0, x^1, x^2 */ - /* AV_DOVI_MAPPING_MMR */ - uint8_t mmr_order[AV_DOVI_MAX_PIECES]; /* [1, 3] */ - int64_t mmr_constant[AV_DOVI_MAX_PIECES]; - int64_t mmr_coef[AV_DOVI_MAX_PIECES][3/* order - 1 */][7]; -} AVDOVIReshapingCurve; - -enum AVDOVINLQMethod { - AV_DOVI_NLQ_NONE = -1, - AV_DOVI_NLQ_LINEAR_DZ = 0, -}; - -/** - * Coefficients of the non-linear inverse quantization. For the interpretation - * of these, see ETSI GS CCM 001. - */ -typedef struct AVDOVINLQParams { - uint16_t nlq_offset; - uint64_t vdr_in_max; - /* AV_DOVI_NLQ_LINEAR_DZ */ - uint64_t linear_deadzone_slope; - uint64_t linear_deadzone_threshold; -} AVDOVINLQParams; - -/** - * Dolby Vision RPU data mapping parameters. - * - * @note sizeof(AVDOVIDataMapping) is not part of the public ABI. - */ -typedef struct AVDOVIDataMapping { - uint8_t vdr_rpu_id; - uint8_t mapping_color_space; - uint8_t mapping_chroma_format_idc; - AVDOVIReshapingCurve curves[3]; /* per component */ - - /* Non-linear inverse quantization */ - enum AVDOVINLQMethod nlq_method_idc; - uint32_t num_x_partitions; - uint32_t num_y_partitions; - AVDOVINLQParams nlq[3]; /* per component */ -} AVDOVIDataMapping; - -/** - * Dolby Vision RPU colorspace metadata parameters. - * - * @note sizeof(AVDOVIColorMetadata) is not part of the public ABI. - */ -typedef struct AVDOVIColorMetadata { - uint8_t dm_metadata_id; - uint8_t scene_refresh_flag; - - /** - * Coefficients of the custom Dolby Vision IPT-PQ matrices. These are to be - * used instead of the matrices indicated by the frame's colorspace tags. - * The output of rgb_to_lms_matrix is to be fed into a BT.2020 LMS->RGB - * matrix based on a Hunt-Pointer-Estevez transform, but without any - * crosstalk. (See the definition of the ICtCp colorspace for more - * information.) - */ - AVRational ycc_to_rgb_matrix[9]; /* before PQ linearization */ - AVRational ycc_to_rgb_offset[3]; /* input offset of neutral value */ - AVRational rgb_to_lms_matrix[9]; /* after PQ linearization */ - - /** - * Extra signal metadata (see Dolby patents for more info). - */ - uint16_t signal_eotf; - uint16_t signal_eotf_param0; - uint16_t signal_eotf_param1; - uint32_t signal_eotf_param2; - uint8_t signal_bit_depth; - uint8_t signal_color_space; - uint8_t signal_chroma_format; - uint8_t signal_full_range_flag; /* [0, 3] */ - uint16_t source_min_pq; - uint16_t source_max_pq; - uint16_t source_diagonal; -} AVDOVIColorMetadata; - -/** - * Combined struct representing a combination of header, mapping and color - * metadata, for attaching to frames as side data. - * - * @note The struct must be allocated with av_dovi_metadata_alloc() and - * its size is not a part of the public ABI. - */ - -typedef struct AVDOVIMetadata { - /** - * Offset in bytes from the beginning of this structure at which the - * respective structs start. - */ - size_t header_offset; /* AVDOVIRpuDataHeader */ - size_t mapping_offset; /* AVDOVIDataMapping */ - size_t color_offset; /* AVDOVIColorMetadata */ -} AVDOVIMetadata; - -static av_always_inline AVDOVIRpuDataHeader * -av_dovi_get_header(const AVDOVIMetadata *data) -{ - return (AVDOVIRpuDataHeader *)((uint8_t *) data + data->header_offset); -} - -static av_always_inline AVDOVIDataMapping * -av_dovi_get_mapping(const AVDOVIMetadata *data) -{ - return (AVDOVIDataMapping *)((uint8_t *) data + data->mapping_offset); -} - -static av_always_inline AVDOVIColorMetadata * -av_dovi_get_color(const AVDOVIMetadata *data) -{ - return (AVDOVIColorMetadata *)((uint8_t *) data + data->color_offset); -} - -/** - * Allocate an AVDOVIMetadata structure and initialize its - * fields to default values. - * - * @param size If this parameter is non-NULL, the size in bytes of the - * allocated struct will be written here on success - * - * @return the newly allocated struct or NULL on failure - */ -AVDOVIMetadata *av_dovi_metadata_alloc(size_t *size); - -#endif /* AVUTIL_DOVI_META_H */ diff --git a/gostream/ffmpeg/include/libavutil/downmix_info.h b/gostream/ffmpeg/include/libavutil/downmix_info.h deleted file mode 100644 index 221cf5bf9ba..00000000000 --- a/gostream/ffmpeg/include/libavutil/downmix_info.h +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2014 Tim Walker - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_DOWNMIX_INFO_H -#define AVUTIL_DOWNMIX_INFO_H - -#include "frame.h" - -/** - * @file - * audio downmix medatata - */ - -/** - * @addtogroup lavu_audio - * @{ - */ - -/** - * @defgroup downmix_info Audio downmix metadata - * @{ - */ - -/** - * Possible downmix types. - */ -enum AVDownmixType { - AV_DOWNMIX_TYPE_UNKNOWN, /**< Not indicated. */ - AV_DOWNMIX_TYPE_LORO, /**< Lo/Ro 2-channel downmix (Stereo). */ - AV_DOWNMIX_TYPE_LTRT, /**< Lt/Rt 2-channel downmix, Dolby Surround compatible. */ - AV_DOWNMIX_TYPE_DPLII, /**< Lt/Rt 2-channel downmix, Dolby Pro Logic II compatible. */ - AV_DOWNMIX_TYPE_NB /**< Number of downmix types. Not part of ABI. */ -}; - -/** - * This structure describes optional metadata relevant to a downmix procedure. - * - * All fields are set by the decoder to the value indicated in the audio - * bitstream (if present), or to a "sane" default otherwise. - */ -typedef struct AVDownmixInfo { - /** - * Type of downmix preferred by the mastering engineer. - */ - enum AVDownmixType preferred_downmix_type; - - /** - * Absolute scale factor representing the nominal level of the center - * channel during a regular downmix. - */ - double center_mix_level; - - /** - * Absolute scale factor representing the nominal level of the center - * channel during an Lt/Rt compatible downmix. - */ - double center_mix_level_ltrt; - - /** - * Absolute scale factor representing the nominal level of the surround - * channels during a regular downmix. - */ - double surround_mix_level; - - /** - * Absolute scale factor representing the nominal level of the surround - * channels during an Lt/Rt compatible downmix. - */ - double surround_mix_level_ltrt; - - /** - * Absolute scale factor representing the level at which the LFE data is - * mixed into L/R channels during downmixing. - */ - double lfe_mix_level; -} AVDownmixInfo; - -/** - * Get a frame's AV_FRAME_DATA_DOWNMIX_INFO side data for editing. - * - * If the side data is absent, it is created and added to the frame. - * - * @param frame the frame for which the side data is to be obtained or created - * - * @return the AVDownmixInfo structure to be edited by the caller, or NULL if - * the structure cannot be allocated. - */ -AVDownmixInfo *av_downmix_info_update_side_data(AVFrame *frame); - -/** - * @} - */ - -/** - * @} - */ - -#endif /* AVUTIL_DOWNMIX_INFO_H */ diff --git a/gostream/ffmpeg/include/libavutil/encryption_info.h b/gostream/ffmpeg/include/libavutil/encryption_info.h deleted file mode 100644 index 8fe7ebfe432..00000000000 --- a/gostream/ffmpeg/include/libavutil/encryption_info.h +++ /dev/null @@ -1,205 +0,0 @@ -/** - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_ENCRYPTION_INFO_H -#define AVUTIL_ENCRYPTION_INFO_H - -#include -#include - -typedef struct AVSubsampleEncryptionInfo { - /** The number of bytes that are clear. */ - unsigned int bytes_of_clear_data; - - /** - * The number of bytes that are protected. If using pattern encryption, - * the pattern applies to only the protected bytes; if not using pattern - * encryption, all these bytes are encrypted. - */ - unsigned int bytes_of_protected_data; -} AVSubsampleEncryptionInfo; - -/** - * This describes encryption info for a packet. This contains frame-specific - * info for how to decrypt the packet before passing it to the decoder. - * - * The size of this struct is not part of the public ABI. - */ -typedef struct AVEncryptionInfo { - /** The fourcc encryption scheme, in big-endian byte order. */ - uint32_t scheme; - - /** - * Only used for pattern encryption. This is the number of 16-byte blocks - * that are encrypted. - */ - uint32_t crypt_byte_block; - - /** - * Only used for pattern encryption. This is the number of 16-byte blocks - * that are clear. - */ - uint32_t skip_byte_block; - - /** - * The ID of the key used to encrypt the packet. This should always be - * 16 bytes long, but may be changed in the future. - */ - uint8_t *key_id; - uint32_t key_id_size; - - /** - * The initialization vector. This may have been zero-filled to be the - * correct block size. This should always be 16 bytes long, but may be - * changed in the future. - */ - uint8_t *iv; - uint32_t iv_size; - - /** - * An array of subsample encryption info specifying how parts of the sample - * are encrypted. If there are no subsamples, then the whole sample is - * encrypted. - */ - AVSubsampleEncryptionInfo *subsamples; - uint32_t subsample_count; -} AVEncryptionInfo; - -/** - * This describes info used to initialize an encryption key system. - * - * The size of this struct is not part of the public ABI. - */ -typedef struct AVEncryptionInitInfo { - /** - * A unique identifier for the key system this is for, can be NULL if it - * is not known. This should always be 16 bytes, but may change in the - * future. - */ - uint8_t* system_id; - uint32_t system_id_size; - - /** - * An array of key IDs this initialization data is for. All IDs are the - * same length. Can be NULL if there are no known key IDs. - */ - uint8_t** key_ids; - /** The number of key IDs. */ - uint32_t num_key_ids; - /** - * The number of bytes in each key ID. This should always be 16, but may - * change in the future. - */ - uint32_t key_id_size; - - /** - * Key-system specific initialization data. This data is copied directly - * from the file and the format depends on the specific key system. This - * can be NULL if there is no initialization data; in that case, there - * will be at least one key ID. - */ - uint8_t* data; - uint32_t data_size; - - /** - * An optional pointer to the next initialization info in the list. - */ - struct AVEncryptionInitInfo *next; -} AVEncryptionInitInfo; - -/** - * Allocates an AVEncryptionInfo structure and sub-pointers to hold the given - * number of subsamples. This will allocate pointers for the key ID, IV, - * and subsample entries, set the size members, and zero-initialize the rest. - * - * @param subsample_count The number of subsamples. - * @param key_id_size The number of bytes in the key ID, should be 16. - * @param iv_size The number of bytes in the IV, should be 16. - * - * @return The new AVEncryptionInfo structure, or NULL on error. - */ -AVEncryptionInfo *av_encryption_info_alloc(uint32_t subsample_count, uint32_t key_id_size, uint32_t iv_size); - -/** - * Allocates an AVEncryptionInfo structure with a copy of the given data. - * @return The new AVEncryptionInfo structure, or NULL on error. - */ -AVEncryptionInfo *av_encryption_info_clone(const AVEncryptionInfo *info); - -/** - * Frees the given encryption info object. This MUST NOT be used to free the - * side-data data pointer, that should use normal side-data methods. - */ -void av_encryption_info_free(AVEncryptionInfo *info); - -/** - * Creates a copy of the AVEncryptionInfo that is contained in the given side - * data. The resulting object should be passed to av_encryption_info_free() - * when done. - * - * @return The new AVEncryptionInfo structure, or NULL on error. - */ -AVEncryptionInfo *av_encryption_info_get_side_data(const uint8_t *side_data, size_t side_data_size); - -/** - * Allocates and initializes side data that holds a copy of the given encryption - * info. The resulting pointer should be either freed using av_free or given - * to av_packet_add_side_data(). - * - * @return The new side-data pointer, or NULL. - */ -uint8_t *av_encryption_info_add_side_data( - const AVEncryptionInfo *info, size_t *side_data_size); - - -/** - * Allocates an AVEncryptionInitInfo structure and sub-pointers to hold the - * given sizes. This will allocate pointers and set all the fields. - * - * @return The new AVEncryptionInitInfo structure, or NULL on error. - */ -AVEncryptionInitInfo *av_encryption_init_info_alloc( - uint32_t system_id_size, uint32_t num_key_ids, uint32_t key_id_size, uint32_t data_size); - -/** - * Frees the given encryption init info object. This MUST NOT be used to free - * the side-data data pointer, that should use normal side-data methods. - */ -void av_encryption_init_info_free(AVEncryptionInitInfo* info); - -/** - * Creates a copy of the AVEncryptionInitInfo that is contained in the given - * side data. The resulting object should be passed to - * av_encryption_init_info_free() when done. - * - * @return The new AVEncryptionInitInfo structure, or NULL on error. - */ -AVEncryptionInitInfo *av_encryption_init_info_get_side_data( - const uint8_t* side_data, size_t side_data_size); - -/** - * Allocates and initializes side data that holds a copy of the given encryption - * init info. The resulting pointer should be either freed using av_free or - * given to av_packet_add_side_data(). - * - * @return The new side-data pointer, or NULL. - */ -uint8_t *av_encryption_init_info_add_side_data( - const AVEncryptionInitInfo *info, size_t *side_data_size); - -#endif /* AVUTIL_ENCRYPTION_INFO_H */ diff --git a/gostream/ffmpeg/include/libavutil/error.h b/gostream/ffmpeg/include/libavutil/error.h deleted file mode 100644 index 0d3269aa6da..00000000000 --- a/gostream/ffmpeg/include/libavutil/error.h +++ /dev/null @@ -1,128 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * error code definitions - */ - -#ifndef AVUTIL_ERROR_H -#define AVUTIL_ERROR_H - -#include -#include - -#include "macros.h" - -/** - * @addtogroup lavu_error - * - * @{ - */ - - -/* error handling */ -#if EDOM > 0 -#define AVERROR(e) (-(e)) ///< Returns a negative error code from a POSIX error code, to return from library functions. -#define AVUNERROR(e) (-(e)) ///< Returns a POSIX error code from a library function error return value. -#else -/* Some platforms have E* and errno already negated. */ -#define AVERROR(e) (e) -#define AVUNERROR(e) (e) -#endif - -#define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d)) - -#define AVERROR_BSF_NOT_FOUND FFERRTAG(0xF8,'B','S','F') ///< Bitstream filter not found -#define AVERROR_BUG FFERRTAG( 'B','U','G','!') ///< Internal bug, also see AVERROR_BUG2 -#define AVERROR_BUFFER_TOO_SMALL FFERRTAG( 'B','U','F','S') ///< Buffer too small -#define AVERROR_DECODER_NOT_FOUND FFERRTAG(0xF8,'D','E','C') ///< Decoder not found -#define AVERROR_DEMUXER_NOT_FOUND FFERRTAG(0xF8,'D','E','M') ///< Demuxer not found -#define AVERROR_ENCODER_NOT_FOUND FFERRTAG(0xF8,'E','N','C') ///< Encoder not found -#define AVERROR_EOF FFERRTAG( 'E','O','F',' ') ///< End of file -#define AVERROR_EXIT FFERRTAG( 'E','X','I','T') ///< Immediate exit was requested; the called function should not be restarted -#define AVERROR_EXTERNAL FFERRTAG( 'E','X','T',' ') ///< Generic error in an external library -#define AVERROR_FILTER_NOT_FOUND FFERRTAG(0xF8,'F','I','L') ///< Filter not found -#define AVERROR_INVALIDDATA FFERRTAG( 'I','N','D','A') ///< Invalid data found when processing input -#define AVERROR_MUXER_NOT_FOUND FFERRTAG(0xF8,'M','U','X') ///< Muxer not found -#define AVERROR_OPTION_NOT_FOUND FFERRTAG(0xF8,'O','P','T') ///< Option not found -#define AVERROR_PATCHWELCOME FFERRTAG( 'P','A','W','E') ///< Not yet implemented in FFmpeg, patches welcome -#define AVERROR_PROTOCOL_NOT_FOUND FFERRTAG(0xF8,'P','R','O') ///< Protocol not found - -#define AVERROR_STREAM_NOT_FOUND FFERRTAG(0xF8,'S','T','R') ///< Stream not found -/** - * This is semantically identical to AVERROR_BUG - * it has been introduced in Libav after our AVERROR_BUG and with a modified value. - */ -#define AVERROR_BUG2 FFERRTAG( 'B','U','G',' ') -#define AVERROR_UNKNOWN FFERRTAG( 'U','N','K','N') ///< Unknown error, typically from an external library -#define AVERROR_EXPERIMENTAL (-0x2bb2afa8) ///< Requested feature is flagged experimental. Set strict_std_compliance if you really want to use it. -#define AVERROR_INPUT_CHANGED (-0x636e6701) ///< Input changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_OUTPUT_CHANGED) -#define AVERROR_OUTPUT_CHANGED (-0x636e6702) ///< Output changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_INPUT_CHANGED) -/* HTTP & RTSP errors */ -#define AVERROR_HTTP_BAD_REQUEST FFERRTAG(0xF8,'4','0','0') -#define AVERROR_HTTP_UNAUTHORIZED FFERRTAG(0xF8,'4','0','1') -#define AVERROR_HTTP_FORBIDDEN FFERRTAG(0xF8,'4','0','3') -#define AVERROR_HTTP_NOT_FOUND FFERRTAG(0xF8,'4','0','4') -#define AVERROR_HTTP_OTHER_4XX FFERRTAG(0xF8,'4','X','X') -#define AVERROR_HTTP_SERVER_ERROR FFERRTAG(0xF8,'5','X','X') - -#define AV_ERROR_MAX_STRING_SIZE 64 - -/** - * Put a description of the AVERROR code errnum in errbuf. - * In case of failure the global variable errno is set to indicate the - * error. Even in case of failure av_strerror() will print a generic - * error message indicating the errnum provided to errbuf. - * - * @param errnum error code to describe - * @param errbuf buffer to which description is written - * @param errbuf_size the size in bytes of errbuf - * @return 0 on success, a negative value if a description for errnum - * cannot be found - */ -int av_strerror(int errnum, char *errbuf, size_t errbuf_size); - -/** - * Fill the provided buffer with a string containing an error string - * corresponding to the AVERROR code errnum. - * - * @param errbuf a buffer - * @param errbuf_size size in bytes of errbuf - * @param errnum error code to describe - * @return the buffer in input, filled with the error description - * @see av_strerror() - */ -static inline char *av_make_error_string(char *errbuf, size_t errbuf_size, int errnum) -{ - av_strerror(errnum, errbuf, errbuf_size); - return errbuf; -} - -/** - * Convenience macro, the return value should be used only directly in - * function arguments but never stand-alone. - */ -#define av_err2str(errnum) \ - av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, AV_ERROR_MAX_STRING_SIZE, errnum) - -/** - * @} - */ - -#endif /* AVUTIL_ERROR_H */ diff --git a/gostream/ffmpeg/include/libavutil/eval.h b/gostream/ffmpeg/include/libavutil/eval.h deleted file mode 100644 index ee8cffb057a..00000000000 --- a/gostream/ffmpeg/include/libavutil/eval.h +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2002 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * simple arithmetic expression evaluator - */ - -#ifndef AVUTIL_EVAL_H -#define AVUTIL_EVAL_H - -typedef struct AVExpr AVExpr; - -/** - * Parse and evaluate an expression. - * Note, this is significantly slower than av_expr_eval(). - * - * @param res a pointer to a double where is put the result value of - * the expression, or NAN in case of error - * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)" - * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0} - * @param const_values a zero terminated array of values for the identifiers from const_names - * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers - * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument - * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers - * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments - * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2 - * @param log_offset log level offset, can be used to silence error messages - * @param log_ctx parent logging context - * @return >= 0 in case of success, a negative value corresponding to an - * AVERROR code otherwise - */ -int av_expr_parse_and_eval(double *res, const char *s, - const char * const *const_names, const double *const_values, - const char * const *func1_names, double (* const *funcs1)(void *, double), - const char * const *func2_names, double (* const *funcs2)(void *, double, double), - void *opaque, int log_offset, void *log_ctx); - -/** - * Parse an expression. - * - * @param expr a pointer where is put an AVExpr containing the parsed - * value in case of successful parsing, or NULL otherwise. - * The pointed to AVExpr must be freed with av_expr_free() by the user - * when it is not needed anymore. - * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)" - * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0} - * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers - * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument - * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers - * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments - * @param log_offset log level offset, can be used to silence error messages - * @param log_ctx parent logging context - * @return >= 0 in case of success, a negative value corresponding to an - * AVERROR code otherwise - */ -int av_expr_parse(AVExpr **expr, const char *s, - const char * const *const_names, - const char * const *func1_names, double (* const *funcs1)(void *, double), - const char * const *func2_names, double (* const *funcs2)(void *, double, double), - int log_offset, void *log_ctx); - -/** - * Evaluate a previously parsed expression. - * - * @param e the AVExpr to evaluate - * @param const_values a zero terminated array of values for the identifiers from av_expr_parse() const_names - * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2 - * @return the value of the expression - */ -double av_expr_eval(AVExpr *e, const double *const_values, void *opaque); - -/** - * Track the presence of variables and their number of occurrences in a parsed expression - * - * @param e the AVExpr to track variables in - * @param counter a zero-initialized array where the count of each variable will be stored - * @param size size of array - * @return 0 on success, a negative value indicates that no expression or array was passed - * or size was zero - */ -int av_expr_count_vars(AVExpr *e, unsigned *counter, int size); - -/** - * Track the presence of user provided functions and their number of occurrences - * in a parsed expression. - * - * @param e the AVExpr to track user provided functions in - * @param counter a zero-initialized array where the count of each function will be stored - * if you passed 5 functions with 2 arguments to av_expr_parse() - * then for arg=2 this will use upto 5 entries. - * @param size size of array - * @param arg number of arguments the counted functions have - * @return 0 on success, a negative value indicates that no expression or array was passed - * or size was zero - */ -int av_expr_count_func(AVExpr *e, unsigned *counter, int size, int arg); - -/** - * Free a parsed expression previously created with av_expr_parse(). - */ -void av_expr_free(AVExpr *e); - -/** - * Parse the string in numstr and return its value as a double. If - * the string is empty, contains only whitespaces, or does not contain - * an initial substring that has the expected syntax for a - * floating-point number, no conversion is performed. In this case, - * returns a value of zero and the value returned in tail is the value - * of numstr. - * - * @param numstr a string representing a number, may contain one of - * the International System number postfixes, for example 'K', 'M', - * 'G'. If 'i' is appended after the postfix, powers of 2 are used - * instead of powers of 10. The 'B' postfix multiplies the value by - * 8, and can be appended after another postfix or used alone. This - * allows using for example 'KB', 'MiB', 'G' and 'B' as postfix. - * @param tail if non-NULL puts here the pointer to the char next - * after the last parsed character - */ -double av_strtod(const char *numstr, char **tail); - -#endif /* AVUTIL_EVAL_H */ diff --git a/gostream/ffmpeg/include/libavutil/executor.h b/gostream/ffmpeg/include/libavutil/executor.h deleted file mode 100644 index c602bcb613b..00000000000 --- a/gostream/ffmpeg/include/libavutil/executor.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2023 Nuo Mi - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_EXECUTOR_H -#define AVUTIL_EXECUTOR_H - -typedef struct AVExecutor AVExecutor; -typedef struct AVTask AVTask; - -struct AVTask { - AVTask *next; -}; - -typedef struct AVTaskCallbacks { - void *user_data; - - int local_context_size; - - // return 1 if a's priority > b's priority - int (*priority_higher)(const AVTask *a, const AVTask *b); - - // task is ready for run - int (*ready)(const AVTask *t, void *user_data); - - // run the task - int (*run)(AVTask *t, void *local_context, void *user_data); -} AVTaskCallbacks; - -/** - * Alloc executor - * @param callbacks callback structure for executor - * @param thread_count worker thread number - * @return return the executor - */ -AVExecutor* av_executor_alloc(const AVTaskCallbacks *callbacks, int thread_count); - -/** - * Free executor - * @param e pointer to executor - */ -void av_executor_free(AVExecutor **e); - -/** - * Add task to executor - * @param e pointer to executor - * @param t pointer to task. If NULL, it will wakeup one work thread - */ -void av_executor_execute(AVExecutor *e, AVTask *t); - -#endif //AVUTIL_EXECUTOR_H diff --git a/gostream/ffmpeg/include/libavutil/ffversion.h b/gostream/ffmpeg/include/libavutil/ffversion.h deleted file mode 100644 index f4535f1d049..00000000000 --- a/gostream/ffmpeg/include/libavutil/ffversion.h +++ /dev/null @@ -1,5 +0,0 @@ -/* Automatically generated by version.sh, do not manually edit! */ -#ifndef AVUTIL_FFVERSION_H -#define AVUTIL_FFVERSION_H -#define FFMPEG_VERSION "n6.1-3-g466799d4f5" -#endif /* AVUTIL_FFVERSION_H */ diff --git a/gostream/ffmpeg/include/libavutil/fifo.h b/gostream/ffmpeg/include/libavutil/fifo.h deleted file mode 100644 index ce3a2aed7ce..00000000000 --- a/gostream/ffmpeg/include/libavutil/fifo.h +++ /dev/null @@ -1,448 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_fifo - * A generic FIFO API - */ - -#ifndef AVUTIL_FIFO_H -#define AVUTIL_FIFO_H - -#include -#include - -#include "attributes.h" -#include "version.h" - -/** - * @defgroup lavu_fifo AVFifo - * @ingroup lavu_data - * - * @{ - * A generic FIFO API - */ - -typedef struct AVFifo AVFifo; - -/** - * Callback for writing or reading from a FIFO, passed to (and invoked from) the - * av_fifo_*_cb() functions. It may be invoked multiple times from a single - * av_fifo_*_cb() call and may process less data than the maximum size indicated - * by nb_elems. - * - * @param opaque the opaque pointer provided to the av_fifo_*_cb() function - * @param buf the buffer for reading or writing the data, depending on which - * av_fifo_*_cb function is called - * @param nb_elems On entry contains the maximum number of elements that can be - * read from / written into buf. On success, the callback should - * update it to contain the number of elements actually written. - * - * @return 0 on success, a negative error code on failure (will be returned from - * the invoking av_fifo_*_cb() function) - */ -typedef int AVFifoCB(void *opaque, void *buf, size_t *nb_elems); - -/** - * Automatically resize the FIFO on writes, so that the data fits. This - * automatic resizing happens up to a limit that can be modified with - * av_fifo_auto_grow_limit(). - */ -#define AV_FIFO_FLAG_AUTO_GROW (1 << 0) - -/** - * Allocate and initialize an AVFifo with a given element size. - * - * @param elems initial number of elements that can be stored in the FIFO - * @param elem_size Size in bytes of a single element. Further operations on - * the returned FIFO will implicitly use this element size. - * @param flags a combination of AV_FIFO_FLAG_* - * - * @return newly-allocated AVFifo on success, a negative error code on failure - */ -AVFifo *av_fifo_alloc2(size_t elems, size_t elem_size, - unsigned int flags); - -/** - * @return Element size for FIFO operations. This element size is set at - * FIFO allocation and remains constant during its lifetime - */ -size_t av_fifo_elem_size(const AVFifo *f); - -/** - * Set the maximum size (in elements) to which the FIFO can be resized - * automatically. Has no effect unless AV_FIFO_FLAG_AUTO_GROW is used. - */ -void av_fifo_auto_grow_limit(AVFifo *f, size_t max_elems); - -/** - * @return number of elements available for reading from the given FIFO. - */ -size_t av_fifo_can_read(const AVFifo *f); - -/** - * @return Number of elements that can be written into the given FIFO without - * growing it. - * - * In other words, this number of elements or less is guaranteed to fit - * into the FIFO. More data may be written when the - * AV_FIFO_FLAG_AUTO_GROW flag was specified at FIFO creation, but this - * may involve memory allocation, which can fail. - */ -size_t av_fifo_can_write(const AVFifo *f); - -/** - * Enlarge an AVFifo. - * - * On success, the FIFO will be large enough to hold exactly - * inc + av_fifo_can_read() + av_fifo_can_write() - * elements. In case of failure, the old FIFO is kept unchanged. - * - * @param f AVFifo to resize - * @param inc number of elements to allocate for, in addition to the current - * allocated size - * @return a non-negative number on success, a negative error code on failure - */ -int av_fifo_grow2(AVFifo *f, size_t inc); - -/** - * Write data into a FIFO. - * - * In case nb_elems > av_fifo_can_write(f) and the AV_FIFO_FLAG_AUTO_GROW flag - * was not specified at FIFO creation, nothing is written and an error - * is returned. - * - * Calling function is guaranteed to succeed if nb_elems <= av_fifo_can_write(f). - * - * @param f the FIFO buffer - * @param buf Data to be written. nb_elems * av_fifo_elem_size(f) bytes will be - * read from buf on success. - * @param nb_elems number of elements to write into FIFO - * - * @return a non-negative number on success, a negative error code on failure - */ -int av_fifo_write(AVFifo *f, const void *buf, size_t nb_elems); - -/** - * Write data from a user-provided callback into a FIFO. - * - * @param f the FIFO buffer - * @param read_cb Callback supplying the data to the FIFO. May be called - * multiple times. - * @param opaque opaque user data to be provided to read_cb - * @param nb_elems Should point to the maximum number of elements that can be - * written. Will be updated to contain the number of elements - * actually written. - * - * @return non-negative number on success, a negative error code on failure - */ -int av_fifo_write_from_cb(AVFifo *f, AVFifoCB read_cb, - void *opaque, size_t *nb_elems); - -/** - * Read data from a FIFO. - * - * In case nb_elems > av_fifo_can_read(f), nothing is read and an error - * is returned. - * - * @param f the FIFO buffer - * @param buf Buffer to store the data. nb_elems * av_fifo_elem_size(f) bytes - * will be written into buf on success. - * @param nb_elems number of elements to read from FIFO - * - * @return a non-negative number on success, a negative error code on failure - */ -int av_fifo_read(AVFifo *f, void *buf, size_t nb_elems); - -/** - * Feed data from a FIFO into a user-provided callback. - * - * @param f the FIFO buffer - * @param write_cb Callback the data will be supplied to. May be called - * multiple times. - * @param opaque opaque user data to be provided to write_cb - * @param nb_elems Should point to the maximum number of elements that can be - * read. Will be updated to contain the total number of elements - * actually sent to the callback. - * - * @return non-negative number on success, a negative error code on failure - */ -int av_fifo_read_to_cb(AVFifo *f, AVFifoCB write_cb, - void *opaque, size_t *nb_elems); - -/** - * Read data from a FIFO without modifying FIFO state. - * - * Returns an error if an attempt is made to peek to nonexistent elements - * (i.e. if offset + nb_elems is larger than av_fifo_can_read(f)). - * - * @param f the FIFO buffer - * @param buf Buffer to store the data. nb_elems * av_fifo_elem_size(f) bytes - * will be written into buf. - * @param nb_elems number of elements to read from FIFO - * @param offset number of initial elements to skip. - * - * @return a non-negative number on success, a negative error code on failure - */ -int av_fifo_peek(const AVFifo *f, void *buf, size_t nb_elems, size_t offset); - -/** - * Feed data from a FIFO into a user-provided callback. - * - * @param f the FIFO buffer - * @param write_cb Callback the data will be supplied to. May be called - * multiple times. - * @param opaque opaque user data to be provided to write_cb - * @param nb_elems Should point to the maximum number of elements that can be - * read. Will be updated to contain the total number of elements - * actually sent to the callback. - * @param offset number of initial elements to skip; offset + *nb_elems must not - * be larger than av_fifo_can_read(f). - * - * @return a non-negative number on success, a negative error code on failure - */ -int av_fifo_peek_to_cb(const AVFifo *f, AVFifoCB write_cb, void *opaque, - size_t *nb_elems, size_t offset); - -/** - * Discard the specified amount of data from an AVFifo. - * @param size number of elements to discard, MUST NOT be larger than - * av_fifo_can_read(f) - */ -void av_fifo_drain2(AVFifo *f, size_t size); - -/* - * Empty the AVFifo. - * @param f AVFifo to reset - */ -void av_fifo_reset2(AVFifo *f); - -/** - * Free an AVFifo and reset pointer to NULL. - * @param f Pointer to an AVFifo to free. *f == NULL is allowed. - */ -void av_fifo_freep2(AVFifo **f); - - -#if FF_API_FIFO_OLD_API -typedef struct AVFifoBuffer { - uint8_t *buffer; - uint8_t *rptr, *wptr, *end; - uint32_t rndx, wndx; -} AVFifoBuffer; - -/** - * Initialize an AVFifoBuffer. - * @param size of FIFO - * @return AVFifoBuffer or NULL in case of memory allocation failure - * @deprecated use av_fifo_alloc2() - */ -attribute_deprecated -AVFifoBuffer *av_fifo_alloc(unsigned int size); - -/** - * Initialize an AVFifoBuffer. - * @param nmemb number of elements - * @param size size of the single element - * @return AVFifoBuffer or NULL in case of memory allocation failure - * @deprecated use av_fifo_alloc2() - */ -attribute_deprecated -AVFifoBuffer *av_fifo_alloc_array(size_t nmemb, size_t size); - -/** - * Free an AVFifoBuffer. - * @param f AVFifoBuffer to free - * @deprecated use the AVFifo API with av_fifo_freep2() - */ -attribute_deprecated -void av_fifo_free(AVFifoBuffer *f); - -/** - * Free an AVFifoBuffer and reset pointer to NULL. - * @param f AVFifoBuffer to free - * @deprecated use the AVFifo API with av_fifo_freep2() - */ -attribute_deprecated -void av_fifo_freep(AVFifoBuffer **f); - -/** - * Reset the AVFifoBuffer to the state right after av_fifo_alloc, in particular it is emptied. - * @param f AVFifoBuffer to reset - * @deprecated use av_fifo_reset2() with the new AVFifo-API - */ -attribute_deprecated -void av_fifo_reset(AVFifoBuffer *f); - -/** - * Return the amount of data in bytes in the AVFifoBuffer, that is the - * amount of data you can read from it. - * @param f AVFifoBuffer to read from - * @return size - * @deprecated use av_fifo_can_read() with the new AVFifo-API - */ -attribute_deprecated -int av_fifo_size(const AVFifoBuffer *f); - -/** - * Return the amount of space in bytes in the AVFifoBuffer, that is the - * amount of data you can write into it. - * @param f AVFifoBuffer to write into - * @return size - * @deprecated use av_fifo_can_write() with the new AVFifo-API - */ -attribute_deprecated -int av_fifo_space(const AVFifoBuffer *f); - -/** - * Feed data at specific position from an AVFifoBuffer to a user-supplied callback. - * Similar as av_fifo_gereric_read but without discarding data. - * @param f AVFifoBuffer to read from - * @param offset offset from current read position - * @param buf_size number of bytes to read - * @param func generic read function - * @param dest data destination - * - * @return a non-negative number on success, a negative error code on failure - * - * @deprecated use the new AVFifo-API with av_fifo_peek() when func == NULL, - * av_fifo_peek_to_cb() otherwise - */ -attribute_deprecated -int av_fifo_generic_peek_at(AVFifoBuffer *f, void *dest, int offset, int buf_size, void (*func)(void*, void*, int)); - -/** - * Feed data from an AVFifoBuffer to a user-supplied callback. - * Similar as av_fifo_gereric_read but without discarding data. - * @param f AVFifoBuffer to read from - * @param buf_size number of bytes to read - * @param func generic read function - * @param dest data destination - * - * @return a non-negative number on success, a negative error code on failure - * - * @deprecated use the new AVFifo-API with av_fifo_peek() when func == NULL, - * av_fifo_peek_to_cb() otherwise - */ -attribute_deprecated -int av_fifo_generic_peek(AVFifoBuffer *f, void *dest, int buf_size, void (*func)(void*, void*, int)); - -/** - * Feed data from an AVFifoBuffer to a user-supplied callback. - * @param f AVFifoBuffer to read from - * @param buf_size number of bytes to read - * @param func generic read function - * @param dest data destination - * - * @return a non-negative number on success, a negative error code on failure - * - * @deprecated use the new AVFifo-API with av_fifo_read() when func == NULL, - * av_fifo_read_to_cb() otherwise - */ -attribute_deprecated -int av_fifo_generic_read(AVFifoBuffer *f, void *dest, int buf_size, void (*func)(void*, void*, int)); - -/** - * Feed data from a user-supplied callback to an AVFifoBuffer. - * @param f AVFifoBuffer to write to - * @param src data source; non-const since it may be used as a - * modifiable context by the function defined in func - * @param size number of bytes to write - * @param func generic write function; the first parameter is src, - * the second is dest_buf, the third is dest_buf_size. - * func must return the number of bytes written to dest_buf, or <= 0 to - * indicate no more data available to write. - * If func is NULL, src is interpreted as a simple byte array for source data. - * @return the number of bytes written to the FIFO or a negative error code on failure - * - * @deprecated use the new AVFifo-API with av_fifo_write() when func == NULL, - * av_fifo_write_from_cb() otherwise - */ -attribute_deprecated -int av_fifo_generic_write(AVFifoBuffer *f, void *src, int size, int (*func)(void*, void*, int)); - -/** - * Resize an AVFifoBuffer. - * In case of reallocation failure, the old FIFO is kept unchanged. - * - * @param f AVFifoBuffer to resize - * @param size new AVFifoBuffer size in bytes - * @return <0 for failure, >=0 otherwise - * - * @deprecated use the new AVFifo-API with av_fifo_grow2() to increase FIFO size, - * decreasing FIFO size is not supported - */ -attribute_deprecated -int av_fifo_realloc2(AVFifoBuffer *f, unsigned int size); - -/** - * Enlarge an AVFifoBuffer. - * In case of reallocation failure, the old FIFO is kept unchanged. - * The new fifo size may be larger than the requested size. - * - * @param f AVFifoBuffer to resize - * @param additional_space the amount of space in bytes to allocate in addition to av_fifo_size() - * @return <0 for failure, >=0 otherwise - * - * @deprecated use the new AVFifo-API with av_fifo_grow2(); note that unlike - * this function it adds to the allocated size, rather than to the used size - */ -attribute_deprecated -int av_fifo_grow(AVFifoBuffer *f, unsigned int additional_space); - -/** - * Read and discard the specified amount of data from an AVFifoBuffer. - * @param f AVFifoBuffer to read from - * @param size amount of data to read in bytes - * - * @deprecated use the new AVFifo-API with av_fifo_drain2() - */ -attribute_deprecated -void av_fifo_drain(AVFifoBuffer *f, int size); - -#if FF_API_FIFO_PEEK2 -/** - * Return a pointer to the data stored in a FIFO buffer at a certain offset. - * The FIFO buffer is not modified. - * - * @param f AVFifoBuffer to peek at, f must be non-NULL - * @param offs an offset in bytes, its absolute value must be less - * than the used buffer size or the returned pointer will - * point outside to the buffer data. - * The used buffer size can be checked with av_fifo_size(). - * @deprecated use the new AVFifo-API with av_fifo_peek() or av_fifo_peek_to_cb() - */ -attribute_deprecated -static inline uint8_t *av_fifo_peek2(const AVFifoBuffer *f, int offs) -{ - uint8_t *ptr = f->rptr + offs; - if (ptr >= f->end) - ptr = f->buffer + (ptr - f->end); - else if (ptr < f->buffer) - ptr = f->end - (f->buffer - ptr); - return ptr; -} -#endif -#endif - -/** - * @} - */ - -#endif /* AVUTIL_FIFO_H */ diff --git a/gostream/ffmpeg/include/libavutil/file.h b/gostream/ffmpeg/include/libavutil/file.h deleted file mode 100644 index fc87a9cd6a6..00000000000 --- a/gostream/ffmpeg/include/libavutil/file.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_FILE_H -#define AVUTIL_FILE_H - -#include -#include - -#include "version.h" -#include "attributes.h" - -/** - * @file - * Misc file utilities. - */ - -/** - * Read the file with name filename, and put its content in a newly - * allocated buffer or map it with mmap() when available. - * In case of success set *bufptr to the read or mmapped buffer, and - * *size to the size in bytes of the buffer in *bufptr. - * Unlike mmap this function succeeds with zero sized files, in this - * case *bufptr will be set to NULL and *size will be set to 0. - * The returned buffer must be released with av_file_unmap(). - * - * @param filename path to the file - * @param[out] bufptr pointee is set to the mapped or allocated buffer - * @param[out] size pointee is set to the size in bytes of the buffer - * @param log_offset loglevel offset used for logging - * @param log_ctx context used for logging - * @return a non negative number in case of success, a negative value - * corresponding to an AVERROR error code in case of failure - */ -av_warn_unused_result -int av_file_map(const char *filename, uint8_t **bufptr, size_t *size, - int log_offset, void *log_ctx); - -/** - * Unmap or free the buffer bufptr created by av_file_map(). - * - * @param bufptr the buffer previously created with av_file_map() - * @param size size in bytes of bufptr, must be the same as returned - * by av_file_map() - */ -void av_file_unmap(uint8_t *bufptr, size_t size); - -#if FF_API_AV_FOPEN_UTF8 -/** - * Wrapper to work around the lack of mkstemp() on mingw. - * Also, tries to create file in /tmp first, if possible. - * *prefix can be a character constant; *filename will be allocated internally. - * @return file descriptor of opened file (or negative value corresponding to an - * AVERROR code on error) - * and opened file name in **filename. - * @note On very old libcs it is necessary to set a secure umask before - * calling this, av_tempfile() can't call umask itself as it is used in - * libraries and could interfere with the calling application. - * @deprecated as fd numbers cannot be passed saftely between libs on some platforms - */ -attribute_deprecated -int av_tempfile(const char *prefix, char **filename, int log_offset, void *log_ctx); -#endif - -#endif /* AVUTIL_FILE_H */ diff --git a/gostream/ffmpeg/include/libavutil/film_grain_params.h b/gostream/ffmpeg/include/libavutil/film_grain_params.h deleted file mode 100644 index f3bd0a4a6a3..00000000000 --- a/gostream/ffmpeg/include/libavutil/film_grain_params.h +++ /dev/null @@ -1,260 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_FILM_GRAIN_PARAMS_H -#define AVUTIL_FILM_GRAIN_PARAMS_H - -#include "frame.h" - -enum AVFilmGrainParamsType { - AV_FILM_GRAIN_PARAMS_NONE = 0, - - /** - * The union is valid when interpreted as AVFilmGrainAOMParams (codec.aom) - */ - AV_FILM_GRAIN_PARAMS_AV1, - - /** - * The union is valid when interpreted as AVFilmGrainH274Params (codec.h274) - */ - AV_FILM_GRAIN_PARAMS_H274, -}; - -/** - * This structure describes how to handle film grain synthesis for AOM codecs. - * - * @note The struct must be allocated as part of AVFilmGrainParams using - * av_film_grain_params_alloc(). Its size is not a part of the public ABI. - */ -typedef struct AVFilmGrainAOMParams { - /** - * Number of points, and the scale and value for each point of the - * piecewise linear scaling function for the uma plane. - */ - int num_y_points; - uint8_t y_points[14][2 /* value, scaling */]; - - /** - * Signals whether to derive the chroma scaling function from the luma. - * Not equivalent to copying the luma values and scales. - */ - int chroma_scaling_from_luma; - - /** - * If chroma_scaling_from_luma is set to 0, signals the chroma scaling - * function parameters. - */ - int num_uv_points[2 /* cb, cr */]; - uint8_t uv_points[2 /* cb, cr */][10][2 /* value, scaling */]; - - /** - * Specifies the shift applied to the chroma components. For AV1, its within - * [8; 11] and determines the range and quantization of the film grain. - */ - int scaling_shift; - - /** - * Specifies the auto-regression lag. - */ - int ar_coeff_lag; - - /** - * Luma auto-regression coefficients. The number of coefficients is given by - * 2 * ar_coeff_lag * (ar_coeff_lag + 1). - */ - int8_t ar_coeffs_y[24]; - - /** - * Chroma auto-regression coefficients. The number of coefficients is given by - * 2 * ar_coeff_lag * (ar_coeff_lag + 1) + !!num_y_points. - */ - int8_t ar_coeffs_uv[2 /* cb, cr */][25]; - - /** - * Specifies the range of the auto-regressive coefficients. Values of 6, - * 7, 8 and so on represent a range of [-2, 2), [-1, 1), [-0.5, 0.5) and - * so on. For AV1 must be between 6 and 9. - */ - int ar_coeff_shift; - - /** - * Signals the down shift applied to the generated gaussian numbers during - * synthesis. - */ - int grain_scale_shift; - - /** - * Specifies the luma/chroma multipliers for the index to the component - * scaling function. - */ - int uv_mult[2 /* cb, cr */]; - int uv_mult_luma[2 /* cb, cr */]; - - /** - * Offset used for component scaling function. For AV1 its a 9-bit value - * with a range [-256, 255] - */ - int uv_offset[2 /* cb, cr */]; - - /** - * Signals whether to overlap film grain blocks. - */ - int overlap_flag; - - /** - * Signals to clip to limited color levels after film grain application. - */ - int limit_output_range; -} AVFilmGrainAOMParams; - -/** - * This structure describes how to handle film grain synthesis for codecs using - * the ITU-T H.274 Versatile suplemental enhancement information message. - * - * @note The struct must be allocated as part of AVFilmGrainParams using - * av_film_grain_params_alloc(). Its size is not a part of the public ABI. - */ -typedef struct AVFilmGrainH274Params { - /** - * Specifies the film grain simulation mode. - * 0 = Frequency filtering, 1 = Auto-regression - */ - int model_id; - - /** - * Specifies the bit depth used for the luma component. - */ - int bit_depth_luma; - - /** - * Specifies the bit depth used for the chroma components. - */ - int bit_depth_chroma; - - enum AVColorRange color_range; - enum AVColorPrimaries color_primaries; - enum AVColorTransferCharacteristic color_trc; - enum AVColorSpace color_space; - - /** - * Specifies the blending mode used to blend the simulated film grain - * with the decoded images. - * - * 0 = Additive, 1 = Multiplicative - */ - int blending_mode_id; - - /** - * Specifies a scale factor used in the film grain characterization equations. - */ - int log2_scale_factor; - - /** - * Indicates if the modelling of film grain for a given component is present. - */ - int component_model_present[3 /* y, cb, cr */]; - - /** - * Specifies the number of intensity intervals for which a specific set of - * model values has been estimated, with a range of [1, 256]. - */ - uint16_t num_intensity_intervals[3 /* y, cb, cr */]; - - /** - * Specifies the number of model values present for each intensity interval - * in which the film grain has been modelled, with a range of [1, 6]. - */ - uint8_t num_model_values[3 /* y, cb, cr */]; - - /** - * Specifies the lower ounds of each intensity interval for whichthe set of - * model values applies for the component. - */ - uint8_t intensity_interval_lower_bound[3 /* y, cb, cr */][256 /* intensity interval */]; - - /** - * Specifies the upper bound of each intensity interval for which the set of - * model values applies for the component. - */ - uint8_t intensity_interval_upper_bound[3 /* y, cb, cr */][256 /* intensity interval */]; - - /** - * Specifies the model values for the component for each intensity interval. - * - When model_id == 0, the following applies: - * For comp_model_value[y], the range of values is [0, 2^bit_depth_luma - 1] - * For comp_model_value[cb..cr], the range of values is [0, 2^bit_depth_chroma - 1] - * - Otherwise, the following applies: - * For comp_model_value[y], the range of values is [-2^(bit_depth_luma - 1), 2^(bit_depth_luma - 1) - 1] - * For comp_model_value[cb..cr], the range of values is [-2^(bit_depth_chroma - 1), 2^(bit_depth_chroma - 1) - 1] - */ - int16_t comp_model_value[3 /* y, cb, cr */][256 /* intensity interval */][6 /* model value */]; -} AVFilmGrainH274Params; - -/** - * This structure describes how to handle film grain synthesis in video - * for specific codecs. Must be present on every frame where film grain is - * meant to be synthesised for correct presentation. - * - * @note The struct must be allocated with av_film_grain_params_alloc() and - * its size is not a part of the public ABI. - */ -typedef struct AVFilmGrainParams { - /** - * Specifies the codec for which this structure is valid. - */ - enum AVFilmGrainParamsType type; - - /** - * Seed to use for the synthesis process, if the codec allows for it. - * - * @note For H.264, this refers to `pic_offset` as defined in - * SMPTE RDD 5-2006. - */ - uint64_t seed; - - /** - * Additional fields may be added both here and in any structure included. - * If a codec's film grain structure differs slightly over another - * codec's, fields within may change meaning depending on the type. - */ - union { - AVFilmGrainAOMParams aom; - AVFilmGrainH274Params h274; - } codec; -} AVFilmGrainParams; - -/** - * Allocate an AVFilmGrainParams structure and set its fields to - * default values. The resulting struct can be freed using av_freep(). - * If size is not NULL it will be set to the number of bytes allocated. - * - * @return An AVFilmGrainParams filled with default values or NULL - * on failure. - */ -AVFilmGrainParams *av_film_grain_params_alloc(size_t *size); - -/** - * Allocate a complete AVFilmGrainParams and add it to the frame. - * - * @param frame The frame which side data is added to. - * - * @return The AVFilmGrainParams structure to be filled by caller. - */ -AVFilmGrainParams *av_film_grain_params_create_side_data(AVFrame *frame); - -#endif /* AVUTIL_FILM_GRAIN_PARAMS_H */ diff --git a/gostream/ffmpeg/include/libavutil/frame.h b/gostream/ffmpeg/include/libavutil/frame.h deleted file mode 100644 index c0c1b23db7c..00000000000 --- a/gostream/ffmpeg/include/libavutil/frame.h +++ /dev/null @@ -1,1056 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_frame - * reference-counted frame API - */ - -#ifndef AVUTIL_FRAME_H -#define AVUTIL_FRAME_H - -#include -#include - -#include "avutil.h" -#include "buffer.h" -#include "channel_layout.h" -#include "dict.h" -#include "rational.h" -#include "samplefmt.h" -#include "pixfmt.h" -#include "version.h" - - -/** - * @defgroup lavu_frame AVFrame - * @ingroup lavu_data - * - * @{ - * AVFrame is an abstraction for reference-counted raw multimedia data. - */ - -enum AVFrameSideDataType { - /** - * The data is the AVPanScan struct defined in libavcodec. - */ - AV_FRAME_DATA_PANSCAN, - /** - * ATSC A53 Part 4 Closed Captions. - * A53 CC bitstream is stored as uint8_t in AVFrameSideData.data. - * The number of bytes of CC data is AVFrameSideData.size. - */ - AV_FRAME_DATA_A53_CC, - /** - * Stereoscopic 3d metadata. - * The data is the AVStereo3D struct defined in libavutil/stereo3d.h. - */ - AV_FRAME_DATA_STEREO3D, - /** - * The data is the AVMatrixEncoding enum defined in libavutil/channel_layout.h. - */ - AV_FRAME_DATA_MATRIXENCODING, - /** - * Metadata relevant to a downmix procedure. - * The data is the AVDownmixInfo struct defined in libavutil/downmix_info.h. - */ - AV_FRAME_DATA_DOWNMIX_INFO, - /** - * ReplayGain information in the form of the AVReplayGain struct. - */ - AV_FRAME_DATA_REPLAYGAIN, - /** - * This side data contains a 3x3 transformation matrix describing an affine - * transformation that needs to be applied to the frame for correct - * presentation. - * - * See libavutil/display.h for a detailed description of the data. - */ - AV_FRAME_DATA_DISPLAYMATRIX, - /** - * Active Format Description data consisting of a single byte as specified - * in ETSI TS 101 154 using AVActiveFormatDescription enum. - */ - AV_FRAME_DATA_AFD, - /** - * Motion vectors exported by some codecs (on demand through the export_mvs - * flag set in the libavcodec AVCodecContext flags2 option). - * The data is the AVMotionVector struct defined in - * libavutil/motion_vector.h. - */ - AV_FRAME_DATA_MOTION_VECTORS, - /** - * Recommmends skipping the specified number of samples. This is exported - * only if the "skip_manual" AVOption is set in libavcodec. - * This has the same format as AV_PKT_DATA_SKIP_SAMPLES. - * @code - * u32le number of samples to skip from start of this packet - * u32le number of samples to skip from end of this packet - * u8 reason for start skip - * u8 reason for end skip (0=padding silence, 1=convergence) - * @endcode - */ - AV_FRAME_DATA_SKIP_SAMPLES, - /** - * This side data must be associated with an audio frame and corresponds to - * enum AVAudioServiceType defined in avcodec.h. - */ - AV_FRAME_DATA_AUDIO_SERVICE_TYPE, - /** - * Mastering display metadata associated with a video frame. The payload is - * an AVMasteringDisplayMetadata type and contains information about the - * mastering display color volume. - */ - AV_FRAME_DATA_MASTERING_DISPLAY_METADATA, - /** - * The GOP timecode in 25 bit timecode format. Data format is 64-bit integer. - * This is set on the first frame of a GOP that has a temporal reference of 0. - */ - AV_FRAME_DATA_GOP_TIMECODE, - - /** - * The data represents the AVSphericalMapping structure defined in - * libavutil/spherical.h. - */ - AV_FRAME_DATA_SPHERICAL, - - /** - * Content light level (based on CTA-861.3). This payload contains data in - * the form of the AVContentLightMetadata struct. - */ - AV_FRAME_DATA_CONTENT_LIGHT_LEVEL, - - /** - * The data contains an ICC profile as an opaque octet buffer following the - * format described by ISO 15076-1 with an optional name defined in the - * metadata key entry "name". - */ - AV_FRAME_DATA_ICC_PROFILE, - - /** - * Timecode which conforms to SMPTE ST 12-1. The data is an array of 4 uint32_t - * where the first uint32_t describes how many (1-3) of the other timecodes are used. - * The timecode format is described in the documentation of av_timecode_get_smpte_from_framenum() - * function in libavutil/timecode.h. - */ - AV_FRAME_DATA_S12M_TIMECODE, - - /** - * HDR dynamic metadata associated with a video frame. The payload is - * an AVDynamicHDRPlus type and contains information for color - * volume transform - application 4 of SMPTE 2094-40:2016 standard. - */ - AV_FRAME_DATA_DYNAMIC_HDR_PLUS, - - /** - * Regions Of Interest, the data is an array of AVRegionOfInterest type, the number of - * array element is implied by AVFrameSideData.size / AVRegionOfInterest.self_size. - */ - AV_FRAME_DATA_REGIONS_OF_INTEREST, - - /** - * Encoding parameters for a video frame, as described by AVVideoEncParams. - */ - AV_FRAME_DATA_VIDEO_ENC_PARAMS, - - /** - * User data unregistered metadata associated with a video frame. - * This is the H.26[45] UDU SEI message, and shouldn't be used for any other purpose - * The data is stored as uint8_t in AVFrameSideData.data which is 16 bytes of - * uuid_iso_iec_11578 followed by AVFrameSideData.size - 16 bytes of user_data_payload_byte. - */ - AV_FRAME_DATA_SEI_UNREGISTERED, - - /** - * Film grain parameters for a frame, described by AVFilmGrainParams. - * Must be present for every frame which should have film grain applied. - */ - AV_FRAME_DATA_FILM_GRAIN_PARAMS, - - /** - * Bounding boxes for object detection and classification, - * as described by AVDetectionBBoxHeader. - */ - AV_FRAME_DATA_DETECTION_BBOXES, - - /** - * Dolby Vision RPU raw data, suitable for passing to x265 - * or other libraries. Array of uint8_t, with NAL emulation - * bytes intact. - */ - AV_FRAME_DATA_DOVI_RPU_BUFFER, - - /** - * Parsed Dolby Vision metadata, suitable for passing to a software - * implementation. The payload is the AVDOVIMetadata struct defined in - * libavutil/dovi_meta.h. - */ - AV_FRAME_DATA_DOVI_METADATA, - - /** - * HDR Vivid dynamic metadata associated with a video frame. The payload is - * an AVDynamicHDRVivid type and contains information for color - * volume transform - CUVA 005.1-2021. - */ - AV_FRAME_DATA_DYNAMIC_HDR_VIVID, - - /** - * Ambient viewing environment metadata, as defined by H.274. - */ - AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT, - - /** - * Provide encoder-specific hinting information about changed/unchanged - * portions of a frame. It can be used to pass information about which - * macroblocks can be skipped because they didn't change from the - * corresponding ones in the previous frame. This could be useful for - * applications which know this information in advance to speed up - * encoding. - */ - AV_FRAME_DATA_VIDEO_HINT, -}; - -enum AVActiveFormatDescription { - AV_AFD_SAME = 8, - AV_AFD_4_3 = 9, - AV_AFD_16_9 = 10, - AV_AFD_14_9 = 11, - AV_AFD_4_3_SP_14_9 = 13, - AV_AFD_16_9_SP_14_9 = 14, - AV_AFD_SP_4_3 = 15, -}; - - -/** - * Structure to hold side data for an AVFrame. - * - * sizeof(AVFrameSideData) is not a part of the public ABI, so new fields may be added - * to the end with a minor bump. - */ -typedef struct AVFrameSideData { - enum AVFrameSideDataType type; - uint8_t *data; - size_t size; - AVDictionary *metadata; - AVBufferRef *buf; -} AVFrameSideData; - -/** - * Structure describing a single Region Of Interest. - * - * When multiple regions are defined in a single side-data block, they - * should be ordered from most to least important - some encoders are only - * capable of supporting a limited number of distinct regions, so will have - * to truncate the list. - * - * When overlapping regions are defined, the first region containing a given - * area of the frame applies. - */ -typedef struct AVRegionOfInterest { - /** - * Must be set to the size of this data structure (that is, - * sizeof(AVRegionOfInterest)). - */ - uint32_t self_size; - /** - * Distance in pixels from the top edge of the frame to the top and - * bottom edges and from the left edge of the frame to the left and - * right edges of the rectangle defining this region of interest. - * - * The constraints on a region are encoder dependent, so the region - * actually affected may be slightly larger for alignment or other - * reasons. - */ - int top; - int bottom; - int left; - int right; - /** - * Quantisation offset. - * - * Must be in the range -1 to +1. A value of zero indicates no quality - * change. A negative value asks for better quality (less quantisation), - * while a positive value asks for worse quality (greater quantisation). - * - * The range is calibrated so that the extreme values indicate the - * largest possible offset - if the rest of the frame is encoded with the - * worst possible quality, an offset of -1 indicates that this region - * should be encoded with the best possible quality anyway. Intermediate - * values are then interpolated in some codec-dependent way. - * - * For example, in 10-bit H.264 the quantisation parameter varies between - * -12 and 51. A typical qoffset value of -1/10 therefore indicates that - * this region should be encoded with a QP around one-tenth of the full - * range better than the rest of the frame. So, if most of the frame - * were to be encoded with a QP of around 30, this region would get a QP - * of around 24 (an offset of approximately -1/10 * (51 - -12) = -6.3). - * An extreme value of -1 would indicate that this region should be - * encoded with the best possible quality regardless of the treatment of - * the rest of the frame - that is, should be encoded at a QP of -12. - */ - AVRational qoffset; -} AVRegionOfInterest; - -/** - * This structure describes decoded (raw) audio or video data. - * - * AVFrame must be allocated using av_frame_alloc(). Note that this only - * allocates the AVFrame itself, the buffers for the data must be managed - * through other means (see below). - * AVFrame must be freed with av_frame_free(). - * - * AVFrame is typically allocated once and then reused multiple times to hold - * different data (e.g. a single AVFrame to hold frames received from a - * decoder). In such a case, av_frame_unref() will free any references held by - * the frame and reset it to its original clean state before it - * is reused again. - * - * The data described by an AVFrame is usually reference counted through the - * AVBuffer API. The underlying buffer references are stored in AVFrame.buf / - * AVFrame.extended_buf. An AVFrame is considered to be reference counted if at - * least one reference is set, i.e. if AVFrame.buf[0] != NULL. In such a case, - * every single data plane must be contained in one of the buffers in - * AVFrame.buf or AVFrame.extended_buf. - * There may be a single buffer for all the data, or one separate buffer for - * each plane, or anything in between. - * - * sizeof(AVFrame) is not a part of the public ABI, so new fields may be added - * to the end with a minor bump. - * - * Fields can be accessed through AVOptions, the name string used, matches the - * C structure field name for fields accessible through AVOptions. The AVClass - * for AVFrame can be obtained from avcodec_get_frame_class() - */ -typedef struct AVFrame { -#define AV_NUM_DATA_POINTERS 8 - /** - * pointer to the picture/channel planes. - * This might be different from the first allocated byte. For video, - * it could even point to the end of the image data. - * - * All pointers in data and extended_data must point into one of the - * AVBufferRef in buf or extended_buf. - * - * Some decoders access areas outside 0,0 - width,height, please - * see avcodec_align_dimensions2(). Some filters and swscale can read - * up to 16 bytes beyond the planes, if these filters are to be used, - * then 16 extra bytes must be allocated. - * - * NOTE: Pointers not needed by the format MUST be set to NULL. - * - * @attention In case of video, the data[] pointers can point to the - * end of image data in order to reverse line order, when used in - * combination with negative values in the linesize[] array. - */ - uint8_t *data[AV_NUM_DATA_POINTERS]; - - /** - * For video, a positive or negative value, which is typically indicating - * the size in bytes of each picture line, but it can also be: - * - the negative byte size of lines for vertical flipping - * (with data[n] pointing to the end of the data - * - a positive or negative multiple of the byte size as for accessing - * even and odd fields of a frame (possibly flipped) - * - * For audio, only linesize[0] may be set. For planar audio, each channel - * plane must be the same size. - * - * For video the linesizes should be multiples of the CPUs alignment - * preference, this is 16 or 32 for modern desktop CPUs. - * Some code requires such alignment other code can be slower without - * correct alignment, for yet other it makes no difference. - * - * @note The linesize may be larger than the size of usable data -- there - * may be extra padding present for performance reasons. - * - * @attention In case of video, line size values can be negative to achieve - * a vertically inverted iteration over image lines. - */ - int linesize[AV_NUM_DATA_POINTERS]; - - /** - * pointers to the data planes/channels. - * - * For video, this should simply point to data[]. - * - * For planar audio, each channel has a separate data pointer, and - * linesize[0] contains the size of each channel buffer. - * For packed audio, there is just one data pointer, and linesize[0] - * contains the total size of the buffer for all channels. - * - * Note: Both data and extended_data should always be set in a valid frame, - * but for planar audio with more channels that can fit in data, - * extended_data must be used in order to access all channels. - */ - uint8_t **extended_data; - - /** - * @name Video dimensions - * Video frames only. The coded dimensions (in pixels) of the video frame, - * i.e. the size of the rectangle that contains some well-defined values. - * - * @note The part of the frame intended for display/presentation is further - * restricted by the @ref cropping "Cropping rectangle". - * @{ - */ - int width, height; - /** - * @} - */ - - /** - * number of audio samples (per channel) described by this frame - */ - int nb_samples; - - /** - * format of the frame, -1 if unknown or unset - * Values correspond to enum AVPixelFormat for video frames, - * enum AVSampleFormat for audio) - */ - int format; - -#if FF_API_FRAME_KEY - /** - * 1 -> keyframe, 0-> not - * - * @deprecated Use AV_FRAME_FLAG_KEY instead - */ - attribute_deprecated - int key_frame; -#endif - - /** - * Picture type of the frame. - */ - enum AVPictureType pict_type; - - /** - * Sample aspect ratio for the video frame, 0/1 if unknown/unspecified. - */ - AVRational sample_aspect_ratio; - - /** - * Presentation timestamp in time_base units (time when frame should be shown to user). - */ - int64_t pts; - - /** - * DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used) - * This is also the Presentation time of this AVFrame calculated from - * only AVPacket.dts values without pts values. - */ - int64_t pkt_dts; - - /** - * Time base for the timestamps in this frame. - * In the future, this field may be set on frames output by decoders or - * filters, but its value will be by default ignored on input to encoders - * or filters. - */ - AVRational time_base; - -#if FF_API_FRAME_PICTURE_NUMBER - /** - * picture number in bitstream order - */ - attribute_deprecated - int coded_picture_number; - /** - * picture number in display order - */ - attribute_deprecated - int display_picture_number; -#endif - - /** - * quality (between 1 (good) and FF_LAMBDA_MAX (bad)) - */ - int quality; - - /** - * Frame owner's private data. - * - * This field may be set by the code that allocates/owns the frame data. - * It is then not touched by any library functions, except: - * - it is copied to other references by av_frame_copy_props() (and hence by - * av_frame_ref()); - * - it is set to NULL when the frame is cleared by av_frame_unref() - * - on the caller's explicit request. E.g. libavcodec encoders/decoders - * will copy this field to/from @ref AVPacket "AVPackets" if the caller sets - * @ref AV_CODEC_FLAG_COPY_OPAQUE. - * - * @see opaque_ref the reference-counted analogue - */ - void *opaque; - - /** - * Number of fields in this frame which should be repeated, i.e. the total - * duration of this frame should be repeat_pict + 2 normal field durations. - * - * For interlaced frames this field may be set to 1, which signals that this - * frame should be presented as 3 fields: beginning with the first field (as - * determined by AV_FRAME_FLAG_TOP_FIELD_FIRST being set or not), followed - * by the second field, and then the first field again. - * - * For progressive frames this field may be set to a multiple of 2, which - * signals that this frame's duration should be (repeat_pict + 2) / 2 - * normal frame durations. - * - * @note This field is computed from MPEG2 repeat_first_field flag and its - * associated flags, H.264 pic_struct from picture timing SEI, and - * their analogues in other codecs. Typically it should only be used when - * higher-layer timing information is not available. - */ - int repeat_pict; - -#if FF_API_INTERLACED_FRAME - /** - * The content of the picture is interlaced. - * - * @deprecated Use AV_FRAME_FLAG_INTERLACED instead - */ - attribute_deprecated - int interlaced_frame; - - /** - * If the content is interlaced, is top field displayed first. - * - * @deprecated Use AV_FRAME_FLAG_TOP_FIELD_FIRST instead - */ - attribute_deprecated - int top_field_first; -#endif - -#if FF_API_PALETTE_HAS_CHANGED - /** - * Tell user application that palette has changed from previous frame. - */ - attribute_deprecated - int palette_has_changed; -#endif - -#if FF_API_REORDERED_OPAQUE - /** - * reordered opaque 64 bits (generally an integer or a double precision float - * PTS but can be anything). - * The user sets AVCodecContext.reordered_opaque to represent the input at - * that time, - * the decoder reorders values as needed and sets AVFrame.reordered_opaque - * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque - * - * @deprecated Use AV_CODEC_FLAG_COPY_OPAQUE instead - */ - attribute_deprecated - int64_t reordered_opaque; -#endif - - /** - * Sample rate of the audio data. - */ - int sample_rate; - -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * Channel layout of the audio data. - * @deprecated use ch_layout instead - */ - attribute_deprecated - uint64_t channel_layout; -#endif - - /** - * AVBuffer references backing the data for this frame. All the pointers in - * data and extended_data must point inside one of the buffers in buf or - * extended_buf. This array must be filled contiguously -- if buf[i] is - * non-NULL then buf[j] must also be non-NULL for all j < i. - * - * There may be at most one AVBuffer per data plane, so for video this array - * always contains all the references. For planar audio with more than - * AV_NUM_DATA_POINTERS channels, there may be more buffers than can fit in - * this array. Then the extra AVBufferRef pointers are stored in the - * extended_buf array. - */ - AVBufferRef *buf[AV_NUM_DATA_POINTERS]; - - /** - * For planar audio which requires more than AV_NUM_DATA_POINTERS - * AVBufferRef pointers, this array will hold all the references which - * cannot fit into AVFrame.buf. - * - * Note that this is different from AVFrame.extended_data, which always - * contains all the pointers. This array only contains the extra pointers, - * which cannot fit into AVFrame.buf. - * - * This array is always allocated using av_malloc() by whoever constructs - * the frame. It is freed in av_frame_unref(). - */ - AVBufferRef **extended_buf; - /** - * Number of elements in extended_buf. - */ - int nb_extended_buf; - - AVFrameSideData **side_data; - int nb_side_data; - -/** - * @defgroup lavu_frame_flags AV_FRAME_FLAGS - * @ingroup lavu_frame - * Flags describing additional frame properties. - * - * @{ - */ - -/** - * The frame data may be corrupted, e.g. due to decoding errors. - */ -#define AV_FRAME_FLAG_CORRUPT (1 << 0) -/** - * A flag to mark frames that are keyframes. - */ -#define AV_FRAME_FLAG_KEY (1 << 1) -/** - * A flag to mark the frames which need to be decoded, but shouldn't be output. - */ -#define AV_FRAME_FLAG_DISCARD (1 << 2) -/** - * A flag to mark frames whose content is interlaced. - */ -#define AV_FRAME_FLAG_INTERLACED (1 << 3) -/** - * A flag to mark frames where the top field is displayed first if the content - * is interlaced. - */ -#define AV_FRAME_FLAG_TOP_FIELD_FIRST (1 << 4) -/** - * @} - */ - - /** - * Frame flags, a combination of @ref lavu_frame_flags - */ - int flags; - - /** - * MPEG vs JPEG YUV range. - * - encoding: Set by user - * - decoding: Set by libavcodec - */ - enum AVColorRange color_range; - - enum AVColorPrimaries color_primaries; - - enum AVColorTransferCharacteristic color_trc; - - /** - * YUV colorspace type. - * - encoding: Set by user - * - decoding: Set by libavcodec - */ - enum AVColorSpace colorspace; - - enum AVChromaLocation chroma_location; - - /** - * frame timestamp estimated using various heuristics, in stream time base - * - encoding: unused - * - decoding: set by libavcodec, read by user. - */ - int64_t best_effort_timestamp; - -#if FF_API_FRAME_PKT - /** - * reordered pos from the last AVPacket that has been input into the decoder - * - encoding: unused - * - decoding: Read by user. - * @deprecated use AV_CODEC_FLAG_COPY_OPAQUE to pass through arbitrary user - * data from packets to frames - */ - attribute_deprecated - int64_t pkt_pos; -#endif - -#if FF_API_PKT_DURATION - /** - * duration of the corresponding packet, expressed in - * AVStream->time_base units, 0 if unknown. - * - encoding: unused - * - decoding: Read by user. - * - * @deprecated use duration instead - */ - attribute_deprecated - int64_t pkt_duration; -#endif - - /** - * metadata. - * - encoding: Set by user. - * - decoding: Set by libavcodec. - */ - AVDictionary *metadata; - - /** - * decode error flags of the frame, set to a combination of - * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there - * were errors during the decoding. - * - encoding: unused - * - decoding: set by libavcodec, read by user. - */ - int decode_error_flags; -#define FF_DECODE_ERROR_INVALID_BITSTREAM 1 -#define FF_DECODE_ERROR_MISSING_REFERENCE 2 -#define FF_DECODE_ERROR_CONCEALMENT_ACTIVE 4 -#define FF_DECODE_ERROR_DECODE_SLICES 8 - -#if FF_API_OLD_CHANNEL_LAYOUT - /** - * number of audio channels, only used for audio. - * - encoding: unused - * - decoding: Read by user. - * @deprecated use ch_layout instead - */ - attribute_deprecated - int channels; -#endif - -#if FF_API_FRAME_PKT - /** - * size of the corresponding packet containing the compressed - * frame. - * It is set to a negative value if unknown. - * - encoding: unused - * - decoding: set by libavcodec, read by user. - * @deprecated use AV_CODEC_FLAG_COPY_OPAQUE to pass through arbitrary user - * data from packets to frames - */ - attribute_deprecated - int pkt_size; -#endif - - /** - * For hwaccel-format frames, this should be a reference to the - * AVHWFramesContext describing the frame. - */ - AVBufferRef *hw_frames_ctx; - - /** - * Frame owner's private data. - * - * This field may be set by the code that allocates/owns the frame data. - * It is then not touched by any library functions, except: - * - a new reference to the underlying buffer is propagated by - * av_frame_copy_props() (and hence by av_frame_ref()); - * - it is unreferenced in av_frame_unref(); - * - on the caller's explicit request. E.g. libavcodec encoders/decoders - * will propagate a new reference to/from @ref AVPacket "AVPackets" if the - * caller sets @ref AV_CODEC_FLAG_COPY_OPAQUE. - * - * @see opaque the plain pointer analogue - */ - AVBufferRef *opaque_ref; - - /** - * @anchor cropping - * @name Cropping - * Video frames only. The number of pixels to discard from the the - * top/bottom/left/right border of the frame to obtain the sub-rectangle of - * the frame intended for presentation. - * @{ - */ - size_t crop_top; - size_t crop_bottom; - size_t crop_left; - size_t crop_right; - /** - * @} - */ - - /** - * AVBufferRef for internal use by a single libav* library. - * Must not be used to transfer data between libraries. - * Has to be NULL when ownership of the frame leaves the respective library. - * - * Code outside the FFmpeg libs should never check or change the contents of the buffer ref. - * - * FFmpeg calls av_buffer_unref() on it when the frame is unreferenced. - * av_frame_copy_props() calls create a new reference with av_buffer_ref() - * for the target frame's private_ref field. - */ - AVBufferRef *private_ref; - - /** - * Channel layout of the audio data. - */ - AVChannelLayout ch_layout; - - /** - * Duration of the frame, in the same units as pts. 0 if unknown. - */ - int64_t duration; -} AVFrame; - - -/** - * Allocate an AVFrame and set its fields to default values. The resulting - * struct must be freed using av_frame_free(). - * - * @return An AVFrame filled with default values or NULL on failure. - * - * @note this only allocates the AVFrame itself, not the data buffers. Those - * must be allocated through other means, e.g. with av_frame_get_buffer() or - * manually. - */ -AVFrame *av_frame_alloc(void); - -/** - * Free the frame and any dynamically allocated objects in it, - * e.g. extended_data. If the frame is reference counted, it will be - * unreferenced first. - * - * @param frame frame to be freed. The pointer will be set to NULL. - */ -void av_frame_free(AVFrame **frame); - -/** - * Set up a new reference to the data described by the source frame. - * - * Copy frame properties from src to dst and create a new reference for each - * AVBufferRef from src. - * - * If src is not reference counted, new buffers are allocated and the data is - * copied. - * - * @warning: dst MUST have been either unreferenced with av_frame_unref(dst), - * or newly allocated with av_frame_alloc() before calling this - * function, or undefined behavior will occur. - * - * @return 0 on success, a negative AVERROR on error - */ -int av_frame_ref(AVFrame *dst, const AVFrame *src); - -/** - * Ensure the destination frame refers to the same data described by the source - * frame, either by creating a new reference for each AVBufferRef from src if - * they differ from those in dst, by allocating new buffers and copying data if - * src is not reference counted, or by unrefencing it if src is empty. - * - * Frame properties on dst will be replaced by those from src. - * - * @return 0 on success, a negative AVERROR on error. On error, dst is - * unreferenced. - */ -int av_frame_replace(AVFrame *dst, const AVFrame *src); - -/** - * Create a new frame that references the same data as src. - * - * This is a shortcut for av_frame_alloc()+av_frame_ref(). - * - * @return newly created AVFrame on success, NULL on error. - */ -AVFrame *av_frame_clone(const AVFrame *src); - -/** - * Unreference all the buffers referenced by frame and reset the frame fields. - */ -void av_frame_unref(AVFrame *frame); - -/** - * Move everything contained in src to dst and reset src. - * - * @warning: dst is not unreferenced, but directly overwritten without reading - * or deallocating its contents. Call av_frame_unref(dst) manually - * before calling this function to ensure that no memory is leaked. - */ -void av_frame_move_ref(AVFrame *dst, AVFrame *src); - -/** - * Allocate new buffer(s) for audio or video data. - * - * The following fields must be set on frame before calling this function: - * - format (pixel format for video, sample format for audio) - * - width and height for video - * - nb_samples and ch_layout for audio - * - * This function will fill AVFrame.data and AVFrame.buf arrays and, if - * necessary, allocate and fill AVFrame.extended_data and AVFrame.extended_buf. - * For planar formats, one buffer will be allocated for each plane. - * - * @warning: if frame already has been allocated, calling this function will - * leak memory. In addition, undefined behavior can occur in certain - * cases. - * - * @param frame frame in which to store the new buffers. - * @param align Required buffer size alignment. If equal to 0, alignment will be - * chosen automatically for the current CPU. It is highly - * recommended to pass 0 here unless you know what you are doing. - * - * @return 0 on success, a negative AVERROR on error. - */ -int av_frame_get_buffer(AVFrame *frame, int align); - -/** - * Check if the frame data is writable. - * - * @return A positive value if the frame data is writable (which is true if and - * only if each of the underlying buffers has only one reference, namely the one - * stored in this frame). Return 0 otherwise. - * - * If 1 is returned the answer is valid until av_buffer_ref() is called on any - * of the underlying AVBufferRefs (e.g. through av_frame_ref() or directly). - * - * @see av_frame_make_writable(), av_buffer_is_writable() - */ -int av_frame_is_writable(AVFrame *frame); - -/** - * Ensure that the frame data is writable, avoiding data copy if possible. - * - * Do nothing if the frame is writable, allocate new buffers and copy the data - * if it is not. Non-refcounted frames behave as non-writable, i.e. a copy - * is always made. - * - * @return 0 on success, a negative AVERROR on error. - * - * @see av_frame_is_writable(), av_buffer_is_writable(), - * av_buffer_make_writable() - */ -int av_frame_make_writable(AVFrame *frame); - -/** - * Copy the frame data from src to dst. - * - * This function does not allocate anything, dst must be already initialized and - * allocated with the same parameters as src. - * - * This function only copies the frame data (i.e. the contents of the data / - * extended data arrays), not any other properties. - * - * @return >= 0 on success, a negative AVERROR on error. - */ -int av_frame_copy(AVFrame *dst, const AVFrame *src); - -/** - * Copy only "metadata" fields from src to dst. - * - * Metadata for the purpose of this function are those fields that do not affect - * the data layout in the buffers. E.g. pts, sample rate (for audio) or sample - * aspect ratio (for video), but not width/height or channel layout. - * Side data is also copied. - */ -int av_frame_copy_props(AVFrame *dst, const AVFrame *src); - -/** - * Get the buffer reference a given data plane is stored in. - * - * @param frame the frame to get the plane's buffer from - * @param plane index of the data plane of interest in frame->extended_data. - * - * @return the buffer reference that contains the plane or NULL if the input - * frame is not valid. - */ -AVBufferRef *av_frame_get_plane_buffer(const AVFrame *frame, int plane); - -/** - * Add a new side data to a frame. - * - * @param frame a frame to which the side data should be added - * @param type type of the added side data - * @param size size of the side data - * - * @return newly added side data on success, NULL on error - */ -AVFrameSideData *av_frame_new_side_data(AVFrame *frame, - enum AVFrameSideDataType type, - size_t size); - -/** - * Add a new side data to a frame from an existing AVBufferRef - * - * @param frame a frame to which the side data should be added - * @param type the type of the added side data - * @param buf an AVBufferRef to add as side data. The ownership of - * the reference is transferred to the frame. - * - * @return newly added side data on success, NULL on error. On failure - * the frame is unchanged and the AVBufferRef remains owned by - * the caller. - */ -AVFrameSideData *av_frame_new_side_data_from_buf(AVFrame *frame, - enum AVFrameSideDataType type, - AVBufferRef *buf); - -/** - * @return a pointer to the side data of a given type on success, NULL if there - * is no side data with such type in this frame. - */ -AVFrameSideData *av_frame_get_side_data(const AVFrame *frame, - enum AVFrameSideDataType type); - -/** - * Remove and free all side data instances of the given type. - */ -void av_frame_remove_side_data(AVFrame *frame, enum AVFrameSideDataType type); - - -/** - * Flags for frame cropping. - */ -enum { - /** - * Apply the maximum possible cropping, even if it requires setting the - * AVFrame.data[] entries to unaligned pointers. Passing unaligned data - * to FFmpeg API is generally not allowed, and causes undefined behavior - * (such as crashes). You can pass unaligned data only to FFmpeg APIs that - * are explicitly documented to accept it. Use this flag only if you - * absolutely know what you are doing. - */ - AV_FRAME_CROP_UNALIGNED = 1 << 0, -}; - -/** - * Crop the given video AVFrame according to its crop_left/crop_top/crop_right/ - * crop_bottom fields. If cropping is successful, the function will adjust the - * data pointers and the width/height fields, and set the crop fields to 0. - * - * In all cases, the cropping boundaries will be rounded to the inherent - * alignment of the pixel format. In some cases, such as for opaque hwaccel - * formats, the left/top cropping is ignored. The crop fields are set to 0 even - * if the cropping was rounded or ignored. - * - * @param frame the frame which should be cropped - * @param flags Some combination of AV_FRAME_CROP_* flags, or 0. - * - * @return >= 0 on success, a negative AVERROR on error. If the cropping fields - * were invalid, AVERROR(ERANGE) is returned, and nothing is changed. - */ -int av_frame_apply_cropping(AVFrame *frame, int flags); - -/** - * @return a string identifying the side data type - */ -const char *av_frame_side_data_name(enum AVFrameSideDataType type); - -/** - * @} - */ - -#endif /* AVUTIL_FRAME_H */ diff --git a/gostream/ffmpeg/include/libavutil/hash.h b/gostream/ffmpeg/include/libavutil/hash.h deleted file mode 100644 index 94151ded7cb..00000000000 --- a/gostream/ffmpeg/include/libavutil/hash.h +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copyright (C) 2013 Reimar Döffinger - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_hash_generic - * Generic hashing API - */ - -#ifndef AVUTIL_HASH_H -#define AVUTIL_HASH_H - -#include -#include - -/** - * @defgroup lavu_hash Hash Functions - * @ingroup lavu_crypto - * Hash functions useful in multimedia. - * - * Hash functions are widely used in multimedia, from error checking and - * concealment to internal regression testing. libavutil has efficient - * implementations of a variety of hash functions that may be useful for - * FFmpeg and other multimedia applications. - * - * @{ - * - * @defgroup lavu_hash_generic Generic Hashing API - * An abstraction layer for all hash functions supported by libavutil. - * - * If your application needs to support a wide range of different hash - * functions, then the Generic Hashing API is for you. It provides a generic, - * reusable API for @ref lavu_hash "all hash functions" implemented in libavutil. - * If you just need to use one particular hash function, use the @ref lavu_hash - * "individual hash" directly. - * - * @section Sample Code - * - * A basic template for using the Generic Hashing API follows: - * - * @code - * struct AVHashContext *ctx = NULL; - * const char *hash_name = NULL; - * uint8_t *output_buf = NULL; - * - * // Select from a string returned by av_hash_names() - * hash_name = ...; - * - * // Allocate a hash context - * ret = av_hash_alloc(&ctx, hash_name); - * if (ret < 0) - * return ret; - * - * // Initialize the hash context - * av_hash_init(ctx); - * - * // Update the hash context with data - * while (data_left) { - * av_hash_update(ctx, data, size); - * } - * - * // Now we have no more data, so it is time to finalize the hash and get the - * // output. But we need to first allocate an output buffer. Note that you can - * // use any memory allocation function, including malloc(), not just - * // av_malloc(). - * output_buf = av_malloc(av_hash_get_size(ctx)); - * if (!output_buf) - * return AVERROR(ENOMEM); - * - * // Finalize the hash context. - * // You can use any of the av_hash_final*() functions provided, for other - * // output formats. If you do so, be sure to adjust the memory allocation - * // above. See the function documentation below for the exact amount of extra - * // memory needed. - * av_hash_final(ctx, output_buffer); - * - * // Free the context - * av_hash_freep(&ctx); - * @endcode - * - * @section Hash Function-Specific Information - * If the CRC32 hash is selected, the #AV_CRC_32_IEEE polynomial will be - * used. - * - * If the Murmur3 hash is selected, the default seed will be used. See @ref - * lavu_murmur3_seedinfo "Murmur3" for more information. - * - * @{ - */ - -/** - * @example ffhash.c - * This example is a simple command line application that takes one or more - * arguments. It demonstrates a typical use of the hashing API with allocation, - * initialization, updating, and finalizing. - */ - -struct AVHashContext; - -/** - * Allocate a hash context for the algorithm specified by name. - * - * @return >= 0 for success, a negative error code for failure - * - * @note The context is not initialized after a call to this function; you must - * call av_hash_init() to do so. - */ -int av_hash_alloc(struct AVHashContext **ctx, const char *name); - -/** - * Get the names of available hash algorithms. - * - * This function can be used to enumerate the algorithms. - * - * @param[in] i Index of the hash algorithm, starting from 0 - * @return Pointer to a static string or `NULL` if `i` is out of range - */ -const char *av_hash_names(int i); - -/** - * Get the name of the algorithm corresponding to the given hash context. - */ -const char *av_hash_get_name(const struct AVHashContext *ctx); - -/** - * Maximum value that av_hash_get_size() will currently return. - * - * You can use this if you absolutely want or need to use static allocation for - * the output buffer and are fine with not supporting hashes newly added to - * libavutil without recompilation. - * - * @warning - * Adding new hashes with larger sizes, and increasing the macro while doing - * so, will not be considered an ABI change. To prevent your code from - * overflowing a buffer, either dynamically allocate the output buffer with - * av_hash_get_size(), or limit your use of the Hashing API to hashes that are - * already in FFmpeg during the time of compilation. - */ -#define AV_HASH_MAX_SIZE 64 - -/** - * Get the size of the resulting hash value in bytes. - * - * The maximum value this function will currently return is available as macro - * #AV_HASH_MAX_SIZE. - * - * @param[in] ctx Hash context - * @return Size of the hash value in bytes - */ -int av_hash_get_size(const struct AVHashContext *ctx); - -/** - * Initialize or reset a hash context. - * - * @param[in,out] ctx Hash context - */ -void av_hash_init(struct AVHashContext *ctx); - -/** - * Update a hash context with additional data. - * - * @param[in,out] ctx Hash context - * @param[in] src Data to be added to the hash context - * @param[in] len Size of the additional data - */ -void av_hash_update(struct AVHashContext *ctx, const uint8_t *src, size_t len); - -/** - * Finalize a hash context and compute the actual hash value. - * - * The minimum size of `dst` buffer is given by av_hash_get_size() or - * #AV_HASH_MAX_SIZE. The use of the latter macro is discouraged. - * - * It is not safe to update or finalize a hash context again, if it has already - * been finalized. - * - * @param[in,out] ctx Hash context - * @param[out] dst Where the final hash value will be stored - * - * @see av_hash_final_bin() provides an alternative API - */ -void av_hash_final(struct AVHashContext *ctx, uint8_t *dst); - -/** - * Finalize a hash context and store the actual hash value in a buffer. - * - * It is not safe to update or finalize a hash context again, if it has already - * been finalized. - * - * If `size` is smaller than the hash size (given by av_hash_get_size()), the - * hash is truncated; if size is larger, the buffer is padded with 0. - * - * @param[in,out] ctx Hash context - * @param[out] dst Where the final hash value will be stored - * @param[in] size Number of bytes to write to `dst` - */ -void av_hash_final_bin(struct AVHashContext *ctx, uint8_t *dst, int size); - -/** - * Finalize a hash context and store the hexadecimal representation of the - * actual hash value as a string. - * - * It is not safe to update or finalize a hash context again, if it has already - * been finalized. - * - * The string is always 0-terminated. - * - * If `size` is smaller than `2 * hash_size + 1`, where `hash_size` is the - * value returned by av_hash_get_size(), the string will be truncated. - * - * @param[in,out] ctx Hash context - * @param[out] dst Where the string will be stored - * @param[in] size Maximum number of bytes to write to `dst` - */ -void av_hash_final_hex(struct AVHashContext *ctx, uint8_t *dst, int size); - -/** - * Finalize a hash context and store the Base64 representation of the - * actual hash value as a string. - * - * It is not safe to update or finalize a hash context again, if it has already - * been finalized. - * - * The string is always 0-terminated. - * - * If `size` is smaller than AV_BASE64_SIZE(hash_size), where `hash_size` is - * the value returned by av_hash_get_size(), the string will be truncated. - * - * @param[in,out] ctx Hash context - * @param[out] dst Where the final hash value will be stored - * @param[in] size Maximum number of bytes to write to `dst` - */ -void av_hash_final_b64(struct AVHashContext *ctx, uint8_t *dst, int size); - -/** - * Free hash context and set hash context pointer to `NULL`. - * - * @param[in,out] ctx Pointer to hash context - */ -void av_hash_freep(struct AVHashContext **ctx); - -/** - * @} - * @} - */ - -#endif /* AVUTIL_HASH_H */ diff --git a/gostream/ffmpeg/include/libavutil/hdr_dynamic_metadata.h b/gostream/ffmpeg/include/libavutil/hdr_dynamic_metadata.h deleted file mode 100644 index 09e9d8bbccf..00000000000 --- a/gostream/ffmpeg/include/libavutil/hdr_dynamic_metadata.h +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright (c) 2018 Mohammad Izadi - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HDR_DYNAMIC_METADATA_H -#define AVUTIL_HDR_DYNAMIC_METADATA_H - -#include "frame.h" -#include "rational.h" - -/** - * Option for overlapping elliptical pixel selectors in an image. - */ -enum AVHDRPlusOverlapProcessOption { - AV_HDR_PLUS_OVERLAP_PROCESS_WEIGHTED_AVERAGING = 0, - AV_HDR_PLUS_OVERLAP_PROCESS_LAYERING = 1, -}; - -/** - * Represents the percentile at a specific percentage in - * a distribution. - */ -typedef struct AVHDRPlusPercentile { - /** - * The percentage value corresponding to a specific percentile linearized - * RGB value in the processing window in the scene. The value shall be in - * the range of 0 to100, inclusive. - */ - uint8_t percentage; - - /** - * The linearized maxRGB value at a specific percentile in the processing - * window in the scene. The value shall be in the range of 0 to 1, inclusive - * and in multiples of 0.00001. - */ - AVRational percentile; -} AVHDRPlusPercentile; - -/** - * Color transform parameters at a processing window in a dynamic metadata for - * SMPTE 2094-40. - */ -typedef struct AVHDRPlusColorTransformParams { - /** - * The relative x coordinate of the top left pixel of the processing - * window. The value shall be in the range of 0 and 1, inclusive and - * in multiples of 1/(width of Picture - 1). The value 1 corresponds - * to the absolute coordinate of width of Picture - 1. The value for - * first processing window shall be 0. - */ - AVRational window_upper_left_corner_x; - - /** - * The relative y coordinate of the top left pixel of the processing - * window. The value shall be in the range of 0 and 1, inclusive and - * in multiples of 1/(height of Picture - 1). The value 1 corresponds - * to the absolute coordinate of height of Picture - 1. The value for - * first processing window shall be 0. - */ - AVRational window_upper_left_corner_y; - - /** - * The relative x coordinate of the bottom right pixel of the processing - * window. The value shall be in the range of 0 and 1, inclusive and - * in multiples of 1/(width of Picture - 1). The value 1 corresponds - * to the absolute coordinate of width of Picture - 1. The value for - * first processing window shall be 1. - */ - AVRational window_lower_right_corner_x; - - /** - * The relative y coordinate of the bottom right pixel of the processing - * window. The value shall be in the range of 0 and 1, inclusive and - * in multiples of 1/(height of Picture - 1). The value 1 corresponds - * to the absolute coordinate of height of Picture - 1. The value for - * first processing window shall be 1. - */ - AVRational window_lower_right_corner_y; - - /** - * The x coordinate of the center position of the concentric internal and - * external ellipses of the elliptical pixel selector in the processing - * window. The value shall be in the range of 0 to (width of Picture - 1), - * inclusive and in multiples of 1 pixel. - */ - uint16_t center_of_ellipse_x; - - /** - * The y coordinate of the center position of the concentric internal and - * external ellipses of the elliptical pixel selector in the processing - * window. The value shall be in the range of 0 to (height of Picture - 1), - * inclusive and in multiples of 1 pixel. - */ - uint16_t center_of_ellipse_y; - - /** - * The clockwise rotation angle in degree of arc with respect to the - * positive direction of the x-axis of the concentric internal and external - * ellipses of the elliptical pixel selector in the processing window. The - * value shall be in the range of 0 to 180, inclusive and in multiples of 1. - */ - uint8_t rotation_angle; - - /** - * The semi-major axis value of the internal ellipse of the elliptical pixel - * selector in amount of pixels in the processing window. The value shall be - * in the range of 1 to 65535, inclusive and in multiples of 1 pixel. - */ - uint16_t semimajor_axis_internal_ellipse; - - /** - * The semi-major axis value of the external ellipse of the elliptical pixel - * selector in amount of pixels in the processing window. The value - * shall not be less than semimajor_axis_internal_ellipse of the current - * processing window. The value shall be in the range of 1 to 65535, - * inclusive and in multiples of 1 pixel. - */ - uint16_t semimajor_axis_external_ellipse; - - /** - * The semi-minor axis value of the external ellipse of the elliptical pixel - * selector in amount of pixels in the processing window. The value shall be - * in the range of 1 to 65535, inclusive and in multiples of 1 pixel. - */ - uint16_t semiminor_axis_external_ellipse; - - /** - * Overlap process option indicates one of the two methods of combining - * rendered pixels in the processing window in an image with at least one - * elliptical pixel selector. For overlapping elliptical pixel selectors - * in an image, overlap_process_option shall have the same value. - */ - enum AVHDRPlusOverlapProcessOption overlap_process_option; - - /** - * The maximum of the color components of linearized RGB values in the - * processing window in the scene. The values should be in the range of 0 to - * 1, inclusive and in multiples of 0.00001. maxscl[ 0 ], maxscl[ 1 ], and - * maxscl[ 2 ] are corresponding to R, G, B color components respectively. - */ - AVRational maxscl[3]; - - /** - * The average of linearized maxRGB values in the processing window in the - * scene. The value should be in the range of 0 to 1, inclusive and in - * multiples of 0.00001. - */ - AVRational average_maxrgb; - - /** - * The number of linearized maxRGB values at given percentiles in the - * processing window in the scene. The maximum value shall be 15. - */ - uint8_t num_distribution_maxrgb_percentiles; - - /** - * The linearized maxRGB values at given percentiles in the - * processing window in the scene. - */ - AVHDRPlusPercentile distribution_maxrgb[15]; - - /** - * The fraction of selected pixels in the image that contains the brightest - * pixel in the scene. The value shall be in the range of 0 to 1, inclusive - * and in multiples of 0.001. - */ - AVRational fraction_bright_pixels; - - /** - * This flag indicates that the metadata for the tone mapping function in - * the processing window is present (for value of 1). - */ - uint8_t tone_mapping_flag; - - /** - * The x coordinate of the separation point between the linear part and the - * curved part of the tone mapping function. The value shall be in the range - * of 0 to 1, excluding 0 and in multiples of 1/4095. - */ - AVRational knee_point_x; - - /** - * The y coordinate of the separation point between the linear part and the - * curved part of the tone mapping function. The value shall be in the range - * of 0 to 1, excluding 0 and in multiples of 1/4095. - */ - AVRational knee_point_y; - - /** - * The number of the intermediate anchor parameters of the tone mapping - * function in the processing window. The maximum value shall be 15. - */ - uint8_t num_bezier_curve_anchors; - - /** - * The intermediate anchor parameters of the tone mapping function in the - * processing window in the scene. The values should be in the range of 0 - * to 1, inclusive and in multiples of 1/1023. - */ - AVRational bezier_curve_anchors[15]; - - /** - * This flag shall be equal to 0 in bitstreams conforming to this version of - * this Specification. Other values are reserved for future use. - */ - uint8_t color_saturation_mapping_flag; - - /** - * The color saturation gain in the processing window in the scene. The - * value shall be in the range of 0 to 63/8, inclusive and in multiples of - * 1/8. The default value shall be 1. - */ - AVRational color_saturation_weight; -} AVHDRPlusColorTransformParams; - -/** - * This struct represents dynamic metadata for color volume transform - - * application 4 of SMPTE 2094-40:2016 standard. - * - * To be used as payload of a AVFrameSideData or AVPacketSideData with the - * appropriate type. - * - * @note The struct should be allocated with - * av_dynamic_hdr_plus_alloc() and its size is not a part of - * the public ABI. - */ -typedef struct AVDynamicHDRPlus { - /** - * Country code by Rec. ITU-T T.35 Annex A. The value shall be 0xB5. - */ - uint8_t itu_t_t35_country_code; - - /** - * Application version in the application defining document in ST-2094 - * suite. The value shall be set to 0. - */ - uint8_t application_version; - - /** - * The number of processing windows. The value shall be in the range - * of 1 to 3, inclusive. - */ - uint8_t num_windows; - - /** - * The color transform parameters for every processing window. - */ - AVHDRPlusColorTransformParams params[3]; - - /** - * The nominal maximum display luminance of the targeted system display, - * in units of 0.0001 candelas per square metre. The value shall be in - * the range of 0 to 10000, inclusive. - */ - AVRational targeted_system_display_maximum_luminance; - - /** - * This flag shall be equal to 0 in bit streams conforming to this version - * of this Specification. The value 1 is reserved for future use. - */ - uint8_t targeted_system_display_actual_peak_luminance_flag; - - /** - * The number of rows in the targeted system_display_actual_peak_luminance - * array. The value shall be in the range of 2 to 25, inclusive. - */ - uint8_t num_rows_targeted_system_display_actual_peak_luminance; - - /** - * The number of columns in the - * targeted_system_display_actual_peak_luminance array. The value shall be - * in the range of 2 to 25, inclusive. - */ - uint8_t num_cols_targeted_system_display_actual_peak_luminance; - - /** - * The normalized actual peak luminance of the targeted system display. The - * values should be in the range of 0 to 1, inclusive and in multiples of - * 1/15. - */ - AVRational targeted_system_display_actual_peak_luminance[25][25]; - - /** - * This flag shall be equal to 0 in bitstreams conforming to this version of - * this Specification. The value 1 is reserved for future use. - */ - uint8_t mastering_display_actual_peak_luminance_flag; - - /** - * The number of rows in the mastering_display_actual_peak_luminance array. - * The value shall be in the range of 2 to 25, inclusive. - */ - uint8_t num_rows_mastering_display_actual_peak_luminance; - - /** - * The number of columns in the mastering_display_actual_peak_luminance - * array. The value shall be in the range of 2 to 25, inclusive. - */ - uint8_t num_cols_mastering_display_actual_peak_luminance; - - /** - * The normalized actual peak luminance of the mastering display used for - * mastering the image essence. The values should be in the range of 0 to 1, - * inclusive and in multiples of 1/15. - */ - AVRational mastering_display_actual_peak_luminance[25][25]; -} AVDynamicHDRPlus; - -/** - * Allocate an AVDynamicHDRPlus structure and set its fields to - * default values. The resulting struct can be freed using av_freep(). - * - * @return An AVDynamicHDRPlus filled with default values or NULL - * on failure. - */ -AVDynamicHDRPlus *av_dynamic_hdr_plus_alloc(size_t *size); - -/** - * Allocate a complete AVDynamicHDRPlus and add it to the frame. - * @param frame The frame which side data is added to. - * - * @return The AVDynamicHDRPlus structure to be filled by caller or NULL - * on failure. - */ -AVDynamicHDRPlus *av_dynamic_hdr_plus_create_side_data(AVFrame *frame); - -/** - * Parse the user data registered ITU-T T.35 to AVbuffer (AVDynamicHDRPlus). - * The T.35 buffer must begin with the application mode, skipping the - * country code, terminal provider codes, and application identifier. - * @param s A pointer containing the decoded AVDynamicHDRPlus structure. - * @param data The byte array containing the raw ITU-T T.35 data. - * @param size Size of the data array in bytes. - * - * @return >= 0 on success. Otherwise, returns the appropriate AVERROR. - */ -int av_dynamic_hdr_plus_from_t35(AVDynamicHDRPlus *s, const uint8_t *data, - size_t size); - -#define AV_HDR_PLUS_MAX_PAYLOAD_SIZE 907 - -/** - * Serialize dynamic HDR10+ metadata to a user data registered ITU-T T.35 buffer, - * excluding the first 48 bytes of the header, and beginning with the application mode. - * @param s A pointer containing the decoded AVDynamicHDRPlus structure. - * @param data[in,out] A pointer to pointer to a byte buffer to be filled with the - * serialized metadata. - * If *data is NULL, a buffer be will be allocated and a pointer to - * it stored in its place. The caller assumes ownership of the buffer. - * May be NULL, in which case the function will only store the - * required buffer size in *size. - * @param size[in,out] A pointer to a size to be set to the returned buffer's size. - * If *data is not NULL, *size must contain the size of the input - * buffer. May be NULL only if *data is NULL. - * - * @return >= 0 on success. Otherwise, returns the appropriate AVERROR. - */ -int av_dynamic_hdr_plus_to_t35(const AVDynamicHDRPlus *s, uint8_t **data, size_t *size); - -#endif /* AVUTIL_HDR_DYNAMIC_METADATA_H */ diff --git a/gostream/ffmpeg/include/libavutil/hdr_dynamic_vivid_metadata.h b/gostream/ffmpeg/include/libavutil/hdr_dynamic_vivid_metadata.h deleted file mode 100644 index 4524a815578..00000000000 --- a/gostream/ffmpeg/include/libavutil/hdr_dynamic_vivid_metadata.h +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (c) 2021 Limin Wang - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HDR_DYNAMIC_VIVID_METADATA_H -#define AVUTIL_HDR_DYNAMIC_VIVID_METADATA_H - -#include "frame.h" -#include "rational.h" - -/** - * HDR Vivid three spline params. - */ -typedef struct AVHDRVivid3SplineParams { - /** - * The mode of three Spline. the value shall be in the range - * of 0 to 3, inclusive. - */ - int th_mode; - - /** - * three_Spline_TH_enable_MB is in the range of 0.0 to 1.0, inclusive - * and in multiples of 1.0/255. - * - */ - AVRational th_enable_mb; - - /** - * 3Spline_TH_enable of three Spline. - * The value shall be in the range of 0.0 to 1.0, inclusive. - * and in multiples of 1.0/4095. - */ - AVRational th_enable; - - /** - * 3Spline_TH_Delta1 of three Spline. - * The value shall be in the range of 0.0 to 0.25, inclusive, - * and in multiples of 0.25/1023. - */ - AVRational th_delta1; - - /** - * 3Spline_TH_Delta2 of three Spline. - * The value shall be in the range of 0.0 to 0.25, inclusive, - * and in multiples of 0.25/1023. - */ - AVRational th_delta2; - - /** - * 3Spline_enable_Strength of three Spline. - * The value shall be in the range of 0.0 to 1.0, inclusive, - * and in multiples of 1.0/255. - */ - AVRational enable_strength; -} AVHDRVivid3SplineParams; - -/** - * Color tone mapping parameters at a processing window in a dynamic metadata for - * CUVA 005.1:2021. - */ -typedef struct AVHDRVividColorToneMappingParams { - /** - * The nominal maximum display luminance of the targeted system display, - * in multiples of 1.0/4095 candelas per square metre. The value shall be in - * the range of 0.0 to 1.0, inclusive. - */ - AVRational targeted_system_display_maximum_luminance; - - /** - * This flag indicates that transfer the base paramter(for value of 1) - */ - int base_enable_flag; - - /** - * base_param_m_p in the base parameter, - * in multiples of 1.0/16383. The value shall be in - * the range of 0.0 to 1.0, inclusive. - */ - AVRational base_param_m_p; - - /** - * base_param_m_m in the base parameter, - * in multiples of 1.0/10. The value shall be in - * the range of 0.0 to 6.3, inclusive. - */ - AVRational base_param_m_m; - - /** - * base_param_m_a in the base parameter, - * in multiples of 1.0/1023. The value shall be in - * the range of 0.0 to 1.0 inclusive. - */ - AVRational base_param_m_a; - - /** - * base_param_m_b in the base parameter, - * in multiples of 1/1023. The value shall be in - * the range of 0.0 to 1.0, inclusive. - */ - AVRational base_param_m_b; - - /** - * base_param_m_n in the base parameter, - * in multiples of 1.0/10. The value shall be in - * the range of 0.0 to 6.3, inclusive. - */ - AVRational base_param_m_n; - - /** - * indicates k1_0 in the base parameter, - * base_param_k1 <= 1: k1_0 = base_param_k1 - * base_param_k1 > 1: reserved - */ - int base_param_k1; - - /** - * indicates k2_0 in the base parameter, - * base_param_k2 <= 1: k2_0 = base_param_k2 - * base_param_k2 > 1: reserved - */ - int base_param_k2; - - /** - * indicates k3_0 in the base parameter, - * base_param_k3 == 1: k3_0 = base_param_k3 - * base_param_k3 == 2: k3_0 = maximum_maxrgb - * base_param_k3 > 2: reserved - */ - int base_param_k3; - - /** - * This flag indicates that delta mode of base paramter(for value of 1) - */ - int base_param_Delta_enable_mode; - - /** - * base_param_Delta in the base parameter, - * in multiples of 1.0/127. The value shall be in - * the range of 0.0 to 1.0, inclusive. - */ - AVRational base_param_Delta; - - /** - * indicates 3Spline_enable_flag in the base parameter, - * This flag indicates that transfer three Spline of base paramter(for value of 1) - */ - int three_Spline_enable_flag; - - /** - * The number of three Spline. The value shall be in the range - * of 1 to 2, inclusive. - */ - int three_Spline_num; - -#if FF_API_HDR_VIVID_THREE_SPLINE - /** - * The mode of three Spline. the value shall be in the range - * of 0 to 3, inclusive. - * @deprecated Use three_spline instead - */ - attribute_deprecated - int three_Spline_TH_mode; - - /** - * three_Spline_TH_enable_MB is in the range of 0.0 to 1.0, inclusive - * and in multiples of 1.0/255. - * @deprecated Use three_spline instead - */ - attribute_deprecated - AVRational three_Spline_TH_enable_MB; - - /** - * 3Spline_TH_enable of three Spline. - * The value shall be in the range of 0.0 to 1.0, inclusive. - * and in multiples of 1.0/4095. - * @deprecated Use three_spline instead - */ - attribute_deprecated - AVRational three_Spline_TH_enable; - - /** - * 3Spline_TH_Delta1 of three Spline. - * The value shall be in the range of 0.0 to 0.25, inclusive, - * and in multiples of 0.25/1023. - * @deprecated Use three_spline instead - */ - attribute_deprecated - AVRational three_Spline_TH_Delta1; - - /** - * 3Spline_TH_Delta2 of three Spline. - * The value shall be in the range of 0.0 to 0.25, inclusive, - * and in multiples of 0.25/1023. - * @deprecated Use three_spline instead - */ - attribute_deprecated - AVRational three_Spline_TH_Delta2; - - /** - * 3Spline_enable_Strength of three Spline. - * The value shall be in the range of 0.0 to 1.0, inclusive, - * and in multiples of 1.0/255. - * @deprecated Use three_spline instead - */ - attribute_deprecated - AVRational three_Spline_enable_Strength; -#endif - - AVHDRVivid3SplineParams three_spline[2]; -} AVHDRVividColorToneMappingParams; - - -/** - * Color transform parameters at a processing window in a dynamic metadata for - * CUVA 005.1:2021. - */ -typedef struct AVHDRVividColorTransformParams { - /** - * Indicates the minimum brightness of the displayed content. - * The values should be in the range of 0.0 to 1.0, - * inclusive and in multiples of 1/4095. - */ - AVRational minimum_maxrgb; - - /** - * Indicates the average brightness of the displayed content. - * The values should be in the range of 0.0 to 1.0, - * inclusive and in multiples of 1/4095. - */ - AVRational average_maxrgb; - - /** - * Indicates the variance brightness of the displayed content. - * The values should be in the range of 0.0 to 1.0, - * inclusive and in multiples of 1/4095. - */ - AVRational variance_maxrgb; - - /** - * Indicates the maximum brightness of the displayed content. - * The values should be in the range of 0.0 to 1.0, inclusive - * and in multiples of 1/4095. - */ - AVRational maximum_maxrgb; - - /** - * This flag indicates that the metadata for the tone mapping function in - * the processing window is present (for value of 1). - */ - int tone_mapping_mode_flag; - - /** - * The number of tone mapping param. The value shall be in the range - * of 1 to 2, inclusive. - */ - int tone_mapping_param_num; - - /** - * The color tone mapping parameters. - */ - AVHDRVividColorToneMappingParams tm_params[2]; - - /** - * This flag indicates that the metadata for the color saturation mapping in - * the processing window is present (for value of 1). - */ - int color_saturation_mapping_flag; - - /** - * The number of color saturation param. The value shall be in the range - * of 0 to 7, inclusive. - */ - int color_saturation_num; - - /** - * Indicates the color correction strength parameter. - * The values should be in the range of 0.0 to 2.0, inclusive - * and in multiples of 1/128. - */ - AVRational color_saturation_gain[8]; -} AVHDRVividColorTransformParams; - -/** - * This struct represents dynamic metadata for color volume transform - - * CUVA 005.1:2021 standard - * - * To be used as payload of a AVFrameSideData or AVPacketSideData with the - * appropriate type. - * - * @note The struct should be allocated with - * av_dynamic_hdr_vivid_alloc() and its size is not a part of - * the public ABI. - */ -typedef struct AVDynamicHDRVivid { - /** - * The system start code. The value shall be set to 0x01. - */ - uint8_t system_start_code; - - /** - * The number of processing windows. The value shall be set to 0x01 - * if the system_start_code is 0x01. - */ - uint8_t num_windows; - - /** - * The color transform parameters for every processing window. - */ - AVHDRVividColorTransformParams params[3]; -} AVDynamicHDRVivid; - -/** - * Allocate an AVDynamicHDRVivid structure and set its fields to - * default values. The resulting struct can be freed using av_freep(). - * - * @return An AVDynamicHDRVivid filled with default values or NULL - * on failure. - */ -AVDynamicHDRVivid *av_dynamic_hdr_vivid_alloc(size_t *size); - -/** - * Allocate a complete AVDynamicHDRVivid and add it to the frame. - * @param frame The frame which side data is added to. - * - * @return The AVDynamicHDRVivid structure to be filled by caller or NULL - * on failure. - */ -AVDynamicHDRVivid *av_dynamic_hdr_vivid_create_side_data(AVFrame *frame); - -#endif /* AVUTIL_HDR_DYNAMIC_VIVID_METADATA_H */ diff --git a/gostream/ffmpeg/include/libavutil/hmac.h b/gostream/ffmpeg/include/libavutil/hmac.h deleted file mode 100644 index ca4da6a689d..00000000000 --- a/gostream/ffmpeg/include/libavutil/hmac.h +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2012 Martin Storsjo - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HMAC_H -#define AVUTIL_HMAC_H - -#include - -/** - * @defgroup lavu_hmac HMAC - * @ingroup lavu_crypto - * @{ - */ - -enum AVHMACType { - AV_HMAC_MD5, - AV_HMAC_SHA1, - AV_HMAC_SHA224, - AV_HMAC_SHA256, - AV_HMAC_SHA384, - AV_HMAC_SHA512, -}; - -typedef struct AVHMAC AVHMAC; - -/** - * Allocate an AVHMAC context. - * @param type The hash function used for the HMAC. - */ -AVHMAC *av_hmac_alloc(enum AVHMACType type); - -/** - * Free an AVHMAC context. - * @param ctx The context to free, may be NULL - */ -void av_hmac_free(AVHMAC *ctx); - -/** - * Initialize an AVHMAC context with an authentication key. - * @param ctx The HMAC context - * @param key The authentication key - * @param keylen The length of the key, in bytes - */ -void av_hmac_init(AVHMAC *ctx, const uint8_t *key, unsigned int keylen); - -/** - * Hash data with the HMAC. - * @param ctx The HMAC context - * @param data The data to hash - * @param len The length of the data, in bytes - */ -void av_hmac_update(AVHMAC *ctx, const uint8_t *data, unsigned int len); - -/** - * Finish hashing and output the HMAC digest. - * @param ctx The HMAC context - * @param out The output buffer to write the digest into - * @param outlen The length of the out buffer, in bytes - * @return The number of bytes written to out, or a negative error code. - */ -int av_hmac_final(AVHMAC *ctx, uint8_t *out, unsigned int outlen); - -/** - * Hash an array of data with a key. - * @param ctx The HMAC context - * @param data The data to hash - * @param len The length of the data, in bytes - * @param key The authentication key - * @param keylen The length of the key, in bytes - * @param out The output buffer to write the digest into - * @param outlen The length of the out buffer, in bytes - * @return The number of bytes written to out, or a negative error code. - */ -int av_hmac_calc(AVHMAC *ctx, const uint8_t *data, unsigned int len, - const uint8_t *key, unsigned int keylen, - uint8_t *out, unsigned int outlen); - -/** - * @} - */ - -#endif /* AVUTIL_HMAC_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext.h b/gostream/ffmpeg/include/libavutil/hwcontext.h deleted file mode 100644 index 7ff08c86085..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext.h +++ /dev/null @@ -1,610 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_H -#define AVUTIL_HWCONTEXT_H - -#include "buffer.h" -#include "frame.h" -#include "log.h" -#include "pixfmt.h" - -enum AVHWDeviceType { - AV_HWDEVICE_TYPE_NONE, - AV_HWDEVICE_TYPE_VDPAU, - AV_HWDEVICE_TYPE_CUDA, - AV_HWDEVICE_TYPE_VAAPI, - AV_HWDEVICE_TYPE_DXVA2, - AV_HWDEVICE_TYPE_QSV, - AV_HWDEVICE_TYPE_VIDEOTOOLBOX, - AV_HWDEVICE_TYPE_D3D11VA, - AV_HWDEVICE_TYPE_DRM, - AV_HWDEVICE_TYPE_OPENCL, - AV_HWDEVICE_TYPE_MEDIACODEC, - AV_HWDEVICE_TYPE_VULKAN, -}; - -typedef struct AVHWDeviceInternal AVHWDeviceInternal; - -/** - * This struct aggregates all the (hardware/vendor-specific) "high-level" state, - * i.e. state that is not tied to a concrete processing configuration. - * E.g., in an API that supports hardware-accelerated encoding and decoding, - * this struct will (if possible) wrap the state that is common to both encoding - * and decoding and from which specific instances of encoders or decoders can be - * derived. - * - * This struct is reference-counted with the AVBuffer mechanism. The - * av_hwdevice_ctx_alloc() constructor yields a reference, whose data field - * points to the actual AVHWDeviceContext. Further objects derived from - * AVHWDeviceContext (such as AVHWFramesContext, describing a frame pool with - * specific properties) will hold an internal reference to it. After all the - * references are released, the AVHWDeviceContext itself will be freed, - * optionally invoking a user-specified callback for uninitializing the hardware - * state. - */ -typedef struct AVHWDeviceContext { - /** - * A class for logging. Set by av_hwdevice_ctx_alloc(). - */ - const AVClass *av_class; - - /** - * Private data used internally by libavutil. Must not be accessed in any - * way by the caller. - */ - AVHWDeviceInternal *internal; - - /** - * This field identifies the underlying API used for hardware access. - * - * This field is set when this struct is allocated and never changed - * afterwards. - */ - enum AVHWDeviceType type; - - /** - * The format-specific data, allocated and freed by libavutil along with - * this context. - * - * Should be cast by the user to the format-specific context defined in the - * corresponding header (hwcontext_*.h) and filled as described in the - * documentation before calling av_hwdevice_ctx_init(). - * - * After calling av_hwdevice_ctx_init() this struct should not be modified - * by the caller. - */ - void *hwctx; - - /** - * This field may be set by the caller before calling av_hwdevice_ctx_init(). - * - * If non-NULL, this callback will be called when the last reference to - * this context is unreferenced, immediately before it is freed. - * - * @note when other objects (e.g an AVHWFramesContext) are derived from this - * struct, this callback will be invoked after all such child objects - * are fully uninitialized and their respective destructors invoked. - */ - void (*free)(struct AVHWDeviceContext *ctx); - - /** - * Arbitrary user data, to be used e.g. by the free() callback. - */ - void *user_opaque; -} AVHWDeviceContext; - -typedef struct AVHWFramesInternal AVHWFramesInternal; - -/** - * This struct describes a set or pool of "hardware" frames (i.e. those with - * data not located in normal system memory). All the frames in the pool are - * assumed to be allocated in the same way and interchangeable. - * - * This struct is reference-counted with the AVBuffer mechanism and tied to a - * given AVHWDeviceContext instance. The av_hwframe_ctx_alloc() constructor - * yields a reference, whose data field points to the actual AVHWFramesContext - * struct. - */ -typedef struct AVHWFramesContext { - /** - * A class for logging. - */ - const AVClass *av_class; - - /** - * Private data used internally by libavutil. Must not be accessed in any - * way by the caller. - */ - AVHWFramesInternal *internal; - - /** - * A reference to the parent AVHWDeviceContext. This reference is owned and - * managed by the enclosing AVHWFramesContext, but the caller may derive - * additional references from it. - */ - AVBufferRef *device_ref; - - /** - * The parent AVHWDeviceContext. This is simply a pointer to - * device_ref->data provided for convenience. - * - * Set by libavutil in av_hwframe_ctx_init(). - */ - AVHWDeviceContext *device_ctx; - - /** - * The format-specific data, allocated and freed automatically along with - * this context. - * - * Should be cast by the user to the format-specific context defined in the - * corresponding header (hwframe_*.h) and filled as described in the - * documentation before calling av_hwframe_ctx_init(). - * - * After any frames using this context are created, the contents of this - * struct should not be modified by the caller. - */ - void *hwctx; - - /** - * This field may be set by the caller before calling av_hwframe_ctx_init(). - * - * If non-NULL, this callback will be called when the last reference to - * this context is unreferenced, immediately before it is freed. - */ - void (*free)(struct AVHWFramesContext *ctx); - - /** - * Arbitrary user data, to be used e.g. by the free() callback. - */ - void *user_opaque; - - /** - * A pool from which the frames are allocated by av_hwframe_get_buffer(). - * This field may be set by the caller before calling av_hwframe_ctx_init(). - * The buffers returned by calling av_buffer_pool_get() on this pool must - * have the properties described in the documentation in the corresponding hw - * type's header (hwcontext_*.h). The pool will be freed strictly before - * this struct's free() callback is invoked. - * - * This field may be NULL, then libavutil will attempt to allocate a pool - * internally. Note that certain device types enforce pools allocated at - * fixed size (frame count), which cannot be extended dynamically. In such a - * case, initial_pool_size must be set appropriately. - */ - AVBufferPool *pool; - - /** - * Initial size of the frame pool. If a device type does not support - * dynamically resizing the pool, then this is also the maximum pool size. - * - * May be set by the caller before calling av_hwframe_ctx_init(). Must be - * set if pool is NULL and the device type does not support dynamic pools. - */ - int initial_pool_size; - - /** - * The pixel format identifying the underlying HW surface type. - * - * Must be a hwaccel format, i.e. the corresponding descriptor must have the - * AV_PIX_FMT_FLAG_HWACCEL flag set. - * - * Must be set by the user before calling av_hwframe_ctx_init(). - */ - enum AVPixelFormat format; - - /** - * The pixel format identifying the actual data layout of the hardware - * frames. - * - * Must be set by the caller before calling av_hwframe_ctx_init(). - * - * @note when the underlying API does not provide the exact data layout, but - * only the colorspace/bit depth, this field should be set to the fully - * planar version of that format (e.g. for 8-bit 420 YUV it should be - * AV_PIX_FMT_YUV420P, not AV_PIX_FMT_NV12 or anything else). - */ - enum AVPixelFormat sw_format; - - /** - * The allocated dimensions of the frames in this pool. - * - * Must be set by the user before calling av_hwframe_ctx_init(). - */ - int width, height; -} AVHWFramesContext; - -/** - * Look up an AVHWDeviceType by name. - * - * @param name String name of the device type (case-insensitive). - * @return The type from enum AVHWDeviceType, or AV_HWDEVICE_TYPE_NONE if - * not found. - */ -enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name); - -/** Get the string name of an AVHWDeviceType. - * - * @param type Type from enum AVHWDeviceType. - * @return Pointer to a static string containing the name, or NULL if the type - * is not valid. - */ -const char *av_hwdevice_get_type_name(enum AVHWDeviceType type); - -/** - * Iterate over supported device types. - * - * @param prev AV_HWDEVICE_TYPE_NONE initially, then the previous type - * returned by this function in subsequent iterations. - * @return The next usable device type from enum AVHWDeviceType, or - * AV_HWDEVICE_TYPE_NONE if there are no more. - */ -enum AVHWDeviceType av_hwdevice_iterate_types(enum AVHWDeviceType prev); - -/** - * Allocate an AVHWDeviceContext for a given hardware type. - * - * @param type the type of the hardware device to allocate. - * @return a reference to the newly created AVHWDeviceContext on success or NULL - * on failure. - */ -AVBufferRef *av_hwdevice_ctx_alloc(enum AVHWDeviceType type); - -/** - * Finalize the device context before use. This function must be called after - * the context is filled with all the required information and before it is - * used in any way. - * - * @param ref a reference to the AVHWDeviceContext - * @return 0 on success, a negative AVERROR code on failure - */ -int av_hwdevice_ctx_init(AVBufferRef *ref); - -/** - * Open a device of the specified type and create an AVHWDeviceContext for it. - * - * This is a convenience function intended to cover the simple cases. Callers - * who need to fine-tune device creation/management should open the device - * manually and then wrap it in an AVHWDeviceContext using - * av_hwdevice_ctx_alloc()/av_hwdevice_ctx_init(). - * - * The returned context is already initialized and ready for use, the caller - * should not call av_hwdevice_ctx_init() on it. The user_opaque/free fields of - * the created AVHWDeviceContext are set by this function and should not be - * touched by the caller. - * - * @param device_ctx On success, a reference to the newly-created device context - * will be written here. The reference is owned by the caller - * and must be released with av_buffer_unref() when no longer - * needed. On failure, NULL will be written to this pointer. - * @param type The type of the device to create. - * @param device A type-specific string identifying the device to open. - * @param opts A dictionary of additional (type-specific) options to use in - * opening the device. The dictionary remains owned by the caller. - * @param flags currently unused - * - * @return 0 on success, a negative AVERROR code on failure. - */ -int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type, - const char *device, AVDictionary *opts, int flags); - -/** - * Create a new device of the specified type from an existing device. - * - * If the source device is a device of the target type or was originally - * derived from such a device (possibly through one or more intermediate - * devices of other types), then this will return a reference to the - * existing device of the same type as is requested. - * - * Otherwise, it will attempt to derive a new device from the given source - * device. If direct derivation to the new type is not implemented, it will - * attempt the same derivation from each ancestor of the source device in - * turn looking for an implemented derivation method. - * - * @param dst_ctx On success, a reference to the newly-created - * AVHWDeviceContext. - * @param type The type of the new device to create. - * @param src_ctx A reference to an existing AVHWDeviceContext which will be - * used to create the new device. - * @param flags Currently unused; should be set to zero. - * @return Zero on success, a negative AVERROR code on failure. - */ -int av_hwdevice_ctx_create_derived(AVBufferRef **dst_ctx, - enum AVHWDeviceType type, - AVBufferRef *src_ctx, int flags); - -/** - * Create a new device of the specified type from an existing device. - * - * This function performs the same action as av_hwdevice_ctx_create_derived, - * however, it is able to set options for the new device to be derived. - * - * @param dst_ctx On success, a reference to the newly-created - * AVHWDeviceContext. - * @param type The type of the new device to create. - * @param src_ctx A reference to an existing AVHWDeviceContext which will be - * used to create the new device. - * @param options Options for the new device to create, same format as in - * av_hwdevice_ctx_create. - * @param flags Currently unused; should be set to zero. - * @return Zero on success, a negative AVERROR code on failure. - */ -int av_hwdevice_ctx_create_derived_opts(AVBufferRef **dst_ctx, - enum AVHWDeviceType type, - AVBufferRef *src_ctx, - AVDictionary *options, int flags); - -/** - * Allocate an AVHWFramesContext tied to a given device context. - * - * @param device_ctx a reference to a AVHWDeviceContext. This function will make - * a new reference for internal use, the one passed to the - * function remains owned by the caller. - * @return a reference to the newly created AVHWFramesContext on success or NULL - * on failure. - */ -AVBufferRef *av_hwframe_ctx_alloc(AVBufferRef *device_ctx); - -/** - * Finalize the context before use. This function must be called after the - * context is filled with all the required information and before it is attached - * to any frames. - * - * @param ref a reference to the AVHWFramesContext - * @return 0 on success, a negative AVERROR code on failure - */ -int av_hwframe_ctx_init(AVBufferRef *ref); - -/** - * Allocate a new frame attached to the given AVHWFramesContext. - * - * @param hwframe_ctx a reference to an AVHWFramesContext - * @param frame an empty (freshly allocated or unreffed) frame to be filled with - * newly allocated buffers. - * @param flags currently unused, should be set to zero - * @return 0 on success, a negative AVERROR code on failure - */ -int av_hwframe_get_buffer(AVBufferRef *hwframe_ctx, AVFrame *frame, int flags); - -/** - * Copy data to or from a hw surface. At least one of dst/src must have an - * AVHWFramesContext attached. - * - * If src has an AVHWFramesContext attached, then the format of dst (if set) - * must use one of the formats returned by av_hwframe_transfer_get_formats(src, - * AV_HWFRAME_TRANSFER_DIRECTION_FROM). - * If dst has an AVHWFramesContext attached, then the format of src must use one - * of the formats returned by av_hwframe_transfer_get_formats(dst, - * AV_HWFRAME_TRANSFER_DIRECTION_TO) - * - * dst may be "clean" (i.e. with data/buf pointers unset), in which case the - * data buffers will be allocated by this function using av_frame_get_buffer(). - * If dst->format is set, then this format will be used, otherwise (when - * dst->format is AV_PIX_FMT_NONE) the first acceptable format will be chosen. - * - * The two frames must have matching allocated dimensions (i.e. equal to - * AVHWFramesContext.width/height), since not all device types support - * transferring a sub-rectangle of the whole surface. The display dimensions - * (i.e. AVFrame.width/height) may be smaller than the allocated dimensions, but - * also have to be equal for both frames. When the display dimensions are - * smaller than the allocated dimensions, the content of the padding in the - * destination frame is unspecified. - * - * @param dst the destination frame. dst is not touched on failure. - * @param src the source frame. - * @param flags currently unused, should be set to zero - * @return 0 on success, a negative AVERROR error code on failure. - */ -int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags); - -enum AVHWFrameTransferDirection { - /** - * Transfer the data from the queried hw frame. - */ - AV_HWFRAME_TRANSFER_DIRECTION_FROM, - - /** - * Transfer the data to the queried hw frame. - */ - AV_HWFRAME_TRANSFER_DIRECTION_TO, -}; - -/** - * Get a list of possible source or target formats usable in - * av_hwframe_transfer_data(). - * - * @param hwframe_ctx the frame context to obtain the information for - * @param dir the direction of the transfer - * @param formats the pointer to the output format list will be written here. - * The list is terminated with AV_PIX_FMT_NONE and must be freed - * by the caller when no longer needed using av_free(). - * If this function returns successfully, the format list will - * have at least one item (not counting the terminator). - * On failure, the contents of this pointer are unspecified. - * @param flags currently unused, should be set to zero - * @return 0 on success, a negative AVERROR code on failure. - */ -int av_hwframe_transfer_get_formats(AVBufferRef *hwframe_ctx, - enum AVHWFrameTransferDirection dir, - enum AVPixelFormat **formats, int flags); - - -/** - * This struct describes the constraints on hardware frames attached to - * a given device with a hardware-specific configuration. This is returned - * by av_hwdevice_get_hwframe_constraints() and must be freed by - * av_hwframe_constraints_free() after use. - */ -typedef struct AVHWFramesConstraints { - /** - * A list of possible values for format in the hw_frames_ctx, - * terminated by AV_PIX_FMT_NONE. This member will always be filled. - */ - enum AVPixelFormat *valid_hw_formats; - - /** - * A list of possible values for sw_format in the hw_frames_ctx, - * terminated by AV_PIX_FMT_NONE. Can be NULL if this information is - * not known. - */ - enum AVPixelFormat *valid_sw_formats; - - /** - * The minimum size of frames in this hw_frames_ctx. - * (Zero if not known.) - */ - int min_width; - int min_height; - - /** - * The maximum size of frames in this hw_frames_ctx. - * (INT_MAX if not known / no limit.) - */ - int max_width; - int max_height; -} AVHWFramesConstraints; - -/** - * Allocate a HW-specific configuration structure for a given HW device. - * After use, the user must free all members as required by the specific - * hardware structure being used, then free the structure itself with - * av_free(). - * - * @param device_ctx a reference to the associated AVHWDeviceContext. - * @return The newly created HW-specific configuration structure on - * success or NULL on failure. - */ -void *av_hwdevice_hwconfig_alloc(AVBufferRef *device_ctx); - -/** - * Get the constraints on HW frames given a device and the HW-specific - * configuration to be used with that device. If no HW-specific - * configuration is provided, returns the maximum possible capabilities - * of the device. - * - * @param ref a reference to the associated AVHWDeviceContext. - * @param hwconfig a filled HW-specific configuration structure, or NULL - * to return the maximum possible capabilities of the device. - * @return AVHWFramesConstraints structure describing the constraints - * on the device, or NULL if not available. - */ -AVHWFramesConstraints *av_hwdevice_get_hwframe_constraints(AVBufferRef *ref, - const void *hwconfig); - -/** - * Free an AVHWFrameConstraints structure. - * - * @param constraints The (filled or unfilled) AVHWFrameConstraints structure. - */ -void av_hwframe_constraints_free(AVHWFramesConstraints **constraints); - - -/** - * Flags to apply to frame mappings. - */ -enum { - /** - * The mapping must be readable. - */ - AV_HWFRAME_MAP_READ = 1 << 0, - /** - * The mapping must be writeable. - */ - AV_HWFRAME_MAP_WRITE = 1 << 1, - /** - * The mapped frame will be overwritten completely in subsequent - * operations, so the current frame data need not be loaded. Any values - * which are not overwritten are unspecified. - */ - AV_HWFRAME_MAP_OVERWRITE = 1 << 2, - /** - * The mapping must be direct. That is, there must not be any copying in - * the map or unmap steps. Note that performance of direct mappings may - * be much lower than normal memory. - */ - AV_HWFRAME_MAP_DIRECT = 1 << 3, -}; - -/** - * Map a hardware frame. - * - * This has a number of different possible effects, depending on the format - * and origin of the src and dst frames. On input, src should be a usable - * frame with valid buffers and dst should be blank (typically as just created - * by av_frame_alloc()). src should have an associated hwframe context, and - * dst may optionally have a format and associated hwframe context. - * - * If src was created by mapping a frame from the hwframe context of dst, - * then this function undoes the mapping - dst is replaced by a reference to - * the frame that src was originally mapped from. - * - * If both src and dst have an associated hwframe context, then this function - * attempts to map the src frame from its hardware context to that of dst and - * then fill dst with appropriate data to be usable there. This will only be - * possible if the hwframe contexts and associated devices are compatible - - * given compatible devices, av_hwframe_ctx_create_derived() can be used to - * create a hwframe context for dst in which mapping should be possible. - * - * If src has a hwframe context but dst does not, then the src frame is - * mapped to normal memory and should thereafter be usable as a normal frame. - * If the format is set on dst, then the mapping will attempt to create dst - * with that format and fail if it is not possible. If format is unset (is - * AV_PIX_FMT_NONE) then dst will be mapped with whatever the most appropriate - * format to use is (probably the sw_format of the src hwframe context). - * - * A return value of AVERROR(ENOSYS) indicates that the mapping is not - * possible with the given arguments and hwframe setup, while other return - * values indicate that it failed somehow. - * - * On failure, the destination frame will be left blank, except for the - * hw_frames_ctx/format fields thay may have been set by the caller - those will - * be preserved as they were. - * - * @param dst Destination frame, to contain the mapping. - * @param src Source frame, to be mapped. - * @param flags Some combination of AV_HWFRAME_MAP_* flags. - * @return Zero on success, negative AVERROR code on failure. - */ -int av_hwframe_map(AVFrame *dst, const AVFrame *src, int flags); - - -/** - * Create and initialise an AVHWFramesContext as a mapping of another existing - * AVHWFramesContext on a different device. - * - * av_hwframe_ctx_init() should not be called after this. - * - * @param derived_frame_ctx On success, a reference to the newly created - * AVHWFramesContext. - * @param format The AVPixelFormat for the derived context. - * @param derived_device_ctx A reference to the device to create the new - * AVHWFramesContext on. - * @param source_frame_ctx A reference to an existing AVHWFramesContext - * which will be mapped to the derived context. - * @param flags Some combination of AV_HWFRAME_MAP_* flags, defining the - * mapping parameters to apply to frames which are allocated - * in the derived device. - * @return Zero on success, negative AVERROR code on failure. - */ -int av_hwframe_ctx_create_derived(AVBufferRef **derived_frame_ctx, - enum AVPixelFormat format, - AVBufferRef *derived_device_ctx, - AVBufferRef *source_frame_ctx, - int flags); - -#endif /* AVUTIL_HWCONTEXT_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_cuda.h b/gostream/ffmpeg/include/libavutil/hwcontext_cuda.h deleted file mode 100644 index cbad434fead..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_cuda.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - - -#ifndef AVUTIL_HWCONTEXT_CUDA_H -#define AVUTIL_HWCONTEXT_CUDA_H - -#ifndef CUDA_VERSION -#include -#endif - -#include "pixfmt.h" - -/** - * @file - * An API-specific header for AV_HWDEVICE_TYPE_CUDA. - * - * This API supports dynamic frame pools. AVHWFramesContext.pool must return - * AVBufferRefs whose data pointer is a CUdeviceptr. - */ - -typedef struct AVCUDADeviceContextInternal AVCUDADeviceContextInternal; - -/** - * This struct is allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVCUDADeviceContext { - CUcontext cuda_ctx; - CUstream stream; - AVCUDADeviceContextInternal *internal; -} AVCUDADeviceContext; - -/** - * AVHWFramesContext.hwctx is currently not used - */ - -/** - * @defgroup hwcontext_cuda Device context creation flags - * - * Flags for av_hwdevice_ctx_create. - * - * @{ - */ - -/** - * Use primary device context instead of creating a new one. - */ -#define AV_CUDA_USE_PRIMARY_CONTEXT (1 << 0) - -/** - * Use current device context instead of creating a new one. - */ -#define AV_CUDA_USE_CURRENT_CONTEXT (1 << 1) - -/** - * @} - */ - -#endif /* AVUTIL_HWCONTEXT_CUDA_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_d3d11va.h b/gostream/ffmpeg/include/libavutil/hwcontext_d3d11va.h deleted file mode 100644 index 77d2d72f1b9..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_d3d11va.h +++ /dev/null @@ -1,178 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_D3D11VA_H -#define AVUTIL_HWCONTEXT_D3D11VA_H - -/** - * @file - * An API-specific header for AV_HWDEVICE_TYPE_D3D11VA. - * - * The default pool implementation will be fixed-size if initial_pool_size is - * set (and allocate elements from an array texture). Otherwise it will allocate - * individual textures. Be aware that decoding requires a single array texture. - * - * Using sw_format==AV_PIX_FMT_YUV420P has special semantics, and maps to - * DXGI_FORMAT_420_OPAQUE. av_hwframe_transfer_data() is not supported for - * this format. Refer to MSDN for details. - * - * av_hwdevice_ctx_create() for this device type supports a key named "debug" - * for the AVDictionary entry. If this is set to any value, the device creation - * code will try to load various supported D3D debugging layers. - */ - -#include -#include - -/** - * This struct is allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVD3D11VADeviceContext { - /** - * Device used for texture creation and access. This can also be used to - * set the libavcodec decoding device. - * - * Must be set by the user. This is the only mandatory field - the other - * device context fields are set from this and are available for convenience. - * - * Deallocating the AVHWDeviceContext will always release this interface, - * and it does not matter whether it was user-allocated. - */ - ID3D11Device *device; - - /** - * If unset, this will be set from the device field on init. - * - * Deallocating the AVHWDeviceContext will always release this interface, - * and it does not matter whether it was user-allocated. - */ - ID3D11DeviceContext *device_context; - - /** - * If unset, this will be set from the device field on init. - * - * Deallocating the AVHWDeviceContext will always release this interface, - * and it does not matter whether it was user-allocated. - */ - ID3D11VideoDevice *video_device; - - /** - * If unset, this will be set from the device_context field on init. - * - * Deallocating the AVHWDeviceContext will always release this interface, - * and it does not matter whether it was user-allocated. - */ - ID3D11VideoContext *video_context; - - /** - * Callbacks for locking. They protect accesses to device_context and - * video_context calls. They also protect access to the internal staging - * texture (for av_hwframe_transfer_data() calls). They do NOT protect - * access to hwcontext or decoder state in general. - * - * If unset on init, the hwcontext implementation will set them to use an - * internal mutex. - * - * The underlying lock must be recursive. lock_ctx is for free use by the - * locking implementation. - */ - void (*lock)(void *lock_ctx); - void (*unlock)(void *lock_ctx); - void *lock_ctx; -} AVD3D11VADeviceContext; - -/** - * D3D11 frame descriptor for pool allocation. - * - * In user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs - * with the data pointer pointing at an object of this type describing the - * planes of the frame. - * - * This has no use outside of custom allocation, and AVFrame AVBufferRef do not - * necessarily point to an instance of this struct. - */ -typedef struct AVD3D11FrameDescriptor { - /** - * The texture in which the frame is located. The reference count is - * managed by the AVBufferRef, and destroying the reference will release - * the interface. - * - * Normally stored in AVFrame.data[0]. - */ - ID3D11Texture2D *texture; - - /** - * The index into the array texture element representing the frame, or 0 - * if the texture is not an array texture. - * - * Normally stored in AVFrame.data[1] (cast from intptr_t). - */ - intptr_t index; -} AVD3D11FrameDescriptor; - -/** - * This struct is allocated as AVHWFramesContext.hwctx - */ -typedef struct AVD3D11VAFramesContext { - /** - * The canonical texture used for pool allocation. If this is set to NULL - * on init, the hwframes implementation will allocate and set an array - * texture if initial_pool_size > 0. - * - * The only situation when the API user should set this is: - * - the user wants to do manual pool allocation (setting - * AVHWFramesContext.pool), instead of letting AVHWFramesContext - * allocate the pool - * - of an array texture - * - and wants it to use it for decoding - * - this has to be done before calling av_hwframe_ctx_init() - * - * Deallocating the AVHWFramesContext will always release this interface, - * and it does not matter whether it was user-allocated. - * - * This is in particular used by the libavcodec D3D11VA hwaccel, which - * requires a single array texture. It will create ID3D11VideoDecoderOutputView - * objects for each array texture element on decoder initialization. - */ - ID3D11Texture2D *texture; - - /** - * D3D11_TEXTURE2D_DESC.BindFlags used for texture creation. The user must - * at least set D3D11_BIND_DECODER if the frames context is to be used for - * video decoding. - * This field is ignored/invalid if a user-allocated texture is provided. - */ - UINT BindFlags; - - /** - * D3D11_TEXTURE2D_DESC.MiscFlags used for texture creation. - * This field is ignored/invalid if a user-allocated texture is provided. - */ - UINT MiscFlags; - - /** - * In case if texture structure member above is not NULL contains the same texture - * pointer for all elements and different indexes into the array texture. - * In case if texture structure member above is NULL, all elements contains - * pointers to separate non-array textures and 0 indexes. - * This field is ignored/invalid if a user-allocated texture is provided. - */ - AVD3D11FrameDescriptor *texture_infos; -} AVD3D11VAFramesContext; - -#endif /* AVUTIL_HWCONTEXT_D3D11VA_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_drm.h b/gostream/ffmpeg/include/libavutil/hwcontext_drm.h deleted file mode 100644 index 42709f215ef..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_drm.h +++ /dev/null @@ -1,169 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_DRM_H -#define AVUTIL_HWCONTEXT_DRM_H - -#include -#include - -/** - * @file - * API-specific header for AV_HWDEVICE_TYPE_DRM. - * - * Internal frame allocation is not currently supported - all frames - * must be allocated by the user. Thus AVHWFramesContext is always - * NULL, though this may change if support for frame allocation is - * added in future. - */ - -enum { - /** - * The maximum number of layers/planes in a DRM frame. - */ - AV_DRM_MAX_PLANES = 4 -}; - -/** - * DRM object descriptor. - * - * Describes a single DRM object, addressing it as a PRIME file - * descriptor. - */ -typedef struct AVDRMObjectDescriptor { - /** - * DRM PRIME fd for the object. - */ - int fd; - /** - * Total size of the object. - * - * (This includes any parts not which do not contain image data.) - */ - size_t size; - /** - * Format modifier applied to the object (DRM_FORMAT_MOD_*). - * - * If the format modifier is unknown then this should be set to - * DRM_FORMAT_MOD_INVALID. - */ - uint64_t format_modifier; -} AVDRMObjectDescriptor; - -/** - * DRM plane descriptor. - * - * Describes a single plane of a layer, which is contained within - * a single object. - */ -typedef struct AVDRMPlaneDescriptor { - /** - * Index of the object containing this plane in the objects - * array of the enclosing frame descriptor. - */ - int object_index; - /** - * Offset within that object of this plane. - */ - ptrdiff_t offset; - /** - * Pitch (linesize) of this plane. - */ - ptrdiff_t pitch; -} AVDRMPlaneDescriptor; - -/** - * DRM layer descriptor. - * - * Describes a single layer within a frame. This has the structure - * defined by its format, and will contain one or more planes. - */ -typedef struct AVDRMLayerDescriptor { - /** - * Format of the layer (DRM_FORMAT_*). - */ - uint32_t format; - /** - * Number of planes in the layer. - * - * This must match the number of planes required by format. - */ - int nb_planes; - /** - * Array of planes in this layer. - */ - AVDRMPlaneDescriptor planes[AV_DRM_MAX_PLANES]; -} AVDRMLayerDescriptor; - -/** - * DRM frame descriptor. - * - * This is used as the data pointer for AV_PIX_FMT_DRM_PRIME frames. - * It is also used by user-allocated frame pools - allocating in - * AVHWFramesContext.pool must return AVBufferRefs which contain - * an object of this type. - * - * The fields of this structure should be set such it can be - * imported directly by EGL using the EGL_EXT_image_dma_buf_import - * and EGL_EXT_image_dma_buf_import_modifiers extensions. - * (Note that the exact layout of a particular format may vary between - * platforms - we only specify that the same platform should be able - * to import it.) - * - * The total number of planes must not exceed AV_DRM_MAX_PLANES, and - * the order of the planes by increasing layer index followed by - * increasing plane index must be the same as the order which would - * be used for the data pointers in the equivalent software format. - */ -typedef struct AVDRMFrameDescriptor { - /** - * Number of DRM objects making up this frame. - */ - int nb_objects; - /** - * Array of objects making up the frame. - */ - AVDRMObjectDescriptor objects[AV_DRM_MAX_PLANES]; - /** - * Number of layers in the frame. - */ - int nb_layers; - /** - * Array of layers in the frame. - */ - AVDRMLayerDescriptor layers[AV_DRM_MAX_PLANES]; -} AVDRMFrameDescriptor; - -/** - * DRM device. - * - * Allocated as AVHWDeviceContext.hwctx. - */ -typedef struct AVDRMDeviceContext { - /** - * File descriptor of DRM device. - * - * This is used as the device to create frames on, and may also be - * used in some derivation and mapping operations. - * - * If no device is required, set to -1. - */ - int fd; -} AVDRMDeviceContext; - -#endif /* AVUTIL_HWCONTEXT_DRM_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_dxva2.h b/gostream/ffmpeg/include/libavutil/hwcontext_dxva2.h deleted file mode 100644 index e1b79bc0dee..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_dxva2.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - - -#ifndef AVUTIL_HWCONTEXT_DXVA2_H -#define AVUTIL_HWCONTEXT_DXVA2_H - -/** - * @file - * An API-specific header for AV_HWDEVICE_TYPE_DXVA2. - * - * Only fixed-size pools are supported. - * - * For user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs - * with the data pointer set to a pointer to IDirect3DSurface9. - */ - -#include -#include - -/** - * This struct is allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVDXVA2DeviceContext { - IDirect3DDeviceManager9 *devmgr; -} AVDXVA2DeviceContext; - -/** - * This struct is allocated as AVHWFramesContext.hwctx - */ -typedef struct AVDXVA2FramesContext { - /** - * The surface type (e.g. DXVA2_VideoProcessorRenderTarget or - * DXVA2_VideoDecoderRenderTarget). Must be set by the caller. - */ - DWORD surface_type; - - /** - * The surface pool. When an external pool is not provided by the caller, - * this will be managed (allocated and filled on init, freed on uninit) by - * libavutil. - */ - IDirect3DSurface9 **surfaces; - int nb_surfaces; - - /** - * Certain drivers require the decoder to be destroyed before the surfaces. - * To allow internally managed pools to work properly in such cases, this - * field is provided. - * - * If it is non-NULL, libavutil will call IDirectXVideoDecoder_Release() on - * it just before the internal surface pool is freed. - * - * This is for convenience only. Some code uses other methods to manage the - * decoder reference. - */ - IDirectXVideoDecoder *decoder_to_release; -} AVDXVA2FramesContext; - -#endif /* AVUTIL_HWCONTEXT_DXVA2_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_mediacodec.h b/gostream/ffmpeg/include/libavutil/hwcontext_mediacodec.h deleted file mode 100644 index fc0263cabce..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_mediacodec.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_MEDIACODEC_H -#define AVUTIL_HWCONTEXT_MEDIACODEC_H - -/** - * MediaCodec details. - * - * Allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVMediaCodecDeviceContext { - /** - * android/view/Surface handle, to be filled by the user. - * - * This is the default surface used by decoders on this device. - */ - void *surface; - - /** - * Pointer to ANativeWindow. - * - * It both surface and native_window is NULL, try to create it - * automatically if create_window is true and OS support - * createPersistentInputSurface. - * - * It can be used as output surface for decoder and input surface for - * encoder. - */ - void *native_window; - - /** - * Enable createPersistentInputSurface automatically. - * - * Disabled by default. - * - * It can be enabled by setting this flag directly, or by setting - * AVDictionary of av_hwdevice_ctx_create(), with "create_window" as key. - * The second method is useful for ffmpeg cmdline, e.g., we can enable it - * via: - * -init_hw_device mediacodec=mediacodec,create_window=1 - */ - int create_window; -} AVMediaCodecDeviceContext; - -#endif /* AVUTIL_HWCONTEXT_MEDIACODEC_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_opencl.h b/gostream/ffmpeg/include/libavutil/hwcontext_opencl.h deleted file mode 100644 index ef54486c95f..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_opencl.h +++ /dev/null @@ -1,100 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_OPENCL_H -#define AVUTIL_HWCONTEXT_OPENCL_H - -#ifdef __APPLE__ -#include -#else -#include -#endif - -#include "frame.h" - -/** - * @file - * API-specific header for AV_HWDEVICE_TYPE_OPENCL. - * - * Pools allocated internally are always dynamic, and are primarily intended - * to be used in OpenCL-only cases. If interoperation is required, it is - * typically required to allocate frames in the other API and then map the - * frames context to OpenCL with av_hwframe_ctx_create_derived(). - */ - -/** - * OpenCL frame descriptor for pool allocation. - * - * In user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs - * with the data pointer pointing at an object of this type describing the - * planes of the frame. - */ -typedef struct AVOpenCLFrameDescriptor { - /** - * Number of planes in the frame. - */ - int nb_planes; - /** - * OpenCL image2d objects for each plane of the frame. - */ - cl_mem planes[AV_NUM_DATA_POINTERS]; -} AVOpenCLFrameDescriptor; - -/** - * OpenCL device details. - * - * Allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVOpenCLDeviceContext { - /** - * The primary device ID of the device. If multiple OpenCL devices - * are associated with the context then this is the one which will - * be used for all operations internal to FFmpeg. - */ - cl_device_id device_id; - /** - * The OpenCL context which will contain all operations and frames on - * this device. - */ - cl_context context; - /** - * The default command queue for this device, which will be used by all - * frames contexts which do not have their own command queue. If not - * intialised by the user, a default queue will be created on the - * primary device. - */ - cl_command_queue command_queue; -} AVOpenCLDeviceContext; - -/** - * OpenCL-specific data associated with a frame pool. - * - * Allocated as AVHWFramesContext.hwctx. - */ -typedef struct AVOpenCLFramesContext { - /** - * The command queue used for internal asynchronous operations on this - * device (av_hwframe_transfer_data(), av_hwframe_map()). - * - * If this is not set, the command queue from the associated device is - * used instead. - */ - cl_command_queue command_queue; -} AVOpenCLFramesContext; - -#endif /* AVUTIL_HWCONTEXT_OPENCL_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_qsv.h b/gostream/ffmpeg/include/libavutil/hwcontext_qsv.h deleted file mode 100644 index e2dba8ad838..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_qsv.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_QSV_H -#define AVUTIL_HWCONTEXT_QSV_H - -#include - -/** - * @file - * An API-specific header for AV_HWDEVICE_TYPE_QSV. - * - * This API does not support dynamic frame pools. AVHWFramesContext.pool must - * contain AVBufferRefs whose data pointer points to an mfxFrameSurface1 struct. - */ - -/** - * This struct is allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVQSVDeviceContext { - mfxSession session; - /** - * The mfxLoader handle used for mfxSession creation - * - * This field is only available for oneVPL user. For non-oneVPL user, this - * field must be set to NULL. - * - * Filled by the user before calling av_hwdevice_ctx_init() and should be - * cast to mfxLoader handle. Deallocating the AVHWDeviceContext will always - * release this interface. - */ - void *loader; -} AVQSVDeviceContext; - -/** - * This struct is allocated as AVHWFramesContext.hwctx - */ -typedef struct AVQSVFramesContext { - mfxFrameSurface1 *surfaces; - int nb_surfaces; - - /** - * A combination of MFX_MEMTYPE_* describing the frame pool. - */ - int frame_type; -} AVQSVFramesContext; - -#endif /* AVUTIL_HWCONTEXT_QSV_H */ - diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_vaapi.h b/gostream/ffmpeg/include/libavutil/hwcontext_vaapi.h deleted file mode 100644 index 0b2e071cb33..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_vaapi.h +++ /dev/null @@ -1,117 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_VAAPI_H -#define AVUTIL_HWCONTEXT_VAAPI_H - -#include - -/** - * @file - * API-specific header for AV_HWDEVICE_TYPE_VAAPI. - * - * Dynamic frame pools are supported, but note that any pool used as a render - * target is required to be of fixed size in order to be be usable as an - * argument to vaCreateContext(). - * - * For user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs - * with the data pointer set to a VASurfaceID. - */ - -enum { - /** - * The quirks field has been set by the user and should not be detected - * automatically by av_hwdevice_ctx_init(). - */ - AV_VAAPI_DRIVER_QUIRK_USER_SET = (1 << 0), - /** - * The driver does not destroy parameter buffers when they are used by - * vaRenderPicture(). Additional code will be required to destroy them - * separately afterwards. - */ - AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS = (1 << 1), - - /** - * The driver does not support the VASurfaceAttribMemoryType attribute, - * so the surface allocation code will not try to use it. - */ - AV_VAAPI_DRIVER_QUIRK_ATTRIB_MEMTYPE = (1 << 2), - - /** - * The driver does not support surface attributes at all. - * The surface allocation code will never pass them to surface allocation, - * and the results of the vaQuerySurfaceAttributes() call will be faked. - */ - AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES = (1 << 3), -}; - -/** - * VAAPI connection details. - * - * Allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVVAAPIDeviceContext { - /** - * The VADisplay handle, to be filled by the user. - */ - VADisplay display; - /** - * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(), - * with reference to a table of known drivers, unless the - * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user - * may need to refer to this field when performing any later - * operations using VAAPI with the same VADisplay. - */ - unsigned int driver_quirks; -} AVVAAPIDeviceContext; - -/** - * VAAPI-specific data associated with a frame pool. - * - * Allocated as AVHWFramesContext.hwctx. - */ -typedef struct AVVAAPIFramesContext { - /** - * Set by the user to apply surface attributes to all surfaces in - * the frame pool. If null, default settings are used. - */ - VASurfaceAttrib *attributes; - int nb_attributes; - /** - * The surfaces IDs of all surfaces in the pool after creation. - * Only valid if AVHWFramesContext.initial_pool_size was positive. - * These are intended to be used as the render_targets arguments to - * vaCreateContext(). - */ - VASurfaceID *surface_ids; - int nb_surfaces; -} AVVAAPIFramesContext; - -/** - * VAAPI hardware pipeline configuration details. - * - * Allocated with av_hwdevice_hwconfig_alloc(). - */ -typedef struct AVVAAPIHWConfig { - /** - * ID of a VAAPI pipeline configuration. - */ - VAConfigID config_id; -} AVVAAPIHWConfig; - -#endif /* AVUTIL_HWCONTEXT_VAAPI_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_vdpau.h b/gostream/ffmpeg/include/libavutil/hwcontext_vdpau.h deleted file mode 100644 index 1b7ea1e443b..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_vdpau.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_VDPAU_H -#define AVUTIL_HWCONTEXT_VDPAU_H - -#include - -/** - * @file - * An API-specific header for AV_HWDEVICE_TYPE_VDPAU. - * - * This API supports dynamic frame pools. AVHWFramesContext.pool must return - * AVBufferRefs whose data pointer is a VdpVideoSurface. - */ - -/** - * This struct is allocated as AVHWDeviceContext.hwctx - */ -typedef struct AVVDPAUDeviceContext { - VdpDevice device; - VdpGetProcAddress *get_proc_address; -} AVVDPAUDeviceContext; - -/** - * AVHWFramesContext.hwctx is currently not used - */ - -#endif /* AVUTIL_HWCONTEXT_VDPAU_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_videotoolbox.h b/gostream/ffmpeg/include/libavutil/hwcontext_videotoolbox.h deleted file mode 100644 index 25dde85df58..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_videotoolbox.h +++ /dev/null @@ -1,96 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_VIDEOTOOLBOX_H -#define AVUTIL_HWCONTEXT_VIDEOTOOLBOX_H - -#include - -#include - -#include "frame.h" -#include "pixfmt.h" - -/** - * @file - * An API-specific header for AV_HWDEVICE_TYPE_VIDEOTOOLBOX. - * - * This API supports frame allocation using a native CVPixelBufferPool - * instead of an AVBufferPool. - * - * If the API user sets a custom pool, AVHWFramesContext.pool must return - * AVBufferRefs whose data pointer is a CVImageBufferRef or CVPixelBufferRef. - * Note that the underlying CVPixelBuffer could be retained by OS frameworks - * depending on application usage, so it is preferable to let CoreVideo manage - * the pool using the default implementation. - * - * Currently AVHWDeviceContext.hwctx and AVHWFramesContext.hwctx are always - * NULL. - */ - -/** - * Convert a VideoToolbox (actually CoreVideo) format to AVPixelFormat. - * Returns AV_PIX_FMT_NONE if no known equivalent was found. - */ -enum AVPixelFormat av_map_videotoolbox_format_to_pixfmt(uint32_t cv_fmt); - -/** - * Convert an AVPixelFormat to a VideoToolbox (actually CoreVideo) format. - * Returns 0 if no known equivalent was found. - */ -uint32_t av_map_videotoolbox_format_from_pixfmt(enum AVPixelFormat pix_fmt); - -/** - * Same as av_map_videotoolbox_format_from_pixfmt function, but can map and - * return full range pixel formats via a flag. - */ -uint32_t av_map_videotoolbox_format_from_pixfmt2(enum AVPixelFormat pix_fmt, bool full_range); - -/** - * Convert an AVChromaLocation to a VideoToolbox/CoreVideo chroma location string. - * Returns 0 if no known equivalent was found. - */ -CFStringRef av_map_videotoolbox_chroma_loc_from_av(enum AVChromaLocation loc); - -/** - * Convert an AVColorSpace to a VideoToolbox/CoreVideo color matrix string. - * Returns 0 if no known equivalent was found. - */ -CFStringRef av_map_videotoolbox_color_matrix_from_av(enum AVColorSpace space); - -/** - * Convert an AVColorPrimaries to a VideoToolbox/CoreVideo color primaries string. - * Returns 0 if no known equivalent was found. - */ -CFStringRef av_map_videotoolbox_color_primaries_from_av(enum AVColorPrimaries pri); - -/** - * Convert an AVColorTransferCharacteristic to a VideoToolbox/CoreVideo color transfer - * function string. - * Returns 0 if no known equivalent was found. - */ -CFStringRef av_map_videotoolbox_color_trc_from_av(enum AVColorTransferCharacteristic trc); - -/** - * Update a CVPixelBufferRef's metadata to based on an AVFrame. - * Returns 0 if no known equivalent was found. - */ -int av_vt_pixbuf_set_attachments(void *log_ctx, - CVPixelBufferRef pixbuf, const struct AVFrame *src); - -#endif /* AVUTIL_HWCONTEXT_VIDEOTOOLBOX_H */ diff --git a/gostream/ffmpeg/include/libavutil/hwcontext_vulkan.h b/gostream/ffmpeg/include/libavutil/hwcontext_vulkan.h deleted file mode 100644 index 895794c8679..00000000000 --- a/gostream/ffmpeg/include/libavutil/hwcontext_vulkan.h +++ /dev/null @@ -1,345 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_HWCONTEXT_VULKAN_H -#define AVUTIL_HWCONTEXT_VULKAN_H - -#if defined(_WIN32) && !defined(VK_USE_PLATFORM_WIN32_KHR) -#define VK_USE_PLATFORM_WIN32_KHR -#endif -#include - -#include "pixfmt.h" -#include "frame.h" - -typedef struct AVVkFrame AVVkFrame; - -/** - * @file - * API-specific header for AV_HWDEVICE_TYPE_VULKAN. - * - * For user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs - * with the data pointer set to an AVVkFrame. - */ - -/** - * Main Vulkan context, allocated as AVHWDeviceContext.hwctx. - * All of these can be set before init to change what the context uses - */ -typedef struct AVVulkanDeviceContext { - /** - * Custom memory allocator, else NULL - */ - const VkAllocationCallbacks *alloc; - - /** - * Pointer to the instance-provided vkGetInstanceProcAddr loading function. - * If NULL, will pick either libvulkan or libvolk, depending on libavutil's - * compilation settings, and set this field. - */ - PFN_vkGetInstanceProcAddr get_proc_addr; - - /** - * Vulkan instance. Must be at least version 1.3. - */ - VkInstance inst; - - /** - * Physical device - */ - VkPhysicalDevice phys_dev; - - /** - * Active device - */ - VkDevice act_dev; - - /** - * This structure should be set to the set of features that present and enabled - * during device creation. When a device is created by FFmpeg, it will default to - * enabling all that are present of the shaderImageGatherExtended, - * fragmentStoresAndAtomics, shaderInt64 and vertexPipelineStoresAndAtomics features. - */ - VkPhysicalDeviceFeatures2 device_features; - - /** - * Enabled instance extensions. - * If supplying your own device context, set this to an array of strings, with - * each entry containing the specified Vulkan extension string to enable. - * Duplicates are possible and accepted. - * If no extensions are enabled, set these fields to NULL, and 0 respectively. - */ - const char * const *enabled_inst_extensions; - int nb_enabled_inst_extensions; - - /** - * Enabled device extensions. By default, VK_KHR_external_memory_fd, - * VK_EXT_external_memory_dma_buf, VK_EXT_image_drm_format_modifier, - * VK_KHR_external_semaphore_fd and VK_EXT_external_memory_host are enabled if found. - * If supplying your own device context, these fields takes the same format as - * the above fields, with the same conditions that duplicates are possible - * and accepted, and that NULL and 0 respectively means no extensions are enabled. - */ - const char * const *enabled_dev_extensions; - int nb_enabled_dev_extensions; - - /** - * Queue family index for graphics operations, and the number of queues - * enabled for it. If unavaiable, will be set to -1. Not required. - * av_hwdevice_create() will attempt to find a dedicated queue for each - * queue family, or pick the one with the least unrelated flags set. - * Queue indices here may overlap if a queue has to share capabilities. - */ - int queue_family_index; - int nb_graphics_queues; - - /** - * Queue family index for transfer operations and the number of queues - * enabled. Required. - */ - int queue_family_tx_index; - int nb_tx_queues; - - /** - * Queue family index for compute operations and the number of queues - * enabled. Required. - */ - int queue_family_comp_index; - int nb_comp_queues; - - /** - * Queue family index for video encode ops, and the amount of queues enabled. - * If the device doesn't support such, queue_family_encode_index will be -1. - * Not required. - */ - int queue_family_encode_index; - int nb_encode_queues; - - /** - * Queue family index for video decode ops, and the amount of queues enabled. - * If the device doesn't support such, queue_family_decode_index will be -1. - * Not required. - */ - int queue_family_decode_index; - int nb_decode_queues; - - /** - * Locks a queue, preventing other threads from submitting any command - * buffers to this queue. - * If set to NULL, will be set to lavu-internal functions that utilize a - * mutex. - */ - void (*lock_queue)(struct AVHWDeviceContext *ctx, uint32_t queue_family, uint32_t index); - - /** - * Similar to lock_queue(), unlocks a queue. Must only be called after locking. - */ - void (*unlock_queue)(struct AVHWDeviceContext *ctx, uint32_t queue_family, uint32_t index); -} AVVulkanDeviceContext; - -/** - * Defines the behaviour of frame allocation. - */ -typedef enum AVVkFrameFlags { - /* Unless this flag is set, autodetected flags will be OR'd based on the - * device and tiling during av_hwframe_ctx_init(). */ - AV_VK_FRAME_FLAG_NONE = (1ULL << 0), - -#if FF_API_VULKAN_CONTIGUOUS_MEMORY - /* DEPRECATED: does nothing. Replaced by multiplane images. */ - AV_VK_FRAME_FLAG_CONTIGUOUS_MEMORY = (1ULL << 1), -#endif - - /* Disables multiplane images. - * This is required to export/import images from CUDA. */ - AV_VK_FRAME_FLAG_DISABLE_MULTIPLANE = (1ULL << 2), -} AVVkFrameFlags; - -/** - * Allocated as AVHWFramesContext.hwctx, used to set pool-specific options - */ -typedef struct AVVulkanFramesContext { - /** - * Controls the tiling of allocated frames. - * If left as VK_IMAGE_TILING_OPTIMAL (0), will use optimal tiling. - * Can be set to VK_IMAGE_TILING_LINEAR to force linear images, - * or VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT to force DMABUF-backed - * images. - * @note Imported frames from other APIs ignore this. - */ - VkImageTiling tiling; - - /** - * Defines extra usage of output frames. If non-zero, all flags MUST be - * supported by the VkFormat. Otherwise, will use supported flags amongst: - * - VK_IMAGE_USAGE_SAMPLED_BIT - * - VK_IMAGE_USAGE_STORAGE_BIT - * - VK_IMAGE_USAGE_TRANSFER_SRC_BIT - * - VK_IMAGE_USAGE_TRANSFER_DST_BIT - */ - VkImageUsageFlagBits usage; - - /** - * Extension data for image creation. - * If DRM tiling is used, a VkImageDrmFormatModifierListCreateInfoEXT structure - * can be added to specify the exact modifier to use. - * - * Additional structures may be added at av_hwframe_ctx_init() time, - * which will be freed automatically on uninit(), so users must only free - * any structures they've allocated themselves. - */ - void *create_pnext; - - /** - * Extension data for memory allocation. Must have as many entries as - * the number of planes of the sw_format. - * This will be chained to VkExportMemoryAllocateInfo, which is used - * to make all pool images exportable to other APIs if the necessary - * extensions are present in enabled_dev_extensions. - */ - void *alloc_pnext[AV_NUM_DATA_POINTERS]; - - /** - * A combination of AVVkFrameFlags. Unless AV_VK_FRAME_FLAG_NONE is set, - * autodetected flags will be OR'd based on the device and tiling during - * av_hwframe_ctx_init(). - */ - AVVkFrameFlags flags; - - /** - * Flags to set during image creation. If unset, defaults to - * VK_IMAGE_CREATE_ALIAS_BIT. - */ - VkImageCreateFlags img_flags; - - /** - * Vulkan format for each image. MUST be compatible with the pixel format. - * If unset, will be automatically set. - * There are at most two compatible formats for a frame - a multiplane - * format, and a single-plane multi-image format. - */ - VkFormat format[AV_NUM_DATA_POINTERS]; - - /** - * Number of layers each image will have. - */ - int nb_layers; - - /** - * Locks a frame, preventing other threads from changing frame properties. - * Users SHOULD only ever lock just before command submission in order - * to get accurate frame properties, and unlock immediately after command - * submission without waiting for it to finish. - * - * If unset, will be set to lavu-internal functions that utilize a mutex. - */ - void (*lock_frame)(struct AVHWFramesContext *fc, AVVkFrame *vkf); - - /** - * Similar to lock_frame(), unlocks a frame. Must only be called after locking. - */ - void (*unlock_frame)(struct AVHWFramesContext *fc, AVVkFrame *vkf); -} AVVulkanFramesContext; - -/* - * Frame structure. - * - * @note the size of this structure is not part of the ABI, to allocate - * you must use @av_vk_frame_alloc(). - */ -struct AVVkFrame { - /** - * Vulkan images to which the memory is bound to. - * May be one for multiplane formats, or multiple. - */ - VkImage img[AV_NUM_DATA_POINTERS]; - - /** - * Tiling for the frame. - */ - VkImageTiling tiling; - - /** - * Memory backing the images. Either one, or as many as there are planes - * in the sw_format. - * In case of having multiple VkImages, but one memory, the offset field - * will indicate the bound offset for each image. - */ - VkDeviceMemory mem[AV_NUM_DATA_POINTERS]; - size_t size[AV_NUM_DATA_POINTERS]; - - /** - * OR'd flags for all memory allocated - */ - VkMemoryPropertyFlagBits flags; - - /** - * Updated after every barrier. One per VkImage. - */ - VkAccessFlagBits access[AV_NUM_DATA_POINTERS]; - VkImageLayout layout[AV_NUM_DATA_POINTERS]; - - /** - * Synchronization timeline semaphores, one for each VkImage. - * Must not be freed manually. Must be waited on at every submission using - * the value in sem_value, and must be signalled at every submission, - * using an incremented value. - */ - VkSemaphore sem[AV_NUM_DATA_POINTERS]; - - /** - * Up to date semaphore value at which each image becomes accessible. - * One per VkImage. - * Clients must wait on this value when submitting a command queue, - * and increment it when signalling. - */ - uint64_t sem_value[AV_NUM_DATA_POINTERS]; - - /** - * Internal data. - */ - struct AVVkFrameInternal *internal; - - /** - * Describes the binding offset of each image to the VkDeviceMemory. - * One per VkImage. - */ - ptrdiff_t offset[AV_NUM_DATA_POINTERS]; - - /** - * Queue family of the images. Must be VK_QUEUE_FAMILY_IGNORED if - * the image was allocated with the CONCURRENT concurrency option. - * One per VkImage. - */ - uint32_t queue_family[AV_NUM_DATA_POINTERS]; -}; - -/** - * Allocates a single AVVkFrame and initializes everything as 0. - * @note Must be freed via av_free() - */ -AVVkFrame *av_vk_frame_alloc(void); - -/** - * Returns the optimal per-plane Vulkan format for a given sw_format, - * one for each plane. - * Returns NULL on unsupported formats. - */ -const VkFormat *av_vkfmt_from_pixfmt(enum AVPixelFormat p); - -#endif /* AVUTIL_HWCONTEXT_VULKAN_H */ diff --git a/gostream/ffmpeg/include/libavutil/imgutils.h b/gostream/ffmpeg/include/libavutil/imgutils.h deleted file mode 100644 index fa3bb101b13..00000000000 --- a/gostream/ffmpeg/include/libavutil/imgutils.h +++ /dev/null @@ -1,347 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_IMGUTILS_H -#define AVUTIL_IMGUTILS_H - -/** - * @file - * misc image utilities - * - * @addtogroup lavu_picture - * @{ - */ - -#include -#include -#include "pixdesc.h" -#include "pixfmt.h" -#include "rational.h" - -/** - * Compute the max pixel step for each plane of an image with a - * format described by pixdesc. - * - * The pixel step is the distance in bytes between the first byte of - * the group of bytes which describe a pixel component and the first - * byte of the successive group in the same plane for the same - * component. - * - * @param max_pixsteps an array which is filled with the max pixel step - * for each plane. Since a plane may contain different pixel - * components, the computed max_pixsteps[plane] is relative to the - * component in the plane with the max pixel step. - * @param max_pixstep_comps an array which is filled with the component - * for each plane which has the max pixel step. May be NULL. - * @param pixdesc the AVPixFmtDescriptor for the image, describing its format - */ -void av_image_fill_max_pixsteps(int max_pixsteps[4], int max_pixstep_comps[4], - const AVPixFmtDescriptor *pixdesc); - -/** - * Compute the size of an image line with format pix_fmt and width - * width for the plane plane. - * - * @return the computed size in bytes - */ -int av_image_get_linesize(enum AVPixelFormat pix_fmt, int width, int plane); - -/** - * Fill plane linesizes for an image with pixel format pix_fmt and - * width width. - * - * @param linesizes array to be filled with the linesize for each plane - * @param pix_fmt the AVPixelFormat of the image - * @param width width of the image in pixels - * @return >= 0 in case of success, a negative error code otherwise - */ -int av_image_fill_linesizes(int linesizes[4], enum AVPixelFormat pix_fmt, int width); - -/** - * Fill plane sizes for an image with pixel format pix_fmt and height height. - * - * @param size the array to be filled with the size of each image plane - * @param pix_fmt the AVPixelFormat of the image - * @param height height of the image in pixels - * @param linesizes the array containing the linesize for each - * plane, should be filled by av_image_fill_linesizes() - * @return >= 0 in case of success, a negative error code otherwise - * - * @note The linesize parameters have the type ptrdiff_t here, while they are - * int for av_image_fill_linesizes(). - */ -int av_image_fill_plane_sizes(size_t size[4], enum AVPixelFormat pix_fmt, - int height, const ptrdiff_t linesizes[4]); - -/** - * Fill plane data pointers for an image with pixel format pix_fmt and - * height height. - * - * @param data pointers array to be filled with the pointer for each image plane - * @param pix_fmt the AVPixelFormat of the image - * @param height height of the image in pixels - * @param ptr the pointer to a buffer which will contain the image - * @param linesizes the array containing the linesize for each - * plane, should be filled by av_image_fill_linesizes() - * @return the size in bytes required for the image buffer, a negative - * error code in case of failure - */ -int av_image_fill_pointers(uint8_t *data[4], enum AVPixelFormat pix_fmt, int height, - uint8_t *ptr, const int linesizes[4]); - -/** - * Allocate an image with size w and h and pixel format pix_fmt, and - * fill pointers and linesizes accordingly. - * The allocated image buffer has to be freed by using - * av_freep(&pointers[0]). - * - * @param pointers array to be filled with the pointer for each image plane - * @param linesizes the array filled with the linesize for each plane - * @param w width of the image in pixels - * @param h height of the image in pixels - * @param pix_fmt the AVPixelFormat of the image - * @param align the value to use for buffer size alignment - * @return the size in bytes required for the image buffer, a negative - * error code in case of failure - */ -int av_image_alloc(uint8_t *pointers[4], int linesizes[4], - int w, int h, enum AVPixelFormat pix_fmt, int align); - -/** - * Copy image plane from src to dst. - * That is, copy "height" number of lines of "bytewidth" bytes each. - * The first byte of each successive line is separated by *_linesize - * bytes. - * - * bytewidth must be contained by both absolute values of dst_linesize - * and src_linesize, otherwise the function behavior is undefined. - * - * @param dst destination plane to copy to - * @param dst_linesize linesize for the image plane in dst - * @param src source plane to copy from - * @param src_linesize linesize for the image plane in src - * @param height height (number of lines) of the plane - */ -void av_image_copy_plane(uint8_t *dst, int dst_linesize, - const uint8_t *src, int src_linesize, - int bytewidth, int height); - -/** - * Copy image data located in uncacheable (e.g. GPU mapped) memory. Where - * available, this function will use special functionality for reading from such - * memory, which may result in greatly improved performance compared to plain - * av_image_copy_plane(). - * - * bytewidth must be contained by both absolute values of dst_linesize - * and src_linesize, otherwise the function behavior is undefined. - * - * @note The linesize parameters have the type ptrdiff_t here, while they are - * int for av_image_copy_plane(). - * @note On x86, the linesizes currently need to be aligned to the cacheline - * size (i.e. 64) to get improved performance. - */ -void av_image_copy_plane_uc_from(uint8_t *dst, ptrdiff_t dst_linesize, - const uint8_t *src, ptrdiff_t src_linesize, - ptrdiff_t bytewidth, int height); - -/** - * Copy image in src_data to dst_data. - * - * @param dst_data destination image data buffer to copy to - * @param dst_linesizes linesizes for the image in dst_data - * @param src_data source image data buffer to copy from - * @param src_linesizes linesizes for the image in src_data - * @param pix_fmt the AVPixelFormat of the image - * @param width width of the image in pixels - * @param height height of the image in pixels - */ -void av_image_copy(uint8_t * const dst_data[4], const int dst_linesizes[4], - const uint8_t * const src_data[4], const int src_linesizes[4], - enum AVPixelFormat pix_fmt, int width, int height); - -/** - * Wrapper around av_image_copy() to workaround the limitation - * that the conversion from uint8_t * const * to const uint8_t * const * - * is not performed automatically in C. - * @see av_image_copy() - */ -static inline -void av_image_copy2(uint8_t * const dst_data[4], const int dst_linesizes[4], - uint8_t * const src_data[4], const int src_linesizes[4], - enum AVPixelFormat pix_fmt, int width, int height) -{ - av_image_copy(dst_data, dst_linesizes, - (const uint8_t * const *)src_data, src_linesizes, - pix_fmt, width, height); -} - -/** - * Copy image data located in uncacheable (e.g. GPU mapped) memory. Where - * available, this function will use special functionality for reading from such - * memory, which may result in greatly improved performance compared to plain - * av_image_copy(). - * - * The data pointers and the linesizes must be aligned to the maximum required - * by the CPU architecture. - * - * @note The linesize parameters have the type ptrdiff_t here, while they are - * int for av_image_copy(). - * @note On x86, the linesizes currently need to be aligned to the cacheline - * size (i.e. 64) to get improved performance. - */ -void av_image_copy_uc_from(uint8_t * const dst_data[4], const ptrdiff_t dst_linesizes[4], - const uint8_t * const src_data[4], const ptrdiff_t src_linesizes[4], - enum AVPixelFormat pix_fmt, int width, int height); - -/** - * Setup the data pointers and linesizes based on the specified image - * parameters and the provided array. - * - * The fields of the given image are filled in by using the src - * address which points to the image data buffer. Depending on the - * specified pixel format, one or multiple image data pointers and - * line sizes will be set. If a planar format is specified, several - * pointers will be set pointing to the different picture planes and - * the line sizes of the different planes will be stored in the - * lines_sizes array. Call with src == NULL to get the required - * size for the src buffer. - * - * To allocate the buffer and fill in the dst_data and dst_linesize in - * one call, use av_image_alloc(). - * - * @param dst_data data pointers to be filled in - * @param dst_linesize linesizes for the image in dst_data to be filled in - * @param src buffer which will contain or contains the actual image data, can be NULL - * @param pix_fmt the pixel format of the image - * @param width the width of the image in pixels - * @param height the height of the image in pixels - * @param align the value used in src for linesize alignment - * @return the size in bytes required for src, a negative error code - * in case of failure - */ -int av_image_fill_arrays(uint8_t *dst_data[4], int dst_linesize[4], - const uint8_t *src, - enum AVPixelFormat pix_fmt, int width, int height, int align); - -/** - * Return the size in bytes of the amount of data required to store an - * image with the given parameters. - * - * @param pix_fmt the pixel format of the image - * @param width the width of the image in pixels - * @param height the height of the image in pixels - * @param align the assumed linesize alignment - * @return the buffer size in bytes, a negative error code in case of failure - */ -int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align); - -/** - * Copy image data from an image into a buffer. - * - * av_image_get_buffer_size() can be used to compute the required size - * for the buffer to fill. - * - * @param dst a buffer into which picture data will be copied - * @param dst_size the size in bytes of dst - * @param src_data pointers containing the source image data - * @param src_linesize linesizes for the image in src_data - * @param pix_fmt the pixel format of the source image - * @param width the width of the source image in pixels - * @param height the height of the source image in pixels - * @param align the assumed linesize alignment for dst - * @return the number of bytes written to dst, or a negative value - * (error code) on error - */ -int av_image_copy_to_buffer(uint8_t *dst, int dst_size, - const uint8_t * const src_data[4], const int src_linesize[4], - enum AVPixelFormat pix_fmt, int width, int height, int align); - -/** - * Check if the given dimension of an image is valid, meaning that all - * bytes of the image can be addressed with a signed int. - * - * @param w the width of the picture - * @param h the height of the picture - * @param log_offset the offset to sum to the log level for logging with log_ctx - * @param log_ctx the parent logging context, it may be NULL - * @return >= 0 if valid, a negative error code otherwise - */ -int av_image_check_size(unsigned int w, unsigned int h, int log_offset, void *log_ctx); - -/** - * Check if the given dimension of an image is valid, meaning that all - * bytes of a plane of an image with the specified pix_fmt can be addressed - * with a signed int. - * - * @param w the width of the picture - * @param h the height of the picture - * @param max_pixels the maximum number of pixels the user wants to accept - * @param pix_fmt the pixel format, can be AV_PIX_FMT_NONE if unknown. - * @param log_offset the offset to sum to the log level for logging with log_ctx - * @param log_ctx the parent logging context, it may be NULL - * @return >= 0 if valid, a negative error code otherwise - */ -int av_image_check_size2(unsigned int w, unsigned int h, int64_t max_pixels, enum AVPixelFormat pix_fmt, int log_offset, void *log_ctx); - -/** - * Check if the given sample aspect ratio of an image is valid. - * - * It is considered invalid if the denominator is 0 or if applying the ratio - * to the image size would make the smaller dimension less than 1. If the - * sar numerator is 0, it is considered unknown and will return as valid. - * - * @param w width of the image - * @param h height of the image - * @param sar sample aspect ratio of the image - * @return 0 if valid, a negative AVERROR code otherwise - */ -int av_image_check_sar(unsigned int w, unsigned int h, AVRational sar); - -/** - * Overwrite the image data with black. This is suitable for filling a - * sub-rectangle of an image, meaning the padding between the right most pixel - * and the left most pixel on the next line will not be overwritten. For some - * formats, the image size might be rounded up due to inherent alignment. - * - * If the pixel format has alpha, the alpha is cleared to opaque. - * - * This can return an error if the pixel format is not supported. Normally, all - * non-hwaccel pixel formats should be supported. - * - * Passing NULL for dst_data is allowed. Then the function returns whether the - * operation would have succeeded. (It can return an error if the pix_fmt is - * not supported.) - * - * @param dst_data data pointers to destination image - * @param dst_linesize linesizes for the destination image - * @param pix_fmt the pixel format of the image - * @param range the color range of the image (important for colorspaces such as YUV) - * @param width the width of the image in pixels - * @param height the height of the image in pixels - * @return 0 if the image data was cleared, a negative AVERROR code otherwise - */ -int av_image_fill_black(uint8_t * const dst_data[4], const ptrdiff_t dst_linesize[4], - enum AVPixelFormat pix_fmt, enum AVColorRange range, - int width, int height); - -/** - * @} - */ - - -#endif /* AVUTIL_IMGUTILS_H */ diff --git a/gostream/ffmpeg/include/libavutil/intfloat.h b/gostream/ffmpeg/include/libavutil/intfloat.h deleted file mode 100644 index fe3d7ec4a5b..00000000000 --- a/gostream/ffmpeg/include/libavutil/intfloat.h +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2011 Mans Rullgard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_INTFLOAT_H -#define AVUTIL_INTFLOAT_H - -#include -#include "attributes.h" - -union av_intfloat32 { - uint32_t i; - float f; -}; - -union av_intfloat64 { - uint64_t i; - double f; -}; - -/** - * Reinterpret a 32-bit integer as a float. - */ -static av_always_inline float av_int2float(uint32_t i) -{ - union av_intfloat32 v; - v.i = i; - return v.f; -} - -/** - * Reinterpret a float as a 32-bit integer. - */ -static av_always_inline uint32_t av_float2int(float f) -{ - union av_intfloat32 v; - v.f = f; - return v.i; -} - -/** - * Reinterpret a 64-bit integer as a double. - */ -static av_always_inline double av_int2double(uint64_t i) -{ - union av_intfloat64 v; - v.i = i; - return v.f; -} - -/** - * Reinterpret a double as a 64-bit integer. - */ -static av_always_inline uint64_t av_double2int(double f) -{ - union av_intfloat64 v; - v.f = f; - return v.i; -} - -#endif /* AVUTIL_INTFLOAT_H */ diff --git a/gostream/ffmpeg/include/libavutil/intreadwrite.h b/gostream/ffmpeg/include/libavutil/intreadwrite.h deleted file mode 100644 index 21df7887f36..00000000000 --- a/gostream/ffmpeg/include/libavutil/intreadwrite.h +++ /dev/null @@ -1,642 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_INTREADWRITE_H -#define AVUTIL_INTREADWRITE_H - -#include -#include "libavutil/avconfig.h" -#include "attributes.h" -#include "bswap.h" - -typedef union { - uint64_t u64; - uint32_t u32[2]; - uint16_t u16[4]; - uint8_t u8 [8]; - double f64; - float f32[2]; -} av_alias av_alias64; - -typedef union { - uint32_t u32; - uint16_t u16[2]; - uint8_t u8 [4]; - float f32; -} av_alias av_alias32; - -typedef union { - uint16_t u16; - uint8_t u8 [2]; -} av_alias av_alias16; - -/* - * Arch-specific headers can provide any combination of - * AV_[RW][BLN](16|24|32|48|64) and AV_(COPY|SWAP|ZERO)(64|128) macros. - * Preprocessor symbols must be defined, even if these are implemented - * as inline functions. - * - * R/W means read/write, B/L/N means big/little/native endianness. - * The following macros require aligned access, compared to their - * unaligned variants: AV_(COPY|SWAP|ZERO)(64|128), AV_[RW]N[8-64]A. - * Incorrect usage may range from abysmal performance to crash - * depending on the platform. - * - * The unaligned variants are AV_[RW][BLN][8-64] and AV_COPY*U. - */ - -#ifdef HAVE_AV_CONFIG_H - -#include "config.h" - -#if ARCH_ARM -# include "arm/intreadwrite.h" -#elif ARCH_AVR32 -# include "avr32/intreadwrite.h" -#elif ARCH_MIPS -# include "mips/intreadwrite.h" -#elif ARCH_PPC -# include "ppc/intreadwrite.h" -#elif ARCH_X86 -# include "x86/intreadwrite.h" -#endif - -#endif /* HAVE_AV_CONFIG_H */ - -/* - * Map AV_RNXX <-> AV_R[BL]XX for all variants provided by per-arch headers. - */ - -#if AV_HAVE_BIGENDIAN - -# if defined(AV_RN16) && !defined(AV_RB16) -# define AV_RB16(p) AV_RN16(p) -# elif !defined(AV_RN16) && defined(AV_RB16) -# define AV_RN16(p) AV_RB16(p) -# endif - -# if defined(AV_WN16) && !defined(AV_WB16) -# define AV_WB16(p, v) AV_WN16(p, v) -# elif !defined(AV_WN16) && defined(AV_WB16) -# define AV_WN16(p, v) AV_WB16(p, v) -# endif - -# if defined(AV_RN24) && !defined(AV_RB24) -# define AV_RB24(p) AV_RN24(p) -# elif !defined(AV_RN24) && defined(AV_RB24) -# define AV_RN24(p) AV_RB24(p) -# endif - -# if defined(AV_WN24) && !defined(AV_WB24) -# define AV_WB24(p, v) AV_WN24(p, v) -# elif !defined(AV_WN24) && defined(AV_WB24) -# define AV_WN24(p, v) AV_WB24(p, v) -# endif - -# if defined(AV_RN32) && !defined(AV_RB32) -# define AV_RB32(p) AV_RN32(p) -# elif !defined(AV_RN32) && defined(AV_RB32) -# define AV_RN32(p) AV_RB32(p) -# endif - -# if defined(AV_WN32) && !defined(AV_WB32) -# define AV_WB32(p, v) AV_WN32(p, v) -# elif !defined(AV_WN32) && defined(AV_WB32) -# define AV_WN32(p, v) AV_WB32(p, v) -# endif - -# if defined(AV_RN48) && !defined(AV_RB48) -# define AV_RB48(p) AV_RN48(p) -# elif !defined(AV_RN48) && defined(AV_RB48) -# define AV_RN48(p) AV_RB48(p) -# endif - -# if defined(AV_WN48) && !defined(AV_WB48) -# define AV_WB48(p, v) AV_WN48(p, v) -# elif !defined(AV_WN48) && defined(AV_WB48) -# define AV_WN48(p, v) AV_WB48(p, v) -# endif - -# if defined(AV_RN64) && !defined(AV_RB64) -# define AV_RB64(p) AV_RN64(p) -# elif !defined(AV_RN64) && defined(AV_RB64) -# define AV_RN64(p) AV_RB64(p) -# endif - -# if defined(AV_WN64) && !defined(AV_WB64) -# define AV_WB64(p, v) AV_WN64(p, v) -# elif !defined(AV_WN64) && defined(AV_WB64) -# define AV_WN64(p, v) AV_WB64(p, v) -# endif - -#else /* AV_HAVE_BIGENDIAN */ - -# if defined(AV_RN16) && !defined(AV_RL16) -# define AV_RL16(p) AV_RN16(p) -# elif !defined(AV_RN16) && defined(AV_RL16) -# define AV_RN16(p) AV_RL16(p) -# endif - -# if defined(AV_WN16) && !defined(AV_WL16) -# define AV_WL16(p, v) AV_WN16(p, v) -# elif !defined(AV_WN16) && defined(AV_WL16) -# define AV_WN16(p, v) AV_WL16(p, v) -# endif - -# if defined(AV_RN24) && !defined(AV_RL24) -# define AV_RL24(p) AV_RN24(p) -# elif !defined(AV_RN24) && defined(AV_RL24) -# define AV_RN24(p) AV_RL24(p) -# endif - -# if defined(AV_WN24) && !defined(AV_WL24) -# define AV_WL24(p, v) AV_WN24(p, v) -# elif !defined(AV_WN24) && defined(AV_WL24) -# define AV_WN24(p, v) AV_WL24(p, v) -# endif - -# if defined(AV_RN32) && !defined(AV_RL32) -# define AV_RL32(p) AV_RN32(p) -# elif !defined(AV_RN32) && defined(AV_RL32) -# define AV_RN32(p) AV_RL32(p) -# endif - -# if defined(AV_WN32) && !defined(AV_WL32) -# define AV_WL32(p, v) AV_WN32(p, v) -# elif !defined(AV_WN32) && defined(AV_WL32) -# define AV_WN32(p, v) AV_WL32(p, v) -# endif - -# if defined(AV_RN48) && !defined(AV_RL48) -# define AV_RL48(p) AV_RN48(p) -# elif !defined(AV_RN48) && defined(AV_RL48) -# define AV_RN48(p) AV_RL48(p) -# endif - -# if defined(AV_WN48) && !defined(AV_WL48) -# define AV_WL48(p, v) AV_WN48(p, v) -# elif !defined(AV_WN48) && defined(AV_WL48) -# define AV_WN48(p, v) AV_WL48(p, v) -# endif - -# if defined(AV_RN64) && !defined(AV_RL64) -# define AV_RL64(p) AV_RN64(p) -# elif !defined(AV_RN64) && defined(AV_RL64) -# define AV_RN64(p) AV_RL64(p) -# endif - -# if defined(AV_WN64) && !defined(AV_WL64) -# define AV_WL64(p, v) AV_WN64(p, v) -# elif !defined(AV_WN64) && defined(AV_WL64) -# define AV_WN64(p, v) AV_WL64(p, v) -# endif - -#endif /* !AV_HAVE_BIGENDIAN */ - -/* - * Define AV_[RW]N helper macros to simplify definitions not provided - * by per-arch headers. - */ - -#if defined(__GNUC__) || defined(__clang__) - -union unaligned_64 { uint64_t l; } __attribute__((packed)) av_alias; -union unaligned_32 { uint32_t l; } __attribute__((packed)) av_alias; -union unaligned_16 { uint16_t l; } __attribute__((packed)) av_alias; - -# define AV_RN(s, p) (((const union unaligned_##s *) (p))->l) -# define AV_WN(s, p, v) ((((union unaligned_##s *) (p))->l) = (v)) - -#elif defined(_MSC_VER) && (defined(_M_ARM) || defined(_M_X64) || defined(_M_ARM64)) && AV_HAVE_FAST_UNALIGNED - -# define AV_RN(s, p) (*((const __unaligned uint##s##_t*)(p))) -# define AV_WN(s, p, v) (*((__unaligned uint##s##_t*)(p)) = (v)) - -#elif AV_HAVE_FAST_UNALIGNED - -# define AV_RN(s, p) (((const av_alias##s*)(p))->u##s) -# define AV_WN(s, p, v) (((av_alias##s*)(p))->u##s = (v)) - -#else - -#ifndef AV_RB16 -# define AV_RB16(x) \ - ((((const uint8_t*)(x))[0] << 8) | \ - ((const uint8_t*)(x))[1]) -#endif -#ifndef AV_WB16 -# define AV_WB16(p, val) do { \ - uint16_t d = (val); \ - ((uint8_t*)(p))[1] = (d); \ - ((uint8_t*)(p))[0] = (d)>>8; \ - } while(0) -#endif - -#ifndef AV_RL16 -# define AV_RL16(x) \ - ((((const uint8_t*)(x))[1] << 8) | \ - ((const uint8_t*)(x))[0]) -#endif -#ifndef AV_WL16 -# define AV_WL16(p, val) do { \ - uint16_t d = (val); \ - ((uint8_t*)(p))[0] = (d); \ - ((uint8_t*)(p))[1] = (d)>>8; \ - } while(0) -#endif - -#ifndef AV_RB32 -# define AV_RB32(x) \ - (((uint32_t)((const uint8_t*)(x))[0] << 24) | \ - (((const uint8_t*)(x))[1] << 16) | \ - (((const uint8_t*)(x))[2] << 8) | \ - ((const uint8_t*)(x))[3]) -#endif -#ifndef AV_WB32 -# define AV_WB32(p, val) do { \ - uint32_t d = (val); \ - ((uint8_t*)(p))[3] = (d); \ - ((uint8_t*)(p))[2] = (d)>>8; \ - ((uint8_t*)(p))[1] = (d)>>16; \ - ((uint8_t*)(p))[0] = (d)>>24; \ - } while(0) -#endif - -#ifndef AV_RL32 -# define AV_RL32(x) \ - (((uint32_t)((const uint8_t*)(x))[3] << 24) | \ - (((const uint8_t*)(x))[2] << 16) | \ - (((const uint8_t*)(x))[1] << 8) | \ - ((const uint8_t*)(x))[0]) -#endif -#ifndef AV_WL32 -# define AV_WL32(p, val) do { \ - uint32_t d = (val); \ - ((uint8_t*)(p))[0] = (d); \ - ((uint8_t*)(p))[1] = (d)>>8; \ - ((uint8_t*)(p))[2] = (d)>>16; \ - ((uint8_t*)(p))[3] = (d)>>24; \ - } while(0) -#endif - -#ifndef AV_RB64 -# define AV_RB64(x) \ - (((uint64_t)((const uint8_t*)(x))[0] << 56) | \ - ((uint64_t)((const uint8_t*)(x))[1] << 48) | \ - ((uint64_t)((const uint8_t*)(x))[2] << 40) | \ - ((uint64_t)((const uint8_t*)(x))[3] << 32) | \ - ((uint64_t)((const uint8_t*)(x))[4] << 24) | \ - ((uint64_t)((const uint8_t*)(x))[5] << 16) | \ - ((uint64_t)((const uint8_t*)(x))[6] << 8) | \ - (uint64_t)((const uint8_t*)(x))[7]) -#endif -#ifndef AV_WB64 -# define AV_WB64(p, val) do { \ - uint64_t d = (val); \ - ((uint8_t*)(p))[7] = (d); \ - ((uint8_t*)(p))[6] = (d)>>8; \ - ((uint8_t*)(p))[5] = (d)>>16; \ - ((uint8_t*)(p))[4] = (d)>>24; \ - ((uint8_t*)(p))[3] = (d)>>32; \ - ((uint8_t*)(p))[2] = (d)>>40; \ - ((uint8_t*)(p))[1] = (d)>>48; \ - ((uint8_t*)(p))[0] = (d)>>56; \ - } while(0) -#endif - -#ifndef AV_RL64 -# define AV_RL64(x) \ - (((uint64_t)((const uint8_t*)(x))[7] << 56) | \ - ((uint64_t)((const uint8_t*)(x))[6] << 48) | \ - ((uint64_t)((const uint8_t*)(x))[5] << 40) | \ - ((uint64_t)((const uint8_t*)(x))[4] << 32) | \ - ((uint64_t)((const uint8_t*)(x))[3] << 24) | \ - ((uint64_t)((const uint8_t*)(x))[2] << 16) | \ - ((uint64_t)((const uint8_t*)(x))[1] << 8) | \ - (uint64_t)((const uint8_t*)(x))[0]) -#endif -#ifndef AV_WL64 -# define AV_WL64(p, val) do { \ - uint64_t d = (val); \ - ((uint8_t*)(p))[0] = (d); \ - ((uint8_t*)(p))[1] = (d)>>8; \ - ((uint8_t*)(p))[2] = (d)>>16; \ - ((uint8_t*)(p))[3] = (d)>>24; \ - ((uint8_t*)(p))[4] = (d)>>32; \ - ((uint8_t*)(p))[5] = (d)>>40; \ - ((uint8_t*)(p))[6] = (d)>>48; \ - ((uint8_t*)(p))[7] = (d)>>56; \ - } while(0) -#endif - -#if AV_HAVE_BIGENDIAN -# define AV_RN(s, p) AV_RB##s(p) -# define AV_WN(s, p, v) AV_WB##s(p, v) -#else -# define AV_RN(s, p) AV_RL##s(p) -# define AV_WN(s, p, v) AV_WL##s(p, v) -#endif - -#endif /* HAVE_FAST_UNALIGNED */ - -#ifndef AV_RN16 -# define AV_RN16(p) AV_RN(16, p) -#endif - -#ifndef AV_RN32 -# define AV_RN32(p) AV_RN(32, p) -#endif - -#ifndef AV_RN64 -# define AV_RN64(p) AV_RN(64, p) -#endif - -#ifndef AV_WN16 -# define AV_WN16(p, v) AV_WN(16, p, v) -#endif - -#ifndef AV_WN32 -# define AV_WN32(p, v) AV_WN(32, p, v) -#endif - -#ifndef AV_WN64 -# define AV_WN64(p, v) AV_WN(64, p, v) -#endif - -#if AV_HAVE_BIGENDIAN -# define AV_RB(s, p) AV_RN##s(p) -# define AV_WB(s, p, v) AV_WN##s(p, v) -# define AV_RL(s, p) av_bswap##s(AV_RN##s(p)) -# define AV_WL(s, p, v) AV_WN##s(p, av_bswap##s(v)) -#else -# define AV_RB(s, p) av_bswap##s(AV_RN##s(p)) -# define AV_WB(s, p, v) AV_WN##s(p, av_bswap##s(v)) -# define AV_RL(s, p) AV_RN##s(p) -# define AV_WL(s, p, v) AV_WN##s(p, v) -#endif - -#define AV_RB8(x) (((const uint8_t*)(x))[0]) -#define AV_WB8(p, d) do { ((uint8_t*)(p))[0] = (d); } while(0) - -#define AV_RL8(x) AV_RB8(x) -#define AV_WL8(p, d) AV_WB8(p, d) - -#ifndef AV_RB16 -# define AV_RB16(p) AV_RB(16, p) -#endif -#ifndef AV_WB16 -# define AV_WB16(p, v) AV_WB(16, p, v) -#endif - -#ifndef AV_RL16 -# define AV_RL16(p) AV_RL(16, p) -#endif -#ifndef AV_WL16 -# define AV_WL16(p, v) AV_WL(16, p, v) -#endif - -#ifndef AV_RB32 -# define AV_RB32(p) AV_RB(32, p) -#endif -#ifndef AV_WB32 -# define AV_WB32(p, v) AV_WB(32, p, v) -#endif - -#ifndef AV_RL32 -# define AV_RL32(p) AV_RL(32, p) -#endif -#ifndef AV_WL32 -# define AV_WL32(p, v) AV_WL(32, p, v) -#endif - -#ifndef AV_RB64 -# define AV_RB64(p) AV_RB(64, p) -#endif -#ifndef AV_WB64 -# define AV_WB64(p, v) AV_WB(64, p, v) -#endif - -#ifndef AV_RL64 -# define AV_RL64(p) AV_RL(64, p) -#endif -#ifndef AV_WL64 -# define AV_WL64(p, v) AV_WL(64, p, v) -#endif - -#ifndef AV_RB24 -# define AV_RB24(x) \ - ((((const uint8_t*)(x))[0] << 16) | \ - (((const uint8_t*)(x))[1] << 8) | \ - ((const uint8_t*)(x))[2]) -#endif -#ifndef AV_WB24 -# define AV_WB24(p, d) do { \ - ((uint8_t*)(p))[2] = (d); \ - ((uint8_t*)(p))[1] = (d)>>8; \ - ((uint8_t*)(p))[0] = (d)>>16; \ - } while(0) -#endif - -#ifndef AV_RL24 -# define AV_RL24(x) \ - ((((const uint8_t*)(x))[2] << 16) | \ - (((const uint8_t*)(x))[1] << 8) | \ - ((const uint8_t*)(x))[0]) -#endif -#ifndef AV_WL24 -# define AV_WL24(p, d) do { \ - ((uint8_t*)(p))[0] = (d); \ - ((uint8_t*)(p))[1] = (d)>>8; \ - ((uint8_t*)(p))[2] = (d)>>16; \ - } while(0) -#endif - -#ifndef AV_RB48 -# define AV_RB48(x) \ - (((uint64_t)((const uint8_t*)(x))[0] << 40) | \ - ((uint64_t)((const uint8_t*)(x))[1] << 32) | \ - ((uint64_t)((const uint8_t*)(x))[2] << 24) | \ - ((uint64_t)((const uint8_t*)(x))[3] << 16) | \ - ((uint64_t)((const uint8_t*)(x))[4] << 8) | \ - (uint64_t)((const uint8_t*)(x))[5]) -#endif -#ifndef AV_WB48 -# define AV_WB48(p, darg) do { \ - uint64_t d = (darg); \ - ((uint8_t*)(p))[5] = (d); \ - ((uint8_t*)(p))[4] = (d)>>8; \ - ((uint8_t*)(p))[3] = (d)>>16; \ - ((uint8_t*)(p))[2] = (d)>>24; \ - ((uint8_t*)(p))[1] = (d)>>32; \ - ((uint8_t*)(p))[0] = (d)>>40; \ - } while(0) -#endif - -#ifndef AV_RL48 -# define AV_RL48(x) \ - (((uint64_t)((const uint8_t*)(x))[5] << 40) | \ - ((uint64_t)((const uint8_t*)(x))[4] << 32) | \ - ((uint64_t)((const uint8_t*)(x))[3] << 24) | \ - ((uint64_t)((const uint8_t*)(x))[2] << 16) | \ - ((uint64_t)((const uint8_t*)(x))[1] << 8) | \ - (uint64_t)((const uint8_t*)(x))[0]) -#endif -#ifndef AV_WL48 -# define AV_WL48(p, darg) do { \ - uint64_t d = (darg); \ - ((uint8_t*)(p))[0] = (d); \ - ((uint8_t*)(p))[1] = (d)>>8; \ - ((uint8_t*)(p))[2] = (d)>>16; \ - ((uint8_t*)(p))[3] = (d)>>24; \ - ((uint8_t*)(p))[4] = (d)>>32; \ - ((uint8_t*)(p))[5] = (d)>>40; \ - } while(0) -#endif - -/* - * The AV_[RW]NA macros access naturally aligned data - * in a type-safe way. - */ - -#define AV_RNA(s, p) (((const av_alias##s*)(p))->u##s) -#define AV_WNA(s, p, v) (((av_alias##s*)(p))->u##s = (v)) - -#ifndef AV_RN16A -# define AV_RN16A(p) AV_RNA(16, p) -#endif - -#ifndef AV_RN32A -# define AV_RN32A(p) AV_RNA(32, p) -#endif - -#ifndef AV_RN64A -# define AV_RN64A(p) AV_RNA(64, p) -#endif - -#ifndef AV_WN16A -# define AV_WN16A(p, v) AV_WNA(16, p, v) -#endif - -#ifndef AV_WN32A -# define AV_WN32A(p, v) AV_WNA(32, p, v) -#endif - -#ifndef AV_WN64A -# define AV_WN64A(p, v) AV_WNA(64, p, v) -#endif - -#if AV_HAVE_BIGENDIAN -# define AV_RLA(s, p) av_bswap##s(AV_RN##s##A(p)) -# define AV_WLA(s, p, v) AV_WN##s##A(p, av_bswap##s(v)) -#else -# define AV_RLA(s, p) AV_RN##s##A(p) -# define AV_WLA(s, p, v) AV_WN##s##A(p, v) -#endif - -#ifndef AV_RL64A -# define AV_RL64A(p) AV_RLA(64, p) -#endif -#ifndef AV_WL64A -# define AV_WL64A(p, v) AV_WLA(64, p, v) -#endif - -/* - * The AV_COPYxxU macros are suitable for copying data to/from unaligned - * memory locations. - */ - -#define AV_COPYU(n, d, s) AV_WN##n(d, AV_RN##n(s)); - -#ifndef AV_COPY16U -# define AV_COPY16U(d, s) AV_COPYU(16, d, s) -#endif - -#ifndef AV_COPY32U -# define AV_COPY32U(d, s) AV_COPYU(32, d, s) -#endif - -#ifndef AV_COPY64U -# define AV_COPY64U(d, s) AV_COPYU(64, d, s) -#endif - -#ifndef AV_COPY128U -# define AV_COPY128U(d, s) \ - do { \ - AV_COPY64U(d, s); \ - AV_COPY64U((char *)(d) + 8, (const char *)(s) + 8); \ - } while(0) -#endif - -/* Parameters for AV_COPY*, AV_SWAP*, AV_ZERO* must be - * naturally aligned. They may be implemented using MMX, - * so emms_c() must be called before using any float code - * afterwards. - */ - -#define AV_COPY(n, d, s) \ - (((av_alias##n*)(d))->u##n = ((const av_alias##n*)(s))->u##n) - -#ifndef AV_COPY16 -# define AV_COPY16(d, s) AV_COPY(16, d, s) -#endif - -#ifndef AV_COPY32 -# define AV_COPY32(d, s) AV_COPY(32, d, s) -#endif - -#ifndef AV_COPY64 -# define AV_COPY64(d, s) AV_COPY(64, d, s) -#endif - -#ifndef AV_COPY128 -# define AV_COPY128(d, s) \ - do { \ - AV_COPY64(d, s); \ - AV_COPY64((char*)(d)+8, (char*)(s)+8); \ - } while(0) -#endif - -#define AV_SWAP(n, a, b) FFSWAP(av_alias##n, *(av_alias##n*)(a), *(av_alias##n*)(b)) - -#ifndef AV_SWAP64 -# define AV_SWAP64(a, b) AV_SWAP(64, a, b) -#endif - -#define AV_ZERO(n, d) (((av_alias##n*)(d))->u##n = 0) - -#ifndef AV_ZERO16 -# define AV_ZERO16(d) AV_ZERO(16, d) -#endif - -#ifndef AV_ZERO32 -# define AV_ZERO32(d) AV_ZERO(32, d) -#endif - -#ifndef AV_ZERO64 -# define AV_ZERO64(d) AV_ZERO(64, d) -#endif - -#ifndef AV_ZERO128 -# define AV_ZERO128(d) \ - do { \ - AV_ZERO64(d); \ - AV_ZERO64((char*)(d)+8); \ - } while(0) -#endif - -#endif /* AVUTIL_INTREADWRITE_H */ diff --git a/gostream/ffmpeg/include/libavutil/lfg.h b/gostream/ffmpeg/include/libavutil/lfg.h deleted file mode 100644 index e75a986f12d..00000000000 --- a/gostream/ffmpeg/include/libavutil/lfg.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Lagged Fibonacci PRNG - * Copyright (c) 2008 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_LFG_H -#define AVUTIL_LFG_H - -#include - -/** - * Context structure for the Lagged Fibonacci PRNG. - * The exact layout, types and content of this struct may change and should - * not be accessed directly. Only its `sizeof()` is guaranteed to stay the same - * to allow easy instanciation. - */ -typedef struct AVLFG { - unsigned int state[64]; - int index; -} AVLFG; - -void av_lfg_init(AVLFG *c, unsigned int seed); - -/** - * Seed the state of the ALFG using binary data. - * - * @return 0 on success, negative value (AVERROR) on failure. - */ -int av_lfg_init_from_data(AVLFG *c, const uint8_t *data, unsigned int length); - -/** - * Get the next random unsigned 32-bit number using an ALFG. - * - * Please also consider a simple LCG like state= state*1664525+1013904223, - * it may be good enough and faster for your specific use case. - */ -static inline unsigned int av_lfg_get(AVLFG *c){ - unsigned a = c->state[c->index & 63] = c->state[(c->index-24) & 63] + c->state[(c->index-55) & 63]; - c->index += 1U; - return a; -} - -/** - * Get the next random unsigned 32-bit number using a MLFG. - * - * Please also consider av_lfg_get() above, it is faster. - */ -static inline unsigned int av_mlfg_get(AVLFG *c){ - unsigned int a= c->state[(c->index-55) & 63]; - unsigned int b= c->state[(c->index-24) & 63]; - a = c->state[c->index & 63] = 2*a*b+a+b; - c->index += 1U; - return a; -} - -/** - * Get the next two numbers generated by a Box-Muller Gaussian - * generator using the random numbers issued by lfg. - * - * @param lfg pointer to the contex structure - * @param out array where the two generated numbers are placed - */ -void av_bmg_get(AVLFG *lfg, double out[2]); - -#endif /* AVUTIL_LFG_H */ diff --git a/gostream/ffmpeg/include/libavutil/log.h b/gostream/ffmpeg/include/libavutil/log.h deleted file mode 100644 index ab7ceabe223..00000000000 --- a/gostream/ffmpeg/include/libavutil/log.h +++ /dev/null @@ -1,387 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_LOG_H -#define AVUTIL_LOG_H - -#include -#include "attributes.h" -#include "version.h" - -typedef enum { - AV_CLASS_CATEGORY_NA = 0, - AV_CLASS_CATEGORY_INPUT, - AV_CLASS_CATEGORY_OUTPUT, - AV_CLASS_CATEGORY_MUXER, - AV_CLASS_CATEGORY_DEMUXER, - AV_CLASS_CATEGORY_ENCODER, - AV_CLASS_CATEGORY_DECODER, - AV_CLASS_CATEGORY_FILTER, - AV_CLASS_CATEGORY_BITSTREAM_FILTER, - AV_CLASS_CATEGORY_SWSCALER, - AV_CLASS_CATEGORY_SWRESAMPLER, - AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT = 40, - AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT, - AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT, - AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT, - AV_CLASS_CATEGORY_DEVICE_OUTPUT, - AV_CLASS_CATEGORY_DEVICE_INPUT, - AV_CLASS_CATEGORY_NB ///< not part of ABI/API -}AVClassCategory; - -#define AV_IS_INPUT_DEVICE(category) \ - (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT) || \ - ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT) || \ - ((category) == AV_CLASS_CATEGORY_DEVICE_INPUT)) - -#define AV_IS_OUTPUT_DEVICE(category) \ - (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT) || \ - ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT) || \ - ((category) == AV_CLASS_CATEGORY_DEVICE_OUTPUT)) - -struct AVOptionRanges; - -/** - * Describe the class of an AVClass context structure. That is an - * arbitrary struct of which the first field is a pointer to an - * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.). - */ -typedef struct AVClass { - /** - * The name of the class; usually it is the same name as the - * context structure type to which the AVClass is associated. - */ - const char* class_name; - - /** - * A pointer to a function which returns the name of a context - * instance ctx associated with the class. - */ - const char* (*item_name)(void* ctx); - - /** - * a pointer to the first option specified in the class if any or NULL - * - * @see av_set_default_options() - */ - const struct AVOption *option; - - /** - * LIBAVUTIL_VERSION with which this structure was created. - * This is used to allow fields to be added without requiring major - * version bumps everywhere. - */ - - int version; - - /** - * Offset in the structure where log_level_offset is stored. - * 0 means there is no such variable - */ - int log_level_offset_offset; - - /** - * Offset in the structure where a pointer to the parent context for - * logging is stored. For example a decoder could pass its AVCodecContext - * to eval as such a parent context, which an av_log() implementation - * could then leverage to display the parent context. - * The offset can be NULL. - */ - int parent_log_context_offset; - - /** - * Category used for visualization (like color) - * This is only set if the category is equal for all objects using this class. - * available since version (51 << 16 | 56 << 8 | 100) - */ - AVClassCategory category; - - /** - * Callback to return the category. - * available since version (51 << 16 | 59 << 8 | 100) - */ - AVClassCategory (*get_category)(void* ctx); - - /** - * Callback to return the supported/allowed ranges. - * available since version (52.12) - */ - int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags); - - /** - * Return next AVOptions-enabled child or NULL - */ - void* (*child_next)(void *obj, void *prev); - - /** - * Iterate over the AVClasses corresponding to potential AVOptions-enabled - * children. - * - * @param iter pointer to opaque iteration state. The caller must initialize - * *iter to NULL before the first call. - * @return AVClass for the next AVOptions-enabled child or NULL if there are - * no more such children. - * - * @note The difference between child_next and this is that child_next - * iterates over _already existing_ objects, while child_class_iterate - * iterates over _all possible_ children. - */ - const struct AVClass* (*child_class_iterate)(void **iter); -} AVClass; - -/** - * @addtogroup lavu_log - * - * @{ - * - * @defgroup lavu_log_constants Logging Constants - * - * @{ - */ - -/** - * Print no output. - */ -#define AV_LOG_QUIET -8 - -/** - * Something went really wrong and we will crash now. - */ -#define AV_LOG_PANIC 0 - -/** - * Something went wrong and recovery is not possible. - * For example, no header was found for a format which depends - * on headers or an illegal combination of parameters is used. - */ -#define AV_LOG_FATAL 8 - -/** - * Something went wrong and cannot losslessly be recovered. - * However, not all future data is affected. - */ -#define AV_LOG_ERROR 16 - -/** - * Something somehow does not look correct. This may or may not - * lead to problems. An example would be the use of '-vstrict -2'. - */ -#define AV_LOG_WARNING 24 - -/** - * Standard information. - */ -#define AV_LOG_INFO 32 - -/** - * Detailed information. - */ -#define AV_LOG_VERBOSE 40 - -/** - * Stuff which is only useful for libav* developers. - */ -#define AV_LOG_DEBUG 48 - -/** - * Extremely verbose debugging, useful for libav* development. - */ -#define AV_LOG_TRACE 56 - -#define AV_LOG_MAX_OFFSET (AV_LOG_TRACE - AV_LOG_QUIET) - -/** - * @} - */ - -/** - * Sets additional colors for extended debugging sessions. - * @code - av_log(ctx, AV_LOG_DEBUG|AV_LOG_C(134), "Message in purple\n"); - @endcode - * Requires 256color terminal support. Uses outside debugging is not - * recommended. - */ -#define AV_LOG_C(x) ((x) << 8) - -/** - * Send the specified message to the log if the level is less than or equal - * to the current av_log_level. By default, all logging messages are sent to - * stderr. This behavior can be altered by setting a different logging callback - * function. - * @see av_log_set_callback - * - * @param avcl A pointer to an arbitrary struct of which the first field is a - * pointer to an AVClass struct or NULL if general log. - * @param level The importance level of the message expressed using a @ref - * lavu_log_constants "Logging Constant". - * @param fmt The format string (printf-compatible) that specifies how - * subsequent arguments are converted to output. - */ -void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4); - -/** - * Send the specified message to the log once with the initial_level and then with - * the subsequent_level. By default, all logging messages are sent to - * stderr. This behavior can be altered by setting a different logging callback - * function. - * @see av_log - * - * @param avcl A pointer to an arbitrary struct of which the first field is a - * pointer to an AVClass struct or NULL if general log. - * @param initial_level importance level of the message expressed using a @ref - * lavu_log_constants "Logging Constant" for the first occurance. - * @param subsequent_level importance level of the message expressed using a @ref - * lavu_log_constants "Logging Constant" after the first occurance. - * @param fmt The format string (printf-compatible) that specifies how - * subsequent arguments are converted to output. - * @param state a variable to keep trak of if a message has already been printed - * this must be initialized to 0 before the first use. The same state - * must not be accessed by 2 Threads simultaneously. - */ -void av_log_once(void* avcl, int initial_level, int subsequent_level, int *state, const char *fmt, ...) av_printf_format(5, 6); - - -/** - * Send the specified message to the log if the level is less than or equal - * to the current av_log_level. By default, all logging messages are sent to - * stderr. This behavior can be altered by setting a different logging callback - * function. - * @see av_log_set_callback - * - * @param avcl A pointer to an arbitrary struct of which the first field is a - * pointer to an AVClass struct. - * @param level The importance level of the message expressed using a @ref - * lavu_log_constants "Logging Constant". - * @param fmt The format string (printf-compatible) that specifies how - * subsequent arguments are converted to output. - * @param vl The arguments referenced by the format string. - */ -void av_vlog(void *avcl, int level, const char *fmt, va_list vl); - -/** - * Get the current log level - * - * @see lavu_log_constants - * - * @return Current log level - */ -int av_log_get_level(void); - -/** - * Set the log level - * - * @see lavu_log_constants - * - * @param level Logging level - */ -void av_log_set_level(int level); - -/** - * Set the logging callback - * - * @note The callback must be thread safe, even if the application does not use - * threads itself as some codecs are multithreaded. - * - * @see av_log_default_callback - * - * @param callback A logging function with a compatible signature. - */ -void av_log_set_callback(void (*callback)(void*, int, const char*, va_list)); - -/** - * Default logging callback - * - * It prints the message to stderr, optionally colorizing it. - * - * @param avcl A pointer to an arbitrary struct of which the first field is a - * pointer to an AVClass struct. - * @param level The importance level of the message expressed using a @ref - * lavu_log_constants "Logging Constant". - * @param fmt The format string (printf-compatible) that specifies how - * subsequent arguments are converted to output. - * @param vl The arguments referenced by the format string. - */ -void av_log_default_callback(void *avcl, int level, const char *fmt, - va_list vl); - -/** - * Return the context name - * - * @param ctx The AVClass context - * - * @return The AVClass class_name - */ -const char* av_default_item_name(void* ctx); -AVClassCategory av_default_get_category(void *ptr); - -/** - * Format a line of log the same way as the default callback. - * @param line buffer to receive the formatted line - * @param line_size size of the buffer - * @param print_prefix used to store whether the prefix must be printed; - * must point to a persistent integer initially set to 1 - */ -void av_log_format_line(void *ptr, int level, const char *fmt, va_list vl, - char *line, int line_size, int *print_prefix); - -/** - * Format a line of log the same way as the default callback. - * @param line buffer to receive the formatted line; - * may be NULL if line_size is 0 - * @param line_size size of the buffer; at most line_size-1 characters will - * be written to the buffer, plus one null terminator - * @param print_prefix used to store whether the prefix must be printed; - * must point to a persistent integer initially set to 1 - * @return Returns a negative value if an error occurred, otherwise returns - * the number of characters that would have been written for a - * sufficiently large buffer, not including the terminating null - * character. If the return value is not less than line_size, it means - * that the log message was truncated to fit the buffer. - */ -int av_log_format_line2(void *ptr, int level, const char *fmt, va_list vl, - char *line, int line_size, int *print_prefix); - -/** - * Skip repeated messages, this requires the user app to use av_log() instead of - * (f)printf as the 2 would otherwise interfere and lead to - * "Last message repeated x times" messages below (f)printf messages with some - * bad luck. - * Also to receive the last, "last repeated" line if any, the user app must - * call av_log(NULL, AV_LOG_QUIET, "%s", ""); at the end - */ -#define AV_LOG_SKIP_REPEATED 1 - -/** - * Include the log severity in messages originating from codecs. - * - * Results in messages such as: - * [rawvideo @ 0xDEADBEEF] [error] encode did not produce valid pts - */ -#define AV_LOG_PRINT_LEVEL 2 - -void av_log_set_flags(int arg); -int av_log_get_flags(void); - -/** - * @} - */ - -#endif /* AVUTIL_LOG_H */ diff --git a/gostream/ffmpeg/include/libavutil/lzo.h b/gostream/ffmpeg/include/libavutil/lzo.h deleted file mode 100644 index c03403992d5..00000000000 --- a/gostream/ffmpeg/include/libavutil/lzo.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - * LZO 1x decompression - * copyright (c) 2006 Reimar Doeffinger - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_LZO_H -#define AVUTIL_LZO_H - -/** - * @defgroup lavu_lzo LZO - * @ingroup lavu_crypto - * - * @{ - */ - -#include - -/** @name Error flags returned by av_lzo1x_decode - * @{ */ -/// end of the input buffer reached before decoding finished -#define AV_LZO_INPUT_DEPLETED 1 -/// decoded data did not fit into output buffer -#define AV_LZO_OUTPUT_FULL 2 -/// a reference to previously decoded data was wrong -#define AV_LZO_INVALID_BACKPTR 4 -/// a non-specific error in the compressed bitstream -#define AV_LZO_ERROR 8 -/** @} */ - -#define AV_LZO_INPUT_PADDING 8 -#define AV_LZO_OUTPUT_PADDING 12 - -/** - * @brief Decodes LZO 1x compressed data. - * @param out output buffer - * @param outlen size of output buffer, number of bytes left are returned here - * @param in input buffer - * @param inlen size of input buffer, number of bytes left are returned here - * @return 0 on success, otherwise a combination of the error flags above - * - * Make sure all buffers are appropriately padded, in must provide - * AV_LZO_INPUT_PADDING, out must provide AV_LZO_OUTPUT_PADDING additional bytes. - */ -int av_lzo1x_decode(void *out, int *outlen, const void *in, int *inlen); - -/** - * @} - */ - -#endif /* AVUTIL_LZO_H */ diff --git a/gostream/ffmpeg/include/libavutil/macros.h b/gostream/ffmpeg/include/libavutil/macros.h deleted file mode 100644 index 2a7567c3ea8..00000000000 --- a/gostream/ffmpeg/include/libavutil/macros.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu - * Utility Preprocessor macros - */ - -#ifndef AVUTIL_MACROS_H -#define AVUTIL_MACROS_H - -#include "libavutil/avconfig.h" - -#if AV_HAVE_BIGENDIAN -# define AV_NE(be, le) (be) -#else -# define AV_NE(be, le) (le) -#endif - -/** - * Comparator. - * For two numerical expressions x and y, gives 1 if x > y, -1 if x < y, and 0 - * if x == y. This is useful for instance in a qsort comparator callback. - * Furthermore, compilers are able to optimize this to branchless code, and - * there is no risk of overflow with signed types. - * As with many macros, this evaluates its argument multiple times, it thus - * must not have a side-effect. - */ -#define FFDIFFSIGN(x,y) (((x)>(y)) - ((x)<(y))) - -#define FFMAX(a,b) ((a) > (b) ? (a) : (b)) -#define FFMAX3(a,b,c) FFMAX(FFMAX(a,b),c) -#define FFMIN(a,b) ((a) > (b) ? (b) : (a)) -#define FFMIN3(a,b,c) FFMIN(FFMIN(a,b),c) - -#define FFSWAP(type,a,b) do{type SWAP_tmp= b; b= a; a= SWAP_tmp;}while(0) -#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0])) - -#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24)) -#define MKBETAG(a,b,c,d) ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24)) - -/** - * @addtogroup preproc_misc Preprocessor String Macros - * - * String manipulation macros - * - * @{ - */ - -#define AV_STRINGIFY(s) AV_TOSTRING(s) -#define AV_TOSTRING(s) #s - -#define AV_GLUE(a, b) a ## b -#define AV_JOIN(a, b) AV_GLUE(a, b) - -/** - * @} - */ - -#define AV_PRAGMA(s) _Pragma(#s) - -#define FFALIGN(x, a) (((x)+(a)-1)&~((a)-1)) - -#endif /* AVUTIL_MACROS_H */ diff --git a/gostream/ffmpeg/include/libavutil/mastering_display_metadata.h b/gostream/ffmpeg/include/libavutil/mastering_display_metadata.h deleted file mode 100644 index c23b07c3cd8..00000000000 --- a/gostream/ffmpeg/include/libavutil/mastering_display_metadata.h +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2016 Neil Birkbeck - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_MASTERING_DISPLAY_METADATA_H -#define AVUTIL_MASTERING_DISPLAY_METADATA_H - -#include "frame.h" -#include "rational.h" - - -/** - * Mastering display metadata capable of representing the color volume of - * the display used to master the content (SMPTE 2086:2014). - * - * To be used as payload of a AVFrameSideData or AVPacketSideData with the - * appropriate type. - * - * @note The struct should be allocated with av_mastering_display_metadata_alloc() - * and its size is not a part of the public ABI. - */ -typedef struct AVMasteringDisplayMetadata { - /** - * CIE 1931 xy chromaticity coords of color primaries (r, g, b order). - */ - AVRational display_primaries[3][2]; - - /** - * CIE 1931 xy chromaticity coords of white point. - */ - AVRational white_point[2]; - - /** - * Min luminance of mastering display (cd/m^2). - */ - AVRational min_luminance; - - /** - * Max luminance of mastering display (cd/m^2). - */ - AVRational max_luminance; - - /** - * Flag indicating whether the display primaries (and white point) are set. - */ - int has_primaries; - - /** - * Flag indicating whether the luminance (min_ and max_) have been set. - */ - int has_luminance; - -} AVMasteringDisplayMetadata; - -/** - * Allocate an AVMasteringDisplayMetadata structure and set its fields to - * default values. The resulting struct can be freed using av_freep(). - * - * @return An AVMasteringDisplayMetadata filled with default values or NULL - * on failure. - */ -AVMasteringDisplayMetadata *av_mastering_display_metadata_alloc(void); - -/** - * Allocate a complete AVMasteringDisplayMetadata and add it to the frame. - * - * @param frame The frame which side data is added to. - * - * @return The AVMasteringDisplayMetadata structure to be filled by caller. - */ -AVMasteringDisplayMetadata *av_mastering_display_metadata_create_side_data(AVFrame *frame); - -/** - * Content light level needed by to transmit HDR over HDMI (CTA-861.3). - * - * To be used as payload of a AVFrameSideData or AVPacketSideData with the - * appropriate type. - * - * @note The struct should be allocated with av_content_light_metadata_alloc() - * and its size is not a part of the public ABI. - */ -typedef struct AVContentLightMetadata { - /** - * Max content light level (cd/m^2). - */ - unsigned MaxCLL; - - /** - * Max average light level per frame (cd/m^2). - */ - unsigned MaxFALL; -} AVContentLightMetadata; - -/** - * Allocate an AVContentLightMetadata structure and set its fields to - * default values. The resulting struct can be freed using av_freep(). - * - * @return An AVContentLightMetadata filled with default values or NULL - * on failure. - */ -AVContentLightMetadata *av_content_light_metadata_alloc(size_t *size); - -/** - * Allocate a complete AVContentLightMetadata and add it to the frame. - * - * @param frame The frame which side data is added to. - * - * @return The AVContentLightMetadata structure to be filled by caller. - */ -AVContentLightMetadata *av_content_light_metadata_create_side_data(AVFrame *frame); - -#endif /* AVUTIL_MASTERING_DISPLAY_METADATA_H */ diff --git a/gostream/ffmpeg/include/libavutil/mathematics.h b/gostream/ffmpeg/include/libavutil/mathematics.h deleted file mode 100644 index e213bab68c9..00000000000 --- a/gostream/ffmpeg/include/libavutil/mathematics.h +++ /dev/null @@ -1,300 +0,0 @@ -/* - * copyright (c) 2005-2012 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @addtogroup lavu_math - * Mathematical utilities for working with timestamp and time base. - */ - -#ifndef AVUTIL_MATHEMATICS_H -#define AVUTIL_MATHEMATICS_H - -#include -#include -#include "attributes.h" -#include "rational.h" -#include "intfloat.h" - -#ifndef M_E -#define M_E 2.7182818284590452354 /* e */ -#endif -#ifndef M_Ef -#define M_Ef 2.7182818284590452354f /* e */ -#endif -#ifndef M_LN2 -#define M_LN2 0.69314718055994530942 /* log_e 2 */ -#endif -#ifndef M_LN2f -#define M_LN2f 0.69314718055994530942f /* log_e 2 */ -#endif -#ifndef M_LN10 -#define M_LN10 2.30258509299404568402 /* log_e 10 */ -#endif -#ifndef M_LN10f -#define M_LN10f 2.30258509299404568402f /* log_e 10 */ -#endif -#ifndef M_LOG2_10 -#define M_LOG2_10 3.32192809488736234787 /* log_2 10 */ -#endif -#ifndef M_LOG2_10f -#define M_LOG2_10f 3.32192809488736234787f /* log_2 10 */ -#endif -#ifndef M_PHI -#define M_PHI 1.61803398874989484820 /* phi / golden ratio */ -#endif -#ifndef M_PHIf -#define M_PHIf 1.61803398874989484820f /* phi / golden ratio */ -#endif -#ifndef M_PI -#define M_PI 3.14159265358979323846 /* pi */ -#endif -#ifndef M_PIf -#define M_PIf 3.14159265358979323846f /* pi */ -#endif -#ifndef M_PI_2 -#define M_PI_2 1.57079632679489661923 /* pi/2 */ -#endif -#ifndef M_PI_2f -#define M_PI_2f 1.57079632679489661923f /* pi/2 */ -#endif -#ifndef M_PI_4 -#define M_PI_4 0.78539816339744830962 /* pi/4 */ -#endif -#ifndef M_PI_4f -#define M_PI_4f 0.78539816339744830962f /* pi/4 */ -#endif -#ifndef M_1_PI -#define M_1_PI 0.31830988618379067154 /* 1/pi */ -#endif -#ifndef M_1_PIf -#define M_1_PIf 0.31830988618379067154f /* 1/pi */ -#endif -#ifndef M_2_PI -#define M_2_PI 0.63661977236758134308 /* 2/pi */ -#endif -#ifndef M_2_PIf -#define M_2_PIf 0.63661977236758134308f /* 2/pi */ -#endif -#ifndef M_2_SQRTPI -#define M_2_SQRTPI 1.12837916709551257390 /* 2/sqrt(pi) */ -#endif -#ifndef M_2_SQRTPIf -#define M_2_SQRTPIf 1.12837916709551257390f /* 2/sqrt(pi) */ -#endif -#ifndef M_SQRT1_2 -#define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */ -#endif -#ifndef M_SQRT1_2f -#define M_SQRT1_2f 0.70710678118654752440f /* 1/sqrt(2) */ -#endif -#ifndef M_SQRT2 -#define M_SQRT2 1.41421356237309504880 /* sqrt(2) */ -#endif -#ifndef M_SQRT2f -#define M_SQRT2f 1.41421356237309504880f /* sqrt(2) */ -#endif -#ifndef NAN -#define NAN av_int2float(0x7fc00000) -#endif -#ifndef INFINITY -#define INFINITY av_int2float(0x7f800000) -#endif - -/** - * @addtogroup lavu_math - * - * @{ - */ - -/** - * Rounding methods. - */ -enum AVRounding { - AV_ROUND_ZERO = 0, ///< Round toward zero. - AV_ROUND_INF = 1, ///< Round away from zero. - AV_ROUND_DOWN = 2, ///< Round toward -infinity. - AV_ROUND_UP = 3, ///< Round toward +infinity. - AV_ROUND_NEAR_INF = 5, ///< Round to nearest and halfway cases away from zero. - /** - * Flag telling rescaling functions to pass `INT64_MIN`/`MAX` through - * unchanged, avoiding special cases for #AV_NOPTS_VALUE. - * - * Unlike other values of the enumeration AVRounding, this value is a - * bitmask that must be used in conjunction with another value of the - * enumeration through a bitwise OR, in order to set behavior for normal - * cases. - * - * @code{.c} - * av_rescale_rnd(3, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX); - * // Rescaling 3: - * // Calculating 3 * 1 / 2 - * // 3 / 2 is rounded up to 2 - * // => 2 - * - * av_rescale_rnd(AV_NOPTS_VALUE, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX); - * // Rescaling AV_NOPTS_VALUE: - * // AV_NOPTS_VALUE == INT64_MIN - * // AV_NOPTS_VALUE is passed through - * // => AV_NOPTS_VALUE - * @endcode - */ - AV_ROUND_PASS_MINMAX = 8192, -}; - -/** - * Compute the greatest common divisor of two integer operands. - * - * @param a Operand - * @param b Operand - * @return GCD of a and b up to sign; if a >= 0 and b >= 0, return value is >= 0; - * if a == 0 and b == 0, returns 0. - */ -int64_t av_const av_gcd(int64_t a, int64_t b); - -/** - * Rescale a 64-bit integer with rounding to nearest. - * - * The operation is mathematically equivalent to `a * b / c`, but writing that - * directly can overflow. - * - * This function is equivalent to av_rescale_rnd() with #AV_ROUND_NEAR_INF. - * - * @see av_rescale_rnd(), av_rescale_q(), av_rescale_q_rnd() - */ -int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const; - -/** - * Rescale a 64-bit integer with specified rounding. - * - * The operation is mathematically equivalent to `a * b / c`, but writing that - * directly can overflow, and does not support different rounding methods. - * If the result is not representable then INT64_MIN is returned. - * - * @see av_rescale(), av_rescale_q(), av_rescale_q_rnd() - */ -int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd) av_const; - -/** - * Rescale a 64-bit integer by 2 rational numbers. - * - * The operation is mathematically equivalent to `a * bq / cq`. - * - * This function is equivalent to av_rescale_q_rnd() with #AV_ROUND_NEAR_INF. - * - * @see av_rescale(), av_rescale_rnd(), av_rescale_q_rnd() - */ -int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const; - -/** - * Rescale a 64-bit integer by 2 rational numbers with specified rounding. - * - * The operation is mathematically equivalent to `a * bq / cq`. - * - * @see av_rescale(), av_rescale_rnd(), av_rescale_q() - */ -int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq, - enum AVRounding rnd) av_const; - -/** - * Compare two timestamps each in its own time base. - * - * @return One of the following values: - * - -1 if `ts_a` is before `ts_b` - * - 1 if `ts_a` is after `ts_b` - * - 0 if they represent the same position - * - * @warning - * The result of the function is undefined if one of the timestamps is outside - * the `int64_t` range when represented in the other's timebase. - */ -int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b); - -/** - * Compare the remainders of two integer operands divided by a common divisor. - * - * In other words, compare the least significant `log2(mod)` bits of integers - * `a` and `b`. - * - * @code{.c} - * av_compare_mod(0x11, 0x02, 0x10) < 0 // since 0x11 % 0x10 (0x1) < 0x02 % 0x10 (0x2) - * av_compare_mod(0x11, 0x02, 0x20) > 0 // since 0x11 % 0x20 (0x11) > 0x02 % 0x20 (0x02) - * @endcode - * - * @param a Operand - * @param b Operand - * @param mod Divisor; must be a power of 2 - * @return - * - a negative value if `a % mod < b % mod` - * - a positive value if `a % mod > b % mod` - * - zero if `a % mod == b % mod` - */ -int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod); - -/** - * Rescale a timestamp while preserving known durations. - * - * This function is designed to be called per audio packet to scale the input - * timestamp to a different time base. Compared to a simple av_rescale_q() - * call, this function is robust against possible inconsistent frame durations. - * - * The `last` parameter is a state variable that must be preserved for all - * subsequent calls for the same stream. For the first call, `*last` should be - * initialized to #AV_NOPTS_VALUE. - * - * @param[in] in_tb Input time base - * @param[in] in_ts Input timestamp - * @param[in] fs_tb Duration time base; typically this is finer-grained - * (greater) than `in_tb` and `out_tb` - * @param[in] duration Duration till the next call to this function (i.e. - * duration of the current packet/frame) - * @param[in,out] last Pointer to a timestamp expressed in terms of - * `fs_tb`, acting as a state variable - * @param[in] out_tb Output timebase - * @return Timestamp expressed in terms of `out_tb` - * - * @note In the context of this function, "duration" is in term of samples, not - * seconds. - */ -int64_t av_rescale_delta(AVRational in_tb, int64_t in_ts, AVRational fs_tb, int duration, int64_t *last, AVRational out_tb); - -/** - * Add a value to a timestamp. - * - * This function guarantees that when the same value is repeatly added that - * no accumulation of rounding errors occurs. - * - * @param[in] ts Input timestamp - * @param[in] ts_tb Input timestamp time base - * @param[in] inc Value to be added - * @param[in] inc_tb Time base of `inc` - */ -int64_t av_add_stable(AVRational ts_tb, int64_t ts, AVRational inc_tb, int64_t inc); - -/** - * 0th order modified bessel function of the first kind. - */ -double av_bessel_i0(double x); - -/** - * @} - */ - -#endif /* AVUTIL_MATHEMATICS_H */ diff --git a/gostream/ffmpeg/include/libavutil/md5.h b/gostream/ffmpeg/include/libavutil/md5.h deleted file mode 100644 index fc2eabdb162..00000000000 --- a/gostream/ffmpeg/include/libavutil/md5.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_md5 - * Public header for MD5 hash function implementation. - */ - -#ifndef AVUTIL_MD5_H -#define AVUTIL_MD5_H - -#include -#include - -#include "attributes.h" - -/** - * @defgroup lavu_md5 MD5 - * @ingroup lavu_hash - * MD5 hash function implementation. - * - * @{ - */ - -extern const int av_md5_size; - -struct AVMD5; - -/** - * Allocate an AVMD5 context. - */ -struct AVMD5 *av_md5_alloc(void); - -/** - * Initialize MD5 hashing. - * - * @param ctx pointer to the function context (of size av_md5_size) - */ -void av_md5_init(struct AVMD5 *ctx); - -/** - * Update hash value. - * - * @param ctx hash function context - * @param src input data to update hash with - * @param len input data length - */ -void av_md5_update(struct AVMD5 *ctx, const uint8_t *src, size_t len); - -/** - * Finish hashing and output digest value. - * - * @param ctx hash function context - * @param dst buffer where output digest value is stored - */ -void av_md5_final(struct AVMD5 *ctx, uint8_t *dst); - -/** - * Hash an array of data. - * - * @param dst The output buffer to write the digest into - * @param src The data to hash - * @param len The length of the data, in bytes - */ -void av_md5_sum(uint8_t *dst, const uint8_t *src, size_t len); - -/** - * @} - */ - -#endif /* AVUTIL_MD5_H */ diff --git a/gostream/ffmpeg/include/libavutil/mem.h b/gostream/ffmpeg/include/libavutil/mem.h deleted file mode 100644 index ab7648ac570..00000000000 --- a/gostream/ffmpeg/include/libavutil/mem.h +++ /dev/null @@ -1,607 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_mem - * Memory handling functions - */ - -#ifndef AVUTIL_MEM_H -#define AVUTIL_MEM_H - -#include -#include - -#include "attributes.h" - -/** - * @addtogroup lavu_mem - * Utilities for manipulating memory. - * - * FFmpeg has several applications of memory that are not required of a typical - * program. For example, the computing-heavy components like video decoding and - * encoding can be sped up significantly through the use of aligned memory. - * - * However, for each of FFmpeg's applications of memory, there might not be a - * recognized or standardized API for that specific use. Memory alignment, for - * instance, varies wildly depending on operating systems, architectures, and - * compilers. Hence, this component of @ref libavutil is created to make - * dealing with memory consistently possible on all platforms. - * - * @{ - */ - -/** - * @defgroup lavu_mem_attrs Function Attributes - * Function attributes applicable to memory handling functions. - * - * These function attributes can help compilers emit more useful warnings, or - * generate better code. - * @{ - */ - -/** - * @def av_malloc_attrib - * Function attribute denoting a malloc-like function. - * - * @see Function attribute `malloc` in GCC's documentation - */ - -#if AV_GCC_VERSION_AT_LEAST(3,1) - #define av_malloc_attrib __attribute__((__malloc__)) -#else - #define av_malloc_attrib -#endif - -/** - * @def av_alloc_size(...) - * Function attribute used on a function that allocates memory, whose size is - * given by the specified parameter(s). - * - * @code{.c} - * void *av_malloc(size_t size) av_alloc_size(1); - * void *av_calloc(size_t nmemb, size_t size) av_alloc_size(1, 2); - * @endcode - * - * @param ... One or two parameter indexes, separated by a comma - * - * @see Function attribute `alloc_size` in GCC's documentation - */ - -#if AV_GCC_VERSION_AT_LEAST(4,3) - #define av_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__))) -#else - #define av_alloc_size(...) -#endif - -/** - * @} - */ - -/** - * @defgroup lavu_mem_funcs Heap Management - * Functions responsible for allocating, freeing, and copying memory. - * - * All memory allocation functions have a built-in upper limit of `INT_MAX` - * bytes. This may be changed with av_max_alloc(), although exercise extreme - * caution when doing so. - * - * @{ - */ - -/** - * Allocate a memory block with alignment suitable for all memory accesses - * (including vectors if available on the CPU). - * - * @param size Size in bytes for the memory block to be allocated - * @return Pointer to the allocated block, or `NULL` if the block cannot - * be allocated - * @see av_mallocz() - */ -void *av_malloc(size_t size) av_malloc_attrib av_alloc_size(1); - -/** - * Allocate a memory block with alignment suitable for all memory accesses - * (including vectors if available on the CPU) and zero all the bytes of the - * block. - * - * @param size Size in bytes for the memory block to be allocated - * @return Pointer to the allocated block, or `NULL` if it cannot be allocated - * @see av_malloc() - */ -void *av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1); - -/** - * Allocate a memory block for an array with av_malloc(). - * - * The allocated memory will have size `size * nmemb` bytes. - * - * @param nmemb Number of element - * @param size Size of a single element - * @return Pointer to the allocated block, or `NULL` if the block cannot - * be allocated - * @see av_malloc() - */ -av_alloc_size(1, 2) void *av_malloc_array(size_t nmemb, size_t size); - -/** - * Allocate a memory block for an array with av_mallocz(). - * - * The allocated memory will have size `size * nmemb` bytes. - * - * @param nmemb Number of elements - * @param size Size of the single element - * @return Pointer to the allocated block, or `NULL` if the block cannot - * be allocated - * - * @see av_mallocz() - * @see av_malloc_array() - */ -void *av_calloc(size_t nmemb, size_t size) av_malloc_attrib av_alloc_size(1, 2); - -/** - * Allocate, reallocate, or free a block of memory. - * - * If `ptr` is `NULL` and `size` > 0, allocate a new block. Otherwise, expand or - * shrink that block of memory according to `size`. - * - * @param ptr Pointer to a memory block already allocated with - * av_realloc() or `NULL` - * @param size Size in bytes of the memory block to be allocated or - * reallocated - * - * @return Pointer to a newly-reallocated block or `NULL` if the block - * cannot be reallocated - * - * @warning Unlike av_malloc(), the returned pointer is not guaranteed to be - * correctly aligned. The returned pointer must be freed after even - * if size is zero. - * @see av_fast_realloc() - * @see av_reallocp() - */ -void *av_realloc(void *ptr, size_t size) av_alloc_size(2); - -/** - * Allocate, reallocate, or free a block of memory through a pointer to a - * pointer. - * - * If `*ptr` is `NULL` and `size` > 0, allocate a new block. If `size` is - * zero, free the memory block pointed to by `*ptr`. Otherwise, expand or - * shrink that block of memory according to `size`. - * - * @param[in,out] ptr Pointer to a pointer to a memory block already allocated - * with av_realloc(), or a pointer to `NULL`. The pointer - * is updated on success, or freed on failure. - * @param[in] size Size in bytes for the memory block to be allocated or - * reallocated - * - * @return Zero on success, an AVERROR error code on failure - * - * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be - * correctly aligned. - */ -av_warn_unused_result -int av_reallocp(void *ptr, size_t size); - -/** - * Allocate, reallocate, or free a block of memory. - * - * This function does the same thing as av_realloc(), except: - * - It takes two size arguments and allocates `nelem * elsize` bytes, - * after checking the result of the multiplication for integer overflow. - * - It frees the input block in case of failure, thus avoiding the memory - * leak with the classic - * @code{.c} - * buf = realloc(buf); - * if (!buf) - * return -1; - * @endcode - * pattern. - */ -void *av_realloc_f(void *ptr, size_t nelem, size_t elsize); - -/** - * Allocate, reallocate, or free an array. - * - * If `ptr` is `NULL` and `nmemb` > 0, allocate a new block. - * - * @param ptr Pointer to a memory block already allocated with - * av_realloc() or `NULL` - * @param nmemb Number of elements in the array - * @param size Size of the single element of the array - * - * @return Pointer to a newly-reallocated block or NULL if the block - * cannot be reallocated - * - * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be - * correctly aligned. The returned pointer must be freed after even if - * nmemb is zero. - * @see av_reallocp_array() - */ -av_alloc_size(2, 3) void *av_realloc_array(void *ptr, size_t nmemb, size_t size); - -/** - * Allocate, reallocate an array through a pointer to a pointer. - * - * If `*ptr` is `NULL` and `nmemb` > 0, allocate a new block. - * - * @param[in,out] ptr Pointer to a pointer to a memory block already - * allocated with av_realloc(), or a pointer to `NULL`. - * The pointer is updated on success, or freed on failure. - * @param[in] nmemb Number of elements - * @param[in] size Size of the single element - * - * @return Zero on success, an AVERROR error code on failure - * - * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be - * correctly aligned. *ptr must be freed after even if nmemb is zero. - */ -int av_reallocp_array(void *ptr, size_t nmemb, size_t size); - -/** - * Reallocate the given buffer if it is not large enough, otherwise do nothing. - * - * If the given buffer is `NULL`, then a new uninitialized buffer is allocated. - * - * If the given buffer is not large enough, and reallocation fails, `NULL` is - * returned and `*size` is set to 0, but the original buffer is not changed or - * freed. - * - * A typical use pattern follows: - * - * @code{.c} - * uint8_t *buf = ...; - * uint8_t *new_buf = av_fast_realloc(buf, ¤t_size, size_needed); - * if (!new_buf) { - * // Allocation failed; clean up original buffer - * av_freep(&buf); - * return AVERROR(ENOMEM); - * } - * @endcode - * - * @param[in,out] ptr Already allocated buffer, or `NULL` - * @param[in,out] size Pointer to the size of buffer `ptr`. `*size` is - * updated to the new allocated size, in particular 0 - * in case of failure. - * @param[in] min_size Desired minimal size of buffer `ptr` - * @return `ptr` if the buffer is large enough, a pointer to newly reallocated - * buffer if the buffer was not large enough, or `NULL` in case of - * error - * @see av_realloc() - * @see av_fast_malloc() - */ -void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size); - -/** - * Allocate a buffer, reusing the given one if large enough. - * - * Contrary to av_fast_realloc(), the current buffer contents might not be - * preserved and on error the old buffer is freed, thus no special handling to - * avoid memleaks is necessary. - * - * `*ptr` is allowed to be `NULL`, in which case allocation always happens if - * `size_needed` is greater than 0. - * - * @code{.c} - * uint8_t *buf = ...; - * av_fast_malloc(&buf, ¤t_size, size_needed); - * if (!buf) { - * // Allocation failed; buf already freed - * return AVERROR(ENOMEM); - * } - * @endcode - * - * @param[in,out] ptr Pointer to pointer to an already allocated buffer. - * `*ptr` will be overwritten with pointer to new - * buffer on success or `NULL` on failure - * @param[in,out] size Pointer to the size of buffer `*ptr`. `*size` is - * updated to the new allocated size, in particular 0 - * in case of failure. - * @param[in] min_size Desired minimal size of buffer `*ptr` - * @see av_realloc() - * @see av_fast_mallocz() - */ -void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size); - -/** - * Allocate and clear a buffer, reusing the given one if large enough. - * - * Like av_fast_malloc(), but all newly allocated space is initially cleared. - * Reused buffer is not cleared. - * - * `*ptr` is allowed to be `NULL`, in which case allocation always happens if - * `size_needed` is greater than 0. - * - * @param[in,out] ptr Pointer to pointer to an already allocated buffer. - * `*ptr` will be overwritten with pointer to new - * buffer on success or `NULL` on failure - * @param[in,out] size Pointer to the size of buffer `*ptr`. `*size` is - * updated to the new allocated size, in particular 0 - * in case of failure. - * @param[in] min_size Desired minimal size of buffer `*ptr` - * @see av_fast_malloc() - */ -void av_fast_mallocz(void *ptr, unsigned int *size, size_t min_size); - -/** - * Free a memory block which has been allocated with a function of av_malloc() - * or av_realloc() family. - * - * @param ptr Pointer to the memory block which should be freed. - * - * @note `ptr = NULL` is explicitly allowed. - * @note It is recommended that you use av_freep() instead, to prevent leaving - * behind dangling pointers. - * @see av_freep() - */ -void av_free(void *ptr); - -/** - * Free a memory block which has been allocated with a function of av_malloc() - * or av_realloc() family, and set the pointer pointing to it to `NULL`. - * - * @code{.c} - * uint8_t *buf = av_malloc(16); - * av_free(buf); - * // buf now contains a dangling pointer to freed memory, and accidental - * // dereference of buf will result in a use-after-free, which may be a - * // security risk. - * - * uint8_t *buf = av_malloc(16); - * av_freep(&buf); - * // buf is now NULL, and accidental dereference will only result in a - * // NULL-pointer dereference. - * @endcode - * - * @param ptr Pointer to the pointer to the memory block which should be freed - * @note `*ptr = NULL` is safe and leads to no action. - * @see av_free() - */ -void av_freep(void *ptr); - -/** - * Duplicate a string. - * - * @param s String to be duplicated - * @return Pointer to a newly-allocated string containing a - * copy of `s` or `NULL` if the string cannot be allocated - * @see av_strndup() - */ -char *av_strdup(const char *s) av_malloc_attrib; - -/** - * Duplicate a substring of a string. - * - * @param s String to be duplicated - * @param len Maximum length of the resulting string (not counting the - * terminating byte) - * @return Pointer to a newly-allocated string containing a - * substring of `s` or `NULL` if the string cannot be allocated - */ -char *av_strndup(const char *s, size_t len) av_malloc_attrib; - -/** - * Duplicate a buffer with av_malloc(). - * - * @param p Buffer to be duplicated - * @param size Size in bytes of the buffer copied - * @return Pointer to a newly allocated buffer containing a - * copy of `p` or `NULL` if the buffer cannot be allocated - */ -void *av_memdup(const void *p, size_t size); - -/** - * Overlapping memcpy() implementation. - * - * @param dst Destination buffer - * @param back Number of bytes back to start copying (i.e. the initial size of - * the overlapping window); must be > 0 - * @param cnt Number of bytes to copy; must be >= 0 - * - * @note `cnt > back` is valid, this will copy the bytes we just copied, - * thus creating a repeating pattern with a period length of `back`. - */ -void av_memcpy_backptr(uint8_t *dst, int back, int cnt); - -/** - * @} - */ - -/** - * @defgroup lavu_mem_dynarray Dynamic Array - * - * Utilities to make an array grow when needed. - * - * Sometimes, the programmer would want to have an array that can grow when - * needed. The libavutil dynamic array utilities fill that need. - * - * libavutil supports two systems of appending elements onto a dynamically - * allocated array, the first one storing the pointer to the value in the - * array, and the second storing the value directly. In both systems, the - * caller is responsible for maintaining a variable containing the length of - * the array, as well as freeing of the array after use. - * - * The first system stores pointers to values in a block of dynamically - * allocated memory. Since only pointers are stored, the function does not need - * to know the size of the type. Both av_dynarray_add() and - * av_dynarray_add_nofree() implement this system. - * - * @code - * type **array = NULL; //< an array of pointers to values - * int nb = 0; //< a variable to keep track of the length of the array - * - * type to_be_added = ...; - * type to_be_added2 = ...; - * - * av_dynarray_add(&array, &nb, &to_be_added); - * if (nb == 0) - * return AVERROR(ENOMEM); - * - * av_dynarray_add(&array, &nb, &to_be_added2); - * if (nb == 0) - * return AVERROR(ENOMEM); - * - * // Now: - * // nb == 2 - * // &to_be_added == array[0] - * // &to_be_added2 == array[1] - * - * av_freep(&array); - * @endcode - * - * The second system stores the value directly in a block of memory. As a - * result, the function has to know the size of the type. av_dynarray2_add() - * implements this mechanism. - * - * @code - * type *array = NULL; //< an array of values - * int nb = 0; //< a variable to keep track of the length of the array - * - * type to_be_added = ...; - * type to_be_added2 = ...; - * - * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array), NULL); - * if (!addr) - * return AVERROR(ENOMEM); - * memcpy(addr, &to_be_added, sizeof(to_be_added)); - * - * // Shortcut of the above. - * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array), - * (const void *)&to_be_added2); - * if (!addr) - * return AVERROR(ENOMEM); - * - * // Now: - * // nb == 2 - * // to_be_added == array[0] - * // to_be_added2 == array[1] - * - * av_freep(&array); - * @endcode - * - * @{ - */ - -/** - * Add the pointer to an element to a dynamic array. - * - * The array to grow is supposed to be an array of pointers to - * structures, and the element to add must be a pointer to an already - * allocated structure. - * - * The array is reallocated when its size reaches powers of 2. - * Therefore, the amortized cost of adding an element is constant. - * - * In case of success, the pointer to the array is updated in order to - * point to the new grown array, and the number pointed to by `nb_ptr` - * is incremented. - * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and - * `*nb_ptr` is set to 0. - * - * @param[in,out] tab_ptr Pointer to the array to grow - * @param[in,out] nb_ptr Pointer to the number of elements in the array - * @param[in] elem Element to add - * @see av_dynarray_add_nofree(), av_dynarray2_add() - */ -void av_dynarray_add(void *tab_ptr, int *nb_ptr, void *elem); - -/** - * Add an element to a dynamic array. - * - * Function has the same functionality as av_dynarray_add(), - * but it doesn't free memory on fails. It returns error code - * instead and leave current buffer untouched. - * - * @return >=0 on success, negative otherwise - * @see av_dynarray_add(), av_dynarray2_add() - */ -av_warn_unused_result -int av_dynarray_add_nofree(void *tab_ptr, int *nb_ptr, void *elem); - -/** - * Add an element of size `elem_size` to a dynamic array. - * - * The array is reallocated when its number of elements reaches powers of 2. - * Therefore, the amortized cost of adding an element is constant. - * - * In case of success, the pointer to the array is updated in order to - * point to the new grown array, and the number pointed to by `nb_ptr` - * is incremented. - * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and - * `*nb_ptr` is set to 0. - * - * @param[in,out] tab_ptr Pointer to the array to grow - * @param[in,out] nb_ptr Pointer to the number of elements in the array - * @param[in] elem_size Size in bytes of an element in the array - * @param[in] elem_data Pointer to the data of the element to add. If - * `NULL`, the space of the newly added element is - * allocated but left uninitialized. - * - * @return Pointer to the data of the element to copy in the newly allocated - * space - * @see av_dynarray_add(), av_dynarray_add_nofree() - */ -void *av_dynarray2_add(void **tab_ptr, int *nb_ptr, size_t elem_size, - const uint8_t *elem_data); - -/** - * @} - */ - -/** - * @defgroup lavu_mem_misc Miscellaneous Functions - * - * Other functions related to memory allocation. - * - * @{ - */ - -/** - * Multiply two `size_t` values checking for overflow. - * - * @param[in] a Operand of multiplication - * @param[in] b Operand of multiplication - * @param[out] r Pointer to the result of the operation - * @return 0 on success, AVERROR(EINVAL) on overflow - */ -int av_size_mult(size_t a, size_t b, size_t *r); - -/** - * Set the maximum size that may be allocated in one block. - * - * The value specified with this function is effective for all libavutil's @ref - * lavu_mem_funcs "heap management functions." - * - * By default, the max value is defined as `INT_MAX`. - * - * @param max Value to be set as the new maximum size - * - * @warning Exercise extreme caution when using this function. Don't touch - * this if you do not understand the full consequence of doing so. - */ -void av_max_alloc(size_t max); - -/** - * @} - * @} - */ - -#endif /* AVUTIL_MEM_H */ diff --git a/gostream/ffmpeg/include/libavutil/motion_vector.h b/gostream/ffmpeg/include/libavutil/motion_vector.h deleted file mode 100644 index ec295563889..00000000000 --- a/gostream/ffmpeg/include/libavutil/motion_vector.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_MOTION_VECTOR_H -#define AVUTIL_MOTION_VECTOR_H - -#include - -typedef struct AVMotionVector { - /** - * Where the current macroblock comes from; negative value when it comes - * from the past, positive value when it comes from the future. - * XXX: set exact relative ref frame reference instead of a +/- 1 "direction". - */ - int32_t source; - /** - * Width and height of the block. - */ - uint8_t w, h; - /** - * Absolute source position. Can be outside the frame area. - */ - int16_t src_x, src_y; - /** - * Absolute destination position. Can be outside the frame area. - */ - int16_t dst_x, dst_y; - /** - * Extra flag information. - * Currently unused. - */ - uint64_t flags; - /** - * Motion vector - * src_x = dst_x + motion_x / motion_scale - * src_y = dst_y + motion_y / motion_scale - */ - int32_t motion_x, motion_y; - uint16_t motion_scale; -} AVMotionVector; - -#endif /* AVUTIL_MOTION_VECTOR_H */ diff --git a/gostream/ffmpeg/include/libavutil/murmur3.h b/gostream/ffmpeg/include/libavutil/murmur3.h deleted file mode 100644 index d90bc2fcd13..00000000000 --- a/gostream/ffmpeg/include/libavutil/murmur3.h +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2013 Reimar Döffinger - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_murmur3 - * Public header for MurmurHash3 hash function implementation. - */ - -#ifndef AVUTIL_MURMUR3_H -#define AVUTIL_MURMUR3_H - -#include -#include - -/** - * @defgroup lavu_murmur3 Murmur3 - * @ingroup lavu_hash - * MurmurHash3 hash function implementation. - * - * MurmurHash3 is a non-cryptographic hash function, of which three - * incompatible versions were created by its inventor Austin Appleby: - * - * - 32-bit output - * - 128-bit output for 32-bit platforms - * - 128-bit output for 64-bit platforms - * - * FFmpeg only implements the last variant: 128-bit output designed for 64-bit - * platforms. Even though the hash function was designed for 64-bit platforms, - * the function in reality works on 32-bit systems too, only with reduced - * performance. - * - * @anchor lavu_murmur3_seedinfo - * By design, MurmurHash3 requires a seed to operate. In response to this, - * libavutil provides two functions for hash initiation, one that requires a - * seed (av_murmur3_init_seeded()) and one that uses a fixed arbitrary integer - * as the seed, and therefore does not (av_murmur3_init()). - * - * To make hashes comparable, you should provide the same seed for all calls to - * this hash function -- if you are supplying one yourself, that is. - * - * @{ - */ - -/** - * Allocate an AVMurMur3 hash context. - * - * @return Uninitialized hash context or `NULL` in case of error - */ -struct AVMurMur3 *av_murmur3_alloc(void); - -/** - * Initialize or reinitialize an AVMurMur3 hash context with a seed. - * - * @param[out] c Hash context - * @param[in] seed Random seed - * - * @see av_murmur3_init() - * @see @ref lavu_murmur3_seedinfo "Detailed description" on a discussion of - * seeds for MurmurHash3. - */ -void av_murmur3_init_seeded(struct AVMurMur3 *c, uint64_t seed); - -/** - * Initialize or reinitialize an AVMurMur3 hash context. - * - * Equivalent to av_murmur3_init_seeded() with a built-in seed. - * - * @param[out] c Hash context - * - * @see av_murmur3_init_seeded() - * @see @ref lavu_murmur3_seedinfo "Detailed description" on a discussion of - * seeds for MurmurHash3. - */ -void av_murmur3_init(struct AVMurMur3 *c); - -/** - * Update hash context with new data. - * - * @param[out] c Hash context - * @param[in] src Input data to update hash with - * @param[in] len Number of bytes to read from `src` - */ -void av_murmur3_update(struct AVMurMur3 *c, const uint8_t *src, size_t len); - -/** - * Finish hashing and output digest value. - * - * @param[in,out] c Hash context - * @param[out] dst Buffer where output digest value is stored - */ -void av_murmur3_final(struct AVMurMur3 *c, uint8_t dst[16]); - -/** - * @} - */ - -#endif /* AVUTIL_MURMUR3_H */ diff --git a/gostream/ffmpeg/include/libavutil/opt.h b/gostream/ffmpeg/include/libavutil/opt.h deleted file mode 100644 index 461b5d3b6bb..00000000000 --- a/gostream/ffmpeg/include/libavutil/opt.h +++ /dev/null @@ -1,891 +0,0 @@ -/* - * AVOptions - * copyright (c) 2005 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_OPT_H -#define AVUTIL_OPT_H - -/** - * @file - * AVOptions - */ - -#include "rational.h" -#include "avutil.h" -#include "channel_layout.h" -#include "dict.h" -#include "log.h" -#include "pixfmt.h" -#include "samplefmt.h" - -/** - * @defgroup avoptions AVOptions - * @ingroup lavu_data - * @{ - * AVOptions provide a generic system to declare options on arbitrary structs - * ("objects"). An option can have a help text, a type and a range of possible - * values. Options may then be enumerated, read and written to. - * - * @section avoptions_implement Implementing AVOptions - * This section describes how to add AVOptions capabilities to a struct. - * - * All AVOptions-related information is stored in an AVClass. Therefore - * the first member of the struct should be a pointer to an AVClass describing it. - * The option field of the AVClass must be set to a NULL-terminated static array - * of AVOptions. Each AVOption must have a non-empty name, a type, a default - * value and for number-type AVOptions also a range of allowed values. It must - * also declare an offset in bytes from the start of the struct, where the field - * associated with this AVOption is located. Other fields in the AVOption struct - * should also be set when applicable, but are not required. - * - * The following example illustrates an AVOptions-enabled struct: - * @code - * typedef struct test_struct { - * const AVClass *class; - * int int_opt; - * char *str_opt; - * uint8_t *bin_opt; - * int bin_len; - * } test_struct; - * - * static const AVOption test_options[] = { - * { "test_int", "This is a test option of int type.", offsetof(test_struct, int_opt), - * AV_OPT_TYPE_INT, { .i64 = -1 }, INT_MIN, INT_MAX }, - * { "test_str", "This is a test option of string type.", offsetof(test_struct, str_opt), - * AV_OPT_TYPE_STRING }, - * { "test_bin", "This is a test option of binary type.", offsetof(test_struct, bin_opt), - * AV_OPT_TYPE_BINARY }, - * { NULL }, - * }; - * - * static const AVClass test_class = { - * .class_name = "test class", - * .item_name = av_default_item_name, - * .option = test_options, - * .version = LIBAVUTIL_VERSION_INT, - * }; - * @endcode - * - * Next, when allocating your struct, you must ensure that the AVClass pointer - * is set to the correct value. Then, av_opt_set_defaults() can be called to - * initialize defaults. After that the struct is ready to be used with the - * AVOptions API. - * - * When cleaning up, you may use the av_opt_free() function to automatically - * free all the allocated string and binary options. - * - * Continuing with the above example: - * - * @code - * test_struct *alloc_test_struct(void) - * { - * test_struct *ret = av_mallocz(sizeof(*ret)); - * ret->class = &test_class; - * av_opt_set_defaults(ret); - * return ret; - * } - * void free_test_struct(test_struct **foo) - * { - * av_opt_free(*foo); - * av_freep(foo); - * } - * @endcode - * - * @subsection avoptions_implement_nesting Nesting - * It may happen that an AVOptions-enabled struct contains another - * AVOptions-enabled struct as a member (e.g. AVCodecContext in - * libavcodec exports generic options, while its priv_data field exports - * codec-specific options). In such a case, it is possible to set up the - * parent struct to export a child's options. To do that, simply - * implement AVClass.child_next() and AVClass.child_class_iterate() in the - * parent struct's AVClass. - * Assuming that the test_struct from above now also contains a - * child_struct field: - * - * @code - * typedef struct child_struct { - * AVClass *class; - * int flags_opt; - * } child_struct; - * static const AVOption child_opts[] = { - * { "test_flags", "This is a test option of flags type.", - * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { .i64 = 0 }, INT_MIN, INT_MAX }, - * { NULL }, - * }; - * static const AVClass child_class = { - * .class_name = "child class", - * .item_name = av_default_item_name, - * .option = child_opts, - * .version = LIBAVUTIL_VERSION_INT, - * }; - * - * void *child_next(void *obj, void *prev) - * { - * test_struct *t = obj; - * if (!prev && t->child_struct) - * return t->child_struct; - * return NULL - * } - * const AVClass child_class_iterate(void **iter) - * { - * const AVClass *c = *iter ? NULL : &child_class; - * *iter = (void*)(uintptr_t)c; - * return c; - * } - * @endcode - * Putting child_next() and child_class_iterate() as defined above into - * test_class will now make child_struct's options accessible through - * test_struct (again, proper setup as described above needs to be done on - * child_struct right after it is created). - * - * From the above example it might not be clear why both child_next() - * and child_class_iterate() are needed. The distinction is that child_next() - * iterates over actually existing objects, while child_class_iterate() - * iterates over all possible child classes. E.g. if an AVCodecContext - * was initialized to use a codec which has private options, then its - * child_next() will return AVCodecContext.priv_data and finish - * iterating. OTOH child_class_iterate() on AVCodecContext.av_class will - * iterate over all available codecs with private options. - * - * @subsection avoptions_implement_named_constants Named constants - * It is possible to create named constants for options. Simply set the unit - * field of the option the constants should apply to a string and - * create the constants themselves as options of type AV_OPT_TYPE_CONST - * with their unit field set to the same string. - * Their default_val field should contain the value of the named - * constant. - * For example, to add some named constants for the test_flags option - * above, put the following into the child_opts array: - * @code - * { "test_flags", "This is a test option of flags type.", - * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { .i64 = 0 }, INT_MIN, INT_MAX, "test_unit" }, - * { "flag1", "This is a flag with value 16", 0, AV_OPT_TYPE_CONST, { .i64 = 16 }, 0, 0, "test_unit" }, - * @endcode - * - * @section avoptions_use Using AVOptions - * This section deals with accessing options in an AVOptions-enabled struct. - * Such structs in FFmpeg are e.g. AVCodecContext in libavcodec or - * AVFormatContext in libavformat. - * - * @subsection avoptions_use_examine Examining AVOptions - * The basic functions for examining options are av_opt_next(), which iterates - * over all options defined for one object, and av_opt_find(), which searches - * for an option with the given name. - * - * The situation is more complicated with nesting. An AVOptions-enabled struct - * may have AVOptions-enabled children. Passing the AV_OPT_SEARCH_CHILDREN flag - * to av_opt_find() will make the function search children recursively. - * - * For enumerating there are basically two cases. The first is when you want to - * get all options that may potentially exist on the struct and its children - * (e.g. when constructing documentation). In that case you should call - * av_opt_child_class_iterate() recursively on the parent struct's AVClass. The - * second case is when you have an already initialized struct with all its - * children and you want to get all options that can be actually written or read - * from it. In that case you should call av_opt_child_next() recursively (and - * av_opt_next() on each result). - * - * @subsection avoptions_use_get_set Reading and writing AVOptions - * When setting options, you often have a string read directly from the - * user. In such a case, simply passing it to av_opt_set() is enough. For - * non-string type options, av_opt_set() will parse the string according to the - * option type. - * - * Similarly av_opt_get() will read any option type and convert it to a string - * which will be returned. Do not forget that the string is allocated, so you - * have to free it with av_free(). - * - * In some cases it may be more convenient to put all options into an - * AVDictionary and call av_opt_set_dict() on it. A specific case of this - * are the format/codec open functions in lavf/lavc which take a dictionary - * filled with option as a parameter. This makes it possible to set some options - * that cannot be set otherwise, since e.g. the input file format is not known - * before the file is actually opened. - */ - -enum AVOptionType{ - AV_OPT_TYPE_FLAGS, - AV_OPT_TYPE_INT, - AV_OPT_TYPE_INT64, - AV_OPT_TYPE_DOUBLE, - AV_OPT_TYPE_FLOAT, - AV_OPT_TYPE_STRING, - AV_OPT_TYPE_RATIONAL, - AV_OPT_TYPE_BINARY, ///< offset must point to a pointer immediately followed by an int for the length - AV_OPT_TYPE_DICT, - AV_OPT_TYPE_UINT64, - AV_OPT_TYPE_CONST, - AV_OPT_TYPE_IMAGE_SIZE, ///< offset must point to two consecutive integers - AV_OPT_TYPE_PIXEL_FMT, - AV_OPT_TYPE_SAMPLE_FMT, - AV_OPT_TYPE_VIDEO_RATE, ///< offset must point to AVRational - AV_OPT_TYPE_DURATION, - AV_OPT_TYPE_COLOR, -#if FF_API_OLD_CHANNEL_LAYOUT - AV_OPT_TYPE_CHANNEL_LAYOUT, -#endif - AV_OPT_TYPE_BOOL, - AV_OPT_TYPE_CHLAYOUT, -}; - -/** - * AVOption - */ -typedef struct AVOption { - const char *name; - - /** - * short English help text - * @todo What about other languages? - */ - const char *help; - - /** - * The offset relative to the context structure where the option - * value is stored. It should be 0 for named constants. - */ - int offset; - enum AVOptionType type; - - /** - * the default value for scalar options - */ - union { - int64_t i64; - double dbl; - const char *str; - /* TODO those are unused now */ - AVRational q; - } default_val; - double min; ///< minimum valid value for the option - double max; ///< maximum valid value for the option - - int flags; -#define AV_OPT_FLAG_ENCODING_PARAM 1 ///< a generic parameter which can be set by the user for muxing or encoding -#define AV_OPT_FLAG_DECODING_PARAM 2 ///< a generic parameter which can be set by the user for demuxing or decoding -#define AV_OPT_FLAG_AUDIO_PARAM 8 -#define AV_OPT_FLAG_VIDEO_PARAM 16 -#define AV_OPT_FLAG_SUBTITLE_PARAM 32 -/** - * The option is intended for exporting values to the caller. - */ -#define AV_OPT_FLAG_EXPORT 64 -/** - * The option may not be set through the AVOptions API, only read. - * This flag only makes sense when AV_OPT_FLAG_EXPORT is also set. - */ -#define AV_OPT_FLAG_READONLY 128 -#define AV_OPT_FLAG_BSF_PARAM (1<<8) ///< a generic parameter which can be set by the user for bit stream filtering -#define AV_OPT_FLAG_RUNTIME_PARAM (1<<15) ///< a generic parameter which can be set by the user at runtime -#define AV_OPT_FLAG_FILTERING_PARAM (1<<16) ///< a generic parameter which can be set by the user for filtering -#define AV_OPT_FLAG_DEPRECATED (1<<17) ///< set if option is deprecated, users should refer to AVOption.help text for more information -#define AV_OPT_FLAG_CHILD_CONSTS (1<<18) ///< set if option constants can also reside in child objects -//FIXME think about enc-audio, ... style flags - - /** - * The logical unit to which the option belongs. Non-constant - * options and corresponding named constants share the same - * unit. May be NULL. - */ - const char *unit; -} AVOption; - -/** - * A single allowed range of values, or a single allowed value. - */ -typedef struct AVOptionRange { - const char *str; - /** - * Value range. - * For string ranges this represents the min/max length. - * For dimensions this represents the min/max pixel count or width/height in multi-component case. - */ - double value_min, value_max; - /** - * Value's component range. - * For string this represents the unicode range for chars, 0-127 limits to ASCII. - */ - double component_min, component_max; - /** - * Range flag. - * If set to 1 the struct encodes a range, if set to 0 a single value. - */ - int is_range; -} AVOptionRange; - -/** - * List of AVOptionRange structs. - */ -typedef struct AVOptionRanges { - /** - * Array of option ranges. - * - * Most of option types use just one component. - * Following describes multi-component option types: - * - * AV_OPT_TYPE_IMAGE_SIZE: - * component index 0: range of pixel count (width * height). - * component index 1: range of width. - * component index 2: range of height. - * - * @note To obtain multi-component version of this structure, user must - * provide AV_OPT_MULTI_COMPONENT_RANGE to av_opt_query_ranges or - * av_opt_query_ranges_default function. - * - * Multi-component range can be read as in following example: - * - * @code - * int range_index, component_index; - * AVOptionRanges *ranges; - * AVOptionRange *range[3]; //may require more than 3 in the future. - * av_opt_query_ranges(&ranges, obj, key, AV_OPT_MULTI_COMPONENT_RANGE); - * for (range_index = 0; range_index < ranges->nb_ranges; range_index++) { - * for (component_index = 0; component_index < ranges->nb_components; component_index++) - * range[component_index] = ranges->range[ranges->nb_ranges * component_index + range_index]; - * //do something with range here. - * } - * av_opt_freep_ranges(&ranges); - * @endcode - */ - AVOptionRange **range; - /** - * Number of ranges per component. - */ - int nb_ranges; - /** - * Number of componentes. - */ - int nb_components; -} AVOptionRanges; - -/** - * Show the obj options. - * - * @param req_flags requested flags for the options to show. Show only the - * options for which it is opt->flags & req_flags. - * @param rej_flags rejected flags for the options to show. Show only the - * options for which it is !(opt->flags & req_flags). - * @param av_log_obj log context to use for showing the options - */ -int av_opt_show2(void *obj, void *av_log_obj, int req_flags, int rej_flags); - -/** - * Set the values of all AVOption fields to their default values. - * - * @param s an AVOption-enabled struct (its first member must be a pointer to AVClass) - */ -void av_opt_set_defaults(void *s); - -/** - * Set the values of all AVOption fields to their default values. Only these - * AVOption fields for which (opt->flags & mask) == flags will have their - * default applied to s. - * - * @param s an AVOption-enabled struct (its first member must be a pointer to AVClass) - * @param mask combination of AV_OPT_FLAG_* - * @param flags combination of AV_OPT_FLAG_* - */ -void av_opt_set_defaults2(void *s, int mask, int flags); - -/** - * Parse the key/value pairs list in opts. For each key/value pair - * found, stores the value in the field in ctx that is named like the - * key. ctx must be an AVClass context, storing is done using - * AVOptions. - * - * @param opts options string to parse, may be NULL - * @param key_val_sep a 0-terminated list of characters used to - * separate key from value - * @param pairs_sep a 0-terminated list of characters used to separate - * two pairs from each other - * @return the number of successfully set key/value pairs, or a negative - * value corresponding to an AVERROR code in case of error: - * AVERROR(EINVAL) if opts cannot be parsed, - * the error code issued by av_opt_set() if a key/value pair - * cannot be set - */ -int av_set_options_string(void *ctx, const char *opts, - const char *key_val_sep, const char *pairs_sep); - -/** - * Parse the key-value pairs list in opts. For each key=value pair found, - * set the value of the corresponding option in ctx. - * - * @param ctx the AVClass object to set options on - * @param opts the options string, key-value pairs separated by a - * delimiter - * @param shorthand a NULL-terminated array of options names for shorthand - * notation: if the first field in opts has no key part, - * the key is taken from the first element of shorthand; - * then again for the second, etc., until either opts is - * finished, shorthand is finished or a named option is - * found; after that, all options must be named - * @param key_val_sep a 0-terminated list of characters used to separate - * key from value, for example '=' - * @param pairs_sep a 0-terminated list of characters used to separate - * two pairs from each other, for example ':' or ',' - * @return the number of successfully set key=value pairs, or a negative - * value corresponding to an AVERROR code in case of error: - * AVERROR(EINVAL) if opts cannot be parsed, - * the error code issued by av_set_string3() if a key/value pair - * cannot be set - * - * Options names must use only the following characters: a-z A-Z 0-9 - . / _ - * Separators must use characters distinct from option names and from each - * other. - */ -int av_opt_set_from_string(void *ctx, const char *opts, - const char *const *shorthand, - const char *key_val_sep, const char *pairs_sep); -/** - * Free all allocated objects in obj. - */ -void av_opt_free(void *obj); - -/** - * Check whether a particular flag is set in a flags field. - * - * @param field_name the name of the flag field option - * @param flag_name the name of the flag to check - * @return non-zero if the flag is set, zero if the flag isn't set, - * isn't of the right type, or the flags field doesn't exist. - */ -int av_opt_flag_is_set(void *obj, const char *field_name, const char *flag_name); - -/** - * Set all the options from a given dictionary on an object. - * - * @param obj a struct whose first element is a pointer to AVClass - * @param options options to process. This dictionary will be freed and replaced - * by a new one containing all options not found in obj. - * Of course this new dictionary needs to be freed by caller - * with av_dict_free(). - * - * @return 0 on success, a negative AVERROR if some option was found in obj, - * but could not be set. - * - * @see av_dict_copy() - */ -int av_opt_set_dict(void *obj, struct AVDictionary **options); - - -/** - * Set all the options from a given dictionary on an object. - * - * @param obj a struct whose first element is a pointer to AVClass - * @param options options to process. This dictionary will be freed and replaced - * by a new one containing all options not found in obj. - * Of course this new dictionary needs to be freed by caller - * with av_dict_free(). - * @param search_flags A combination of AV_OPT_SEARCH_*. - * - * @return 0 on success, a negative AVERROR if some option was found in obj, - * but could not be set. - * - * @see av_dict_copy() - */ -int av_opt_set_dict2(void *obj, struct AVDictionary **options, int search_flags); - -/** - * Extract a key-value pair from the beginning of a string. - * - * @param ropts pointer to the options string, will be updated to - * point to the rest of the string (one of the pairs_sep - * or the final NUL) - * @param key_val_sep a 0-terminated list of characters used to separate - * key from value, for example '=' - * @param pairs_sep a 0-terminated list of characters used to separate - * two pairs from each other, for example ':' or ',' - * @param flags flags; see the AV_OPT_FLAG_* values below - * @param rkey parsed key; must be freed using av_free() - * @param rval parsed value; must be freed using av_free() - * - * @return >=0 for success, or a negative value corresponding to an - * AVERROR code in case of error; in particular: - * AVERROR(EINVAL) if no key is present - * - */ -int av_opt_get_key_value(const char **ropts, - const char *key_val_sep, const char *pairs_sep, - unsigned flags, - char **rkey, char **rval); - -enum { - - /** - * Accept to parse a value without a key; the key will then be returned - * as NULL. - */ - AV_OPT_FLAG_IMPLICIT_KEY = 1, -}; - -/** - * @defgroup opt_eval_funcs Evaluating option strings - * @{ - * This group of functions can be used to evaluate option strings - * and get numbers out of them. They do the same thing as av_opt_set(), - * except the result is written into the caller-supplied pointer. - * - * @param obj a struct whose first element is a pointer to AVClass. - * @param o an option for which the string is to be evaluated. - * @param val string to be evaluated. - * @param *_out value of the string will be written here. - * - * @return 0 on success, a negative number on failure. - */ -int av_opt_eval_flags (void *obj, const AVOption *o, const char *val, int *flags_out); -int av_opt_eval_int (void *obj, const AVOption *o, const char *val, int *int_out); -int av_opt_eval_int64 (void *obj, const AVOption *o, const char *val, int64_t *int64_out); -int av_opt_eval_float (void *obj, const AVOption *o, const char *val, float *float_out); -int av_opt_eval_double(void *obj, const AVOption *o, const char *val, double *double_out); -int av_opt_eval_q (void *obj, const AVOption *o, const char *val, AVRational *q_out); -/** - * @} - */ - -#define AV_OPT_SEARCH_CHILDREN (1 << 0) /**< Search in possible children of the - given object first. */ -/** - * The obj passed to av_opt_find() is fake -- only a double pointer to AVClass - * instead of a required pointer to a struct containing AVClass. This is - * useful for searching for options without needing to allocate the corresponding - * object. - */ -#define AV_OPT_SEARCH_FAKE_OBJ (1 << 1) - -/** - * In av_opt_get, return NULL if the option has a pointer type and is set to NULL, - * rather than returning an empty string. - */ -#define AV_OPT_ALLOW_NULL (1 << 2) - -/** - * Allows av_opt_query_ranges and av_opt_query_ranges_default to return more than - * one component for certain option types. - * @see AVOptionRanges for details. - */ -#define AV_OPT_MULTI_COMPONENT_RANGE (1 << 12) - -/** - * Look for an option in an object. Consider only options which - * have all the specified flags set. - * - * @param[in] obj A pointer to a struct whose first element is a - * pointer to an AVClass. - * Alternatively a double pointer to an AVClass, if - * AV_OPT_SEARCH_FAKE_OBJ search flag is set. - * @param[in] name The name of the option to look for. - * @param[in] unit When searching for named constants, name of the unit - * it belongs to. - * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG). - * @param search_flags A combination of AV_OPT_SEARCH_*. - * - * @return A pointer to the option found, or NULL if no option - * was found. - * - * @note Options found with AV_OPT_SEARCH_CHILDREN flag may not be settable - * directly with av_opt_set(). Use special calls which take an options - * AVDictionary (e.g. avformat_open_input()) to set options found with this - * flag. - */ -const AVOption *av_opt_find(void *obj, const char *name, const char *unit, - int opt_flags, int search_flags); - -/** - * Look for an option in an object. Consider only options which - * have all the specified flags set. - * - * @param[in] obj A pointer to a struct whose first element is a - * pointer to an AVClass. - * Alternatively a double pointer to an AVClass, if - * AV_OPT_SEARCH_FAKE_OBJ search flag is set. - * @param[in] name The name of the option to look for. - * @param[in] unit When searching for named constants, name of the unit - * it belongs to. - * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG). - * @param search_flags A combination of AV_OPT_SEARCH_*. - * @param[out] target_obj if non-NULL, an object to which the option belongs will be - * written here. It may be different from obj if AV_OPT_SEARCH_CHILDREN is present - * in search_flags. This parameter is ignored if search_flags contain - * AV_OPT_SEARCH_FAKE_OBJ. - * - * @return A pointer to the option found, or NULL if no option - * was found. - */ -const AVOption *av_opt_find2(void *obj, const char *name, const char *unit, - int opt_flags, int search_flags, void **target_obj); - -/** - * Iterate over all AVOptions belonging to obj. - * - * @param obj an AVOptions-enabled struct or a double pointer to an - * AVClass describing it. - * @param prev result of the previous call to av_opt_next() on this object - * or NULL - * @return next AVOption or NULL - */ -const AVOption *av_opt_next(const void *obj, const AVOption *prev); - -/** - * Iterate over AVOptions-enabled children of obj. - * - * @param prev result of a previous call to this function or NULL - * @return next AVOptions-enabled child or NULL - */ -void *av_opt_child_next(void *obj, void *prev); - -/** - * Iterate over potential AVOptions-enabled children of parent. - * - * @param iter a pointer where iteration state is stored. - * @return AVClass corresponding to next potential child or NULL - */ -const AVClass *av_opt_child_class_iterate(const AVClass *parent, void **iter); - -/** - * @defgroup opt_set_funcs Option setting functions - * @{ - * Those functions set the field of obj with the given name to value. - * - * @param[in] obj A struct whose first element is a pointer to an AVClass. - * @param[in] name the name of the field to set - * @param[in] val The value to set. In case of av_opt_set() if the field is not - * of a string type, then the given string is parsed. - * SI postfixes and some named scalars are supported. - * If the field is of a numeric type, it has to be a numeric or named - * scalar. Behavior with more than one scalar and +- infix operators - * is undefined. - * If the field is of a flags type, it has to be a sequence of numeric - * scalars or named flags separated by '+' or '-'. Prefixing a flag - * with '+' causes it to be set without affecting the other flags; - * similarly, '-' unsets a flag. - * If the field is of a dictionary type, it has to be a ':' separated list of - * key=value parameters. Values containing ':' special characters must be - * escaped. - * @param search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN - * is passed here, then the option may be set on a child of obj. - * - * @return 0 if the value has been set, or an AVERROR code in case of - * error: - * AVERROR_OPTION_NOT_FOUND if no matching option exists - * AVERROR(ERANGE) if the value is out of range - * AVERROR(EINVAL) if the value is not valid - */ -int av_opt_set (void *obj, const char *name, const char *val, int search_flags); -int av_opt_set_int (void *obj, const char *name, int64_t val, int search_flags); -int av_opt_set_double (void *obj, const char *name, double val, int search_flags); -int av_opt_set_q (void *obj, const char *name, AVRational val, int search_flags); -int av_opt_set_bin (void *obj, const char *name, const uint8_t *val, int size, int search_flags); -int av_opt_set_image_size(void *obj, const char *name, int w, int h, int search_flags); -int av_opt_set_pixel_fmt (void *obj, const char *name, enum AVPixelFormat fmt, int search_flags); -int av_opt_set_sample_fmt(void *obj, const char *name, enum AVSampleFormat fmt, int search_flags); -int av_opt_set_video_rate(void *obj, const char *name, AVRational val, int search_flags); -#if FF_API_OLD_CHANNEL_LAYOUT -attribute_deprecated -int av_opt_set_channel_layout(void *obj, const char *name, int64_t ch_layout, int search_flags); -#endif -int av_opt_set_chlayout(void *obj, const char *name, const AVChannelLayout *layout, int search_flags); -/** - * @note Any old dictionary present is discarded and replaced with a copy of the new one. The - * caller still owns val is and responsible for freeing it. - */ -int av_opt_set_dict_val(void *obj, const char *name, const AVDictionary *val, int search_flags); - -/** - * Set a binary option to an integer list. - * - * @param obj AVClass object to set options on - * @param name name of the binary option - * @param val pointer to an integer list (must have the correct type with - * regard to the contents of the list) - * @param term list terminator (usually 0 or -1) - * @param flags search flags - */ -#define av_opt_set_int_list(obj, name, val, term, flags) \ - (av_int_list_length(val, term) > INT_MAX / sizeof(*(val)) ? \ - AVERROR(EINVAL) : \ - av_opt_set_bin(obj, name, (const uint8_t *)(val), \ - av_int_list_length(val, term) * sizeof(*(val)), flags)) - -/** - * @} - */ - -/** - * @defgroup opt_get_funcs Option getting functions - * @{ - * Those functions get a value of the option with the given name from an object. - * - * @param[in] obj a struct whose first element is a pointer to an AVClass. - * @param[in] name name of the option to get. - * @param[in] search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN - * is passed here, then the option may be found in a child of obj. - * @param[out] out_val value of the option will be written here - * @return >=0 on success, a negative error code otherwise - */ -/** - * @note the returned string will be av_malloc()ed and must be av_free()ed by the caller - * - * @note if AV_OPT_ALLOW_NULL is set in search_flags in av_opt_get, and the - * option is of type AV_OPT_TYPE_STRING, AV_OPT_TYPE_BINARY or AV_OPT_TYPE_DICT - * and is set to NULL, *out_val will be set to NULL instead of an allocated - * empty string. - */ -int av_opt_get (void *obj, const char *name, int search_flags, uint8_t **out_val); -int av_opt_get_int (void *obj, const char *name, int search_flags, int64_t *out_val); -int av_opt_get_double (void *obj, const char *name, int search_flags, double *out_val); -int av_opt_get_q (void *obj, const char *name, int search_flags, AVRational *out_val); -int av_opt_get_image_size(void *obj, const char *name, int search_flags, int *w_out, int *h_out); -int av_opt_get_pixel_fmt (void *obj, const char *name, int search_flags, enum AVPixelFormat *out_fmt); -int av_opt_get_sample_fmt(void *obj, const char *name, int search_flags, enum AVSampleFormat *out_fmt); -int av_opt_get_video_rate(void *obj, const char *name, int search_flags, AVRational *out_val); -#if FF_API_OLD_CHANNEL_LAYOUT -attribute_deprecated -int av_opt_get_channel_layout(void *obj, const char *name, int search_flags, int64_t *ch_layout); -#endif -int av_opt_get_chlayout(void *obj, const char *name, int search_flags, AVChannelLayout *layout); -/** - * @param[out] out_val The returned dictionary is a copy of the actual value and must - * be freed with av_dict_free() by the caller - */ -int av_opt_get_dict_val(void *obj, const char *name, int search_flags, AVDictionary **out_val); -/** - * @} - */ -/** - * Gets a pointer to the requested field in a struct. - * This function allows accessing a struct even when its fields are moved or - * renamed since the application making the access has been compiled, - * - * @returns a pointer to the field, it can be cast to the correct type and read - * or written to. - */ -void *av_opt_ptr(const AVClass *avclass, void *obj, const char *name); - -/** - * Free an AVOptionRanges struct and set it to NULL. - */ -void av_opt_freep_ranges(AVOptionRanges **ranges); - -/** - * Get a list of allowed ranges for the given option. - * - * The returned list may depend on other fields in obj like for example profile. - * - * @param flags is a bitmask of flags, undefined flags should not be set and should be ignored - * AV_OPT_SEARCH_FAKE_OBJ indicates that the obj is a double pointer to a AVClass instead of a full instance - * AV_OPT_MULTI_COMPONENT_RANGE indicates that function may return more than one component, @see AVOptionRanges - * - * The result must be freed with av_opt_freep_ranges. - * - * @return number of compontents returned on success, a negative errro code otherwise - */ -int av_opt_query_ranges(AVOptionRanges **, void *obj, const char *key, int flags); - -/** - * Copy options from src object into dest object. - * - * The underlying AVClass of both src and dest must coincide. The guarantee - * below does not apply if this is not fulfilled. - * - * Options that require memory allocation (e.g. string or binary) are malloc'ed in dest object. - * Original memory allocated for such options is freed unless both src and dest options points to the same memory. - * - * Even on error it is guaranteed that allocated options from src and dest - * no longer alias each other afterwards; in particular calling av_opt_free() - * on both src and dest is safe afterwards if dest has been memdup'ed from src. - * - * @param dest Object to copy from - * @param src Object to copy into - * @return 0 on success, negative on error - */ -int av_opt_copy(void *dest, const void *src); - -/** - * Get a default list of allowed ranges for the given option. - * - * This list is constructed without using the AVClass.query_ranges() callback - * and can be used as fallback from within the callback. - * - * @param flags is a bitmask of flags, undefined flags should not be set and should be ignored - * AV_OPT_SEARCH_FAKE_OBJ indicates that the obj is a double pointer to a AVClass instead of a full instance - * AV_OPT_MULTI_COMPONENT_RANGE indicates that function may return more than one component, @see AVOptionRanges - * - * The result must be freed with av_opt_free_ranges. - * - * @return number of compontents returned on success, a negative errro code otherwise - */ -int av_opt_query_ranges_default(AVOptionRanges **, void *obj, const char *key, int flags); - -/** - * Check if given option is set to its default value. - * - * Options o must belong to the obj. This function must not be called to check child's options state. - * @see av_opt_is_set_to_default_by_name(). - * - * @param obj AVClass object to check option on - * @param o option to be checked - * @return >0 when option is set to its default, - * 0 when option is not set its default, - * <0 on error - */ -int av_opt_is_set_to_default(void *obj, const AVOption *o); - -/** - * Check if given option is set to its default value. - * - * @param obj AVClass object to check option on - * @param name option name - * @param search_flags combination of AV_OPT_SEARCH_* - * @return >0 when option is set to its default, - * 0 when option is not set its default, - * <0 on error - */ -int av_opt_is_set_to_default_by_name(void *obj, const char *name, int search_flags); - - -#define AV_OPT_SERIALIZE_SKIP_DEFAULTS 0x00000001 ///< Serialize options that are not set to default values only. -#define AV_OPT_SERIALIZE_OPT_FLAGS_EXACT 0x00000002 ///< Serialize options that exactly match opt_flags only. - -/** - * Serialize object's options. - * - * Create a string containing object's serialized options. - * Such string may be passed back to av_opt_set_from_string() in order to restore option values. - * A key/value or pairs separator occurring in the serialized value or - * name string are escaped through the av_escape() function. - * - * @param[in] obj AVClass object to serialize - * @param[in] opt_flags serialize options with all the specified flags set (AV_OPT_FLAG) - * @param[in] flags combination of AV_OPT_SERIALIZE_* flags - * @param[out] buffer Pointer to buffer that will be allocated with string containg serialized options. - * Buffer must be freed by the caller when is no longer needed. - * @param[in] key_val_sep character used to separate key from value - * @param[in] pairs_sep character used to separate two pairs from each other - * @return >= 0 on success, negative on error - * @warning Separators cannot be neither '\\' nor '\0'. They also cannot be the same. - */ -int av_opt_serialize(void *obj, int opt_flags, int flags, char **buffer, - const char key_val_sep, const char pairs_sep); -/** - * @} - */ - -#endif /* AVUTIL_OPT_H */ diff --git a/gostream/ffmpeg/include/libavutil/parseutils.h b/gostream/ffmpeg/include/libavutil/parseutils.h deleted file mode 100644 index dad5c2775b2..00000000000 --- a/gostream/ffmpeg/include/libavutil/parseutils.h +++ /dev/null @@ -1,197 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_PARSEUTILS_H -#define AVUTIL_PARSEUTILS_H - -#include - -#include "rational.h" - -/** - * @file - * misc parsing utilities - */ - -/** - * Parse str and store the parsed ratio in q. - * - * Note that a ratio with infinite (1/0) or negative value is - * considered valid, so you should check on the returned value if you - * want to exclude those values. - * - * The undefined value can be expressed using the "0:0" string. - * - * @param[in,out] q pointer to the AVRational which will contain the ratio - * @param[in] str the string to parse: it has to be a string in the format - * num:den, a float number or an expression - * @param[in] max the maximum allowed numerator and denominator - * @param[in] log_offset log level offset which is applied to the log - * level of log_ctx - * @param[in] log_ctx parent logging context - * @return >= 0 on success, a negative error code otherwise - */ -int av_parse_ratio(AVRational *q, const char *str, int max, - int log_offset, void *log_ctx); - -#define av_parse_ratio_quiet(rate, str, max) \ - av_parse_ratio(rate, str, max, AV_LOG_MAX_OFFSET, NULL) - -/** - * Parse str and put in width_ptr and height_ptr the detected values. - * - * @param[in,out] width_ptr pointer to the variable which will contain the detected - * width value - * @param[in,out] height_ptr pointer to the variable which will contain the detected - * height value - * @param[in] str the string to parse: it has to be a string in the format - * width x height or a valid video size abbreviation. - * @return >= 0 on success, a negative error code otherwise - */ -int av_parse_video_size(int *width_ptr, int *height_ptr, const char *str); - -/** - * Parse str and store the detected values in *rate. - * - * @param[in,out] rate pointer to the AVRational which will contain the detected - * frame rate - * @param[in] str the string to parse: it has to be a string in the format - * rate_num / rate_den, a float number or a valid video rate abbreviation - * @return >= 0 on success, a negative error code otherwise - */ -int av_parse_video_rate(AVRational *rate, const char *str); - -/** - * Put the RGBA values that correspond to color_string in rgba_color. - * - * @param rgba_color 4-elements array of uint8_t values, where the respective - * red, green, blue and alpha component values are written. - * @param color_string a string specifying a color. It can be the name of - * a color (case insensitive match) or a [0x|#]RRGGBB[AA] sequence, - * possibly followed by "@" and a string representing the alpha - * component. - * The alpha component may be a string composed by "0x" followed by an - * hexadecimal number or a decimal number between 0.0 and 1.0, which - * represents the opacity value (0x00/0.0 means completely transparent, - * 0xff/1.0 completely opaque). - * If the alpha component is not specified then 0xff is assumed. - * The string "random" will result in a random color. - * @param slen length of the initial part of color_string containing the - * color. It can be set to -1 if color_string is a null terminated string - * containing nothing else than the color. - * @param log_ctx a pointer to an arbitrary struct of which the first field - * is a pointer to an AVClass struct (used for av_log()). Can be NULL. - * @return >= 0 in case of success, a negative value in case of - * failure (for example if color_string cannot be parsed). - */ -int av_parse_color(uint8_t *rgba_color, const char *color_string, int slen, - void *log_ctx); - -/** - * Get the name of a color from the internal table of hard-coded named - * colors. - * - * This function is meant to enumerate the color names recognized by - * av_parse_color(). - * - * @param color_idx index of the requested color, starting from 0 - * @param rgb if not NULL, will point to a 3-elements array with the color value in RGB - * @return the color name string or NULL if color_idx is not in the array - */ -const char *av_get_known_color_name(int color_idx, const uint8_t **rgb); - -/** - * Parse timestr and return in *time a corresponding number of - * microseconds. - * - * @param timeval puts here the number of microseconds corresponding - * to the string in timestr. If the string represents a duration, it - * is the number of microseconds contained in the time interval. If - * the string is a date, is the number of microseconds since 1st of - * January, 1970 up to the time of the parsed date. If timestr cannot - * be successfully parsed, set *time to INT64_MIN. - - * @param timestr a string representing a date or a duration. - * - If a date the syntax is: - * @code - * [{YYYY-MM-DD|YYYYMMDD}[T|t| ]]{{HH:MM:SS[.m...]]]}|{HHMMSS[.m...]]]}}[Z] - * now - * @endcode - * If the value is "now" it takes the current time. - * Time is local time unless Z is appended, in which case it is - * interpreted as UTC. - * If the year-month-day part is not specified it takes the current - * year-month-day. - * - If a duration the syntax is: - * @code - * [-][HH:]MM:SS[.m...] - * [-]S+[.m...] - * @endcode - * @param duration flag which tells how to interpret timestr, if not - * zero timestr is interpreted as a duration, otherwise as a date - * @return >= 0 in case of success, a negative value corresponding to an - * AVERROR code otherwise - */ -int av_parse_time(int64_t *timeval, const char *timestr, int duration); - -/** - * Attempt to find a specific tag in a URL. - * - * syntax: '?tag1=val1&tag2=val2...'. Little URL decoding is done. - * Return 1 if found. - */ -int av_find_info_tag(char *arg, int arg_size, const char *tag1, const char *info); - -/** - * Simplified version of strptime - * - * Parse the input string p according to the format string fmt and - * store its results in the structure dt. - * This implementation supports only a subset of the formats supported - * by the standard strptime(). - * - * The supported input field descriptors are listed below. - * - `%%H`: the hour as a decimal number, using a 24-hour clock, in the - * range '00' through '23' - * - `%%J`: hours as a decimal number, in the range '0' through INT_MAX - * - `%%M`: the minute as a decimal number, using a 24-hour clock, in the - * range '00' through '59' - * - `%%S`: the second as a decimal number, using a 24-hour clock, in the - * range '00' through '59' - * - `%%Y`: the year as a decimal number, using the Gregorian calendar - * - `%%m`: the month as a decimal number, in the range '1' through '12' - * - `%%d`: the day of the month as a decimal number, in the range '1' - * through '31' - * - `%%T`: alias for `%%H:%%M:%%S` - * - `%%`: a literal `%` - * - * @return a pointer to the first character not processed in this function - * call. In case the input string contains more characters than - * required by the format string the return value points right after - * the last consumed input character. In case the whole input string - * is consumed the return value points to the null byte at the end of - * the string. On failure NULL is returned. - */ -char *av_small_strptime(const char *p, const char *fmt, struct tm *dt); - -/** - * Convert the decomposed UTC time in tm to a time_t value. - */ -time_t av_timegm(struct tm *tm); - -#endif /* AVUTIL_PARSEUTILS_H */ diff --git a/gostream/ffmpeg/include/libavutil/pixdesc.h b/gostream/ffmpeg/include/libavutil/pixdesc.h deleted file mode 100644 index 0df73e6efe3..00000000000 --- a/gostream/ffmpeg/include/libavutil/pixdesc.h +++ /dev/null @@ -1,435 +0,0 @@ -/* - * pixel format descriptor - * Copyright (c) 2009 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_PIXDESC_H -#define AVUTIL_PIXDESC_H - -#include - -#include "attributes.h" -#include "pixfmt.h" - -typedef struct AVComponentDescriptor { - /** - * Which of the 4 planes contains the component. - */ - int plane; - - /** - * Number of elements between 2 horizontally consecutive pixels. - * Elements are bits for bitstream formats, bytes otherwise. - */ - int step; - - /** - * Number of elements before the component of the first pixel. - * Elements are bits for bitstream formats, bytes otherwise. - */ - int offset; - - /** - * Number of least significant bits that must be shifted away - * to get the value. - */ - int shift; - - /** - * Number of bits in the component. - */ - int depth; -} AVComponentDescriptor; - -/** - * Descriptor that unambiguously describes how the bits of a pixel are - * stored in the up to 4 data planes of an image. It also stores the - * subsampling factors and number of components. - * - * @note This is separate of the colorspace (RGB, YCbCr, YPbPr, JPEG-style YUV - * and all the YUV variants) AVPixFmtDescriptor just stores how values - * are stored not what these values represent. - */ -typedef struct AVPixFmtDescriptor { - const char *name; - uint8_t nb_components; ///< The number of components each pixel has, (1-4) - - /** - * Amount to shift the luma width right to find the chroma width. - * For YV12 this is 1 for example. - * chroma_width = AV_CEIL_RSHIFT(luma_width, log2_chroma_w) - * The note above is needed to ensure rounding up. - * This value only refers to the chroma components. - */ - uint8_t log2_chroma_w; - - /** - * Amount to shift the luma height right to find the chroma height. - * For YV12 this is 1 for example. - * chroma_height= AV_CEIL_RSHIFT(luma_height, log2_chroma_h) - * The note above is needed to ensure rounding up. - * This value only refers to the chroma components. - */ - uint8_t log2_chroma_h; - - /** - * Combination of AV_PIX_FMT_FLAG_... flags. - */ - uint64_t flags; - - /** - * Parameters that describe how pixels are packed. - * If the format has 1 or 2 components, then luma is 0. - * If the format has 3 or 4 components: - * if the RGB flag is set then 0 is red, 1 is green and 2 is blue; - * otherwise 0 is luma, 1 is chroma-U and 2 is chroma-V. - * - * If present, the Alpha channel is always the last component. - */ - AVComponentDescriptor comp[4]; - - /** - * Alternative comma-separated names. - */ - const char *alias; -} AVPixFmtDescriptor; - -/** - * Pixel format is big-endian. - */ -#define AV_PIX_FMT_FLAG_BE (1 << 0) -/** - * Pixel format has a palette in data[1], values are indexes in this palette. - */ -#define AV_PIX_FMT_FLAG_PAL (1 << 1) -/** - * All values of a component are bit-wise packed end to end. - */ -#define AV_PIX_FMT_FLAG_BITSTREAM (1 << 2) -/** - * Pixel format is an HW accelerated format. - */ -#define AV_PIX_FMT_FLAG_HWACCEL (1 << 3) -/** - * At least one pixel component is not in the first data plane. - */ -#define AV_PIX_FMT_FLAG_PLANAR (1 << 4) -/** - * The pixel format contains RGB-like data (as opposed to YUV/grayscale). - */ -#define AV_PIX_FMT_FLAG_RGB (1 << 5) - -/** - * The pixel format has an alpha channel. This is set on all formats that - * support alpha in some way, including AV_PIX_FMT_PAL8. The alpha is always - * straight, never pre-multiplied. - * - * If a codec or a filter does not support alpha, it should set all alpha to - * opaque, or use the equivalent pixel formats without alpha component, e.g. - * AV_PIX_FMT_RGB0 (or AV_PIX_FMT_RGB24 etc.) instead of AV_PIX_FMT_RGBA. - */ -#define AV_PIX_FMT_FLAG_ALPHA (1 << 7) - -/** - * The pixel format is following a Bayer pattern - */ -#define AV_PIX_FMT_FLAG_BAYER (1 << 8) - -/** - * The pixel format contains IEEE-754 floating point values. Precision (double, - * single, or half) should be determined by the pixel size (64, 32, or 16 bits). - */ -#define AV_PIX_FMT_FLAG_FLOAT (1 << 9) - -/** - * Return the number of bits per pixel used by the pixel format - * described by pixdesc. Note that this is not the same as the number - * of bits per sample. - * - * The returned number of bits refers to the number of bits actually - * used for storing the pixel information, that is padding bits are - * not counted. - */ -int av_get_bits_per_pixel(const AVPixFmtDescriptor *pixdesc); - -/** - * Return the number of bits per pixel for the pixel format - * described by pixdesc, including any padding or unused bits. - */ -int av_get_padded_bits_per_pixel(const AVPixFmtDescriptor *pixdesc); - -/** - * @return a pixel format descriptor for provided pixel format or NULL if - * this pixel format is unknown. - */ -const AVPixFmtDescriptor *av_pix_fmt_desc_get(enum AVPixelFormat pix_fmt); - -/** - * Iterate over all pixel format descriptors known to libavutil. - * - * @param prev previous descriptor. NULL to get the first descriptor. - * - * @return next descriptor or NULL after the last descriptor - */ -const AVPixFmtDescriptor *av_pix_fmt_desc_next(const AVPixFmtDescriptor *prev); - -/** - * @return an AVPixelFormat id described by desc, or AV_PIX_FMT_NONE if desc - * is not a valid pointer to a pixel format descriptor. - */ -enum AVPixelFormat av_pix_fmt_desc_get_id(const AVPixFmtDescriptor *desc); - -/** - * Utility function to access log2_chroma_w log2_chroma_h from - * the pixel format AVPixFmtDescriptor. - * - * @param[in] pix_fmt the pixel format - * @param[out] h_shift store log2_chroma_w (horizontal/width shift) - * @param[out] v_shift store log2_chroma_h (vertical/height shift) - * - * @return 0 on success, AVERROR(ENOSYS) on invalid or unknown pixel format - */ -int av_pix_fmt_get_chroma_sub_sample(enum AVPixelFormat pix_fmt, - int *h_shift, int *v_shift); - -/** - * @return number of planes in pix_fmt, a negative AVERROR if pix_fmt is not a - * valid pixel format. - */ -int av_pix_fmt_count_planes(enum AVPixelFormat pix_fmt); - -/** - * @return the name for provided color range or NULL if unknown. - */ -const char *av_color_range_name(enum AVColorRange range); - -/** - * @return the AVColorRange value for name or an AVError if not found. - */ -int av_color_range_from_name(const char *name); - -/** - * @return the name for provided color primaries or NULL if unknown. - */ -const char *av_color_primaries_name(enum AVColorPrimaries primaries); - -/** - * @return the AVColorPrimaries value for name or an AVError if not found. - */ -int av_color_primaries_from_name(const char *name); - -/** - * @return the name for provided color transfer or NULL if unknown. - */ -const char *av_color_transfer_name(enum AVColorTransferCharacteristic transfer); - -/** - * @return the AVColorTransferCharacteristic value for name or an AVError if not found. - */ -int av_color_transfer_from_name(const char *name); - -/** - * @return the name for provided color space or NULL if unknown. - */ -const char *av_color_space_name(enum AVColorSpace space); - -/** - * @return the AVColorSpace value for name or an AVError if not found. - */ -int av_color_space_from_name(const char *name); - -/** - * @return the name for provided chroma location or NULL if unknown. - */ -const char *av_chroma_location_name(enum AVChromaLocation location); - -/** - * @return the AVChromaLocation value for name or an AVError if not found. - */ -int av_chroma_location_from_name(const char *name); - -/** - * Converts AVChromaLocation to swscale x/y chroma position. - * - * The positions represent the chroma (0,0) position in a coordinates system - * with luma (0,0) representing the origin and luma(1,1) representing 256,256 - * - * @param xpos horizontal chroma sample position - * @param ypos vertical chroma sample position - */ -int av_chroma_location_enum_to_pos(int *xpos, int *ypos, enum AVChromaLocation pos); - -/** - * Converts swscale x/y chroma position to AVChromaLocation. - * - * The positions represent the chroma (0,0) position in a coordinates system - * with luma (0,0) representing the origin and luma(1,1) representing 256,256 - * - * @param xpos horizontal chroma sample position - * @param ypos vertical chroma sample position - */ -enum AVChromaLocation av_chroma_location_pos_to_enum(int xpos, int ypos); - -/** - * Return the pixel format corresponding to name. - * - * If there is no pixel format with name name, then looks for a - * pixel format with the name corresponding to the native endian - * format of name. - * For example in a little-endian system, first looks for "gray16", - * then for "gray16le". - * - * Finally if no pixel format has been found, returns AV_PIX_FMT_NONE. - */ -enum AVPixelFormat av_get_pix_fmt(const char *name); - -/** - * Return the short name for a pixel format, NULL in case pix_fmt is - * unknown. - * - * @see av_get_pix_fmt(), av_get_pix_fmt_string() - */ -const char *av_get_pix_fmt_name(enum AVPixelFormat pix_fmt); - -/** - * Print in buf the string corresponding to the pixel format with - * number pix_fmt, or a header if pix_fmt is negative. - * - * @param buf the buffer where to write the string - * @param buf_size the size of buf - * @param pix_fmt the number of the pixel format to print the - * corresponding info string, or a negative value to print the - * corresponding header. - */ -char *av_get_pix_fmt_string(char *buf, int buf_size, - enum AVPixelFormat pix_fmt); - -/** - * Read a line from an image, and write the values of the - * pixel format component c to dst. - * - * @param data the array containing the pointers to the planes of the image - * @param linesize the array containing the linesizes of the image - * @param desc the pixel format descriptor for the image - * @param x the horizontal coordinate of the first pixel to read - * @param y the vertical coordinate of the first pixel to read - * @param w the width of the line to read, that is the number of - * values to write to dst - * @param read_pal_component if not zero and the format is a paletted - * format writes the values corresponding to the palette - * component c in data[1] to dst, rather than the palette indexes in - * data[0]. The behavior is undefined if the format is not paletted. - * @param dst_element_size size of elements in dst array (2 or 4 byte) - */ -void av_read_image_line2(void *dst, const uint8_t *data[4], - const int linesize[4], const AVPixFmtDescriptor *desc, - int x, int y, int c, int w, int read_pal_component, - int dst_element_size); - -void av_read_image_line(uint16_t *dst, const uint8_t *data[4], - const int linesize[4], const AVPixFmtDescriptor *desc, - int x, int y, int c, int w, int read_pal_component); - -/** - * Write the values from src to the pixel format component c of an - * image line. - * - * @param src array containing the values to write - * @param data the array containing the pointers to the planes of the - * image to write into. It is supposed to be zeroed. - * @param linesize the array containing the linesizes of the image - * @param desc the pixel format descriptor for the image - * @param x the horizontal coordinate of the first pixel to write - * @param y the vertical coordinate of the first pixel to write - * @param w the width of the line to write, that is the number of - * values to write to the image line - * @param src_element_size size of elements in src array (2 or 4 byte) - */ -void av_write_image_line2(const void *src, uint8_t *data[4], - const int linesize[4], const AVPixFmtDescriptor *desc, - int x, int y, int c, int w, int src_element_size); - -void av_write_image_line(const uint16_t *src, uint8_t *data[4], - const int linesize[4], const AVPixFmtDescriptor *desc, - int x, int y, int c, int w); - -/** - * Utility function to swap the endianness of a pixel format. - * - * @param[in] pix_fmt the pixel format - * - * @return pixel format with swapped endianness if it exists, - * otherwise AV_PIX_FMT_NONE - */ -enum AVPixelFormat av_pix_fmt_swap_endianness(enum AVPixelFormat pix_fmt); - -#define FF_LOSS_RESOLUTION 0x0001 /**< loss due to resolution change */ -#define FF_LOSS_DEPTH 0x0002 /**< loss due to color depth change */ -#define FF_LOSS_COLORSPACE 0x0004 /**< loss due to color space conversion */ -#define FF_LOSS_ALPHA 0x0008 /**< loss of alpha bits */ -#define FF_LOSS_COLORQUANT 0x0010 /**< loss due to color quantization */ -#define FF_LOSS_CHROMA 0x0020 /**< loss of chroma (e.g. RGB to gray conversion) */ -#define FF_LOSS_EXCESS_RESOLUTION 0x0040 /**< loss due to unneeded extra resolution */ -#define FF_LOSS_EXCESS_DEPTH 0x0080 /**< loss due to unneeded extra color depth */ - - -/** - * Compute what kind of losses will occur when converting from one specific - * pixel format to another. - * When converting from one pixel format to another, information loss may occur. - * For example, when converting from RGB24 to GRAY, the color information will - * be lost. Similarly, other losses occur when converting from some formats to - * other formats. These losses can involve loss of chroma, but also loss of - * resolution, loss of color depth, loss due to the color space conversion, loss - * of the alpha bits or loss due to color quantization. - * av_get_fix_fmt_loss() informs you about the various types of losses - * which will occur when converting from one pixel format to another. - * - * @param[in] dst_pix_fmt destination pixel format - * @param[in] src_pix_fmt source pixel format - * @param[in] has_alpha Whether the source pixel format alpha channel is used. - * @return Combination of flags informing you what kind of losses will occur - * (maximum loss for an invalid dst_pix_fmt). - */ -int av_get_pix_fmt_loss(enum AVPixelFormat dst_pix_fmt, - enum AVPixelFormat src_pix_fmt, - int has_alpha); - -/** - * Compute what kind of losses will occur when converting from one specific - * pixel format to another. - * When converting from one pixel format to another, information loss may occur. - * For example, when converting from RGB24 to GRAY, the color information will - * be lost. Similarly, other losses occur when converting from some formats to - * other formats. These losses can involve loss of chroma, but also loss of - * resolution, loss of color depth, loss due to the color space conversion, loss - * of the alpha bits or loss due to color quantization. - * av_get_fix_fmt_loss() informs you about the various types of losses - * which will occur when converting from one pixel format to another. - * - * @param[in] dst_pix_fmt destination pixel format - * @param[in] src_pix_fmt source pixel format - * @param[in] has_alpha Whether the source pixel format alpha channel is used. - * @return Combination of flags informing you what kind of losses will occur - * (maximum loss for an invalid dst_pix_fmt). - */ -enum AVPixelFormat av_find_best_pix_fmt_of_2(enum AVPixelFormat dst_pix_fmt1, enum AVPixelFormat dst_pix_fmt2, - enum AVPixelFormat src_pix_fmt, int has_alpha, int *loss_ptr); - -#endif /* AVUTIL_PIXDESC_H */ diff --git a/gostream/ffmpeg/include/libavutil/pixelutils.h b/gostream/ffmpeg/include/libavutil/pixelutils.h deleted file mode 100644 index 7a997cde1c8..00000000000 --- a/gostream/ffmpeg/include/libavutil/pixelutils.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_PIXELUTILS_H -#define AVUTIL_PIXELUTILS_H - -#include -#include - -/** - * Sum of abs(src1[x] - src2[x]) - */ -typedef int (*av_pixelutils_sad_fn)(const uint8_t *src1, ptrdiff_t stride1, - const uint8_t *src2, ptrdiff_t stride2); - -/** - * Get a potentially optimized pointer to a Sum-of-absolute-differences - * function (see the av_pixelutils_sad_fn prototype). - * - * @param w_bits 1< - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_PIXFMT_H -#define AVUTIL_PIXFMT_H - -/** - * @file - * pixel format definitions - */ - -#include "libavutil/avconfig.h" -#include "version.h" - -#define AVPALETTE_SIZE 1024 -#define AVPALETTE_COUNT 256 - -/** - * Pixel format. - * - * @note - * AV_PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA - * color is put together as: - * (A << 24) | (R << 16) | (G << 8) | B - * This is stored as BGRA on little-endian CPU architectures and ARGB on - * big-endian CPUs. - * - * @note - * If the resolution is not a multiple of the chroma subsampling factor - * then the chroma plane resolution must be rounded up. - * - * @par - * When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized - * image data is stored in AVFrame.data[0]. The palette is transported in - * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is - * formatted the same as in AV_PIX_FMT_RGB32 described above (i.e., it is - * also endian-specific). Note also that the individual RGB32 palette - * components stored in AVFrame.data[1] should be in the range 0..255. - * This is important as many custom PAL8 video codecs that were designed - * to run on the IBM VGA graphics adapter use 6-bit palette components. - * - * @par - * For all the 8 bits per pixel formats, an RGB32 palette is in data[1] like - * for pal8. This palette is filled in automatically by the function - * allocating the picture. - */ -enum AVPixelFormat { - AV_PIX_FMT_NONE = -1, - AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples) - AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr - AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB... - AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR... - AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) - AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples) - AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples) - AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) - AV_PIX_FMT_GRAY8, ///< Y , 8bpp - AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb - AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb - AV_PIX_FMT_PAL8, ///< 8 bits with AV_PIX_FMT_RGB32 palette - AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV420P and setting color_range - AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV422P and setting color_range - AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV444P and setting color_range - AV_PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1 - AV_PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3 - AV_PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb) - AV_PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits - AV_PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb) - AV_PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb) - AV_PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits - AV_PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb) - AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V) - AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped - - AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB... - AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA... - AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR... - AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA... - - AV_PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian - AV_PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian - AV_PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples) - AV_PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV440P and setting color_range - AV_PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples) - AV_PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian - AV_PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian - - AV_PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian - AV_PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian - AV_PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), big-endian , X=unused/undefined - AV_PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), little-endian, X=unused/undefined - - AV_PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian - AV_PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian - AV_PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), big-endian , X=unused/undefined - AV_PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), little-endian, X=unused/undefined - - /** - * Hardware acceleration through VA-API, data[3] contains a - * VASurfaceID. - */ - AV_PIX_FMT_VAAPI, - - AV_PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian - AV_PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian - AV_PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian - AV_PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian - AV_PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian - AV_PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian - AV_PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer - - AV_PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), little-endian, X=unused/undefined - AV_PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), big-endian, X=unused/undefined - AV_PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), little-endian, X=unused/undefined - AV_PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), big-endian, X=unused/undefined - AV_PIX_FMT_YA8, ///< 8 bits gray, 8 bits alpha - - AV_PIX_FMT_Y400A = AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8 - AV_PIX_FMT_GRAY8A= AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8 - - AV_PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian - AV_PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian - - /** - * The following 12 formats have the disadvantage of needing 1 format for each bit depth. - * Notice that each 9/10 bits sample is stored in 16 bits with extra padding. - * If you want to support multiple bit depths, then using AV_PIX_FMT_YUV420P16* with the bpp stored separately is better. - */ - AV_PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian - AV_PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian - AV_PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian - AV_PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian - AV_PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian - AV_PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian - AV_PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian - AV_PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian - AV_PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian - AV_PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian - AV_PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian - AV_PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian - AV_PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp - AV_PIX_FMT_GBR24P = AV_PIX_FMT_GBRP, // alias for #AV_PIX_FMT_GBRP - AV_PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big-endian - AV_PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little-endian - AV_PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big-endian - AV_PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little-endian - AV_PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big-endian - AV_PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little-endian - AV_PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y & A samples) - AV_PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y & A samples) - AV_PIX_FMT_YUVA420P9BE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), big-endian - AV_PIX_FMT_YUVA420P9LE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), little-endian - AV_PIX_FMT_YUVA422P9BE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), big-endian - AV_PIX_FMT_YUVA422P9LE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), little-endian - AV_PIX_FMT_YUVA444P9BE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), big-endian - AV_PIX_FMT_YUVA444P9LE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), little-endian - AV_PIX_FMT_YUVA420P10BE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian) - AV_PIX_FMT_YUVA420P10LE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian) - AV_PIX_FMT_YUVA422P10BE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian) - AV_PIX_FMT_YUVA422P10LE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian) - AV_PIX_FMT_YUVA444P10BE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian) - AV_PIX_FMT_YUVA444P10LE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian) - AV_PIX_FMT_YUVA420P16BE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian) - AV_PIX_FMT_YUVA420P16LE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian) - AV_PIX_FMT_YUVA422P16BE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian) - AV_PIX_FMT_YUVA422P16LE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian) - AV_PIX_FMT_YUVA444P16BE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian) - AV_PIX_FMT_YUVA444P16LE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian) - - AV_PIX_FMT_VDPAU, ///< HW acceleration through VDPAU, Picture.data[3] contains a VdpVideoSurface - - AV_PIX_FMT_XYZ12LE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as little-endian, the 4 lower bits are set to 0 - AV_PIX_FMT_XYZ12BE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as big-endian, the 4 lower bits are set to 0 - AV_PIX_FMT_NV16, ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples) - AV_PIX_FMT_NV20LE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian - AV_PIX_FMT_NV20BE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian - - AV_PIX_FMT_RGBA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian - AV_PIX_FMT_RGBA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian - AV_PIX_FMT_BGRA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian - AV_PIX_FMT_BGRA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian - - AV_PIX_FMT_YVYU422, ///< packed YUV 4:2:2, 16bpp, Y0 Cr Y1 Cb - - AV_PIX_FMT_YA16BE, ///< 16 bits gray, 16 bits alpha (big-endian) - AV_PIX_FMT_YA16LE, ///< 16 bits gray, 16 bits alpha (little-endian) - - AV_PIX_FMT_GBRAP, ///< planar GBRA 4:4:4:4 32bpp - AV_PIX_FMT_GBRAP16BE, ///< planar GBRA 4:4:4:4 64bpp, big-endian - AV_PIX_FMT_GBRAP16LE, ///< planar GBRA 4:4:4:4 64bpp, little-endian - /** - * HW acceleration through QSV, data[3] contains a pointer to the - * mfxFrameSurface1 structure. - * - * Before FFmpeg 5.0: - * mfxFrameSurface1.Data.MemId contains a pointer when importing - * the following frames as QSV frames: - * - * VAAPI: - * mfxFrameSurface1.Data.MemId contains a pointer to VASurfaceID - * - * DXVA2: - * mfxFrameSurface1.Data.MemId contains a pointer to IDirect3DSurface9 - * - * FFmpeg 5.0 and above: - * mfxFrameSurface1.Data.MemId contains a pointer to the mfxHDLPair - * structure when importing the following frames as QSV frames: - * - * VAAPI: - * mfxHDLPair.first contains a VASurfaceID pointer. - * mfxHDLPair.second is always MFX_INFINITE. - * - * DXVA2: - * mfxHDLPair.first contains IDirect3DSurface9 pointer. - * mfxHDLPair.second is always MFX_INFINITE. - * - * D3D11: - * mfxHDLPair.first contains a ID3D11Texture2D pointer. - * mfxHDLPair.second contains the texture array index of the frame if the - * ID3D11Texture2D is an array texture, or always MFX_INFINITE if it is a - * normal texture. - */ - AV_PIX_FMT_QSV, - /** - * HW acceleration though MMAL, data[3] contains a pointer to the - * MMAL_BUFFER_HEADER_T structure. - */ - AV_PIX_FMT_MMAL, - - AV_PIX_FMT_D3D11VA_VLD, ///< HW decoding through Direct3D11 via old API, Picture.data[3] contains a ID3D11VideoDecoderOutputView pointer - - /** - * HW acceleration through CUDA. data[i] contain CUdeviceptr pointers - * exactly as for system memory frames. - */ - AV_PIX_FMT_CUDA, - - AV_PIX_FMT_0RGB, ///< packed RGB 8:8:8, 32bpp, XRGBXRGB... X=unused/undefined - AV_PIX_FMT_RGB0, ///< packed RGB 8:8:8, 32bpp, RGBXRGBX... X=unused/undefined - AV_PIX_FMT_0BGR, ///< packed BGR 8:8:8, 32bpp, XBGRXBGR... X=unused/undefined - AV_PIX_FMT_BGR0, ///< packed BGR 8:8:8, 32bpp, BGRXBGRX... X=unused/undefined - - AV_PIX_FMT_YUV420P12BE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian - AV_PIX_FMT_YUV420P12LE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian - AV_PIX_FMT_YUV420P14BE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian - AV_PIX_FMT_YUV420P14LE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian - AV_PIX_FMT_YUV422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian - AV_PIX_FMT_YUV422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian - AV_PIX_FMT_YUV422P14BE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian - AV_PIX_FMT_YUV422P14LE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian - AV_PIX_FMT_YUV444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian - AV_PIX_FMT_YUV444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian - AV_PIX_FMT_YUV444P14BE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian - AV_PIX_FMT_YUV444P14LE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian - AV_PIX_FMT_GBRP12BE, ///< planar GBR 4:4:4 36bpp, big-endian - AV_PIX_FMT_GBRP12LE, ///< planar GBR 4:4:4 36bpp, little-endian - AV_PIX_FMT_GBRP14BE, ///< planar GBR 4:4:4 42bpp, big-endian - AV_PIX_FMT_GBRP14LE, ///< planar GBR 4:4:4 42bpp, little-endian - AV_PIX_FMT_YUVJ411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV411P and setting color_range - - AV_PIX_FMT_BAYER_BGGR8, ///< bayer, BGBG..(odd line), GRGR..(even line), 8-bit samples - AV_PIX_FMT_BAYER_RGGB8, ///< bayer, RGRG..(odd line), GBGB..(even line), 8-bit samples - AV_PIX_FMT_BAYER_GBRG8, ///< bayer, GBGB..(odd line), RGRG..(even line), 8-bit samples - AV_PIX_FMT_BAYER_GRBG8, ///< bayer, GRGR..(odd line), BGBG..(even line), 8-bit samples - AV_PIX_FMT_BAYER_BGGR16LE, ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, little-endian - AV_PIX_FMT_BAYER_BGGR16BE, ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, big-endian - AV_PIX_FMT_BAYER_RGGB16LE, ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, little-endian - AV_PIX_FMT_BAYER_RGGB16BE, ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, big-endian - AV_PIX_FMT_BAYER_GBRG16LE, ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, little-endian - AV_PIX_FMT_BAYER_GBRG16BE, ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, big-endian - AV_PIX_FMT_BAYER_GRBG16LE, ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, little-endian - AV_PIX_FMT_BAYER_GRBG16BE, ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, big-endian - -#if FF_API_XVMC - AV_PIX_FMT_XVMC,///< XVideo Motion Acceleration via common packet passing -#endif - - AV_PIX_FMT_YUV440P10LE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian - AV_PIX_FMT_YUV440P10BE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian - AV_PIX_FMT_YUV440P12LE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian - AV_PIX_FMT_YUV440P12BE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian - AV_PIX_FMT_AYUV64LE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), little-endian - AV_PIX_FMT_AYUV64BE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), big-endian - - AV_PIX_FMT_VIDEOTOOLBOX, ///< hardware decoding through Videotoolbox - - AV_PIX_FMT_P010LE, ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, little-endian - AV_PIX_FMT_P010BE, ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, big-endian - - AV_PIX_FMT_GBRAP12BE, ///< planar GBR 4:4:4:4 48bpp, big-endian - AV_PIX_FMT_GBRAP12LE, ///< planar GBR 4:4:4:4 48bpp, little-endian - - AV_PIX_FMT_GBRAP10BE, ///< planar GBR 4:4:4:4 40bpp, big-endian - AV_PIX_FMT_GBRAP10LE, ///< planar GBR 4:4:4:4 40bpp, little-endian - - AV_PIX_FMT_MEDIACODEC, ///< hardware decoding through MediaCodec - - AV_PIX_FMT_GRAY12BE, ///< Y , 12bpp, big-endian - AV_PIX_FMT_GRAY12LE, ///< Y , 12bpp, little-endian - AV_PIX_FMT_GRAY10BE, ///< Y , 10bpp, big-endian - AV_PIX_FMT_GRAY10LE, ///< Y , 10bpp, little-endian - - AV_PIX_FMT_P016LE, ///< like NV12, with 16bpp per component, little-endian - AV_PIX_FMT_P016BE, ///< like NV12, with 16bpp per component, big-endian - - /** - * Hardware surfaces for Direct3D11. - * - * This is preferred over the legacy AV_PIX_FMT_D3D11VA_VLD. The new D3D11 - * hwaccel API and filtering support AV_PIX_FMT_D3D11 only. - * - * data[0] contains a ID3D11Texture2D pointer, and data[1] contains the - * texture array index of the frame as intptr_t if the ID3D11Texture2D is - * an array texture (or always 0 if it's a normal texture). - */ - AV_PIX_FMT_D3D11, - - AV_PIX_FMT_GRAY9BE, ///< Y , 9bpp, big-endian - AV_PIX_FMT_GRAY9LE, ///< Y , 9bpp, little-endian - - AV_PIX_FMT_GBRPF32BE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp, big-endian - AV_PIX_FMT_GBRPF32LE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp, little-endian - AV_PIX_FMT_GBRAPF32BE, ///< IEEE-754 single precision planar GBRA 4:4:4:4, 128bpp, big-endian - AV_PIX_FMT_GBRAPF32LE, ///< IEEE-754 single precision planar GBRA 4:4:4:4, 128bpp, little-endian - - /** - * DRM-managed buffers exposed through PRIME buffer sharing. - * - * data[0] points to an AVDRMFrameDescriptor. - */ - AV_PIX_FMT_DRM_PRIME, - /** - * Hardware surfaces for OpenCL. - * - * data[i] contain 2D image objects (typed in C as cl_mem, used - * in OpenCL as image2d_t) for each plane of the surface. - */ - AV_PIX_FMT_OPENCL, - - AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian - AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian - - AV_PIX_FMT_GRAYF32BE, ///< IEEE-754 single precision Y, 32bpp, big-endian - AV_PIX_FMT_GRAYF32LE, ///< IEEE-754 single precision Y, 32bpp, little-endian - - AV_PIX_FMT_YUVA422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), 12b alpha, big-endian - AV_PIX_FMT_YUVA422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), 12b alpha, little-endian - AV_PIX_FMT_YUVA444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), 12b alpha, big-endian - AV_PIX_FMT_YUVA444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), 12b alpha, little-endian - - AV_PIX_FMT_NV24, ///< planar YUV 4:4:4, 24bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V) - AV_PIX_FMT_NV42, ///< as above, but U and V bytes are swapped - - /** - * Vulkan hardware images. - * - * data[0] points to an AVVkFrame - */ - AV_PIX_FMT_VULKAN, - - AV_PIX_FMT_Y210BE, ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the high bits, big-endian - AV_PIX_FMT_Y210LE, ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the high bits, little-endian - - AV_PIX_FMT_X2RGB10LE, ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G 10B(lsb), little-endian, X=unused/undefined - AV_PIX_FMT_X2RGB10BE, ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G 10B(lsb), big-endian, X=unused/undefined - AV_PIX_FMT_X2BGR10LE, ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G 10R(lsb), little-endian, X=unused/undefined - AV_PIX_FMT_X2BGR10BE, ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G 10R(lsb), big-endian, X=unused/undefined - - AV_PIX_FMT_P210BE, ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high bits, big-endian - AV_PIX_FMT_P210LE, ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high bits, little-endian - - AV_PIX_FMT_P410BE, ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high bits, big-endian - AV_PIX_FMT_P410LE, ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high bits, little-endian - - AV_PIX_FMT_P216BE, ///< interleaved chroma YUV 4:2:2, 32bpp, big-endian - AV_PIX_FMT_P216LE, ///< interleaved chroma YUV 4:2:2, 32bpp, little-endian - - AV_PIX_FMT_P416BE, ///< interleaved chroma YUV 4:4:4, 48bpp, big-endian - AV_PIX_FMT_P416LE, ///< interleaved chroma YUV 4:4:4, 48bpp, little-endian - - AV_PIX_FMT_VUYA, ///< packed VUYA 4:4:4, 32bpp, VUYAVUYA... - - AV_PIX_FMT_RGBAF16BE, ///< IEEE-754 half precision packed RGBA 16:16:16:16, 64bpp, RGBARGBA..., big-endian - AV_PIX_FMT_RGBAF16LE, ///< IEEE-754 half precision packed RGBA 16:16:16:16, 64bpp, RGBARGBA..., little-endian - - AV_PIX_FMT_VUYX, ///< packed VUYX 4:4:4, 32bpp, Variant of VUYA where alpha channel is left undefined - - AV_PIX_FMT_P012LE, ///< like NV12, with 12bpp per component, data in the high bits, zeros in the low bits, little-endian - AV_PIX_FMT_P012BE, ///< like NV12, with 12bpp per component, data in the high bits, zeros in the low bits, big-endian - - AV_PIX_FMT_Y212BE, ///< packed YUV 4:2:2 like YUYV422, 24bpp, data in the high bits, zeros in the low bits, big-endian - AV_PIX_FMT_Y212LE, ///< packed YUV 4:2:2 like YUYV422, 24bpp, data in the high bits, zeros in the low bits, little-endian - - AV_PIX_FMT_XV30BE, ///< packed XVYU 4:4:4, 32bpp, (msb)2X 10V 10Y 10U(lsb), big-endian, variant of Y410 where alpha channel is left undefined - AV_PIX_FMT_XV30LE, ///< packed XVYU 4:4:4, 32bpp, (msb)2X 10V 10Y 10U(lsb), little-endian, variant of Y410 where alpha channel is left undefined - - AV_PIX_FMT_XV36BE, ///< packed XVYU 4:4:4, 48bpp, data in the high bits, zeros in the low bits, big-endian, variant of Y412 where alpha channel is left undefined - AV_PIX_FMT_XV36LE, ///< packed XVYU 4:4:4, 48bpp, data in the high bits, zeros in the low bits, little-endian, variant of Y412 where alpha channel is left undefined - - AV_PIX_FMT_RGBF32BE, ///< IEEE-754 single precision packed RGB 32:32:32, 96bpp, RGBRGB..., big-endian - AV_PIX_FMT_RGBF32LE, ///< IEEE-754 single precision packed RGB 32:32:32, 96bpp, RGBRGB..., little-endian - - AV_PIX_FMT_RGBAF32BE, ///< IEEE-754 single precision packed RGBA 32:32:32:32, 128bpp, RGBARGBA..., big-endian - AV_PIX_FMT_RGBAF32LE, ///< IEEE-754 single precision packed RGBA 32:32:32:32, 128bpp, RGBARGBA..., little-endian - - AV_PIX_FMT_P212BE, ///< interleaved chroma YUV 4:2:2, 24bpp, data in the high bits, big-endian - AV_PIX_FMT_P212LE, ///< interleaved chroma YUV 4:2:2, 24bpp, data in the high bits, little-endian - - AV_PIX_FMT_P412BE, ///< interleaved chroma YUV 4:4:4, 36bpp, data in the high bits, big-endian - AV_PIX_FMT_P412LE, ///< interleaved chroma YUV 4:4:4, 36bpp, data in the high bits, little-endian - - AV_PIX_FMT_GBRAP14BE, ///< planar GBR 4:4:4:4 56bpp, big-endian - AV_PIX_FMT_GBRAP14LE, ///< planar GBR 4:4:4:4 56bpp, little-endian - - AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions -}; - -#if AV_HAVE_BIGENDIAN -# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##be -#else -# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##le -#endif - -#define AV_PIX_FMT_RGB32 AV_PIX_FMT_NE(ARGB, BGRA) -#define AV_PIX_FMT_RGB32_1 AV_PIX_FMT_NE(RGBA, ABGR) -#define AV_PIX_FMT_BGR32 AV_PIX_FMT_NE(ABGR, RGBA) -#define AV_PIX_FMT_BGR32_1 AV_PIX_FMT_NE(BGRA, ARGB) -#define AV_PIX_FMT_0RGB32 AV_PIX_FMT_NE(0RGB, BGR0) -#define AV_PIX_FMT_0BGR32 AV_PIX_FMT_NE(0BGR, RGB0) - -#define AV_PIX_FMT_GRAY9 AV_PIX_FMT_NE(GRAY9BE, GRAY9LE) -#define AV_PIX_FMT_GRAY10 AV_PIX_FMT_NE(GRAY10BE, GRAY10LE) -#define AV_PIX_FMT_GRAY12 AV_PIX_FMT_NE(GRAY12BE, GRAY12LE) -#define AV_PIX_FMT_GRAY14 AV_PIX_FMT_NE(GRAY14BE, GRAY14LE) -#define AV_PIX_FMT_GRAY16 AV_PIX_FMT_NE(GRAY16BE, GRAY16LE) -#define AV_PIX_FMT_YA16 AV_PIX_FMT_NE(YA16BE, YA16LE) -#define AV_PIX_FMT_RGB48 AV_PIX_FMT_NE(RGB48BE, RGB48LE) -#define AV_PIX_FMT_RGB565 AV_PIX_FMT_NE(RGB565BE, RGB565LE) -#define AV_PIX_FMT_RGB555 AV_PIX_FMT_NE(RGB555BE, RGB555LE) -#define AV_PIX_FMT_RGB444 AV_PIX_FMT_NE(RGB444BE, RGB444LE) -#define AV_PIX_FMT_RGBA64 AV_PIX_FMT_NE(RGBA64BE, RGBA64LE) -#define AV_PIX_FMT_BGR48 AV_PIX_FMT_NE(BGR48BE, BGR48LE) -#define AV_PIX_FMT_BGR565 AV_PIX_FMT_NE(BGR565BE, BGR565LE) -#define AV_PIX_FMT_BGR555 AV_PIX_FMT_NE(BGR555BE, BGR555LE) -#define AV_PIX_FMT_BGR444 AV_PIX_FMT_NE(BGR444BE, BGR444LE) -#define AV_PIX_FMT_BGRA64 AV_PIX_FMT_NE(BGRA64BE, BGRA64LE) - -#define AV_PIX_FMT_YUV420P9 AV_PIX_FMT_NE(YUV420P9BE , YUV420P9LE) -#define AV_PIX_FMT_YUV422P9 AV_PIX_FMT_NE(YUV422P9BE , YUV422P9LE) -#define AV_PIX_FMT_YUV444P9 AV_PIX_FMT_NE(YUV444P9BE , YUV444P9LE) -#define AV_PIX_FMT_YUV420P10 AV_PIX_FMT_NE(YUV420P10BE, YUV420P10LE) -#define AV_PIX_FMT_YUV422P10 AV_PIX_FMT_NE(YUV422P10BE, YUV422P10LE) -#define AV_PIX_FMT_YUV440P10 AV_PIX_FMT_NE(YUV440P10BE, YUV440P10LE) -#define AV_PIX_FMT_YUV444P10 AV_PIX_FMT_NE(YUV444P10BE, YUV444P10LE) -#define AV_PIX_FMT_YUV420P12 AV_PIX_FMT_NE(YUV420P12BE, YUV420P12LE) -#define AV_PIX_FMT_YUV422P12 AV_PIX_FMT_NE(YUV422P12BE, YUV422P12LE) -#define AV_PIX_FMT_YUV440P12 AV_PIX_FMT_NE(YUV440P12BE, YUV440P12LE) -#define AV_PIX_FMT_YUV444P12 AV_PIX_FMT_NE(YUV444P12BE, YUV444P12LE) -#define AV_PIX_FMT_YUV420P14 AV_PIX_FMT_NE(YUV420P14BE, YUV420P14LE) -#define AV_PIX_FMT_YUV422P14 AV_PIX_FMT_NE(YUV422P14BE, YUV422P14LE) -#define AV_PIX_FMT_YUV444P14 AV_PIX_FMT_NE(YUV444P14BE, YUV444P14LE) -#define AV_PIX_FMT_YUV420P16 AV_PIX_FMT_NE(YUV420P16BE, YUV420P16LE) -#define AV_PIX_FMT_YUV422P16 AV_PIX_FMT_NE(YUV422P16BE, YUV422P16LE) -#define AV_PIX_FMT_YUV444P16 AV_PIX_FMT_NE(YUV444P16BE, YUV444P16LE) - -#define AV_PIX_FMT_GBRP9 AV_PIX_FMT_NE(GBRP9BE , GBRP9LE) -#define AV_PIX_FMT_GBRP10 AV_PIX_FMT_NE(GBRP10BE, GBRP10LE) -#define AV_PIX_FMT_GBRP12 AV_PIX_FMT_NE(GBRP12BE, GBRP12LE) -#define AV_PIX_FMT_GBRP14 AV_PIX_FMT_NE(GBRP14BE, GBRP14LE) -#define AV_PIX_FMT_GBRP16 AV_PIX_FMT_NE(GBRP16BE, GBRP16LE) -#define AV_PIX_FMT_GBRAP10 AV_PIX_FMT_NE(GBRAP10BE, GBRAP10LE) -#define AV_PIX_FMT_GBRAP12 AV_PIX_FMT_NE(GBRAP12BE, GBRAP12LE) -#define AV_PIX_FMT_GBRAP14 AV_PIX_FMT_NE(GBRAP14BE, GBRAP14LE) -#define AV_PIX_FMT_GBRAP16 AV_PIX_FMT_NE(GBRAP16BE, GBRAP16LE) - -#define AV_PIX_FMT_BAYER_BGGR16 AV_PIX_FMT_NE(BAYER_BGGR16BE, BAYER_BGGR16LE) -#define AV_PIX_FMT_BAYER_RGGB16 AV_PIX_FMT_NE(BAYER_RGGB16BE, BAYER_RGGB16LE) -#define AV_PIX_FMT_BAYER_GBRG16 AV_PIX_FMT_NE(BAYER_GBRG16BE, BAYER_GBRG16LE) -#define AV_PIX_FMT_BAYER_GRBG16 AV_PIX_FMT_NE(BAYER_GRBG16BE, BAYER_GRBG16LE) - -#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE) -#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE) - -#define AV_PIX_FMT_GRAYF32 AV_PIX_FMT_NE(GRAYF32BE, GRAYF32LE) - -#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE) -#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE) -#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE) -#define AV_PIX_FMT_YUVA420P10 AV_PIX_FMT_NE(YUVA420P10BE, YUVA420P10LE) -#define AV_PIX_FMT_YUVA422P10 AV_PIX_FMT_NE(YUVA422P10BE, YUVA422P10LE) -#define AV_PIX_FMT_YUVA444P10 AV_PIX_FMT_NE(YUVA444P10BE, YUVA444P10LE) -#define AV_PIX_FMT_YUVA422P12 AV_PIX_FMT_NE(YUVA422P12BE, YUVA422P12LE) -#define AV_PIX_FMT_YUVA444P12 AV_PIX_FMT_NE(YUVA444P12BE, YUVA444P12LE) -#define AV_PIX_FMT_YUVA420P16 AV_PIX_FMT_NE(YUVA420P16BE, YUVA420P16LE) -#define AV_PIX_FMT_YUVA422P16 AV_PIX_FMT_NE(YUVA422P16BE, YUVA422P16LE) -#define AV_PIX_FMT_YUVA444P16 AV_PIX_FMT_NE(YUVA444P16BE, YUVA444P16LE) - -#define AV_PIX_FMT_XYZ12 AV_PIX_FMT_NE(XYZ12BE, XYZ12LE) -#define AV_PIX_FMT_NV20 AV_PIX_FMT_NE(NV20BE, NV20LE) -#define AV_PIX_FMT_AYUV64 AV_PIX_FMT_NE(AYUV64BE, AYUV64LE) -#define AV_PIX_FMT_P010 AV_PIX_FMT_NE(P010BE, P010LE) -#define AV_PIX_FMT_P012 AV_PIX_FMT_NE(P012BE, P012LE) -#define AV_PIX_FMT_P016 AV_PIX_FMT_NE(P016BE, P016LE) - -#define AV_PIX_FMT_Y210 AV_PIX_FMT_NE(Y210BE, Y210LE) -#define AV_PIX_FMT_Y212 AV_PIX_FMT_NE(Y212BE, Y212LE) -#define AV_PIX_FMT_XV30 AV_PIX_FMT_NE(XV30BE, XV30LE) -#define AV_PIX_FMT_XV36 AV_PIX_FMT_NE(XV36BE, XV36LE) -#define AV_PIX_FMT_X2RGB10 AV_PIX_FMT_NE(X2RGB10BE, X2RGB10LE) -#define AV_PIX_FMT_X2BGR10 AV_PIX_FMT_NE(X2BGR10BE, X2BGR10LE) - -#define AV_PIX_FMT_P210 AV_PIX_FMT_NE(P210BE, P210LE) -#define AV_PIX_FMT_P410 AV_PIX_FMT_NE(P410BE, P410LE) -#define AV_PIX_FMT_P212 AV_PIX_FMT_NE(P212BE, P212LE) -#define AV_PIX_FMT_P412 AV_PIX_FMT_NE(P412BE, P412LE) -#define AV_PIX_FMT_P216 AV_PIX_FMT_NE(P216BE, P216LE) -#define AV_PIX_FMT_P416 AV_PIX_FMT_NE(P416BE, P416LE) - -#define AV_PIX_FMT_RGBAF16 AV_PIX_FMT_NE(RGBAF16BE, RGBAF16LE) - -#define AV_PIX_FMT_RGBF32 AV_PIX_FMT_NE(RGBF32BE, RGBF32LE) -#define AV_PIX_FMT_RGBAF32 AV_PIX_FMT_NE(RGBAF32BE, RGBAF32LE) - -/** - * Chromaticity coordinates of the source primaries. - * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.1 and ITU-T H.273. - */ -enum AVColorPrimaries { - AVCOL_PRI_RESERVED0 = 0, - AVCOL_PRI_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP 177 Annex B - AVCOL_PRI_UNSPECIFIED = 2, - AVCOL_PRI_RESERVED = 3, - AVCOL_PRI_BT470M = 4, ///< also FCC Title 47 Code of Federal Regulations 73.682 (a)(20) - - AVCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM - AVCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC - AVCOL_PRI_SMPTE240M = 7, ///< identical to above, also called "SMPTE C" even though it uses D65 - AVCOL_PRI_FILM = 8, ///< colour filters using Illuminant C - AVCOL_PRI_BT2020 = 9, ///< ITU-R BT2020 - AVCOL_PRI_SMPTE428 = 10, ///< SMPTE ST 428-1 (CIE 1931 XYZ) - AVCOL_PRI_SMPTEST428_1 = AVCOL_PRI_SMPTE428, - AVCOL_PRI_SMPTE431 = 11, ///< SMPTE ST 431-2 (2011) / DCI P3 - AVCOL_PRI_SMPTE432 = 12, ///< SMPTE ST 432-1 (2010) / P3 D65 / Display P3 - AVCOL_PRI_EBU3213 = 22, ///< EBU Tech. 3213-E (nothing there) / one of JEDEC P22 group phosphors - AVCOL_PRI_JEDEC_P22 = AVCOL_PRI_EBU3213, - AVCOL_PRI_NB ///< Not part of ABI -}; - -/** - * Color Transfer Characteristic. - * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.2. - */ -enum AVColorTransferCharacteristic { - AVCOL_TRC_RESERVED0 = 0, - AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361 - AVCOL_TRC_UNSPECIFIED = 2, - AVCOL_TRC_RESERVED = 3, - AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM - AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG - AVCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC - AVCOL_TRC_SMPTE240M = 7, - AVCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics" - AVCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)" - AVCOL_TRC_LOG_SQRT = 10, ///< "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)" - AVCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4 - AVCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut - AVCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC) - AVCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10-bit system - AVCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12-bit system - AVCOL_TRC_SMPTE2084 = 16, ///< SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems - AVCOL_TRC_SMPTEST2084 = AVCOL_TRC_SMPTE2084, - AVCOL_TRC_SMPTE428 = 17, ///< SMPTE ST 428-1 - AVCOL_TRC_SMPTEST428_1 = AVCOL_TRC_SMPTE428, - AVCOL_TRC_ARIB_STD_B67 = 18, ///< ARIB STD-B67, known as "Hybrid log-gamma" - AVCOL_TRC_NB ///< Not part of ABI -}; - -/** - * YUV colorspace type. - * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.3. - */ -enum AVColorSpace { - AVCOL_SPC_RGB = 0, ///< order of coefficients is actually GBR, also IEC 61966-2-1 (sRGB), YZX and ST 428-1 - AVCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / derived in SMPTE RP 177 Annex B - AVCOL_SPC_UNSPECIFIED = 2, - AVCOL_SPC_RESERVED = 3, ///< reserved for future use by ITU-T and ISO/IEC just like 15-255 are - AVCOL_SPC_FCC = 4, ///< FCC Title 47 Code of Federal Regulations 73.682 (a)(20) - AVCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601 - AVCOL_SPC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC / functionally identical to above - AVCOL_SPC_SMPTE240M = 7, ///< derived from 170M primaries and D65 white point, 170M is derived from BT470 System M's primaries - AVCOL_SPC_YCGCO = 8, ///< used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16 - AVCOL_SPC_YCOCG = AVCOL_SPC_YCGCO, - AVCOL_SPC_BT2020_NCL = 9, ///< ITU-R BT2020 non-constant luminance system - AVCOL_SPC_BT2020_CL = 10, ///< ITU-R BT2020 constant luminance system - AVCOL_SPC_SMPTE2085 = 11, ///< SMPTE 2085, Y'D'zD'x - AVCOL_SPC_CHROMA_DERIVED_NCL = 12, ///< Chromaticity-derived non-constant luminance system - AVCOL_SPC_CHROMA_DERIVED_CL = 13, ///< Chromaticity-derived constant luminance system - AVCOL_SPC_ICTCP = 14, ///< ITU-R BT.2100-0, ICtCp - AVCOL_SPC_NB ///< Not part of ABI -}; - -/** - * Visual content value range. - * - * These values are based on definitions that can be found in multiple - * specifications, such as ITU-T BT.709 (3.4 - Quantization of RGB, luminance - * and colour-difference signals), ITU-T BT.2020 (Table 5 - Digital - * Representation) as well as ITU-T BT.2100 (Table 9 - Digital 10- and 12-bit - * integer representation). At the time of writing, the BT.2100 one is - * recommended, as it also defines the full range representation. - * - * Common definitions: - * - For RGB and luma planes such as Y in YCbCr and I in ICtCp, - * 'E' is the original value in range of 0.0 to 1.0. - * - For chroma planes such as Cb,Cr and Ct,Cp, 'E' is the original - * value in range of -0.5 to 0.5. - * - 'n' is the output bit depth. - * - For additional definitions such as rounding and clipping to valid n - * bit unsigned integer range, please refer to BT.2100 (Table 9). - */ -enum AVColorRange { - AVCOL_RANGE_UNSPECIFIED = 0, - - /** - * Narrow or limited range content. - * - * - For luma planes: - * - * (219 * E + 16) * 2^(n-8) - * - * F.ex. the range of 16-235 for 8 bits - * - * - For chroma planes: - * - * (224 * E + 128) * 2^(n-8) - * - * F.ex. the range of 16-240 for 8 bits - */ - AVCOL_RANGE_MPEG = 1, - - /** - * Full range content. - * - * - For RGB and luma planes: - * - * (2^n - 1) * E - * - * F.ex. the range of 0-255 for 8 bits - * - * - For chroma planes: - * - * (2^n - 1) * E + 2^(n - 1) - * - * F.ex. the range of 1-255 for 8 bits - */ - AVCOL_RANGE_JPEG = 2, - AVCOL_RANGE_NB ///< Not part of ABI -}; - -/** - * Location of chroma samples. - * - * Illustration showing the location of the first (top left) chroma sample of the - * image, the left shows only luma, the right - * shows the location of the chroma sample, the 2 could be imagined to overlay - * each other but are drawn separately due to limitations of ASCII - * - * 1st 2nd 1st 2nd horizontal luma sample positions - * v v v v - * ______ ______ - *1st luma line > |X X ... |3 4 X ... X are luma samples, - * | |1 2 1-6 are possible chroma positions - *2nd luma line > |X X ... |5 6 X ... 0 is undefined/unknown position - */ -enum AVChromaLocation { - AVCHROMA_LOC_UNSPECIFIED = 0, - AVCHROMA_LOC_LEFT = 1, ///< MPEG-2/4 4:2:0, H.264 default for 4:2:0 - AVCHROMA_LOC_CENTER = 2, ///< MPEG-1 4:2:0, JPEG 4:2:0, H.263 4:2:0 - AVCHROMA_LOC_TOPLEFT = 3, ///< ITU-R 601, SMPTE 274M 296M S314M(DV 4:1:1), mpeg2 4:2:2 - AVCHROMA_LOC_TOP = 4, - AVCHROMA_LOC_BOTTOMLEFT = 5, - AVCHROMA_LOC_BOTTOM = 6, - AVCHROMA_LOC_NB ///< Not part of ABI -}; - -#endif /* AVUTIL_PIXFMT_H */ diff --git a/gostream/ffmpeg/include/libavutil/random_seed.h b/gostream/ffmpeg/include/libavutil/random_seed.h deleted file mode 100644 index 8a47be96796..00000000000 --- a/gostream/ffmpeg/include/libavutil/random_seed.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2009 Baptiste Coudurier - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_RANDOM_SEED_H -#define AVUTIL_RANDOM_SEED_H - -#include -#include -/** - * @addtogroup lavu_crypto - * @{ - */ - -/** - * Get a seed to use in conjunction with random functions. - * This function tries to provide a good seed at a best effort bases. - * Its possible to call this function multiple times if more bits are needed. - * It can be quite slow, which is why it should only be used as seed for a faster - * PRNG. The quality of the seed depends on the platform. - */ -uint32_t av_get_random_seed(void); - -/** - * Generate cryptographically secure random data, i.e. suitable for use as - * encryption keys and similar. - * - * @param buf buffer into which the random data will be written - * @param len size of buf in bytes - * - * @retval 0 success, len bytes of random data was written - * into buf - * @retval "a negative AVERROR code" random data could not be generated - */ -int av_random_bytes(uint8_t *buf, size_t len); - -/** - * @} - */ - -#endif /* AVUTIL_RANDOM_SEED_H */ diff --git a/gostream/ffmpeg/include/libavutil/rational.h b/gostream/ffmpeg/include/libavutil/rational.h deleted file mode 100644 index 8cbfc8e0669..00000000000 --- a/gostream/ffmpeg/include/libavutil/rational.h +++ /dev/null @@ -1,221 +0,0 @@ -/* - * rational numbers - * Copyright (c) 2003 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_math_rational - * Utilties for rational number calculation. - * @author Michael Niedermayer - */ - -#ifndef AVUTIL_RATIONAL_H -#define AVUTIL_RATIONAL_H - -#include -#include -#include "attributes.h" - -/** - * @defgroup lavu_math_rational AVRational - * @ingroup lavu_math - * Rational number calculation. - * - * While rational numbers can be expressed as floating-point numbers, the - * conversion process is a lossy one, so are floating-point operations. On the - * other hand, the nature of FFmpeg demands highly accurate calculation of - * timestamps. This set of rational number utilities serves as a generic - * interface for manipulating rational numbers as pairs of numerators and - * denominators. - * - * Many of the functions that operate on AVRational's have the suffix `_q`, in - * reference to the mathematical symbol "ℚ" (Q) which denotes the set of all - * rational numbers. - * - * @{ - */ - -/** - * Rational number (pair of numerator and denominator). - */ -typedef struct AVRational{ - int num; ///< Numerator - int den; ///< Denominator -} AVRational; - -/** - * Create an AVRational. - * - * Useful for compilers that do not support compound literals. - * - * @note The return value is not reduced. - * @see av_reduce() - */ -static inline AVRational av_make_q(int num, int den) -{ - AVRational r = { num, den }; - return r; -} - -/** - * Compare two rationals. - * - * @param a First rational - * @param b Second rational - * - * @return One of the following values: - * - 0 if `a == b` - * - 1 if `a > b` - * - -1 if `a < b` - * - `INT_MIN` if one of the values is of the form `0 / 0` - */ -static inline int av_cmp_q(AVRational a, AVRational b){ - const int64_t tmp= a.num * (int64_t)b.den - b.num * (int64_t)a.den; - - if(tmp) return (int)((tmp ^ a.den ^ b.den)>>63)|1; - else if(b.den && a.den) return 0; - else if(a.num && b.num) return (a.num>>31) - (b.num>>31); - else return INT_MIN; -} - -/** - * Convert an AVRational to a `double`. - * @param a AVRational to convert - * @return `a` in floating-point form - * @see av_d2q() - */ -static inline double av_q2d(AVRational a){ - return a.num / (double) a.den; -} - -/** - * Reduce a fraction. - * - * This is useful for framerate calculations. - * - * @param[out] dst_num Destination numerator - * @param[out] dst_den Destination denominator - * @param[in] num Source numerator - * @param[in] den Source denominator - * @param[in] max Maximum allowed values for `dst_num` & `dst_den` - * @return 1 if the operation is exact, 0 otherwise - */ -int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max); - -/** - * Multiply two rationals. - * @param b First rational - * @param c Second rational - * @return b*c - */ -AVRational av_mul_q(AVRational b, AVRational c) av_const; - -/** - * Divide one rational by another. - * @param b First rational - * @param c Second rational - * @return b/c - */ -AVRational av_div_q(AVRational b, AVRational c) av_const; - -/** - * Add two rationals. - * @param b First rational - * @param c Second rational - * @return b+c - */ -AVRational av_add_q(AVRational b, AVRational c) av_const; - -/** - * Subtract one rational from another. - * @param b First rational - * @param c Second rational - * @return b-c - */ -AVRational av_sub_q(AVRational b, AVRational c) av_const; - -/** - * Invert a rational. - * @param q value - * @return 1 / q - */ -static av_always_inline AVRational av_inv_q(AVRational q) -{ - AVRational r = { q.den, q.num }; - return r; -} - -/** - * Convert a double precision floating point number to a rational. - * - * In case of infinity, the returned value is expressed as `{1, 0}` or - * `{-1, 0}` depending on the sign. - * - * @param d `double` to convert - * @param max Maximum allowed numerator and denominator - * @return `d` in AVRational form - * @see av_q2d() - */ -AVRational av_d2q(double d, int max) av_const; - -/** - * Find which of the two rationals is closer to another rational. - * - * @param q Rational to be compared against - * @param q1 Rational to be tested - * @param q2 Rational to be tested - * @return One of the following values: - * - 1 if `q1` is nearer to `q` than `q2` - * - -1 if `q2` is nearer to `q` than `q1` - * - 0 if they have the same distance - */ -int av_nearer_q(AVRational q, AVRational q1, AVRational q2); - -/** - * Find the value in a list of rationals nearest a given reference rational. - * - * @param q Reference rational - * @param q_list Array of rationals terminated by `{0, 0}` - * @return Index of the nearest value found in the array - */ -int av_find_nearest_q_idx(AVRational q, const AVRational* q_list); - -/** - * Convert an AVRational to a IEEE 32-bit `float` expressed in fixed-point - * format. - * - * @param q Rational to be converted - * @return Equivalent floating-point value, expressed as an unsigned 32-bit - * integer. - * @note The returned value is platform-indepedant. - */ -uint32_t av_q2intfloat(AVRational q); - -/** - * Return the best rational so that a and b are multiple of it. - * If the resulting denominator is larger than max_den, return def. - */ -AVRational av_gcd_q(AVRational a, AVRational b, int max_den, AVRational def); - -/** - * @} - */ - -#endif /* AVUTIL_RATIONAL_H */ diff --git a/gostream/ffmpeg/include/libavutil/rc4.h b/gostream/ffmpeg/include/libavutil/rc4.h deleted file mode 100644 index bf0ca6e9424..00000000000 --- a/gostream/ffmpeg/include/libavutil/rc4.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * RC4 encryption/decryption/pseudo-random number generator - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_RC4_H -#define AVUTIL_RC4_H - -#include - -/** - * @defgroup lavu_rc4 RC4 - * @ingroup lavu_crypto - * @{ - */ - -typedef struct AVRC4 { - uint8_t state[256]; - int x, y; -} AVRC4; - -/** - * Allocate an AVRC4 context. - */ -AVRC4 *av_rc4_alloc(void); - -/** - * @brief Initializes an AVRC4 context. - * - * @param d pointer to the AVRC4 context - * @param key buffer containig the key - * @param key_bits must be a multiple of 8 - * @param decrypt 0 for encryption, 1 for decryption, currently has no effect - * @return zero on success, negative value otherwise - */ -int av_rc4_init(struct AVRC4 *d, const uint8_t *key, int key_bits, int decrypt); - -/** - * @brief Encrypts / decrypts using the RC4 algorithm. - * - * @param d pointer to the AVRC4 context - * @param count number of bytes - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst, may be NULL - * @param iv not (yet) used for RC4, should be NULL - * @param decrypt 0 for encryption, 1 for decryption, not (yet) used - */ -void av_rc4_crypt(struct AVRC4 *d, uint8_t *dst, const uint8_t *src, int count, uint8_t *iv, int decrypt); - -/** - * @} - */ - -#endif /* AVUTIL_RC4_H */ diff --git a/gostream/ffmpeg/include/libavutil/replaygain.h b/gostream/ffmpeg/include/libavutil/replaygain.h deleted file mode 100644 index b49bf1a3d96..00000000000 --- a/gostream/ffmpeg/include/libavutil/replaygain.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_REPLAYGAIN_H -#define AVUTIL_REPLAYGAIN_H - -#include - -/** - * ReplayGain information (see - * http://wiki.hydrogenaudio.org/index.php?title=ReplayGain_1.0_specification). - * The size of this struct is a part of the public ABI. - */ -typedef struct AVReplayGain { - /** - * Track replay gain in microbels (divide by 100000 to get the value in dB). - * Should be set to INT32_MIN when unknown. - */ - int32_t track_gain; - /** - * Peak track amplitude, with 100000 representing full scale (but values - * may overflow). 0 when unknown. - */ - uint32_t track_peak; - /** - * Same as track_gain, but for the whole album. - */ - int32_t album_gain; - /** - * Same as track_peak, but for the whole album, - */ - uint32_t album_peak; -} AVReplayGain; - -#endif /* AVUTIL_REPLAYGAIN_H */ diff --git a/gostream/ffmpeg/include/libavutil/ripemd.h b/gostream/ffmpeg/include/libavutil/ripemd.h deleted file mode 100644 index 9df9f905f3d..00000000000 --- a/gostream/ffmpeg/include/libavutil/ripemd.h +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2007 Michael Niedermayer - * Copyright (C) 2013 James Almer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_ripemd - * Public header for RIPEMD hash function implementation. - */ - -#ifndef AVUTIL_RIPEMD_H -#define AVUTIL_RIPEMD_H - -#include -#include - -#include "attributes.h" - -/** - * @defgroup lavu_ripemd RIPEMD - * @ingroup lavu_hash - * RIPEMD hash function implementation. - * - * @{ - */ - -extern const int av_ripemd_size; - -struct AVRIPEMD; - -/** - * Allocate an AVRIPEMD context. - */ -struct AVRIPEMD *av_ripemd_alloc(void); - -/** - * Initialize RIPEMD hashing. - * - * @param context pointer to the function context (of size av_ripemd_size) - * @param bits number of bits in digest (128, 160, 256 or 320 bits) - * @return zero if initialization succeeded, -1 otherwise - */ -int av_ripemd_init(struct AVRIPEMD* context, int bits); - -/** - * Update hash value. - * - * @param context hash function context - * @param data input data to update hash with - * @param len input data length - */ -void av_ripemd_update(struct AVRIPEMD* context, const uint8_t* data, size_t len); - -/** - * Finish hashing and output digest value. - * - * @param context hash function context - * @param digest buffer where output digest value is stored - */ -void av_ripemd_final(struct AVRIPEMD* context, uint8_t *digest); - -/** - * @} - */ - -#endif /* AVUTIL_RIPEMD_H */ diff --git a/gostream/ffmpeg/include/libavutil/samplefmt.h b/gostream/ffmpeg/include/libavutil/samplefmt.h deleted file mode 100644 index 43a57a422cd..00000000000 --- a/gostream/ffmpeg/include/libavutil/samplefmt.h +++ /dev/null @@ -1,269 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_SAMPLEFMT_H -#define AVUTIL_SAMPLEFMT_H - -#include - -/** - * @addtogroup lavu_audio - * @{ - * - * @defgroup lavu_sampfmts Audio sample formats - * - * Audio sample format enumeration and related convenience functions. - * @{ - */ - -/** - * Audio sample formats - * - * - The data described by the sample format is always in native-endian order. - * Sample values can be expressed by native C types, hence the lack of a signed - * 24-bit sample format even though it is a common raw audio data format. - * - * - The floating-point formats are based on full volume being in the range - * [-1.0, 1.0]. Any values outside this range are beyond full volume level. - * - * - The data layout as used in av_samples_fill_arrays() and elsewhere in FFmpeg - * (such as AVFrame in libavcodec) is as follows: - * - * @par - * For planar sample formats, each audio channel is in a separate data plane, - * and linesize is the buffer size, in bytes, for a single plane. All data - * planes must be the same size. For packed sample formats, only the first data - * plane is used, and samples for each channel are interleaved. In this case, - * linesize is the buffer size, in bytes, for the 1 plane. - * - */ -enum AVSampleFormat { - AV_SAMPLE_FMT_NONE = -1, - AV_SAMPLE_FMT_U8, ///< unsigned 8 bits - AV_SAMPLE_FMT_S16, ///< signed 16 bits - AV_SAMPLE_FMT_S32, ///< signed 32 bits - AV_SAMPLE_FMT_FLT, ///< float - AV_SAMPLE_FMT_DBL, ///< double - - AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar - AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar - AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar - AV_SAMPLE_FMT_FLTP, ///< float, planar - AV_SAMPLE_FMT_DBLP, ///< double, planar - AV_SAMPLE_FMT_S64, ///< signed 64 bits - AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar - - AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically -}; - -/** - * Return the name of sample_fmt, or NULL if sample_fmt is not - * recognized. - */ -const char *av_get_sample_fmt_name(enum AVSampleFormat sample_fmt); - -/** - * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE - * on error. - */ -enum AVSampleFormat av_get_sample_fmt(const char *name); - -/** - * Return the planar<->packed alternative form of the given sample format, or - * AV_SAMPLE_FMT_NONE on error. If the passed sample_fmt is already in the - * requested planar/packed format, the format returned is the same as the - * input. - */ -enum AVSampleFormat av_get_alt_sample_fmt(enum AVSampleFormat sample_fmt, int planar); - -/** - * Get the packed alternative form of the given sample format. - * - * If the passed sample_fmt is already in packed format, the format returned is - * the same as the input. - * - * @return the packed alternative form of the given sample format or - AV_SAMPLE_FMT_NONE on error. - */ -enum AVSampleFormat av_get_packed_sample_fmt(enum AVSampleFormat sample_fmt); - -/** - * Get the planar alternative form of the given sample format. - * - * If the passed sample_fmt is already in planar format, the format returned is - * the same as the input. - * - * @return the planar alternative form of the given sample format or - AV_SAMPLE_FMT_NONE on error. - */ -enum AVSampleFormat av_get_planar_sample_fmt(enum AVSampleFormat sample_fmt); - -/** - * Generate a string corresponding to the sample format with - * sample_fmt, or a header if sample_fmt is negative. - * - * @param buf the buffer where to write the string - * @param buf_size the size of buf - * @param sample_fmt the number of the sample format to print the - * corresponding info string, or a negative value to print the - * corresponding header. - * @return the pointer to the filled buffer or NULL if sample_fmt is - * unknown or in case of other errors - */ -char *av_get_sample_fmt_string(char *buf, int buf_size, enum AVSampleFormat sample_fmt); - -/** - * Return number of bytes per sample. - * - * @param sample_fmt the sample format - * @return number of bytes per sample or zero if unknown for the given - * sample format - */ -int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt); - -/** - * Check if the sample format is planar. - * - * @param sample_fmt the sample format to inspect - * @return 1 if the sample format is planar, 0 if it is interleaved - */ -int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt); - -/** - * Get the required buffer size for the given audio parameters. - * - * @param[out] linesize calculated linesize, may be NULL - * @param nb_channels the number of channels - * @param nb_samples the number of samples in a single channel - * @param sample_fmt the sample format - * @param align buffer size alignment (0 = default, 1 = no alignment) - * @return required buffer size, or negative error code on failure - */ -int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples, - enum AVSampleFormat sample_fmt, int align); - -/** - * @} - * - * @defgroup lavu_sampmanip Samples manipulation - * - * Functions that manipulate audio samples - * @{ - */ - -/** - * Fill plane data pointers and linesize for samples with sample - * format sample_fmt. - * - * The audio_data array is filled with the pointers to the samples data planes: - * for planar, set the start point of each channel's data within the buffer, - * for packed, set the start point of the entire buffer only. - * - * The value pointed to by linesize is set to the aligned size of each - * channel's data buffer for planar layout, or to the aligned size of the - * buffer for all channels for packed layout. - * - * The buffer in buf must be big enough to contain all the samples - * (use av_samples_get_buffer_size() to compute its minimum size), - * otherwise the audio_data pointers will point to invalid data. - * - * @see enum AVSampleFormat - * The documentation for AVSampleFormat describes the data layout. - * - * @param[out] audio_data array to be filled with the pointer for each channel - * @param[out] linesize calculated linesize, may be NULL - * @param buf the pointer to a buffer containing the samples - * @param nb_channels the number of channels - * @param nb_samples the number of samples in a single channel - * @param sample_fmt the sample format - * @param align buffer size alignment (0 = default, 1 = no alignment) - * @return minimum size in bytes required for the buffer on success, - * or a negative error code on failure - */ -int av_samples_fill_arrays(uint8_t **audio_data, int *linesize, - const uint8_t *buf, - int nb_channels, int nb_samples, - enum AVSampleFormat sample_fmt, int align); - -/** - * Allocate a samples buffer for nb_samples samples, and fill data pointers and - * linesize accordingly. - * The allocated samples buffer can be freed by using av_freep(&audio_data[0]) - * Allocated data will be initialized to silence. - * - * @see enum AVSampleFormat - * The documentation for AVSampleFormat describes the data layout. - * - * @param[out] audio_data array to be filled with the pointer for each channel - * @param[out] linesize aligned size for audio buffer(s), may be NULL - * @param nb_channels number of audio channels - * @param nb_samples number of samples per channel - * @param sample_fmt the sample format - * @param align buffer size alignment (0 = default, 1 = no alignment) - * @return >=0 on success or a negative error code on failure - * @todo return the size of the allocated buffer in case of success at the next bump - * @see av_samples_fill_arrays() - * @see av_samples_alloc_array_and_samples() - */ -int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels, - int nb_samples, enum AVSampleFormat sample_fmt, int align); - -/** - * Allocate a data pointers array, samples buffer for nb_samples - * samples, and fill data pointers and linesize accordingly. - * - * This is the same as av_samples_alloc(), but also allocates the data - * pointers array. - * - * @see av_samples_alloc() - */ -int av_samples_alloc_array_and_samples(uint8_t ***audio_data, int *linesize, int nb_channels, - int nb_samples, enum AVSampleFormat sample_fmt, int align); - -/** - * Copy samples from src to dst. - * - * @param dst destination array of pointers to data planes - * @param src source array of pointers to data planes - * @param dst_offset offset in samples at which the data will be written to dst - * @param src_offset offset in samples at which the data will be read from src - * @param nb_samples number of samples to be copied - * @param nb_channels number of audio channels - * @param sample_fmt audio sample format - */ -int av_samples_copy(uint8_t * const *dst, uint8_t * const *src, int dst_offset, - int src_offset, int nb_samples, int nb_channels, - enum AVSampleFormat sample_fmt); - -/** - * Fill an audio buffer with silence. - * - * @param audio_data array of pointers to data planes - * @param offset offset in samples at which to start filling - * @param nb_samples number of samples to fill - * @param nb_channels number of audio channels - * @param sample_fmt audio sample format - */ -int av_samples_set_silence(uint8_t * const *audio_data, int offset, int nb_samples, - int nb_channels, enum AVSampleFormat sample_fmt); - -/** - * @} - * @} - */ -#endif /* AVUTIL_SAMPLEFMT_H */ diff --git a/gostream/ffmpeg/include/libavutil/sha.h b/gostream/ffmpeg/include/libavutil/sha.h deleted file mode 100644 index 2e1220abd12..00000000000 --- a/gostream/ffmpeg/include/libavutil/sha.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2007 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_sha - * Public header for SHA-1 & SHA-256 hash function implementations. - */ - -#ifndef AVUTIL_SHA_H -#define AVUTIL_SHA_H - -#include -#include - -#include "attributes.h" - -/** - * @defgroup lavu_sha SHA - * @ingroup lavu_hash - * SHA-1 and SHA-256 (Secure Hash Algorithm) hash function implementations. - * - * This module supports the following SHA hash functions: - * - * - SHA-1: 160 bits - * - SHA-224: 224 bits, as a variant of SHA-2 - * - SHA-256: 256 bits, as a variant of SHA-2 - * - * @see For SHA-384, SHA-512, and variants thereof, see @ref lavu_sha512. - * - * @{ - */ - -extern const int av_sha_size; - -struct AVSHA; - -/** - * Allocate an AVSHA context. - */ -struct AVSHA *av_sha_alloc(void); - -/** - * Initialize SHA-1 or SHA-2 hashing. - * - * @param context pointer to the function context (of size av_sha_size) - * @param bits number of bits in digest (SHA-1 - 160 bits, SHA-2 224 or 256 bits) - * @return zero if initialization succeeded, -1 otherwise - */ -int av_sha_init(struct AVSHA* context, int bits); - -/** - * Update hash value. - * - * @param ctx hash function context - * @param data input data to update hash with - * @param len input data length - */ -void av_sha_update(struct AVSHA *ctx, const uint8_t *data, size_t len); - -/** - * Finish hashing and output digest value. - * - * @param context hash function context - * @param digest buffer where output digest value is stored - */ -void av_sha_final(struct AVSHA* context, uint8_t *digest); - -/** - * @} - */ - -#endif /* AVUTIL_SHA_H */ diff --git a/gostream/ffmpeg/include/libavutil/sha512.h b/gostream/ffmpeg/include/libavutil/sha512.h deleted file mode 100644 index a4a3f23db30..00000000000 --- a/gostream/ffmpeg/include/libavutil/sha512.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2007 Michael Niedermayer - * Copyright (C) 2013 James Almer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_sha512 - * Public header for SHA-512 implementation. - */ - -#ifndef AVUTIL_SHA512_H -#define AVUTIL_SHA512_H - -#include -#include - -#include "attributes.h" - -/** - * @defgroup lavu_sha512 SHA-512 - * @ingroup lavu_hash - * SHA-512 (Secure Hash Algorithm) hash function implementations. - * - * This module supports the following SHA-2 hash functions: - * - * - SHA-512/224: 224 bits - * - SHA-512/256: 256 bits - * - SHA-384: 384 bits - * - SHA-512: 512 bits - * - * @see For SHA-1, SHA-256, and variants thereof, see @ref lavu_sha. - * - * @{ - */ - -extern const int av_sha512_size; - -struct AVSHA512; - -/** - * Allocate an AVSHA512 context. - */ -struct AVSHA512 *av_sha512_alloc(void); - -/** - * Initialize SHA-2 512 hashing. - * - * @param context pointer to the function context (of size av_sha512_size) - * @param bits number of bits in digest (224, 256, 384 or 512 bits) - * @return zero if initialization succeeded, -1 otherwise - */ -int av_sha512_init(struct AVSHA512* context, int bits); - -/** - * Update hash value. - * - * @param context hash function context - * @param data input data to update hash with - * @param len input data length - */ -void av_sha512_update(struct AVSHA512* context, const uint8_t* data, size_t len); - -/** - * Finish hashing and output digest value. - * - * @param context hash function context - * @param digest buffer where output digest value is stored - */ -void av_sha512_final(struct AVSHA512* context, uint8_t *digest); - -/** - * @} - */ - -#endif /* AVUTIL_SHA512_H */ diff --git a/gostream/ffmpeg/include/libavutil/spherical.h b/gostream/ffmpeg/include/libavutil/spherical.h deleted file mode 100644 index 828ac836da5..00000000000 --- a/gostream/ffmpeg/include/libavutil/spherical.h +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2016 Vittorio Giovara - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_video_spherical - * Spherical video - */ - -#ifndef AVUTIL_SPHERICAL_H -#define AVUTIL_SPHERICAL_H - -#include -#include - -/** - * @defgroup lavu_video_spherical Spherical video mapping - * @ingroup lavu_video - * - * A spherical video file contains surfaces that need to be mapped onto a - * sphere. Depending on how the frame was converted, a different distortion - * transformation or surface recomposition function needs to be applied before - * the video should be mapped and displayed. - * @{ - */ - -/** - * Projection of the video surface(s) on a sphere. - */ -enum AVSphericalProjection { - /** - * Video represents a sphere mapped on a flat surface using - * equirectangular projection. - */ - AV_SPHERICAL_EQUIRECTANGULAR, - - /** - * Video frame is split into 6 faces of a cube, and arranged on a - * 3x2 layout. Faces are oriented upwards for the front, left, right, - * and back faces. The up face is oriented so the top of the face is - * forwards and the down face is oriented so the top of the face is - * to the back. - */ - AV_SPHERICAL_CUBEMAP, - - /** - * Video represents a portion of a sphere mapped on a flat surface - * using equirectangular projection. The @ref bounding fields indicate - * the position of the current video in a larger surface. - */ - AV_SPHERICAL_EQUIRECTANGULAR_TILE, -}; - -/** - * This structure describes how to handle spherical videos, outlining - * information about projection, initial layout, and any other view modifier. - * - * @note The struct must be allocated with av_spherical_alloc() and - * its size is not a part of the public ABI. - */ -typedef struct AVSphericalMapping { - /** - * Projection type. - */ - enum AVSphericalProjection projection; - - /** - * @name Initial orientation - * @{ - * There fields describe additional rotations applied to the sphere after - * the video frame is mapped onto it. The sphere is rotated around the - * viewer, who remains stationary. The order of transformation is always - * yaw, followed by pitch, and finally by roll. - * - * The coordinate system matches the one defined in OpenGL, where the - * forward vector (z) is coming out of screen, and it is equivalent to - * a rotation matrix of R = r_y(yaw) * r_x(pitch) * r_z(roll). - * - * A positive yaw rotates the portion of the sphere in front of the viewer - * toward their right. A positive pitch rotates the portion of the sphere - * in front of the viewer upwards. A positive roll tilts the portion of - * the sphere in front of the viewer to the viewer's right. - * - * These values are exported as 16.16 fixed point. - * - * See this equirectangular projection as example: - * - * @code{.unparsed} - * Yaw - * -180 0 180 - * 90 +-------------+-------------+ 180 - * | | | up - * P | | | y| forward - * i | ^ | | /z - * t 0 +-------------X-------------+ 0 Roll | / - * c | | | | / - * h | | | 0|/_____right - * | | | x - * -90 +-------------+-------------+ -180 - * - * X - the default camera center - * ^ - the default up vector - * @endcode - */ - int32_t yaw; ///< Rotation around the up vector [-180, 180]. - int32_t pitch; ///< Rotation around the right vector [-90, 90]. - int32_t roll; ///< Rotation around the forward vector [-180, 180]. - /** - * @} - */ - - /** - * @name Bounding rectangle - * @anchor bounding - * @{ - * These fields indicate the location of the current tile, and where - * it should be mapped relative to the original surface. They are - * exported as 0.32 fixed point, and can be converted to classic - * pixel values with av_spherical_bounds(). - * - * @code{.unparsed} - * +----------------+----------+ - * | |bound_top | - * | +--------+ | - * | bound_left |tile | | - * +<---------->| |<--->+bound_right - * | +--------+ | - * | | | - * | bound_bottom| | - * +----------------+----------+ - * @endcode - * - * If needed, the original video surface dimensions can be derived - * by adding the current stream or frame size to the related bounds, - * like in the following example: - * - * @code{c} - * original_width = tile->width + bound_left + bound_right; - * original_height = tile->height + bound_top + bound_bottom; - * @endcode - * - * @note These values are valid only for the tiled equirectangular - * projection type (@ref AV_SPHERICAL_EQUIRECTANGULAR_TILE), - * and should be ignored in all other cases. - */ - uint32_t bound_left; ///< Distance from the left edge - uint32_t bound_top; ///< Distance from the top edge - uint32_t bound_right; ///< Distance from the right edge - uint32_t bound_bottom; ///< Distance from the bottom edge - /** - * @} - */ - - /** - * Number of pixels to pad from the edge of each cube face. - * - * @note This value is valid for only for the cubemap projection type - * (@ref AV_SPHERICAL_CUBEMAP), and should be ignored in all other - * cases. - */ - uint32_t padding; -} AVSphericalMapping; - -/** - * Allocate a AVSphericalVideo structure and initialize its fields to default - * values. - * - * @return the newly allocated struct or NULL on failure - */ -AVSphericalMapping *av_spherical_alloc(size_t *size); - -/** - * Convert the @ref bounding fields from an AVSphericalVideo - * from 0.32 fixed point to pixels. - * - * @param map The AVSphericalVideo map to read bound values from. - * @param width Width of the current frame or stream. - * @param height Height of the current frame or stream. - * @param left Pixels from the left edge. - * @param top Pixels from the top edge. - * @param right Pixels from the right edge. - * @param bottom Pixels from the bottom edge. - */ -void av_spherical_tile_bounds(const AVSphericalMapping *map, - size_t width, size_t height, - size_t *left, size_t *top, - size_t *right, size_t *bottom); - -/** - * Provide a human-readable name of a given AVSphericalProjection. - * - * @param projection The input AVSphericalProjection. - * - * @return The name of the AVSphericalProjection, or "unknown". - */ -const char *av_spherical_projection_name(enum AVSphericalProjection projection); - -/** - * Get the AVSphericalProjection form a human-readable name. - * - * @param name The input string. - * - * @return The AVSphericalProjection value, or -1 if not found. - */ -int av_spherical_from_name(const char *name); -/** - * @} - */ - -#endif /* AVUTIL_SPHERICAL_H */ diff --git a/gostream/ffmpeg/include/libavutil/stereo3d.h b/gostream/ffmpeg/include/libavutil/stereo3d.h deleted file mode 100644 index 3aab959b799..00000000000 --- a/gostream/ffmpeg/include/libavutil/stereo3d.h +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright (c) 2013 Vittorio Giovara - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu_video_stereo3d - * Stereoscopic video - */ - -#ifndef AVUTIL_STEREO3D_H -#define AVUTIL_STEREO3D_H - -#include - -#include "frame.h" - -/** - * @defgroup lavu_video_stereo3d Stereo3D types and functions - * @ingroup lavu_video - * - * A stereoscopic video file consists in multiple views embedded in a single - * frame, usually describing two views of a scene. This file describes all - * possible codec-independent view arrangements. - * - * @{ - */ - -/** - * List of possible 3D Types - */ -enum AVStereo3DType { - /** - * Video is not stereoscopic (and metadata has to be there). - */ - AV_STEREO3D_2D, - - /** - * Views are next to each other. - * - * @code{.unparsed} - * LLLLRRRR - * LLLLRRRR - * LLLLRRRR - * ... - * @endcode - */ - AV_STEREO3D_SIDEBYSIDE, - - /** - * Views are on top of each other. - * - * @code{.unparsed} - * LLLLLLLL - * LLLLLLLL - * RRRRRRRR - * RRRRRRRR - * @endcode - */ - AV_STEREO3D_TOPBOTTOM, - - /** - * Views are alternated temporally. - * - * @code{.unparsed} - * frame0 frame1 frame2 ... - * LLLLLLLL RRRRRRRR LLLLLLLL - * LLLLLLLL RRRRRRRR LLLLLLLL - * LLLLLLLL RRRRRRRR LLLLLLLL - * ... ... ... - * @endcode - */ - AV_STEREO3D_FRAMESEQUENCE, - - /** - * Views are packed in a checkerboard-like structure per pixel. - * - * @code{.unparsed} - * LRLRLRLR - * RLRLRLRL - * LRLRLRLR - * ... - * @endcode - */ - AV_STEREO3D_CHECKERBOARD, - - /** - * Views are next to each other, but when upscaling - * apply a checkerboard pattern. - * - * @code{.unparsed} - * LLLLRRRR L L L L R R R R - * LLLLRRRR => L L L L R R R R - * LLLLRRRR L L L L R R R R - * LLLLRRRR L L L L R R R R - * @endcode - */ - AV_STEREO3D_SIDEBYSIDE_QUINCUNX, - - /** - * Views are packed per line, as if interlaced. - * - * @code{.unparsed} - * LLLLLLLL - * RRRRRRRR - * LLLLLLLL - * ... - * @endcode - */ - AV_STEREO3D_LINES, - - /** - * Views are packed per column. - * - * @code{.unparsed} - * LRLRLRLR - * LRLRLRLR - * LRLRLRLR - * ... - * @endcode - */ - AV_STEREO3D_COLUMNS, -}; - -/** - * List of possible view types. - */ -enum AVStereo3DView { - /** - * Frame contains two packed views. - */ - AV_STEREO3D_VIEW_PACKED, - - /** - * Frame contains only the left view. - */ - AV_STEREO3D_VIEW_LEFT, - - /** - * Frame contains only the right view. - */ - AV_STEREO3D_VIEW_RIGHT, -}; - -/** - * Inverted views, Right/Bottom represents the left view. - */ -#define AV_STEREO3D_FLAG_INVERT (1 << 0) - -/** - * Stereo 3D type: this structure describes how two videos are packed - * within a single video surface, with additional information as needed. - * - * @note The struct must be allocated with av_stereo3d_alloc() and - * its size is not a part of the public ABI. - */ -typedef struct AVStereo3D { - /** - * How views are packed within the video. - */ - enum AVStereo3DType type; - - /** - * Additional information about the frame packing. - */ - int flags; - - /** - * Determines which views are packed. - */ - enum AVStereo3DView view; -} AVStereo3D; - -/** - * Allocate an AVStereo3D structure and set its fields to default values. - * The resulting struct can be freed using av_freep(). - * - * @return An AVStereo3D filled with default values or NULL on failure. - */ -AVStereo3D *av_stereo3d_alloc(void); - -/** - * Allocate a complete AVFrameSideData and add it to the frame. - * - * @param frame The frame which side data is added to. - * - * @return The AVStereo3D structure to be filled by caller. - */ -AVStereo3D *av_stereo3d_create_side_data(AVFrame *frame); - -/** - * Provide a human-readable name of a given stereo3d type. - * - * @param type The input stereo3d type value. - * - * @return The name of the stereo3d value, or "unknown". - */ -const char *av_stereo3d_type_name(unsigned int type); - -/** - * Get the AVStereo3DType form a human-readable name. - * - * @param name The input string. - * - * @return The AVStereo3DType value, or -1 if not found. - */ -int av_stereo3d_from_name(const char *name); - -/** - * @} - */ - -#endif /* AVUTIL_STEREO3D_H */ diff --git a/gostream/ffmpeg/include/libavutil/tea.h b/gostream/ffmpeg/include/libavutil/tea.h deleted file mode 100644 index dd929bdafdc..00000000000 --- a/gostream/ffmpeg/include/libavutil/tea.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * A 32-bit implementation of the TEA algorithm - * Copyright (c) 2015 Vesselin Bontchev - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_TEA_H -#define AVUTIL_TEA_H - -#include - -/** - * @file - * @brief Public header for libavutil TEA algorithm - * @defgroup lavu_tea TEA - * @ingroup lavu_crypto - * @{ - */ - -extern const int av_tea_size; - -struct AVTEA; - -/** - * Allocate an AVTEA context - * To free the struct: av_free(ptr) - */ -struct AVTEA *av_tea_alloc(void); - -/** - * Initialize an AVTEA context. - * - * @param ctx an AVTEA context - * @param key a key of 16 bytes used for encryption/decryption - * @param rounds the number of rounds in TEA (64 is the "standard") - */ -void av_tea_init(struct AVTEA *ctx, const uint8_t key[16], int rounds); - -/** - * Encrypt or decrypt a buffer using a previously initialized context. - * - * @param ctx an AVTEA context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 8 byte blocks - * @param iv initialization vector for CBC mode, if NULL then ECB will be used - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_tea_crypt(struct AVTEA *ctx, uint8_t *dst, const uint8_t *src, - int count, uint8_t *iv, int decrypt); - -/** - * @} - */ - -#endif /* AVUTIL_TEA_H */ diff --git a/gostream/ffmpeg/include/libavutil/threadmessage.h b/gostream/ffmpeg/include/libavutil/threadmessage.h deleted file mode 100644 index 42ce655f365..00000000000 --- a/gostream/ffmpeg/include/libavutil/threadmessage.h +++ /dev/null @@ -1,115 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with FFmpeg; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_THREADMESSAGE_H -#define AVUTIL_THREADMESSAGE_H - -typedef struct AVThreadMessageQueue AVThreadMessageQueue; - -typedef enum AVThreadMessageFlags { - - /** - * Perform non-blocking operation. - * If this flag is set, send and recv operations are non-blocking and - * return AVERROR(EAGAIN) immediately if they can not proceed. - */ - AV_THREAD_MESSAGE_NONBLOCK = 1, - -} AVThreadMessageFlags; - -/** - * Allocate a new message queue. - * - * @param mq pointer to the message queue - * @param nelem maximum number of elements in the queue - * @param elsize size of each element in the queue - * @return >=0 for success; <0 for error, in particular AVERROR(ENOSYS) if - * lavu was built without thread support - */ -int av_thread_message_queue_alloc(AVThreadMessageQueue **mq, - unsigned nelem, - unsigned elsize); - -/** - * Free a message queue. - * - * The message queue must no longer be in use by another thread. - */ -void av_thread_message_queue_free(AVThreadMessageQueue **mq); - -/** - * Send a message on the queue. - */ -int av_thread_message_queue_send(AVThreadMessageQueue *mq, - void *msg, - unsigned flags); - -/** - * Receive a message from the queue. - */ -int av_thread_message_queue_recv(AVThreadMessageQueue *mq, - void *msg, - unsigned flags); - -/** - * Set the sending error code. - * - * If the error code is set to non-zero, av_thread_message_queue_send() will - * return it immediately. Conventional values, such as AVERROR_EOF or - * AVERROR(EAGAIN), can be used to cause the sending thread to stop or - * suspend its operation. - */ -void av_thread_message_queue_set_err_send(AVThreadMessageQueue *mq, - int err); - -/** - * Set the receiving error code. - * - * If the error code is set to non-zero, av_thread_message_queue_recv() will - * return it immediately when there are no longer available messages. - * Conventional values, such as AVERROR_EOF or AVERROR(EAGAIN), can be used - * to cause the receiving thread to stop or suspend its operation. - */ -void av_thread_message_queue_set_err_recv(AVThreadMessageQueue *mq, - int err); - -/** - * Set the optional free message callback function which will be called if an - * operation is removing messages from the queue. - */ -void av_thread_message_queue_set_free_func(AVThreadMessageQueue *mq, - void (*free_func)(void *msg)); - -/** - * Return the current number of messages in the queue. - * - * @return the current number of messages or AVERROR(ENOSYS) if lavu was built - * without thread support - */ -int av_thread_message_queue_nb_elems(AVThreadMessageQueue *mq); - -/** - * Flush the message queue - * - * This function is mostly equivalent to reading and free-ing every message - * except that it will be done in a single operation (no lock/unlock between - * reads). - */ -void av_thread_message_flush(AVThreadMessageQueue *mq); - -#endif /* AVUTIL_THREADMESSAGE_H */ diff --git a/gostream/ffmpeg/include/libavutil/time.h b/gostream/ffmpeg/include/libavutil/time.h deleted file mode 100644 index dc169b064a0..00000000000 --- a/gostream/ffmpeg/include/libavutil/time.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2000-2003 Fabrice Bellard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_TIME_H -#define AVUTIL_TIME_H - -#include - -/** - * Get the current time in microseconds. - */ -int64_t av_gettime(void); - -/** - * Get the current time in microseconds since some unspecified starting point. - * On platforms that support it, the time comes from a monotonic clock - * This property makes this time source ideal for measuring relative time. - * The returned values may not be monotonic on platforms where a monotonic - * clock is not available. - */ -int64_t av_gettime_relative(void); - -/** - * Indicates with a boolean result if the av_gettime_relative() time source - * is monotonic. - */ -int av_gettime_relative_is_monotonic(void); - -/** - * Sleep for a period of time. Although the duration is expressed in - * microseconds, the actual delay may be rounded to the precision of the - * system timer. - * - * @param usec Number of microseconds to sleep. - * @return zero on success or (negative) error code. - */ -int av_usleep(unsigned usec); - -#endif /* AVUTIL_TIME_H */ diff --git a/gostream/ffmpeg/include/libavutil/timecode.h b/gostream/ffmpeg/include/libavutil/timecode.h deleted file mode 100644 index 060574a1720..00000000000 --- a/gostream/ffmpeg/include/libavutil/timecode.h +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (c) 2006 Smartjog S.A.S, Baptiste Coudurier - * Copyright (c) 2011-2012 Smartjog S.A.S, Clément Bœsch - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * Timecode helpers header - */ - -#ifndef AVUTIL_TIMECODE_H -#define AVUTIL_TIMECODE_H - -#include -#include "rational.h" - -#define AV_TIMECODE_STR_SIZE 23 - -enum AVTimecodeFlag { - AV_TIMECODE_FLAG_DROPFRAME = 1<<0, ///< timecode is drop frame - AV_TIMECODE_FLAG_24HOURSMAX = 1<<1, ///< timecode wraps after 24 hours - AV_TIMECODE_FLAG_ALLOWNEGATIVE = 1<<2, ///< negative time values are allowed -}; - -typedef struct { - int start; ///< timecode frame start (first base frame number) - uint32_t flags; ///< flags such as drop frame, +24 hours support, ... - AVRational rate; ///< frame rate in rational form - unsigned fps; ///< frame per second; must be consistent with the rate field -} AVTimecode; - -/** - * Adjust frame number for NTSC drop frame time code. - * - * @param framenum frame number to adjust - * @param fps frame per second, multiples of 30 - * @return adjusted frame number - * @warning adjustment is only valid for multiples of NTSC 29.97 - */ -int av_timecode_adjust_ntsc_framenum2(int framenum, int fps); - -/** - * Convert frame number to SMPTE 12M binary representation. - * - * @param tc timecode data correctly initialized - * @param framenum frame number - * @return the SMPTE binary representation - * - * See SMPTE ST 314M-2005 Sec 4.4.2.2.1 "Time code pack (TC)" - * the format description as follows: - * bits 0-5: hours, in BCD(6bits) - * bits 6: BGF1 - * bits 7: BGF2 (NTSC) or FIELD (PAL) - * bits 8-14: minutes, in BCD(7bits) - * bits 15: BGF0 (NTSC) or BGF2 (PAL) - * bits 16-22: seconds, in BCD(7bits) - * bits 23: FIELD (NTSC) or BGF0 (PAL) - * bits 24-29: frames, in BCD(6bits) - * bits 30: drop frame flag (0: non drop, 1: drop) - * bits 31: color frame flag (0: unsync mode, 1: sync mode) - * @note BCD numbers (6 or 7 bits): 4 or 5 lower bits for units, 2 higher bits for tens. - * @note Frame number adjustment is automatically done in case of drop timecode, - * you do NOT have to call av_timecode_adjust_ntsc_framenum2(). - * @note The frame number is relative to tc->start. - * @note Color frame (CF) and binary group flags (BGF) bits are set to zero. - */ -uint32_t av_timecode_get_smpte_from_framenum(const AVTimecode *tc, int framenum); - -/** - * Convert sei info to SMPTE 12M binary representation. - * - * @param rate frame rate in rational form - * @param drop drop flag - * @param hh hour - * @param mm minute - * @param ss second - * @param ff frame number - * @return the SMPTE binary representation - */ -uint32_t av_timecode_get_smpte(AVRational rate, int drop, int hh, int mm, int ss, int ff); - -/** - * Load timecode string in buf. - * - * @param buf destination buffer, must be at least AV_TIMECODE_STR_SIZE long - * @param tc timecode data correctly initialized - * @param framenum frame number - * @return the buf parameter - * - * @note Timecode representation can be a negative timecode and have more than - * 24 hours, but will only be honored if the flags are correctly set. - * @note The frame number is relative to tc->start. - */ -char *av_timecode_make_string(const AVTimecode *tc, char *buf, int framenum); - -/** - * Get the timecode string from the SMPTE timecode format. - * - * In contrast to av_timecode_make_smpte_tc_string this function supports 50/60 - * fps timecodes by using the field bit. - * - * @param buf destination buffer, must be at least AV_TIMECODE_STR_SIZE long - * @param rate frame rate of the timecode - * @param tcsmpte the 32-bit SMPTE timecode - * @param prevent_df prevent the use of a drop flag when it is known the DF bit - * is arbitrary - * @param skip_field prevent the use of a field flag when it is known the field - * bit is arbitrary (e.g. because it is used as PC flag) - * @return the buf parameter - */ -char *av_timecode_make_smpte_tc_string2(char *buf, AVRational rate, uint32_t tcsmpte, int prevent_df, int skip_field); - -/** - * Get the timecode string from the SMPTE timecode format. - * - * @param buf destination buffer, must be at least AV_TIMECODE_STR_SIZE long - * @param tcsmpte the 32-bit SMPTE timecode - * @param prevent_df prevent the use of a drop flag when it is known the DF bit - * is arbitrary - * @return the buf parameter - */ -char *av_timecode_make_smpte_tc_string(char *buf, uint32_t tcsmpte, int prevent_df); - -/** - * Get the timecode string from the 25-bit timecode format (MPEG GOP format). - * - * @param buf destination buffer, must be at least AV_TIMECODE_STR_SIZE long - * @param tc25bit the 25-bits timecode - * @return the buf parameter - */ -char *av_timecode_make_mpeg_tc_string(char *buf, uint32_t tc25bit); - -/** - * Init a timecode struct with the passed parameters. - * - * @param log_ctx a pointer to an arbitrary struct of which the first field - * is a pointer to an AVClass struct (used for av_log) - * @param tc pointer to an allocated AVTimecode - * @param rate frame rate in rational form - * @param flags miscellaneous flags such as drop frame, +24 hours, ... - * (see AVTimecodeFlag) - * @param frame_start the first frame number - * @return 0 on success, AVERROR otherwise - */ -int av_timecode_init(AVTimecode *tc, AVRational rate, int flags, int frame_start, void *log_ctx); - -/** - * Init a timecode struct from the passed timecode components. - * - * @param log_ctx a pointer to an arbitrary struct of which the first field - * is a pointer to an AVClass struct (used for av_log) - * @param tc pointer to an allocated AVTimecode - * @param rate frame rate in rational form - * @param flags miscellaneous flags such as drop frame, +24 hours, ... - * (see AVTimecodeFlag) - * @param hh hours - * @param mm minutes - * @param ss seconds - * @param ff frames - * @return 0 on success, AVERROR otherwise - */ -int av_timecode_init_from_components(AVTimecode *tc, AVRational rate, int flags, int hh, int mm, int ss, int ff, void *log_ctx); - -/** - * Parse timecode representation (hh:mm:ss[:;.]ff). - * - * @param log_ctx a pointer to an arbitrary struct of which the first field is a - * pointer to an AVClass struct (used for av_log). - * @param tc pointer to an allocated AVTimecode - * @param rate frame rate in rational form - * @param str timecode string which will determine the frame start - * @return 0 on success, AVERROR otherwise - */ -int av_timecode_init_from_string(AVTimecode *tc, AVRational rate, const char *str, void *log_ctx); - -/** - * Check if the timecode feature is available for the given frame rate - * - * @return 0 if supported, <0 otherwise - */ -int av_timecode_check_frame_rate(AVRational rate); - -#endif /* AVUTIL_TIMECODE_H */ diff --git a/gostream/ffmpeg/include/libavutil/timestamp.h b/gostream/ffmpeg/include/libavutil/timestamp.h deleted file mode 100644 index 9ae64da8a1c..00000000000 --- a/gostream/ffmpeg/include/libavutil/timestamp.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * timestamp utils, mostly useful for debugging/logging purposes - */ - -#ifndef AVUTIL_TIMESTAMP_H -#define AVUTIL_TIMESTAMP_H - -#include "avutil.h" - -#if defined(__cplusplus) && !defined(__STDC_FORMAT_MACROS) && !defined(PRId64) -#error missing -D__STDC_FORMAT_MACROS / #define __STDC_FORMAT_MACROS -#endif - -#define AV_TS_MAX_STRING_SIZE 32 - -/** - * Fill the provided buffer with a string containing a timestamp - * representation. - * - * @param buf a buffer with size in bytes of at least AV_TS_MAX_STRING_SIZE - * @param ts the timestamp to represent - * @return the buffer in input - */ -static inline char *av_ts_make_string(char *buf, int64_t ts) -{ - if (ts == AV_NOPTS_VALUE) snprintf(buf, AV_TS_MAX_STRING_SIZE, "NOPTS"); - else snprintf(buf, AV_TS_MAX_STRING_SIZE, "%" PRId64, ts); - return buf; -} - -/** - * Convenience macro, the return value should be used only directly in - * function arguments but never stand-alone. - */ -#define av_ts2str(ts) av_ts_make_string((char[AV_TS_MAX_STRING_SIZE]){0}, ts) - -/** - * Fill the provided buffer with a string containing a timestamp time - * representation. - * - * @param buf a buffer with size in bytes of at least AV_TS_MAX_STRING_SIZE - * @param ts the timestamp to represent - * @param tb the timebase of the timestamp - * @return the buffer in input - */ -static inline char *av_ts_make_time_string(char *buf, int64_t ts, AVRational *tb) -{ - if (ts == AV_NOPTS_VALUE) snprintf(buf, AV_TS_MAX_STRING_SIZE, "NOPTS"); - else snprintf(buf, AV_TS_MAX_STRING_SIZE, "%.6g", av_q2d(*tb) * ts); - return buf; -} - -/** - * Convenience macro, the return value should be used only directly in - * function arguments but never stand-alone. - */ -#define av_ts2timestr(ts, tb) av_ts_make_time_string((char[AV_TS_MAX_STRING_SIZE]){0}, ts, tb) - -#endif /* AVUTIL_TIMESTAMP_H */ diff --git a/gostream/ffmpeg/include/libavutil/tree.h b/gostream/ffmpeg/include/libavutil/tree.h deleted file mode 100644 index bbb8fbb1262..00000000000 --- a/gostream/ffmpeg/include/libavutil/tree.h +++ /dev/null @@ -1,137 +0,0 @@ -/* - * copyright (c) 2006 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * A tree container. - * @author Michael Niedermayer - */ - -#ifndef AVUTIL_TREE_H -#define AVUTIL_TREE_H - -#include "attributes.h" - -/** - * @addtogroup lavu_tree AVTree - * @ingroup lavu_data - * - * Low-complexity tree container - * - * Insertion, removal, finding equal, largest which is smaller than and - * smallest which is larger than, all have O(log n) worst-case complexity. - * @{ - */ - - -struct AVTreeNode; -extern const int av_tree_node_size; - -/** - * Allocate an AVTreeNode. - */ -struct AVTreeNode *av_tree_node_alloc(void); - -/** - * Find an element. - * @param root a pointer to the root node of the tree - * @param next If next is not NULL, then next[0] will contain the previous - * element and next[1] the next element. If either does not exist, - * then the corresponding entry in next is unchanged. - * @param cmp compare function used to compare elements in the tree, - * API identical to that of Standard C's qsort - * It is guaranteed that the first and only the first argument to cmp() - * will be the key parameter to av_tree_find(), thus it could if the - * user wants, be a different type (like an opaque context). - * @return An element with cmp(key, elem) == 0 or NULL if no such element - * exists in the tree. - */ -void *av_tree_find(const struct AVTreeNode *root, void *key, - int (*cmp)(const void *key, const void *b), void *next[2]); - -/** - * Insert or remove an element. - * - * If *next is NULL, then the supplied element will be removed if it exists. - * If *next is non-NULL, then the supplied element will be inserted, unless - * it already exists in the tree. - * - * @param rootp A pointer to a pointer to the root node of the tree; note that - * the root node can change during insertions, this is required - * to keep the tree balanced. - * @param key pointer to the element key to insert in the tree - * @param next Used to allocate and free AVTreeNodes. For insertion the user - * must set it to an allocated and zeroed object of at least - * av_tree_node_size bytes size. av_tree_insert() will set it to - * NULL if it has been consumed. - * For deleting elements *next is set to NULL by the user and - * av_tree_insert() will set it to the AVTreeNode which was - * used for the removed element. - * This allows the use of flat arrays, which have - * lower overhead compared to many malloced elements. - * You might want to define a function like: - * @code - * void *tree_insert(struct AVTreeNode **rootp, void *key, - * int (*cmp)(void *key, const void *b), - * AVTreeNode **next) - * { - * if (!*next) - * *next = av_mallocz(av_tree_node_size); - * return av_tree_insert(rootp, key, cmp, next); - * } - * void *tree_remove(struct AVTreeNode **rootp, void *key, - * int (*cmp)(void *key, const void *b, AVTreeNode **next)) - * { - * av_freep(next); - * return av_tree_insert(rootp, key, cmp, next); - * } - * @endcode - * @param cmp compare function used to compare elements in the tree, API identical - * to that of Standard C's qsort - * @return If no insertion happened, the found element; if an insertion or - * removal happened, then either key or NULL will be returned. - * Which one it is depends on the tree state and the implementation. You - * should make no assumptions that it's one or the other in the code. - */ -void *av_tree_insert(struct AVTreeNode **rootp, void *key, - int (*cmp)(const void *key, const void *b), - struct AVTreeNode **next); - -void av_tree_destroy(struct AVTreeNode *t); - -/** - * Apply enu(opaque, &elem) to all the elements in the tree in a given range. - * - * @param cmp a comparison function that returns < 0 for an element below the - * range, > 0 for an element above the range and == 0 for an - * element inside the range - * - * @note The cmp function should use the same ordering used to construct the - * tree. - */ -void av_tree_enumerate(struct AVTreeNode *t, void *opaque, - int (*cmp)(void *opaque, void *elem), - int (*enu)(void *opaque, void *elem)); - -/** - * @} - */ - -#endif /* AVUTIL_TREE_H */ diff --git a/gostream/ffmpeg/include/libavutil/twofish.h b/gostream/ffmpeg/include/libavutil/twofish.h deleted file mode 100644 index 67f359e88c2..00000000000 --- a/gostream/ffmpeg/include/libavutil/twofish.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - * An implementation of the TwoFish algorithm - * Copyright (c) 2015 Supraja Meedinti - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_TWOFISH_H -#define AVUTIL_TWOFISH_H - -#include - - -/** - * @file - * @brief Public header for libavutil TWOFISH algorithm - * @defgroup lavu_twofish TWOFISH - * @ingroup lavu_crypto - * @{ - */ - -extern const int av_twofish_size; - -struct AVTWOFISH; - -/** - * Allocate an AVTWOFISH context - * To free the struct: av_free(ptr) - */ -struct AVTWOFISH *av_twofish_alloc(void); - -/** - * Initialize an AVTWOFISH context. - * - * @param ctx an AVTWOFISH context - * @param key a key of size ranging from 1 to 32 bytes used for encryption/decryption - * @param key_bits number of keybits: 128, 192, 256 If less than the required, padded with zeroes to nearest valid value; return value is 0 if key_bits is 128/192/256, -1 if less than 0, 1 otherwise - */ -int av_twofish_init(struct AVTWOFISH *ctx, const uint8_t *key, int key_bits); - -/** - * Encrypt or decrypt a buffer using a previously initialized context - * - * @param ctx an AVTWOFISH context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 16 byte blocks - * @param iv initialization vector for CBC mode, NULL for ECB mode - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_twofish_crypt(struct AVTWOFISH *ctx, uint8_t *dst, const uint8_t *src, int count, uint8_t* iv, int decrypt); - -/** - * @} - */ -#endif /* AVUTIL_TWOFISH_H */ diff --git a/gostream/ffmpeg/include/libavutil/tx.h b/gostream/ffmpeg/include/libavutil/tx.h deleted file mode 100644 index 4696988caea..00000000000 --- a/gostream/ffmpeg/include/libavutil/tx.h +++ /dev/null @@ -1,210 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_TX_H -#define AVUTIL_TX_H - -#include -#include - -typedef struct AVTXContext AVTXContext; - -typedef struct AVComplexFloat { - float re, im; -} AVComplexFloat; - -typedef struct AVComplexDouble { - double re, im; -} AVComplexDouble; - -typedef struct AVComplexInt32 { - int32_t re, im; -} AVComplexInt32; - -enum AVTXType { - /** - * Standard complex to complex FFT with sample data type of AVComplexFloat, - * AVComplexDouble or AVComplexInt32, for each respective variant. - * - * Output is not 1/len normalized. Scaling currently unsupported. - * The stride parameter must be set to the size of a single sample in bytes. - */ - AV_TX_FLOAT_FFT = 0, - AV_TX_DOUBLE_FFT = 2, - AV_TX_INT32_FFT = 4, - - /** - * Standard MDCT with a sample data type of float, double or int32_t, - * respecively. For the float and int32 variants, the scale type is - * 'float', while for the double variant, it's 'double'. - * If scale is NULL, 1.0 will be used as a default. - * - * Length is the frame size, not the window size (which is 2x frame). - * For forward transforms, the stride specifies the spacing between each - * sample in the output array in bytes. The input must be a flat array. - * - * For inverse transforms, the stride specifies the spacing between each - * sample in the input array in bytes. The output must be a flat array. - * - * NOTE: the inverse transform is half-length, meaning the output will not - * contain redundant data. This is what most codecs work with. To do a full - * inverse transform, set the AV_TX_FULL_IMDCT flag on init. - */ - AV_TX_FLOAT_MDCT = 1, - AV_TX_DOUBLE_MDCT = 3, - AV_TX_INT32_MDCT = 5, - - /** - * Real to complex and complex to real DFTs. - * For the float and int32 variants, the scale type is 'float', while for - * the double variant, it's a 'double'. If scale is NULL, 1.0 will be used - * as a default. - * - * For forward transforms (R2C), stride must be the spacing between two - * samples in bytes. For inverse transforms, the stride must be set - * to the spacing between two complex values in bytes. - * - * The forward transform performs a real-to-complex DFT of N samples to - * N/2+1 complex values. - * - * The inverse transform performs a complex-to-real DFT of N/2+1 complex - * values to N real samples. The output is not normalized, but can be - * made so by setting the scale value to 1.0/len. - * NOTE: the inverse transform always overwrites the input. - */ - AV_TX_FLOAT_RDFT = 6, - AV_TX_DOUBLE_RDFT = 7, - AV_TX_INT32_RDFT = 8, - - /** - * Real to real (DCT) transforms. - * - * The forward transform is a DCT-II. - * The inverse transform is a DCT-III. - * - * The input array is always overwritten. DCT-III requires that the - * input be padded with 2 extra samples. Stride must be set to the - * spacing between two samples in bytes. - */ - AV_TX_FLOAT_DCT = 9, - AV_TX_DOUBLE_DCT = 10, - AV_TX_INT32_DCT = 11, - - /** - * Discrete Cosine Transform I - * - * The forward transform is a DCT-I. - * The inverse transform is a DCT-I multiplied by 2/(N + 1). - * - * The input array is always overwritten. - */ - AV_TX_FLOAT_DCT_I = 12, - AV_TX_DOUBLE_DCT_I = 13, - AV_TX_INT32_DCT_I = 14, - - /** - * Discrete Sine Transform I - * - * The forward transform is a DST-I. - * The inverse transform is a DST-I multiplied by 2/(N + 1). - * - * The input array is always overwritten. - */ - AV_TX_FLOAT_DST_I = 15, - AV_TX_DOUBLE_DST_I = 16, - AV_TX_INT32_DST_I = 17, - - /* Not part of the API, do not use */ - AV_TX_NB, -}; - -/** - * Function pointer to a function to perform the transform. - * - * @note Using a different context than the one allocated during av_tx_init() - * is not allowed. - * - * @param s the transform context - * @param out the output array - * @param in the input array - * @param stride the input or output stride in bytes - * - * The out and in arrays must be aligned to the maximum required by the CPU - * architecture unless the AV_TX_UNALIGNED flag was set in av_tx_init(). - * The stride must follow the constraints the transform type has specified. - */ -typedef void (*av_tx_fn)(AVTXContext *s, void *out, void *in, ptrdiff_t stride); - -/** - * Flags for av_tx_init() - */ -enum AVTXFlags { - /** - * Allows for in-place transformations, where input == output. - * May be unsupported or slower for some transform types. - */ - AV_TX_INPLACE = 1ULL << 0, - - /** - * Relaxes alignment requirement for the in and out arrays of av_tx_fn(). - * May be slower with certain transform types. - */ - AV_TX_UNALIGNED = 1ULL << 1, - - /** - * Performs a full inverse MDCT rather than leaving out samples that can be - * derived through symmetry. Requires an output array of 'len' floats, - * rather than the usual 'len/2' floats. - * Ignored for all transforms but inverse MDCTs. - */ - AV_TX_FULL_IMDCT = 1ULL << 2, - - /** - * Perform a real to half-complex RDFT. - * Only the real, or imaginary coefficients will - * be output, depending on the flag used. Only available for forward RDFTs. - * Output array must have enough space to hold N complex values - * (regular size for a real to complex transform). - */ - AV_TX_REAL_TO_REAL = 1ULL << 3, - AV_TX_REAL_TO_IMAGINARY = 1ULL << 4, -}; - -/** - * Initialize a transform context with the given configuration - * (i)MDCTs with an odd length are currently not supported. - * - * @param ctx the context to allocate, will be NULL on error - * @param tx pointer to the transform function pointer to set - * @param type type the type of transform - * @param inv whether to do an inverse or a forward transform - * @param len the size of the transform in samples - * @param scale pointer to the value to scale the output if supported by type - * @param flags a bitmask of AVTXFlags or 0 - * - * @return 0 on success, negative error code on failure - */ -int av_tx_init(AVTXContext **ctx, av_tx_fn *tx, enum AVTXType type, - int inv, int len, const void *scale, uint64_t flags); - -/** - * Frees a context and sets *ctx to NULL, does nothing when *ctx == NULL. - */ -void av_tx_uninit(AVTXContext **ctx); - -#endif /* AVUTIL_TX_H */ diff --git a/gostream/ffmpeg/include/libavutil/uuid.h b/gostream/ffmpeg/include/libavutil/uuid.h deleted file mode 100644 index 748b7ed3c94..00000000000 --- a/gostream/ffmpeg/include/libavutil/uuid.h +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2022 Pierre-Anthony Lemieux - * Zane van Iperen - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * UUID parsing and serialization utilities. - * The library treats the UUID as an opaque sequence of 16 unsigned bytes, - * i.e. ignoring the internal layout of the UUID, which depends on the type - * of the UUID. - * - * @author Pierre-Anthony Lemieux - * @author Zane van Iperen - */ - -#ifndef AVUTIL_UUID_H -#define AVUTIL_UUID_H - -#include -#include - -#define AV_PRI_UUID \ - "%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-" \ - "%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx" - -#define AV_PRI_URN_UUID \ - "urn:uuid:%02hhx%02hhx%02hhx%02hhx-%02hhx%02hhx-" \ - "%02hhx%02hhx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx" - -/* AV_UUID_ARG() is used together with AV_PRI_UUID() or AV_PRI_URN_UUID - * to print UUIDs, e.g. - * av_log(NULL, AV_LOG_DEBUG, "UUID: " AV_PRI_UUID, AV_UUID_ARG(uuid)); - */ -#define AV_UUID_ARG(x) \ - (x)[ 0], (x)[ 1], (x)[ 2], (x)[ 3], \ - (x)[ 4], (x)[ 5], (x)[ 6], (x)[ 7], \ - (x)[ 8], (x)[ 9], (x)[10], (x)[11], \ - (x)[12], (x)[13], (x)[14], (x)[15] - -#define AV_UUID_LEN 16 - -/* Binary representation of a UUID */ -typedef uint8_t AVUUID[AV_UUID_LEN]; - -/** - * Parses a string representation of a UUID formatted according to IETF RFC 4122 - * into an AVUUID. The parsing is case-insensitive. The string must be 37 - * characters long, including the terminating NUL character. - * - * Example string representation: "2fceebd0-7017-433d-bafb-d073a7116696" - * - * @param[in] in String representation of a UUID, - * e.g. 2fceebd0-7017-433d-bafb-d073a7116696 - * @param[out] uu AVUUID - * @return A non-zero value in case of an error. - */ -int av_uuid_parse(const char *in, AVUUID uu); - -/** - * Parses a URN representation of a UUID, as specified at IETF RFC 4122, - * into an AVUUID. The parsing is case-insensitive. The string must be 46 - * characters long, including the terminating NUL character. - * - * Example string representation: "urn:uuid:2fceebd0-7017-433d-bafb-d073a7116696" - * - * @param[in] in URN UUID - * @param[out] uu AVUUID - * @return A non-zero value in case of an error. - */ -int av_uuid_urn_parse(const char *in, AVUUID uu); - -/** - * Parses a string representation of a UUID formatted according to IETF RFC 4122 - * into an AVUUID. The parsing is case-insensitive. - * - * @param[in] in_start Pointer to the first character of the string representation - * @param[in] in_end Pointer to the character after the last character of the - * string representation. That memory location is never - * accessed. It is an error if `in_end - in_start != 36`. - * @param[out] uu AVUUID - * @return A non-zero value in case of an error. - */ -int av_uuid_parse_range(const char *in_start, const char *in_end, AVUUID uu); - -/** - * Serializes a AVUUID into a string representation according to IETF RFC 4122. - * The string is lowercase and always 37 characters long, including the - * terminating NUL character. - * - * @param[in] uu AVUUID - * @param[out] out Pointer to an array of no less than 37 characters. - */ -void av_uuid_unparse(const AVUUID uu, char *out); - -/** - * Compares two UUIDs for equality. - * - * @param[in] uu1 AVUUID - * @param[in] uu2 AVUUID - * @return Nonzero if uu1 and uu2 are identical, 0 otherwise - */ -static inline int av_uuid_equal(const AVUUID uu1, const AVUUID uu2) -{ - return memcmp(uu1, uu2, AV_UUID_LEN) == 0; -} - -/** - * Copies the bytes of src into dest. - * - * @param[out] dest AVUUID - * @param[in] src AVUUID - */ -static inline void av_uuid_copy(AVUUID dest, const AVUUID src) -{ - memcpy(dest, src, AV_UUID_LEN); -} - -/** - * Sets a UUID to the nil UUID, i.e. a UUID with have all - * its 128 bits set to zero. - * - * @param[in,out] uu UUID to be set to the nil UUID - */ -static inline void av_uuid_nil(AVUUID uu) -{ - memset(uu, 0, AV_UUID_LEN); -} - -#endif /* AVUTIL_UUID_H */ diff --git a/gostream/ffmpeg/include/libavutil/version.h b/gostream/ffmpeg/include/libavutil/version.h deleted file mode 100644 index 7c0600da22f..00000000000 --- a/gostream/ffmpeg/include/libavutil/version.h +++ /dev/null @@ -1,128 +0,0 @@ -/* - * copyright (c) 2003 Fabrice Bellard - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -/** - * @file - * @ingroup lavu - * Libavutil version macros - */ - -#ifndef AVUTIL_VERSION_H -#define AVUTIL_VERSION_H - -#include "macros.h" - -/** - * @addtogroup version_utils - * - * Useful to check and match library version in order to maintain - * backward compatibility. - * - * The FFmpeg libraries follow a versioning sheme very similar to - * Semantic Versioning (http://semver.org/) - * The difference is that the component called PATCH is called MICRO in FFmpeg - * and its value is reset to 100 instead of 0 to keep it above or equal to 100. - * Also we do not increase MICRO for every bugfix or change in git master. - * - * Prior to FFmpeg 3.2 point releases did not change any lib version number to - * avoid aliassing different git master checkouts. - * Starting with FFmpeg 3.2, the released library versions will occupy - * a separate MAJOR.MINOR that is not used on the master development branch. - * That is if we branch a release of master 55.10.123 we will bump to 55.11.100 - * for the release and master will continue at 55.12.100 after it. Each new - * point release will then bump the MICRO improving the usefulness of the lib - * versions. - * - * @{ - */ - -#define AV_VERSION_INT(a, b, c) ((a)<<16 | (b)<<8 | (c)) -#define AV_VERSION_DOT(a, b, c) a ##.## b ##.## c -#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c) - -/** - * Extract version components from the full ::AV_VERSION_INT int as returned - * by functions like ::avformat_version() and ::avcodec_version() - */ -#define AV_VERSION_MAJOR(a) ((a) >> 16) -#define AV_VERSION_MINOR(a) (((a) & 0x00FF00) >> 8) -#define AV_VERSION_MICRO(a) ((a) & 0xFF) - -/** - * @} - */ - -/** - * @defgroup lavu_ver Version and Build diagnostics - * - * Macros and function useful to check at compiletime and at runtime - * which version of libavutil is in use. - * - * @{ - */ - -#define LIBAVUTIL_VERSION_MAJOR 58 -#define LIBAVUTIL_VERSION_MINOR 29 -#define LIBAVUTIL_VERSION_MICRO 100 - -#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \ - LIBAVUTIL_VERSION_MINOR, \ - LIBAVUTIL_VERSION_MICRO) -#define LIBAVUTIL_VERSION AV_VERSION(LIBAVUTIL_VERSION_MAJOR, \ - LIBAVUTIL_VERSION_MINOR, \ - LIBAVUTIL_VERSION_MICRO) -#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT - -#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION) - -/** - * @defgroup lavu_depr_guards Deprecation Guards - * FF_API_* defines may be placed below to indicate public API that will be - * dropped at a future version bump. The defines themselves are not part of - * the public API and may change, break or disappear at any time. - * - * @note, when bumping the major version it is recommended to manually - * disable each FF_API_* in its own commit instead of disabling them all - * at once through the bump. This improves the git bisect-ability of the change. - * - * @{ - */ - -#define FF_API_FIFO_PEEK2 (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_FIFO_OLD_API (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_XVMC (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_OLD_CHANNEL_LAYOUT (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_AV_FOPEN_UTF8 (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_PKT_DURATION (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_REORDERED_OPAQUE (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_FRAME_PICTURE_NUMBER (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_HDR_VIVID_THREE_SPLINE (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_FRAME_PKT (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_INTERLACED_FRAME (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_FRAME_KEY (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_PALETTE_HAS_CHANGED (LIBAVUTIL_VERSION_MAJOR < 59) -#define FF_API_VULKAN_CONTIGUOUS_MEMORY (LIBAVUTIL_VERSION_MAJOR < 59) - -/** - * @} - * @} - */ - -#endif /* AVUTIL_VERSION_H */ diff --git a/gostream/ffmpeg/include/libavutil/video_enc_params.h b/gostream/ffmpeg/include/libavutil/video_enc_params.h deleted file mode 100644 index 62265a5c064..00000000000 --- a/gostream/ffmpeg/include/libavutil/video_enc_params.h +++ /dev/null @@ -1,171 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_VIDEO_ENC_PARAMS_H -#define AVUTIL_VIDEO_ENC_PARAMS_H - -#include -#include - -#include "libavutil/avassert.h" -#include "libavutil/frame.h" - -enum AVVideoEncParamsType { - AV_VIDEO_ENC_PARAMS_NONE = -1, - /** - * VP9 stores: - * - per-frame base (luma AC) quantizer index, exported as AVVideoEncParams.qp - * - deltas for luma DC, chroma AC and chroma DC, exported in the - * corresponding entries in AVVideoEncParams.delta_qp - * - per-segment delta, exported as for each block as AVVideoBlockParams.delta_qp - * - * To compute the resulting quantizer index for a block: - * - for luma AC, add the base qp and the per-block delta_qp, saturating to - * unsigned 8-bit. - * - for luma DC and chroma AC/DC, add the corresponding - * AVVideoBlockParams.delta_qp to the luma AC index, again saturating to - * unsigned 8-bit. - */ - AV_VIDEO_ENC_PARAMS_VP9, - - /** - * H.264 stores: - * - in PPS (per-picture): - * * initial QP_Y (luma) value, exported as AVVideoEncParams.qp - * * delta(s) for chroma QP values (same for both, or each separately), - * exported as in the corresponding entries in AVVideoEncParams.delta_qp - * - per-slice QP delta, not exported directly, added to the per-MB value - * - per-MB delta; not exported directly; the final per-MB quantizer - * parameter - QP_Y - minus the value in AVVideoEncParams.qp is exported - * as AVVideoBlockParams.qp_delta. - */ - AV_VIDEO_ENC_PARAMS_H264, - - /* - * MPEG-2-compatible quantizer. - * - * Summing the frame-level qp with the per-block delta_qp gives the - * resulting quantizer for the block. - */ - AV_VIDEO_ENC_PARAMS_MPEG2, -}; - -/** - * Video encoding parameters for a given frame. This struct is allocated along - * with an optional array of per-block AVVideoBlockParams descriptors. - * Must be allocated with av_video_enc_params_alloc(). - */ -typedef struct AVVideoEncParams { - /** - * Number of blocks in the array. - * - * May be 0, in which case no per-block information is present. In this case - * the values of blocks_offset / block_size are unspecified and should not - * be accessed. - */ - unsigned int nb_blocks; - /** - * Offset in bytes from the beginning of this structure at which the array - * of blocks starts. - */ - size_t blocks_offset; - /* - * Size of each block in bytes. May not match sizeof(AVVideoBlockParams). - */ - size_t block_size; - - /** - * Type of the parameters (the codec they are used with). - */ - enum AVVideoEncParamsType type; - - /** - * Base quantisation parameter for the frame. The final quantiser for a - * given block in a given plane is obtained from this value, possibly - * combined with {@code delta_qp} and the per-block delta in a manner - * documented for each type. - */ - int32_t qp; - - /** - * Quantisation parameter offset from the base (per-frame) qp for a given - * plane (first index) and AC/DC coefficients (second index). - */ - int32_t delta_qp[4][2]; -} AVVideoEncParams; - -/** - * Data structure for storing block-level encoding information. - * It is allocated as a part of AVVideoEncParams and should be retrieved with - * av_video_enc_params_block(). - * - * sizeof(AVVideoBlockParams) is not a part of the ABI and new fields may be - * added to it. - */ -typedef struct AVVideoBlockParams { - /** - * Distance in luma pixels from the top-left corner of the visible frame - * to the top-left corner of the block. - * Can be negative if top/right padding is present on the coded frame. - */ - int src_x, src_y; - /** - * Width and height of the block in luma pixels. - */ - int w, h; - - /** - * Difference between this block's final quantization parameter and the - * corresponding per-frame value. - */ - int32_t delta_qp; -} AVVideoBlockParams; - -/** - * Get the block at the specified {@code idx}. Must be between 0 and nb_blocks - 1. - */ -static av_always_inline AVVideoBlockParams* -av_video_enc_params_block(AVVideoEncParams *par, unsigned int idx) -{ - av_assert0(idx < par->nb_blocks); - return (AVVideoBlockParams *)((uint8_t *)par + par->blocks_offset + - idx * par->block_size); -} - -/** - * Allocates memory for AVVideoEncParams of the given type, plus an array of - * {@code nb_blocks} AVVideoBlockParams and initializes the variables. Can be - * freed with a normal av_free() call. - * - * @param out_size if non-NULL, the size in bytes of the resulting data array is - * written here. - */ -AVVideoEncParams *av_video_enc_params_alloc(enum AVVideoEncParamsType type, - unsigned int nb_blocks, size_t *out_size); - -/** - * Allocates memory for AVEncodeInfoFrame plus an array of - * {@code nb_blocks} AVEncodeInfoBlock in the given AVFrame {@code frame} - * as AVFrameSideData of type AV_FRAME_DATA_VIDEO_ENC_PARAMS - * and initializes the variables. - */ -AVVideoEncParams* -av_video_enc_params_create_side_data(AVFrame *frame, enum AVVideoEncParamsType type, - unsigned int nb_blocks); - -#endif /* AVUTIL_VIDEO_ENC_PARAMS_H */ diff --git a/gostream/ffmpeg/include/libavutil/video_hint.h b/gostream/ffmpeg/include/libavutil/video_hint.h deleted file mode 100644 index 1b2196093b6..00000000000 --- a/gostream/ffmpeg/include/libavutil/video_hint.h +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright 2023 Elias Carotti - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_VIDEO_HINT_H -#define AVUTIL_VIDEO_HINT_H - -#include -#include -#include "libavutil/avassert.h" -#include "libavutil/frame.h" - -typedef struct AVVideoRect { - uint32_t x, y; - uint32_t width, height; -} AVVideoRect; - -typedef enum AVVideoHintType { - /* rectangled delimit the constant areas (unchanged), default is changed */ - AV_VIDEO_HINT_TYPE_CONSTANT, - - /* rectangled delimit the constant areas (changed), default is not changed */ - AV_VIDEO_HINT_TYPE_CHANGED, -} AVVideoHintType; - -typedef struct AVVideoHint { - /** - * Number of AVVideoRect present. - * - * May be 0, in which case no per-rectangle information is present. In this - * case the values of rect_offset / rect_size are unspecified and should - * not be accessed. - */ - size_t nb_rects; - - /** - * Offset in bytes from the beginning of this structure at which the array - * of AVVideoRect starts. - */ - size_t rect_offset; - - /** - * Size in bytes of AVVideoRect. - */ - size_t rect_size; - - AVVideoHintType type; -} AVVideoHint; - -static av_always_inline AVVideoRect * -av_video_hint_rects(const AVVideoHint *hints) { - return (AVVideoRect *)((uint8_t *)hints + hints->rect_offset); -} - -static av_always_inline AVVideoRect * -av_video_hint_get_rect(const AVVideoHint *hints, size_t idx) { - return (AVVideoRect *)((uint8_t *)hints + hints->rect_offset + idx * hints->rect_size); -} - -/** - * Allocate memory for the AVVideoHint struct along with an nb_rects-sized - * arrays of AVVideoRect. - * - * The side data contains a list of rectangles for the portions of the frame - * which changed from the last encoded one (and the remainder are assumed to be - * changed), or, alternately (depending on the type parameter) the unchanged - * ones (and the remanining ones are those which changed). - * Macroblocks will thus be hinted either to be P_SKIP-ped or go through the - * regular encoding procedure. - * - * It's responsibility of the caller to fill the AVRects accordingly, and to set - * the proper AVVideoHintType field. - * - * @param out_size if non-NULL, the size in bytes of the resulting data array is - * written here - * - * @return newly allocated AVVideoHint struct (must be freed by the caller using - * av_free()) on success, NULL on memory allocation failure - */ -AVVideoHint *av_video_hint_alloc(size_t nb_rects, - size_t *out_size); - -/** - * Same as av_video_hint_alloc(), except newly-allocated AVVideoHint is attached - * as side data of type AV_FRAME_DATA_VIDEO_HINT_INFO to frame. - */ -AVVideoHint *av_video_hint_create_side_data(AVFrame *frame, - size_t nb_rects); - - -#endif /* AVUTIL_VIDEO_HINT_H */ diff --git a/gostream/ffmpeg/include/libavutil/xtea.h b/gostream/ffmpeg/include/libavutil/xtea.h deleted file mode 100644 index 735427c109a..00000000000 --- a/gostream/ffmpeg/include/libavutil/xtea.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * A 32-bit implementation of the XTEA algorithm - * Copyright (c) 2012 Samuel Pitoiset - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef AVUTIL_XTEA_H -#define AVUTIL_XTEA_H - -#include - -/** - * @file - * @brief Public header for libavutil XTEA algorithm - * @defgroup lavu_xtea XTEA - * @ingroup lavu_crypto - * @{ - */ - -typedef struct AVXTEA { - uint32_t key[16]; -} AVXTEA; - -/** - * Allocate an AVXTEA context. - */ -AVXTEA *av_xtea_alloc(void); - -/** - * Initialize an AVXTEA context. - * - * @param ctx an AVXTEA context - * @param key a key of 16 bytes used for encryption/decryption, - * interpreted as big endian 32 bit numbers - */ -void av_xtea_init(struct AVXTEA *ctx, const uint8_t key[16]); - -/** - * Initialize an AVXTEA context. - * - * @param ctx an AVXTEA context - * @param key a key of 16 bytes used for encryption/decryption, - * interpreted as little endian 32 bit numbers - */ -void av_xtea_le_init(struct AVXTEA *ctx, const uint8_t key[16]); - -/** - * Encrypt or decrypt a buffer using a previously initialized context, - * in big endian format. - * - * @param ctx an AVXTEA context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 8 byte blocks - * @param iv initialization vector for CBC mode, if NULL then ECB will be used - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_xtea_crypt(struct AVXTEA *ctx, uint8_t *dst, const uint8_t *src, - int count, uint8_t *iv, int decrypt); - -/** - * Encrypt or decrypt a buffer using a previously initialized context, - * in little endian format. - * - * @param ctx an AVXTEA context - * @param dst destination array, can be equal to src - * @param src source array, can be equal to dst - * @param count number of 8 byte blocks - * @param iv initialization vector for CBC mode, if NULL then ECB will be used - * @param decrypt 0 for encryption, 1 for decryption - */ -void av_xtea_le_crypt(struct AVXTEA *ctx, uint8_t *dst, const uint8_t *src, - int count, uint8_t *iv, int decrypt); - -/** - * @} - */ - -#endif /* AVUTIL_XTEA_H */ diff --git a/gostream/ffmpeg/include/libswresample/swresample.h b/gostream/ffmpeg/include/libswresample/swresample.h deleted file mode 100644 index d4dcaebdcf3..00000000000 --- a/gostream/ffmpeg/include/libswresample/swresample.h +++ /dev/null @@ -1,650 +0,0 @@ -/* - * Copyright (C) 2011-2013 Michael Niedermayer (michaelni@gmx.at) - * - * This file is part of libswresample - * - * libswresample is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * libswresample is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with libswresample; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef SWRESAMPLE_SWRESAMPLE_H -#define SWRESAMPLE_SWRESAMPLE_H - -/** - * @file - * @ingroup lswr - * libswresample public header - */ - -/** - * @defgroup lswr libswresample - * @{ - * - * Audio resampling, sample format conversion and mixing library. - * - * Interaction with lswr is done through SwrContext, which is - * allocated with swr_alloc() or swr_alloc_set_opts2(). It is opaque, so all parameters - * must be set with the @ref avoptions API. - * - * The first thing you will need to do in order to use lswr is to allocate - * SwrContext. This can be done with swr_alloc() or swr_alloc_set_opts2(). If you - * are using the former, you must set options through the @ref avoptions API. - * The latter function provides the same feature, but it allows you to set some - * common options in the same statement. - * - * For example the following code will setup conversion from planar float sample - * format to interleaved signed 16-bit integer, downsampling from 48kHz to - * 44.1kHz and downmixing from 5.1 channels to stereo (using the default mixing - * matrix). This is using the swr_alloc() function. - * @code - * SwrContext *swr = swr_alloc(); - * av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_5POINT1, 0); - * av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); - * av_opt_set_int(swr, "in_sample_rate", 48000, 0); - * av_opt_set_int(swr, "out_sample_rate", 44100, 0); - * av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_FLTP, 0); - * av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0); - * @endcode - * - * The same job can be done using swr_alloc_set_opts2() as well: - * @code - * SwrContext *swr = NULL; - * int ret = swr_alloc_set_opts2(&swr, // we're allocating a new context - * &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO, // out_ch_layout - * AV_SAMPLE_FMT_S16, // out_sample_fmt - * 44100, // out_sample_rate - * &(AVChannelLayout)AV_CHANNEL_LAYOUT_5POINT1, // in_ch_layout - * AV_SAMPLE_FMT_FLTP, // in_sample_fmt - * 48000, // in_sample_rate - * 0, // log_offset - * NULL); // log_ctx - * @endcode - * - * Once all values have been set, it must be initialized with swr_init(). If - * you need to change the conversion parameters, you can change the parameters - * using @ref avoptions, as described above in the first example; or by using - * swr_alloc_set_opts2(), but with the first argument the allocated context. - * You must then call swr_init() again. - * - * The conversion itself is done by repeatedly calling swr_convert(). - * Note that the samples may get buffered in swr if you provide insufficient - * output space or if sample rate conversion is done, which requires "future" - * samples. Samples that do not require future input can be retrieved at any - * time by using swr_convert() (in_count can be set to 0). - * At the end of conversion the resampling buffer can be flushed by calling - * swr_convert() with NULL in and 0 in_count. - * - * The samples used in the conversion process can be managed with the libavutil - * @ref lavu_sampmanip "samples manipulation" API, including av_samples_alloc() - * function used in the following example. - * - * The delay between input and output, can at any time be found by using - * swr_get_delay(). - * - * The following code demonstrates the conversion loop assuming the parameters - * from above and caller-defined functions get_input() and handle_output(): - * @code - * uint8_t **input; - * int in_samples; - * - * while (get_input(&input, &in_samples)) { - * uint8_t *output; - * int out_samples = av_rescale_rnd(swr_get_delay(swr, 48000) + - * in_samples, 44100, 48000, AV_ROUND_UP); - * av_samples_alloc(&output, NULL, 2, out_samples, - * AV_SAMPLE_FMT_S16, 0); - * out_samples = swr_convert(swr, &output, out_samples, - * input, in_samples); - * handle_output(output, out_samples); - * av_freep(&output); - * } - * @endcode - * - * When the conversion is finished, the conversion - * context and everything associated with it must be freed with swr_free(). - * A swr_close() function is also available, but it exists mainly for - * compatibility with libavresample, and is not required to be called. - * - * There will be no memory leak if the data is not completely flushed before - * swr_free(). - */ - -#include -#include "libavutil/channel_layout.h" -#include "libavutil/frame.h" -#include "libavutil/samplefmt.h" - -#include "libswresample/version_major.h" -#ifndef HAVE_AV_CONFIG_H -/* When included as part of the ffmpeg build, only include the major version - * to avoid unnecessary rebuilds. When included externally, keep including - * the full version information. */ -#include "libswresample/version.h" -#endif - -/** - * @name Option constants - * These constants are used for the @ref avoptions interface for lswr. - * @{ - * - */ - -#define SWR_FLAG_RESAMPLE 1 ///< Force resampling even if equal sample rate -//TODO use int resample ? -//long term TODO can we enable this dynamically? - -/** Dithering algorithms */ -enum SwrDitherType { - SWR_DITHER_NONE = 0, - SWR_DITHER_RECTANGULAR, - SWR_DITHER_TRIANGULAR, - SWR_DITHER_TRIANGULAR_HIGHPASS, - - SWR_DITHER_NS = 64, ///< not part of API/ABI - SWR_DITHER_NS_LIPSHITZ, - SWR_DITHER_NS_F_WEIGHTED, - SWR_DITHER_NS_MODIFIED_E_WEIGHTED, - SWR_DITHER_NS_IMPROVED_E_WEIGHTED, - SWR_DITHER_NS_SHIBATA, - SWR_DITHER_NS_LOW_SHIBATA, - SWR_DITHER_NS_HIGH_SHIBATA, - SWR_DITHER_NB, ///< not part of API/ABI -}; - -/** Resampling Engines */ -enum SwrEngine { - SWR_ENGINE_SWR, /**< SW Resampler */ - SWR_ENGINE_SOXR, /**< SoX Resampler */ - SWR_ENGINE_NB, ///< not part of API/ABI -}; - -/** Resampling Filter Types */ -enum SwrFilterType { - SWR_FILTER_TYPE_CUBIC, /**< Cubic */ - SWR_FILTER_TYPE_BLACKMAN_NUTTALL, /**< Blackman Nuttall windowed sinc */ - SWR_FILTER_TYPE_KAISER, /**< Kaiser windowed sinc */ -}; - -/** - * @} - */ - -/** - * The libswresample context. Unlike libavcodec and libavformat, this structure - * is opaque. This means that if you would like to set options, you must use - * the @ref avoptions API and cannot directly set values to members of the - * structure. - */ -typedef struct SwrContext SwrContext; - -/** - * Get the AVClass for SwrContext. It can be used in combination with - * AV_OPT_SEARCH_FAKE_OBJ for examining options. - * - * @see av_opt_find(). - * @return the AVClass of SwrContext - */ -const AVClass *swr_get_class(void); - -/** - * @name SwrContext constructor functions - * @{ - */ - -/** - * Allocate SwrContext. - * - * If you use this function you will need to set the parameters (manually or - * with swr_alloc_set_opts2()) before calling swr_init(). - * - * @see swr_alloc_set_opts2(), swr_init(), swr_free() - * @return NULL on error, allocated context otherwise - */ -struct SwrContext *swr_alloc(void); - -/** - * Initialize context after user parameters have been set. - * @note The context must be configured using the AVOption API. - * - * @see av_opt_set_int() - * @see av_opt_set_dict() - * - * @param[in,out] s Swr context to initialize - * @return AVERROR error code in case of failure. - */ -int swr_init(struct SwrContext *s); - -/** - * Check whether an swr context has been initialized or not. - * - * @param[in] s Swr context to check - * @see swr_init() - * @return positive if it has been initialized, 0 if not initialized - */ -int swr_is_initialized(struct SwrContext *s); - -#if FF_API_OLD_CHANNEL_LAYOUT -/** - * Allocate SwrContext if needed and set/reset common parameters. - * - * This function does not require s to be allocated with swr_alloc(). On the - * other hand, swr_alloc() can use swr_alloc_set_opts() to set the parameters - * on the allocated context. - * - * @param s existing Swr context if available, or NULL if not - * @param out_ch_layout output channel layout (AV_CH_LAYOUT_*) - * @param out_sample_fmt output sample format (AV_SAMPLE_FMT_*). - * @param out_sample_rate output sample rate (frequency in Hz) - * @param in_ch_layout input channel layout (AV_CH_LAYOUT_*) - * @param in_sample_fmt input sample format (AV_SAMPLE_FMT_*). - * @param in_sample_rate input sample rate (frequency in Hz) - * @param log_offset logging level offset - * @param log_ctx parent logging context, can be NULL - * - * @see swr_init(), swr_free() - * @return NULL on error, allocated context otherwise - * @deprecated use @ref swr_alloc_set_opts2() - */ -attribute_deprecated -struct SwrContext *swr_alloc_set_opts(struct SwrContext *s, - int64_t out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate, - int64_t in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate, - int log_offset, void *log_ctx); -#endif - -/** - * Allocate SwrContext if needed and set/reset common parameters. - * - * This function does not require *ps to be allocated with swr_alloc(). On the - * other hand, swr_alloc() can use swr_alloc_set_opts2() to set the parameters - * on the allocated context. - * - * @param ps Pointer to an existing Swr context if available, or to NULL if not. - * On success, *ps will be set to the allocated context. - * @param out_ch_layout output channel layout (e.g. AV_CHANNEL_LAYOUT_*) - * @param out_sample_fmt output sample format (AV_SAMPLE_FMT_*). - * @param out_sample_rate output sample rate (frequency in Hz) - * @param in_ch_layout input channel layout (e.g. AV_CHANNEL_LAYOUT_*) - * @param in_sample_fmt input sample format (AV_SAMPLE_FMT_*). - * @param in_sample_rate input sample rate (frequency in Hz) - * @param log_offset logging level offset - * @param log_ctx parent logging context, can be NULL - * - * @see swr_init(), swr_free() - * @return 0 on success, a negative AVERROR code on error. - * On error, the Swr context is freed and *ps set to NULL. - */ -int swr_alloc_set_opts2(struct SwrContext **ps, - const AVChannelLayout *out_ch_layout, enum AVSampleFormat out_sample_fmt, int out_sample_rate, - const AVChannelLayout *in_ch_layout, enum AVSampleFormat in_sample_fmt, int in_sample_rate, - int log_offset, void *log_ctx); -/** - * @} - * - * @name SwrContext destructor functions - * @{ - */ - -/** - * Free the given SwrContext and set the pointer to NULL. - * - * @param[in] s a pointer to a pointer to Swr context - */ -void swr_free(struct SwrContext **s); - -/** - * Closes the context so that swr_is_initialized() returns 0. - * - * The context can be brought back to life by running swr_init(), - * swr_init() can also be used without swr_close(). - * This function is mainly provided for simplifying the usecase - * where one tries to support libavresample and libswresample. - * - * @param[in,out] s Swr context to be closed - */ -void swr_close(struct SwrContext *s); - -/** - * @} - * - * @name Core conversion functions - * @{ - */ - -/** Convert audio. - * - * in and in_count can be set to 0 to flush the last few samples out at the - * end. - * - * If more input is provided than output space, then the input will be buffered. - * You can avoid this buffering by using swr_get_out_samples() to retrieve an - * upper bound on the required number of output samples for the given number of - * input samples. Conversion will run directly without copying whenever possible. - * - * @param s allocated Swr context, with parameters set - * @param out output buffers, only the first one need be set in case of packed audio - * @param out_count amount of space available for output in samples per channel - * @param in input buffers, only the first one need to be set in case of packed audio - * @param in_count number of input samples available in one channel - * - * @return number of samples output per channel, negative value on error - */ -int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, - const uint8_t **in , int in_count); - -/** - * Convert the next timestamp from input to output - * timestamps are in 1/(in_sample_rate * out_sample_rate) units. - * - * @note There are 2 slightly differently behaving modes. - * @li When automatic timestamp compensation is not used, (min_compensation >= FLT_MAX) - * in this case timestamps will be passed through with delays compensated - * @li When automatic timestamp compensation is used, (min_compensation < FLT_MAX) - * in this case the output timestamps will match output sample numbers. - * See ffmpeg-resampler(1) for the two modes of compensation. - * - * @param[in] s initialized Swr context - * @param[in] pts timestamp for the next input sample, INT64_MIN if unknown - * @see swr_set_compensation(), swr_drop_output(), and swr_inject_silence() are - * function used internally for timestamp compensation. - * @return the output timestamp for the next output sample - */ -int64_t swr_next_pts(struct SwrContext *s, int64_t pts); - -/** - * @} - * - * @name Low-level option setting functions - * These functons provide a means to set low-level options that is not possible - * with the AVOption API. - * @{ - */ - -/** - * Activate resampling compensation ("soft" compensation). This function is - * internally called when needed in swr_next_pts(). - * - * @param[in,out] s allocated Swr context. If it is not initialized, - * or SWR_FLAG_RESAMPLE is not set, swr_init() is - * called with the flag set. - * @param[in] sample_delta delta in PTS per sample - * @param[in] compensation_distance number of samples to compensate for - * @return >= 0 on success, AVERROR error codes if: - * @li @c s is NULL, - * @li @c compensation_distance is less than 0, - * @li @c compensation_distance is 0 but sample_delta is not, - * @li compensation unsupported by resampler, or - * @li swr_init() fails when called. - */ -int swr_set_compensation(struct SwrContext *s, int sample_delta, int compensation_distance); - -/** - * Set a customized input channel mapping. - * - * @param[in,out] s allocated Swr context, not yet initialized - * @param[in] channel_map customized input channel mapping (array of channel - * indexes, -1 for a muted channel) - * @return >= 0 on success, or AVERROR error code in case of failure. - */ -int swr_set_channel_mapping(struct SwrContext *s, const int *channel_map); - -#if FF_API_OLD_CHANNEL_LAYOUT -/** - * Generate a channel mixing matrix. - * - * This function is the one used internally by libswresample for building the - * default mixing matrix. It is made public just as a utility function for - * building custom matrices. - * - * @param in_layout input channel layout - * @param out_layout output channel layout - * @param center_mix_level mix level for the center channel - * @param surround_mix_level mix level for the surround channel(s) - * @param lfe_mix_level mix level for the low-frequency effects channel - * @param rematrix_maxval if 1.0, coefficients will be normalized to prevent - * overflow. if INT_MAX, coefficients will not be - * normalized. - * @param[out] matrix mixing coefficients; matrix[i + stride * o] is - * the weight of input channel i in output channel o. - * @param stride distance between adjacent input channels in the - * matrix array - * @param matrix_encoding matrixed stereo downmix mode (e.g. dplii) - * @param log_ctx parent logging context, can be NULL - * @return 0 on success, negative AVERROR code on failure - * @deprecated use @ref swr_build_matrix2() - */ -attribute_deprecated -int swr_build_matrix(uint64_t in_layout, uint64_t out_layout, - double center_mix_level, double surround_mix_level, - double lfe_mix_level, double rematrix_maxval, - double rematrix_volume, double *matrix, - int stride, enum AVMatrixEncoding matrix_encoding, - void *log_ctx); -#endif - -/** - * Generate a channel mixing matrix. - * - * This function is the one used internally by libswresample for building the - * default mixing matrix. It is made public just as a utility function for - * building custom matrices. - * - * @param in_layout input channel layout - * @param out_layout output channel layout - * @param center_mix_level mix level for the center channel - * @param surround_mix_level mix level for the surround channel(s) - * @param lfe_mix_level mix level for the low-frequency effects channel - * @param rematrix_maxval if 1.0, coefficients will be normalized to prevent - * overflow. if INT_MAX, coefficients will not be - * normalized. - * @param[out] matrix mixing coefficients; matrix[i + stride * o] is - * the weight of input channel i in output channel o. - * @param stride distance between adjacent input channels in the - * matrix array - * @param matrix_encoding matrixed stereo downmix mode (e.g. dplii) - * @param log_ctx parent logging context, can be NULL - * @return 0 on success, negative AVERROR code on failure - */ -int swr_build_matrix2(const AVChannelLayout *in_layout, const AVChannelLayout *out_layout, - double center_mix_level, double surround_mix_level, - double lfe_mix_level, double maxval, - double rematrix_volume, double *matrix, - ptrdiff_t stride, enum AVMatrixEncoding matrix_encoding, - void *log_context); - -/** - * Set a customized remix matrix. - * - * @param s allocated Swr context, not yet initialized - * @param matrix remix coefficients; matrix[i + stride * o] is - * the weight of input channel i in output channel o - * @param stride offset between lines of the matrix - * @return >= 0 on success, or AVERROR error code in case of failure. - */ -int swr_set_matrix(struct SwrContext *s, const double *matrix, int stride); - -/** - * @} - * - * @name Sample handling functions - * @{ - */ - -/** - * Drops the specified number of output samples. - * - * This function, along with swr_inject_silence(), is called by swr_next_pts() - * if needed for "hard" compensation. - * - * @param s allocated Swr context - * @param count number of samples to be dropped - * - * @return >= 0 on success, or a negative AVERROR code on failure - */ -int swr_drop_output(struct SwrContext *s, int count); - -/** - * Injects the specified number of silence samples. - * - * This function, along with swr_drop_output(), is called by swr_next_pts() - * if needed for "hard" compensation. - * - * @param s allocated Swr context - * @param count number of samples to be dropped - * - * @return >= 0 on success, or a negative AVERROR code on failure - */ -int swr_inject_silence(struct SwrContext *s, int count); - -/** - * Gets the delay the next input sample will experience relative to the next output sample. - * - * Swresample can buffer data if more input has been provided than available - * output space, also converting between sample rates needs a delay. - * This function returns the sum of all such delays. - * The exact delay is not necessarily an integer value in either input or - * output sample rate. Especially when downsampling by a large value, the - * output sample rate may be a poor choice to represent the delay, similarly - * for upsampling and the input sample rate. - * - * @param s swr context - * @param base timebase in which the returned delay will be: - * @li if it's set to 1 the returned delay is in seconds - * @li if it's set to 1000 the returned delay is in milliseconds - * @li if it's set to the input sample rate then the returned - * delay is in input samples - * @li if it's set to the output sample rate then the returned - * delay is in output samples - * @li if it's the least common multiple of in_sample_rate and - * out_sample_rate then an exact rounding-free delay will be - * returned - * @returns the delay in 1 / @c base units. - */ -int64_t swr_get_delay(struct SwrContext *s, int64_t base); - -/** - * Find an upper bound on the number of samples that the next swr_convert - * call will output, if called with in_samples of input samples. This - * depends on the internal state, and anything changing the internal state - * (like further swr_convert() calls) will may change the number of samples - * swr_get_out_samples() returns for the same number of input samples. - * - * @param in_samples number of input samples. - * @note any call to swr_inject_silence(), swr_convert(), swr_next_pts() - * or swr_set_compensation() invalidates this limit - * @note it is recommended to pass the correct available buffer size - * to all functions like swr_convert() even if swr_get_out_samples() - * indicates that less would be used. - * @returns an upper bound on the number of samples that the next swr_convert - * will output or a negative value to indicate an error - */ -int swr_get_out_samples(struct SwrContext *s, int in_samples); - -/** - * @} - * - * @name Configuration accessors - * @{ - */ - -/** - * Return the @ref LIBSWRESAMPLE_VERSION_INT constant. - * - * This is useful to check if the build-time libswresample has the same version - * as the run-time one. - * - * @returns the unsigned int-typed version - */ -unsigned swresample_version(void); - -/** - * Return the swr build-time configuration. - * - * @returns the build-time @c ./configure flags - */ -const char *swresample_configuration(void); - -/** - * Return the swr license. - * - * @returns the license of libswresample, determined at build-time - */ -const char *swresample_license(void); - -/** - * @} - * - * @name AVFrame based API - * @{ - */ - -/** - * Convert the samples in the input AVFrame and write them to the output AVFrame. - * - * Input and output AVFrames must have channel_layout, sample_rate and format set. - * - * If the output AVFrame does not have the data pointers allocated the nb_samples - * field will be set using av_frame_get_buffer() - * is called to allocate the frame. - * - * The output AVFrame can be NULL or have fewer allocated samples than required. - * In this case, any remaining samples not written to the output will be added - * to an internal FIFO buffer, to be returned at the next call to this function - * or to swr_convert(). - * - * If converting sample rate, there may be data remaining in the internal - * resampling delay buffer. swr_get_delay() tells the number of - * remaining samples. To get this data as output, call this function or - * swr_convert() with NULL input. - * - * If the SwrContext configuration does not match the output and - * input AVFrame settings the conversion does not take place and depending on - * which AVFrame is not matching AVERROR_OUTPUT_CHANGED, AVERROR_INPUT_CHANGED - * or the result of a bitwise-OR of them is returned. - * - * @see swr_delay() - * @see swr_convert() - * @see swr_get_delay() - * - * @param swr audio resample context - * @param output output AVFrame - * @param input input AVFrame - * @return 0 on success, AVERROR on failure or nonmatching - * configuration. - */ -int swr_convert_frame(SwrContext *swr, - AVFrame *output, const AVFrame *input); - -/** - * Configure or reconfigure the SwrContext using the information - * provided by the AVFrames. - * - * The original resampling context is reset even on failure. - * The function calls swr_close() internally if the context is open. - * - * @see swr_close(); - * - * @param swr audio resample context - * @param out output AVFrame - * @param in input AVFrame - * @return 0 on success, AVERROR on failure. - */ -int swr_config_frame(SwrContext *swr, const AVFrame *out, const AVFrame *in); - -/** - * @} - * @} - */ - -#endif /* SWRESAMPLE_SWRESAMPLE_H */ diff --git a/gostream/ffmpeg/include/libswresample/version.h b/gostream/ffmpeg/include/libswresample/version.h deleted file mode 100644 index a2668b5e59e..00000000000 --- a/gostream/ffmpeg/include/libswresample/version.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Version macros. - * - * This file is part of libswresample - * - * libswresample is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * libswresample is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with libswresample; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef SWRESAMPLE_VERSION_H -#define SWRESAMPLE_VERSION_H - -/** - * @file - * Libswresample version macros - */ - -#include "libavutil/version.h" - -#include "version_major.h" - -#define LIBSWRESAMPLE_VERSION_MINOR 12 -#define LIBSWRESAMPLE_VERSION_MICRO 100 - -#define LIBSWRESAMPLE_VERSION_INT AV_VERSION_INT(LIBSWRESAMPLE_VERSION_MAJOR, \ - LIBSWRESAMPLE_VERSION_MINOR, \ - LIBSWRESAMPLE_VERSION_MICRO) -#define LIBSWRESAMPLE_VERSION AV_VERSION(LIBSWRESAMPLE_VERSION_MAJOR, \ - LIBSWRESAMPLE_VERSION_MINOR, \ - LIBSWRESAMPLE_VERSION_MICRO) -#define LIBSWRESAMPLE_BUILD LIBSWRESAMPLE_VERSION_INT - -#define LIBSWRESAMPLE_IDENT "SwR" AV_STRINGIFY(LIBSWRESAMPLE_VERSION) - -#endif /* SWRESAMPLE_VERSION_H */ diff --git a/gostream/ffmpeg/include/libswresample/version_major.h b/gostream/ffmpeg/include/libswresample/version_major.h deleted file mode 100644 index 7f265c2073d..00000000000 --- a/gostream/ffmpeg/include/libswresample/version_major.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Version macros. - * - * This file is part of libswresample - * - * libswresample is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * libswresample is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with libswresample; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef SWRESAMPLE_VERSION_MAJOR_H -#define SWRESAMPLE_VERSION_MAJOR_H - -/** - * @file - * Libswresample version macros - */ - -#define LIBSWRESAMPLE_VERSION_MAJOR 4 - -#endif /* SWRESAMPLE_VERSION_MAJOR_H */ diff --git a/gostream/ffmpeg/include/libswscale/swscale.h b/gostream/ffmpeg/include/libswscale/swscale.h deleted file mode 100644 index 9d4612aaf3a..00000000000 --- a/gostream/ffmpeg/include/libswscale/swscale.h +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Copyright (C) 2001-2011 Michael Niedermayer - * - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef SWSCALE_SWSCALE_H -#define SWSCALE_SWSCALE_H - -/** - * @file - * @ingroup libsws - * external API header - */ - -#include - -#include "libavutil/avutil.h" -#include "libavutil/frame.h" -#include "libavutil/log.h" -#include "libavutil/pixfmt.h" -#include "version_major.h" -#ifndef HAVE_AV_CONFIG_H -/* When included as part of the ffmpeg build, only include the major version - * to avoid unnecessary rebuilds. When included externally, keep including - * the full version information. */ -#include "version.h" -#endif - -/** - * @defgroup libsws libswscale - * Color conversion and scaling library. - * - * @{ - * - * Return the LIBSWSCALE_VERSION_INT constant. - */ -unsigned swscale_version(void); - -/** - * Return the libswscale build-time configuration. - */ -const char *swscale_configuration(void); - -/** - * Return the libswscale license. - */ -const char *swscale_license(void); - -/* values for the flags, the stuff on the command line is different */ -#define SWS_FAST_BILINEAR 1 -#define SWS_BILINEAR 2 -#define SWS_BICUBIC 4 -#define SWS_X 8 -#define SWS_POINT 0x10 -#define SWS_AREA 0x20 -#define SWS_BICUBLIN 0x40 -#define SWS_GAUSS 0x80 -#define SWS_SINC 0x100 -#define SWS_LANCZOS 0x200 -#define SWS_SPLINE 0x400 - -#define SWS_SRC_V_CHR_DROP_MASK 0x30000 -#define SWS_SRC_V_CHR_DROP_SHIFT 16 - -#define SWS_PARAM_DEFAULT 123456 - -#define SWS_PRINT_INFO 0x1000 - -//the following 3 flags are not completely implemented -//internal chrominance subsampling info -#define SWS_FULL_CHR_H_INT 0x2000 -//input subsampling info -#define SWS_FULL_CHR_H_INP 0x4000 -#define SWS_DIRECT_BGR 0x8000 -#define SWS_ACCURATE_RND 0x40000 -#define SWS_BITEXACT 0x80000 -#define SWS_ERROR_DIFFUSION 0x800000 - -#define SWS_MAX_REDUCE_CUTOFF 0.002 - -#define SWS_CS_ITU709 1 -#define SWS_CS_FCC 4 -#define SWS_CS_ITU601 5 -#define SWS_CS_ITU624 5 -#define SWS_CS_SMPTE170M 5 -#define SWS_CS_SMPTE240M 7 -#define SWS_CS_DEFAULT 5 -#define SWS_CS_BT2020 9 - -/** - * Return a pointer to yuv<->rgb coefficients for the given colorspace - * suitable for sws_setColorspaceDetails(). - * - * @param colorspace One of the SWS_CS_* macros. If invalid, - * SWS_CS_DEFAULT is used. - */ -const int *sws_getCoefficients(int colorspace); - -// when used for filters they must have an odd number of elements -// coeffs cannot be shared between vectors -typedef struct SwsVector { - double *coeff; ///< pointer to the list of coefficients - int length; ///< number of coefficients in the vector -} SwsVector; - -// vectors can be shared -typedef struct SwsFilter { - SwsVector *lumH; - SwsVector *lumV; - SwsVector *chrH; - SwsVector *chrV; -} SwsFilter; - -struct SwsContext; - -/** - * Return a positive value if pix_fmt is a supported input format, 0 - * otherwise. - */ -int sws_isSupportedInput(enum AVPixelFormat pix_fmt); - -/** - * Return a positive value if pix_fmt is a supported output format, 0 - * otherwise. - */ -int sws_isSupportedOutput(enum AVPixelFormat pix_fmt); - -/** - * @param[in] pix_fmt the pixel format - * @return a positive value if an endianness conversion for pix_fmt is - * supported, 0 otherwise. - */ -int sws_isSupportedEndiannessConversion(enum AVPixelFormat pix_fmt); - -/** - * Allocate an empty SwsContext. This must be filled and passed to - * sws_init_context(). For filling see AVOptions, options.c and - * sws_setColorspaceDetails(). - */ -struct SwsContext *sws_alloc_context(void); - -/** - * Initialize the swscaler context sws_context. - * - * @return zero or positive value on success, a negative value on - * error - */ -av_warn_unused_result -int sws_init_context(struct SwsContext *sws_context, SwsFilter *srcFilter, SwsFilter *dstFilter); - -/** - * Free the swscaler context swsContext. - * If swsContext is NULL, then does nothing. - */ -void sws_freeContext(struct SwsContext *swsContext); - -/** - * Allocate and return an SwsContext. You need it to perform - * scaling/conversion operations using sws_scale(). - * - * @param srcW the width of the source image - * @param srcH the height of the source image - * @param srcFormat the source image format - * @param dstW the width of the destination image - * @param dstH the height of the destination image - * @param dstFormat the destination image format - * @param flags specify which algorithm and options to use for rescaling - * @param param extra parameters to tune the used scaler - * For SWS_BICUBIC param[0] and [1] tune the shape of the basis - * function, param[0] tunes f(1) and param[1] f´(1) - * For SWS_GAUSS param[0] tunes the exponent and thus cutoff - * frequency - * For SWS_LANCZOS param[0] tunes the width of the window function - * @return a pointer to an allocated context, or NULL in case of error - * @note this function is to be removed after a saner alternative is - * written - */ -struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat, - int dstW, int dstH, enum AVPixelFormat dstFormat, - int flags, SwsFilter *srcFilter, - SwsFilter *dstFilter, const double *param); - -/** - * Scale the image slice in srcSlice and put the resulting scaled - * slice in the image in dst. A slice is a sequence of consecutive - * rows in an image. - * - * Slices have to be provided in sequential order, either in - * top-bottom or bottom-top order. If slices are provided in - * non-sequential order the behavior of the function is undefined. - * - * @param c the scaling context previously created with - * sws_getContext() - * @param srcSlice the array containing the pointers to the planes of - * the source slice - * @param srcStride the array containing the strides for each plane of - * the source image - * @param srcSliceY the position in the source image of the slice to - * process, that is the number (counted starting from - * zero) in the image of the first row of the slice - * @param srcSliceH the height of the source slice, that is the number - * of rows in the slice - * @param dst the array containing the pointers to the planes of - * the destination image - * @param dstStride the array containing the strides for each plane of - * the destination image - * @return the height of the output slice - */ -int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[], - const int srcStride[], int srcSliceY, int srcSliceH, - uint8_t *const dst[], const int dstStride[]); - -/** - * Scale source data from src and write the output to dst. - * - * This is merely a convenience wrapper around - * - sws_frame_start() - * - sws_send_slice(0, src->height) - * - sws_receive_slice(0, dst->height) - * - sws_frame_end() - * - * @param c The scaling context - * @param dst The destination frame. See documentation for sws_frame_start() for - * more details. - * @param src The source frame. - * - * @return 0 on success, a negative AVERROR code on failure - */ -int sws_scale_frame(struct SwsContext *c, AVFrame *dst, const AVFrame *src); - -/** - * Initialize the scaling process for a given pair of source/destination frames. - * Must be called before any calls to sws_send_slice() and sws_receive_slice(). - * - * This function will retain references to src and dst, so they must both use - * refcounted buffers (if allocated by the caller, in case of dst). - * - * @param c The scaling context - * @param dst The destination frame. - * - * The data buffers may either be already allocated by the caller or - * left clear, in which case they will be allocated by the scaler. - * The latter may have performance advantages - e.g. in certain cases - * some output planes may be references to input planes, rather than - * copies. - * - * Output data will be written into this frame in successful - * sws_receive_slice() calls. - * @param src The source frame. The data buffers must be allocated, but the - * frame data does not have to be ready at this point. Data - * availability is then signalled by sws_send_slice(). - * @return 0 on success, a negative AVERROR code on failure - * - * @see sws_frame_end() - */ -int sws_frame_start(struct SwsContext *c, AVFrame *dst, const AVFrame *src); - -/** - * Finish the scaling process for a pair of source/destination frames previously - * submitted with sws_frame_start(). Must be called after all sws_send_slice() - * and sws_receive_slice() calls are done, before any new sws_frame_start() - * calls. - * - * @param c The scaling context - */ -void sws_frame_end(struct SwsContext *c); - -/** - * Indicate that a horizontal slice of input data is available in the source - * frame previously provided to sws_frame_start(). The slices may be provided in - * any order, but may not overlap. For vertically subsampled pixel formats, the - * slices must be aligned according to subsampling. - * - * @param c The scaling context - * @param slice_start first row of the slice - * @param slice_height number of rows in the slice - * - * @return a non-negative number on success, a negative AVERROR code on failure. - */ -int sws_send_slice(struct SwsContext *c, unsigned int slice_start, - unsigned int slice_height); - -/** - * Request a horizontal slice of the output data to be written into the frame - * previously provided to sws_frame_start(). - * - * @param c The scaling context - * @param slice_start first row of the slice; must be a multiple of - * sws_receive_slice_alignment() - * @param slice_height number of rows in the slice; must be a multiple of - * sws_receive_slice_alignment(), except for the last slice - * (i.e. when slice_start+slice_height is equal to output - * frame height) - * - * @return a non-negative number if the data was successfully written into the output - * AVERROR(EAGAIN) if more input data needs to be provided before the - * output can be produced - * another negative AVERROR code on other kinds of scaling failure - */ -int sws_receive_slice(struct SwsContext *c, unsigned int slice_start, - unsigned int slice_height); - -/** - * Get the alignment required for slices - * - * @param c The scaling context - * @return alignment required for output slices requested with sws_receive_slice(). - * Slice offsets and sizes passed to sws_receive_slice() must be - * multiples of the value returned from this function. - */ -unsigned int sws_receive_slice_alignment(const struct SwsContext *c); - -/** - * @param c the scaling context - * @param dstRange flag indicating the while-black range of the output (1=jpeg / 0=mpeg) - * @param srcRange flag indicating the while-black range of the input (1=jpeg / 0=mpeg) - * @param table the yuv2rgb coefficients describing the output yuv space, normally ff_yuv2rgb_coeffs[x] - * @param inv_table the yuv2rgb coefficients describing the input yuv space, normally ff_yuv2rgb_coeffs[x] - * @param brightness 16.16 fixed point brightness correction - * @param contrast 16.16 fixed point contrast correction - * @param saturation 16.16 fixed point saturation correction - * - * @return A negative error code on error, non negative otherwise. - * If `LIBSWSCALE_VERSION_MAJOR < 7`, returns -1 if not supported. - */ -int sws_setColorspaceDetails(struct SwsContext *c, const int inv_table[4], - int srcRange, const int table[4], int dstRange, - int brightness, int contrast, int saturation); - -/** - * @return A negative error code on error, non negative otherwise. - * If `LIBSWSCALE_VERSION_MAJOR < 7`, returns -1 if not supported. - */ -int sws_getColorspaceDetails(struct SwsContext *c, int **inv_table, - int *srcRange, int **table, int *dstRange, - int *brightness, int *contrast, int *saturation); - -/** - * Allocate and return an uninitialized vector with length coefficients. - */ -SwsVector *sws_allocVec(int length); - -/** - * Return a normalized Gaussian curve used to filter stuff - * quality = 3 is high quality, lower is lower quality. - */ -SwsVector *sws_getGaussianVec(double variance, double quality); - -/** - * Scale all the coefficients of a by the scalar value. - */ -void sws_scaleVec(SwsVector *a, double scalar); - -/** - * Scale all the coefficients of a so that their sum equals height. - */ -void sws_normalizeVec(SwsVector *a, double height); - -void sws_freeVec(SwsVector *a); - -SwsFilter *sws_getDefaultFilter(float lumaGBlur, float chromaGBlur, - float lumaSharpen, float chromaSharpen, - float chromaHShift, float chromaVShift, - int verbose); -void sws_freeFilter(SwsFilter *filter); - -/** - * Check if context can be reused, otherwise reallocate a new one. - * - * If context is NULL, just calls sws_getContext() to get a new - * context. Otherwise, checks if the parameters are the ones already - * saved in context. If that is the case, returns the current - * context. Otherwise, frees context and gets a new context with - * the new parameters. - * - * Be warned that srcFilter and dstFilter are not checked, they - * are assumed to remain the same. - */ -struct SwsContext *sws_getCachedContext(struct SwsContext *context, - int srcW, int srcH, enum AVPixelFormat srcFormat, - int dstW, int dstH, enum AVPixelFormat dstFormat, - int flags, SwsFilter *srcFilter, - SwsFilter *dstFilter, const double *param); - -/** - * Convert an 8-bit paletted frame into a frame with a color depth of 32 bits. - * - * The output frame will have the same packed format as the palette. - * - * @param src source frame buffer - * @param dst destination frame buffer - * @param num_pixels number of pixels to convert - * @param palette array with [256] entries, which must match color arrangement (RGB or BGR) of src - */ -void sws_convertPalette8ToPacked32(const uint8_t *src, uint8_t *dst, int num_pixels, const uint8_t *palette); - -/** - * Convert an 8-bit paletted frame into a frame with a color depth of 24 bits. - * - * With the palette format "ABCD", the destination frame ends up with the format "ABC". - * - * @param src source frame buffer - * @param dst destination frame buffer - * @param num_pixels number of pixels to convert - * @param palette array with [256] entries, which must match color arrangement (RGB or BGR) of src - */ -void sws_convertPalette8ToPacked24(const uint8_t *src, uint8_t *dst, int num_pixels, const uint8_t *palette); - -/** - * Get the AVClass for swsContext. It can be used in combination with - * AV_OPT_SEARCH_FAKE_OBJ for examining options. - * - * @see av_opt_find(). - */ -const AVClass *sws_get_class(void); - -/** - * @} - */ - -#endif /* SWSCALE_SWSCALE_H */ diff --git a/gostream/ffmpeg/include/libswscale/version.h b/gostream/ffmpeg/include/libswscale/version.h deleted file mode 100644 index c13db31c432..00000000000 --- a/gostream/ffmpeg/include/libswscale/version.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef SWSCALE_VERSION_H -#define SWSCALE_VERSION_H - -/** - * @file - * swscale version macros - */ - -#include "libavutil/version.h" - -#include "version_major.h" - -#define LIBSWSCALE_VERSION_MINOR 5 -#define LIBSWSCALE_VERSION_MICRO 100 - -#define LIBSWSCALE_VERSION_INT AV_VERSION_INT(LIBSWSCALE_VERSION_MAJOR, \ - LIBSWSCALE_VERSION_MINOR, \ - LIBSWSCALE_VERSION_MICRO) -#define LIBSWSCALE_VERSION AV_VERSION(LIBSWSCALE_VERSION_MAJOR, \ - LIBSWSCALE_VERSION_MINOR, \ - LIBSWSCALE_VERSION_MICRO) -#define LIBSWSCALE_BUILD LIBSWSCALE_VERSION_INT - -#define LIBSWSCALE_IDENT "SwS" AV_STRINGIFY(LIBSWSCALE_VERSION) - -#endif /* SWSCALE_VERSION_H */ diff --git a/gostream/ffmpeg/include/libswscale/version_major.h b/gostream/ffmpeg/include/libswscale/version_major.h deleted file mode 100644 index 88577a2b42a..00000000000 --- a/gostream/ffmpeg/include/libswscale/version_major.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of FFmpeg. - * - * FFmpeg is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * FFmpeg is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with FFmpeg; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef SWSCALE_VERSION_MAJOR_H -#define SWSCALE_VERSION_MAJOR_H - -/** - * @file - * swscale version macros - */ - -#define LIBSWSCALE_VERSION_MAJOR 7 - -/** - * FF_API_* defines may be placed below to indicate public API that will be - * dropped at a future version bump. The defines themselves are not part of - * the public API and may change, break or disappear at any time. - */ - -#endif /* SWSCALE_VERSION_MAJOR_H */ diff --git a/gostream/logger.go b/gostream/logger.go deleted file mode 100644 index a3a8eabf395..00000000000 --- a/gostream/logger.go +++ /dev/null @@ -1,4 +0,0 @@ -package gostream - -// Debug is helpful to turn on when the library isn't working quite right. -var Debug = false diff --git a/gostream/media.go b/gostream/media.go deleted file mode 100644 index 868c914ec77..00000000000 --- a/gostream/media.go +++ /dev/null @@ -1,663 +0,0 @@ -package gostream - -import ( - "context" - "strings" - "sync" - "sync/atomic" - - "github.com/pion/mediadevices/pkg/driver" - "github.com/pion/mediadevices/pkg/driver/camera" - "github.com/pion/mediadevices/pkg/prop" - "github.com/pkg/errors" - "go.opencensus.io/trace" - "go.uber.org/multierr" - "go.viam.com/utils" -) - -type ( - // A MediaReader is anything that can read and recycle data. It is expected - // that reader can handle multiple reads at the same time. This would ideally only - // happen during streaming when a specific MIME type is requested. In the future, - // we may be able to notice multiple MIME types and either do deferred encode/decode - // or have the reader do it for us. - MediaReader[T any] interface { - Read(ctx context.Context) (data T, release func(), err error) - Close(ctx context.Context) error - } - - // A MediaReaderFunc is a helper to turn a function into a MediaReader. - MediaReaderFunc[T any] func(ctx context.Context) (T, func(), error) - - // A MediaStream streams media forever until closed. - MediaStream[T any] interface { - // Next returns the next media element in the sequence (best effort). - // Note: This element is mutable and shared globally; it MUST be copied - // before it is mutated. - Next(ctx context.Context) (T, func(), error) - - // Close signals this stream is no longer needed and releases associated - // resources. - Close(ctx context.Context) error - } - - // A MediaSource can produce Streams of Ts. - MediaSource[T any] interface { - // Stream returns a stream that makes a best effort to return consecutive media elements - // that may have a MIME type hint dictated in the context via WithMIMETypeHint. - Stream(ctx context.Context, errHandlers ...ErrorHandler) (MediaStream[T], error) - - // Close cleans up any associated resources with the Source (e.g. a Driver). - Close(ctx context.Context) error - } - - // MediaPropertyProvider providers information about a source. - MediaPropertyProvider[U any] interface { - MediaProperties(ctx context.Context) (U, error) - } -) - -// Read calls the underlying function to get a media. -func (mrf MediaReaderFunc[T]) Read(ctx context.Context) (T, func(), error) { - ctx, span := trace.StartSpan(ctx, "gostream::MediaReaderFunc::Read") - defer span.End() - return mrf(ctx) -} - -// Close does nothing. -func (mrf MediaReaderFunc[T]) Close(ctx context.Context) error { - return nil -} - -// A mediaReaderFuncNoCtx is a helper to turn a function into a MediaReader that cannot -// accept a context argument. -type mediaReaderFuncNoCtx[T any] func() (T, func(), error) - -// Read calls the underlying function to get a media. -func (mrf mediaReaderFuncNoCtx[T]) Read(ctx context.Context) (T, func(), error) { - _, span := trace.StartSpan(ctx, "gostream::MediaReaderFuncNoCtx::Read") - defer span.End() - return mrf() -} - -// Close does nothing. -func (mrf mediaReaderFuncNoCtx[T]) Close(ctx context.Context) error { - return nil -} - -// ReadMedia gets a single media from a source. Using this has less of a guarantee -// than MediaSource.Stream that the Nth media element follows the N-1th media element. -func ReadMedia[T any](ctx context.Context, source MediaSource[T]) (T, func(), error) { - ctx, span := trace.StartSpan(ctx, "gostream::ReadMedia") - defer span.End() - - if reader, ok := source.(MediaReader[T]); ok { - // more efficient if there is a direct way to read - return reader.Read(ctx) - } - stream, err := source.Stream(ctx) - var zero T - if err != nil { - return zero, nil, err - } - defer func() { - utils.UncheckedError(stream.Close(ctx)) - }() - return stream.Next(ctx) -} - -type mediaSource[T any, U any] struct { - driver driver.Driver - reader MediaReader[T] - props U - rootCancelCtx context.Context - rootCancel func() - - producerConsumers map[string]*producerConsumer[T, U] - producerConsumersMu sync.Mutex -} - -type producerConsumer[T any, U any] struct { - rootCancelCtx context.Context - cancelCtx context.Context - interestedConsumers int64 - cancelCtxMu *sync.RWMutex - cancel func() - mimeType string - activeBackgroundWorkers sync.WaitGroup - readWrapper MediaReader[T] - current *mediaRefReleasePairWithError[T] - currentMu sync.RWMutex - producerCond *sync.Cond - consumerCond *sync.Cond - condMu *sync.RWMutex - errHandlers map[*mediaStream[T, U]][]ErrorHandler - listeners int - stateMu sync.Mutex - listenersMu sync.Mutex - errHandlersMu sync.Mutex -} - -// ErrorHandler receives the error returned by a TSource.Next -// regardless of whether or not the error is nil (This allows -// for error handling logic based on consecutively retrieved errors). -type ErrorHandler func(ctx context.Context, mediaErr error) - -// PropertiesFromMediaSource returns properties from underlying driver in the given MediaSource. -func PropertiesFromMediaSource[T, U any](src MediaSource[T]) ([]prop.Media, error) { - d, err := DriverFromMediaSource[T, U](src) - if err != nil { - return nil, err - } - return d.Properties(), nil -} - -// LabelsFromMediaSource returns the labels from the underlying driver in the MediaSource. -func LabelsFromMediaSource[T, U any](src MediaSource[T]) ([]string, error) { - d, err := DriverFromMediaSource[T, U](src) - if err != nil { - return nil, err - } - return strings.Split(d.Info().Label, camera.LabelSeparator), nil -} - -// DriverFromMediaSource returns the underlying driver from the MediaSource. -func DriverFromMediaSource[T, U any](src MediaSource[T]) (driver.Driver, error) { - if asMedia, ok := src.(*mediaSource[T, U]); ok { - if asMedia.driver != nil { - return asMedia.driver, nil - } - } - return nil, errors.Errorf("cannot convert media source (type %T) to type (%T)", src, (*mediaSource[T, U])(nil)) -} - -// newMediaSource instantiates a new media read closer and possibly references the given driver. -func newMediaSource[T, U any](d driver.Driver, r MediaReader[T], p U) MediaSource[T] { - if d != nil { - driverRefs.mu.Lock() - defer driverRefs.mu.Unlock() - - label := d.Info().Label - if rcv, ok := driverRefs.refs[label]; ok { - rcv.Ref() - } else { - driverRefs.refs[label] = utils.NewRefCountedValue(d) - driverRefs.refs[label].Ref() - } - } - - cancelCtx, cancel := context.WithCancel(context.Background()) - ms := &mediaSource[T, U]{ - driver: d, - reader: r, - props: p, - rootCancelCtx: cancelCtx, - rootCancel: cancel, - producerConsumers: map[string]*producerConsumer[T, U]{}, - } - return ms -} - -func (pc *producerConsumer[T, U]) start() { - var startLocalCtx context.Context - var span *trace.Span - - func() { - pc.cancelCtxMu.RLock() - defer pc.cancelCtxMu.RUnlock() - startLocalCtx, span = trace.StartSpan(pc.cancelCtx, "gostream::producerConsumer::start") - }() - - pc.listenersMu.Lock() - defer pc.listenersMu.Unlock() - - pc.listeners++ - - if pc.listeners != 1 { - span.End() - return - } - - pc.activeBackgroundWorkers.Add(1) - - utils.ManagedGo(func() { - defer span.End() - - first := true - for { - pc.cancelCtxMu.RLock() - if pc.cancelCtx.Err() != nil { - pc.cancelCtxMu.RUnlock() - return - } - pc.cancelCtxMu.RUnlock() - - waitForNext := func() (int64, bool) { - _, waitForNextSpan := trace.StartSpan(startLocalCtx, "gostream::producerConsumer::waitForNext") - defer waitForNextSpan.End() - - for { - pc.producerCond.L.Lock() - requests := atomic.LoadInt64(&pc.interestedConsumers) - if requests == 0 { - pc.cancelCtxMu.RLock() - if err := pc.cancelCtx.Err(); err != nil { - pc.cancelCtxMu.RUnlock() - pc.producerCond.L.Unlock() - return 0, false - } - pc.cancelCtxMu.RUnlock() - - pc.producerCond.Wait() - requests = atomic.LoadInt64(&pc.interestedConsumers) - pc.producerCond.L.Unlock() - if requests == 0 { - continue - } - } else { - pc.producerCond.L.Unlock() - } - return requests, true - } - } - requests, cont := waitForNext() - if !cont { - return - } - - pc.cancelCtxMu.RLock() - if err := pc.cancelCtx.Err(); err != nil { - pc.cancelCtxMu.RUnlock() - return - } - pc.cancelCtxMu.RUnlock() - - func() { - var doReadSpan *trace.Span - startLocalCtx, doReadSpan = trace.StartSpan(startLocalCtx, "gostream::producerConsumer (anonymous function to read)") - - defer func() { - pc.producerCond.L.Lock() - atomic.AddInt64(&pc.interestedConsumers, -requests) - pc.consumerCond.Broadcast() - pc.producerCond.L.Unlock() - doReadSpan.End() - }() - - var lastRelease func() - if !first { - // okay to not hold a lock because we are the only both reader AND writer; - // other goroutines are just readers. - lastRelease = pc.current.Release - } else { - first = false - } - - startLocalCtx, span := trace.StartSpan(startLocalCtx, "gostream::producerConsumer::readWrapper::Read") - media, release, err := pc.readWrapper.Read(startLocalCtx) - span.End() - - ref := utils.NewRefCountedValue(struct{}{}) - ref.Ref() - - // hold write lock long enough to set current but not for lastRelease - // since the reader (who will call ref) will hold a similar read lock - // to ref before unlocking. This ordering makes sure that we only ever - // call a deref of the previous media once a new one can be fetched. - pc.currentMu.Lock() - pc.current = &mediaRefReleasePairWithError[T]{media, ref, func() { - if ref.Deref() { - if release != nil { - release() - } - } - }, err} - pc.currentMu.Unlock() - if lastRelease != nil { - lastRelease() - } - }() - } - }, func() { defer pc.activeBackgroundWorkers.Done(); pc.cancel() }) -} - -type mediaRefReleasePairWithError[T any] struct { - Media T - Ref utils.RefCountedValue - Release func() - Err error -} - -func (pc *producerConsumer[T, U]) Stop() { - pc.stateMu.Lock() - defer pc.stateMu.Unlock() - - pc.stop() -} - -// assumes stateMu lock is held. -func (pc *producerConsumer[T, U]) stop() { - var span *trace.Span - func() { - pc.cancelCtxMu.RLock() - defer pc.cancelCtxMu.RUnlock() - _, span = trace.StartSpan(pc.cancelCtx, "gostream::producerConsumer::stop") - }() - - defer span.End() - - pc.cancel() - - pc.producerCond.L.Lock() - pc.producerCond.Signal() - pc.producerCond.L.Unlock() - pc.consumerCond.L.Lock() - pc.consumerCond.Broadcast() - pc.consumerCond.L.Unlock() - pc.activeBackgroundWorkers.Wait() - - // reset - cancelCtx, cancel := context.WithCancel(WithMIMETypeHint(pc.rootCancelCtx, pc.mimeType)) - pc.cancelCtxMu.Lock() - pc.cancelCtx = cancelCtx - pc.cancelCtxMu.Unlock() - pc.cancel = cancel -} - -func (pc *producerConsumer[T, U]) stopOne() { - var span *trace.Span - func() { - pc.cancelCtxMu.RLock() - defer pc.cancelCtxMu.RUnlock() - _, span = trace.StartSpan(pc.cancelCtx, "gostream::producerConsumer::stopOne") - }() - - defer span.End() - - pc.stateMu.Lock() - defer pc.stateMu.Unlock() - pc.listenersMu.Lock() - defer pc.listenersMu.Unlock() - pc.listeners-- - if pc.listeners == 0 { - pc.stop() - } -} - -func (ms *mediaSource[T, U]) MediaProperties(_ context.Context) (U, error) { - return ms.props, nil -} - -// MediaReleasePairWithError contains the result of fetching media. -type MediaReleasePairWithError[T any] struct { - Media T - Release func() - Err error -} - -// NewMediaStreamForChannel returns a MediaStream backed by a channel. -func NewMediaStreamForChannel[T any](ctx context.Context) (context.Context, MediaStream[T], chan<- MediaReleasePairWithError[T]) { - cancelCtx, cancel := context.WithCancel(ctx) - ch := make(chan MediaReleasePairWithError[T]) - return cancelCtx, &mediaStreamFromChannel[T]{ - media: ch, - cancelCtx: cancelCtx, - cancel: cancel, - }, ch -} - -type mediaStreamFromChannel[T any] struct { - media chan MediaReleasePairWithError[T] - cancelCtx context.Context - cancel func() -} - -func (ms *mediaStreamFromChannel[T]) Next(ctx context.Context) (T, func(), error) { - ctx, span := trace.StartSpan(ctx, "gostream::mediaStreamFromChannel::Next") - defer span.End() - - var zero T - if ms.cancelCtx.Err() != nil { - return zero, nil, ms.cancelCtx.Err() - } - if ctx.Err() != nil { - return zero, nil, ctx.Err() - } - - select { - case <-ms.cancelCtx.Done(): - return zero, nil, ms.cancelCtx.Err() - case <-ctx.Done(): - return zero, nil, ctx.Err() - case pair := <-ms.media: - return pair.Media, pair.Release, pair.Err - } -} - -func (ms *mediaStreamFromChannel[T]) Close(ctx context.Context) error { - ms.cancel() - return nil -} - -type mediaStream[T any, U any] struct { - mu sync.Mutex - ms *mediaSource[T, U] - prodCon *producerConsumer[T, U] - cancelCtx context.Context - cancel func() -} - -func (ms *mediaStream[T, U]) Next(ctx context.Context) (T, func(), error) { - ctx, nextSpan := trace.StartSpan(ctx, "gostream::mediaStream::Next") - defer nextSpan.End() - - ms.mu.Lock() - defer ms.mu.Unlock() - // lock keeps us sequential and prevents misuse - - var zero T - if err := ms.cancelCtx.Err(); err != nil { - return zero, nil, err - } - - ms.prodCon.consumerCond.L.Lock() - // Even though interestedConsumers is atomic, this is a critical section! - // That's because if the producer sees zero interested consumers, it's going - // to Wait but we only want it to do that once we are ready to signal it. - // It's also a RLock because we have many consumers (readers) and one producer (writer). - atomic.AddInt64(&ms.prodCon.interestedConsumers, 1) - ms.prodCon.producerCond.Signal() - - select { - case <-ms.cancelCtx.Done(): - ms.prodCon.consumerCond.L.Unlock() - return zero, nil, ms.cancelCtx.Err() - case <-ctx.Done(): - ms.prodCon.consumerCond.L.Unlock() - return zero, nil, ctx.Err() - default: - } - - waitForNext := func() error { - _, span := trace.StartSpan(ctx, "gostream::mediaStream::Next::waitForNext") - defer span.End() - - ms.prodCon.consumerCond.Wait() - ms.prodCon.consumerCond.L.Unlock() - if err := ms.cancelCtx.Err(); err != nil { - return err - } - if err := ctx.Err(); err != nil { - return err - } - return nil - } - - if err := waitForNext(); err != nil { - return zero, nil, err - } - - isAvailable := func() bool { - ms.prodCon.currentMu.RLock() - available := ms.prodCon.current != nil - ms.prodCon.currentMu.RUnlock() - return available - } - for !isAvailable() { - ms.prodCon.consumerCond.L.Lock() - if err := waitForNext(); err != nil { - return zero, nil, err - } - } - - ctx, prodConLockSpan := trace.StartSpan(ctx, "gostream::mediaStream::Next (waiting for ms.prodCon lock)") - defer prodConLockSpan.End() - - // hold a read lock long enough before current.Ref can be dereffed - // due to a new current being set. - ms.prodCon.currentMu.RLock() - defer ms.prodCon.currentMu.RUnlock() - current := ms.prodCon.current - if current.Err != nil { - return zero, nil, current.Err - } - current.Ref.Ref() - return current.Media, current.Release, nil -} - -func (ms *mediaStream[T, U]) Close(ctx context.Context) error { - if parentSpan := trace.FromContext(ctx); parentSpan != nil { - func() { - ms.prodCon.cancelCtxMu.Lock() - defer ms.prodCon.cancelCtxMu.Unlock() - ms.prodCon.cancelCtx = trace.NewContext(ms.prodCon.cancelCtx, parentSpan) - }() - } - - var span *trace.Span - func() { - ms.prodCon.cancelCtxMu.Lock() - defer ms.prodCon.cancelCtxMu.Unlock() - ms.prodCon.cancelCtx, span = trace.StartSpan(ms.prodCon.cancelCtx, "gostream::mediaStream::Close") - }() - - defer span.End() - - ms.cancel() - ms.prodCon.errHandlersMu.Lock() - delete(ms.prodCon.errHandlers, ms) - ms.prodCon.errHandlersMu.Unlock() - ms.prodCon.stopOne() - return nil -} - -func (ms *mediaSource[T, U]) Stream(ctx context.Context, errHandlers ...ErrorHandler) (MediaStream[T], error) { - ms.producerConsumersMu.Lock() - mimeType := MIMETypeHint(ctx, "") - prodCon, ok := ms.producerConsumers[mimeType] - if !ok { - // TODO(erd): better to have no max like this and instead clean up over time. - if len(ms.producerConsumers)+1 == 256 { - return nil, errors.New("reached max producer consumers of 256") - } - cancelCtx, cancel := context.WithCancel(WithMIMETypeHint(ms.rootCancelCtx, mimeType)) - condMu := &sync.RWMutex{} - producerCond := sync.NewCond(condMu) - consumerCond := sync.NewCond(condMu.RLocker()) - - prodCon = &producerConsumer[T, U]{ - rootCancelCtx: ms.rootCancelCtx, - cancelCtx: cancelCtx, - cancelCtxMu: &sync.RWMutex{}, - cancel: cancel, - mimeType: mimeType, - producerCond: producerCond, - consumerCond: consumerCond, - condMu: condMu, - errHandlers: map[*mediaStream[T, U]][]ErrorHandler{}, - } - prodCon.readWrapper = MediaReaderFunc[T](func(ctx context.Context) (T, func(), error) { - media, release, err := ms.reader.Read(ctx) - if err == nil { - return media, release, nil - } - - prodCon.errHandlersMu.Lock() - defer prodCon.errHandlersMu.Unlock() - for _, handlers := range prodCon.errHandlers { - for _, handler := range handlers { - handler(ctx, err) - } - } - var zero T - return zero, nil, err - }) - ms.producerConsumers[mimeType] = prodCon - } - ms.producerConsumersMu.Unlock() - - prodCon.stateMu.Lock() - defer prodCon.stateMu.Unlock() - - if currentSpan := trace.FromContext(ctx); currentSpan != nil { - func() { - prodCon.cancelCtxMu.Lock() - defer prodCon.cancelCtxMu.Unlock() - prodCon.cancelCtx = trace.NewContext(prodCon.cancelCtx, currentSpan) - }() - } - - var cancelCtx context.Context - var cancel context.CancelFunc - func() { - prodCon.cancelCtxMu.RLock() - defer prodCon.cancelCtxMu.RUnlock() - cancelCtx, cancel = context.WithCancel(prodCon.cancelCtx) - }() - stream := &mediaStream[T, U]{ - ms: ms, - prodCon: prodCon, - cancelCtx: cancelCtx, - cancel: cancel, - } - - if len(errHandlers) != 0 { - prodCon.errHandlersMu.Lock() - prodCon.errHandlers[stream] = errHandlers - prodCon.errHandlersMu.Unlock() - } - prodCon.start() - - return stream, nil -} - -func (ms *mediaSource[T, U]) Close(ctx context.Context) error { - func() { - ms.producerConsumersMu.Lock() - defer ms.producerConsumersMu.Unlock() - for _, prodCon := range ms.producerConsumers { - prodCon.Stop() - } - }() - err := ms.reader.Close(ctx) - - if ms.driver == nil { - return err - } - driverRefs.mu.Lock() - defer driverRefs.mu.Unlock() - - label := ms.driver.Info().Label - if rcv, ok := driverRefs.refs[label]; ok { - if rcv.Deref() { - delete(driverRefs.refs, label) - return multierr.Combine(err, ms.driver.Close()) - } - } else { - return multierr.Combine(err, ms.driver.Close()) - } - - // Do not close if a driver is being referenced. Client will decide what to do if - // they encounter this error. - return multierr.Combine(err, &DriverInUseError{label}) -} diff --git a/gostream/media_test.go b/gostream/media_test.go deleted file mode 100644 index 2c8e36e5b49..00000000000 --- a/gostream/media_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package gostream - -import ( - "context" - _ "embed" - "image" - "image/color" - "testing" - - "github.com/pion/mediadevices/pkg/prop" - "go.viam.com/test" - - "go.viam.com/rdk/rimage" -) - -type imageSource struct { - Images []image.Image - idx int -} - -// Returns the next image or nil if there are no more images left. This should never error. -func (is *imageSource) Read(_ context.Context) (image.Image, func(), error) { - if is.idx >= len(is.Images) { - return nil, func() {}, nil - } - img := is.Images[is.idx] - is.idx++ - return img, func() {}, nil -} - -func (is *imageSource) Close(_ context.Context) error { - return nil -} - -func createImage(c color.Color) image.Image { - w, h := 640, 480 - img := image.NewRGBA(image.Rect(0, 0, w, h)) - for x := 0; x < w; x++ { - for y := 0; y < h; y++ { - img.Set(x, y, c) - } - } - return img -} - -func TestReadMedia(t *testing.T) { - colors := []image.Image{ - createImage(rimage.Red), - createImage(rimage.Blue), - createImage(rimage.Green), - createImage(rimage.Yellow), - createImage(rimage.Purple), - createImage(rimage.Cyan), - } - - imgSource := imageSource{Images: colors} - videoSrc := NewVideoSource(&imgSource, prop.Video{}) - // Test all images are returned in order. - for i, expected := range colors { - actual, _, err := ReadMedia(context.Background(), videoSrc) - test.That(t, err, test.ShouldBeNil) - test.That(t, actual, test.ShouldNotBeNil) - for j, col := range colors { - if col == expected { - continue - } - if actual == col { - t.Logf("did not expect actual color to equal other color at %d when expecting %d", j, i) - } - } - test.That(t, actual, test.ShouldEqual, expected) - } - - // Test image comparison can fail if two images are not the same - imgSource.Images = []image.Image{createImage(rimage.Red)} - videoSrc = NewVideoSource(&imgSource, prop.Video{}) - - blue := createImage(rimage.Blue) - red, _, err := ReadMedia(context.Background(), videoSrc) - test.That(t, err, test.ShouldBeNil) - test.That(t, red, test.ShouldNotEqual, blue) -} diff --git a/gostream/media_utils.go b/gostream/media_utils.go deleted file mode 100644 index 32a4ffc3721..00000000000 --- a/gostream/media_utils.go +++ /dev/null @@ -1,97 +0,0 @@ -package gostream - -import ( - "context" - "sync" - - "go.uber.org/multierr" -) - -// NewEmbeddedMediaStream returns a media stream from a media source that is -// intended to be embedded/composed by another source. It defers the creation -// of its media stream. -func NewEmbeddedMediaStream[T, U any](src MediaSource[T]) MediaStream[T] { - return &embeddedMediaStream[T, U]{src: src} -} - -type embeddedMediaStream[T, U any] struct { - mu sync.Mutex - src MediaSource[T] - stream MediaStream[T] -} - -func (ems *embeddedMediaStream[T, U]) initStream(ctx context.Context) error { - if ems.stream != nil { - return nil - } - stream, err := ems.src.Stream(ctx) - if err != nil { - return err - } - ems.stream = stream - return nil -} - -func (ems *embeddedMediaStream[T, U]) Next(ctx context.Context) (T, func(), error) { - ems.mu.Lock() - defer ems.mu.Unlock() - if err := ems.initStream(ctx); err != nil { - var zero T - return zero, nil, err - } - return ems.stream.Next(ctx) -} - -func (ems *embeddedMediaStream[T, U]) Close(ctx context.Context) error { - ems.mu.Lock() - defer ems.mu.Unlock() - if ems.stream == nil { - return nil - } - return ems.stream.Close(ctx) -} - -// NewEmbeddedMediaStreamFromReader returns a media stream from a media reader that is -// intended to be embedded/composed by another source. It defers the creation -// of its media stream. -func NewEmbeddedMediaStreamFromReader[T, U any](reader MediaReader[T], p U) MediaStream[T] { - src := newMediaSource[T](nil, MediaReaderFunc[T](reader.Read), p) - stream := NewEmbeddedMediaStream[T, U](src) - return &embeddedMediaReaderStream[T, U]{ - src: src, - stream: stream, - } -} - -type embeddedMediaReaderStream[T, U any] struct { - src MediaSource[T] - stream MediaStream[T] -} - -func (emrs *embeddedMediaReaderStream[T, U]) Next(ctx context.Context) (T, func(), error) { - return emrs.stream.Next(ctx) -} - -func (emrs *embeddedMediaReaderStream[T, U]) Close(ctx context.Context) error { - return multierr.Combine(emrs.stream.Close(ctx), emrs.src.Close(ctx)) -} - -type contextValue byte - -const contextValueMIMETypeHint contextValue = iota - -// WithMIMETypeHint provides a hint to readers that media should be encoded to -// this type. -func WithMIMETypeHint(ctx context.Context, mimeType string) context.Context { - return context.WithValue(ctx, contextValueMIMETypeHint, mimeType) -} - -// MIMETypeHint gets the hint of what MIME type to use in encoding; if nothing is -// set, the default provided is used. -func MIMETypeHint(ctx context.Context, defaultType string) string { - val, ok := ctx.Value(contextValueMIMETypeHint).(string) - if !ok || val == "" { - return defaultType - } - return val -} diff --git a/gostream/query.go b/gostream/query.go deleted file mode 100644 index ece276598ff..00000000000 --- a/gostream/query.go +++ /dev/null @@ -1,583 +0,0 @@ -package gostream - -import ( - "image" - "math" - "regexp" - "strings" - "time" - - "github.com/edaniels/golog" - "github.com/pion/mediadevices" - "github.com/pion/mediadevices/pkg/driver" - "github.com/pion/mediadevices/pkg/driver/availability" - "github.com/pion/mediadevices/pkg/driver/camera" - "github.com/pion/mediadevices/pkg/frame" - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "github.com/pkg/errors" -) - -// below adapted from github.com/pion/mediadevices - -// ErrNotFound happens when there is no driver found in a query. -var ErrNotFound = errors.New("failed to find the best driver that fits the constraints") - -// DefaultConstraints are suitable for finding any available device. -var DefaultConstraints = mediadevices.MediaStreamConstraints{ - Video: func(constraint *mediadevices.MediaTrackConstraints) { - constraint.Width = prop.IntRanged{640, 4096, 1920} - constraint.Height = prop.IntRanged{400, 2160, 1080} - constraint.FrameRate = prop.FloatRanged{0, 200, 60} - constraint.FrameFormat = prop.FrameFormatOneOf{ - frame.FormatI420, - frame.FormatI444, - frame.FormatYUY2, - frame.FormatYUYV, - frame.FormatUYVY, - frame.FormatRGBA, - frame.FormatMJPEG, - frame.FormatNV12, - frame.FormatZ16, - frame.FormatNV21, // gives blue tinted image? - } - }, -} - -// GetNamedScreenSource attempts to find a screen device by the given name. -func GetNamedScreenSource( - name string, - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[image.Image], error) { - d, selectedMedia, err := getScreenDriver(constraints, &name, logger) - if err != nil { - return nil, err - } - return newVideoSourceFromDriver(d, selectedMedia) -} - -// GetPatternedScreenSource attempts to find a screen device by the given label pattern. -func GetPatternedScreenSource( - labelPattern *regexp.Regexp, - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[image.Image], error) { - d, selectedMedia, err := getScreenDriverPattern(constraints, labelPattern, logger) - if err != nil { - return nil, err - } - return newVideoSourceFromDriver(d, selectedMedia) -} - -// GetNamedVideoSource attempts to find a video device (not a screen) by the given name. -func GetNamedVideoSource( - name string, - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[image.Image], error) { - d, selectedMedia, err := getUserVideoDriver(constraints, &name, logger) - if err != nil { - return nil, err - } - return newVideoSourceFromDriver(d, selectedMedia) -} - -// GetPatternedVideoSource attempts to find a video device (not a screen) by the given label pattern. -func GetPatternedVideoSource( - labelPattern *regexp.Regexp, - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[image.Image], error) { - d, selectedMedia, err := getUserVideoDriverPattern(constraints, labelPattern, logger) - if err != nil { - return nil, err - } - return newVideoSourceFromDriver(d, selectedMedia) -} - -// GetAnyScreenSource attempts to find any suitable screen device. -func GetAnyScreenSource( - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[image.Image], error) { - d, selectedMedia, err := getScreenDriver(constraints, nil, logger) - if err != nil { - return nil, err - } - return newVideoSourceFromDriver(d, selectedMedia) -} - -// GetAnyVideoSource attempts to find any suitable video device (not a screen). -func GetAnyVideoSource( - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[image.Image], error) { - d, selectedMedia, err := getUserVideoDriver(constraints, nil, logger) - if err != nil { - return nil, err - } - return newVideoSourceFromDriver(d, selectedMedia) -} - -// GetAnyAudioSource attempts to find any suitable audio device. -func GetAnyAudioSource( - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[wave.Audio], error) { - d, selectedMedia, err := getUserAudioDriver(constraints, nil, logger) - if err != nil { - return nil, err - } - return newAudioSourceFromDriver(d, selectedMedia, logger) -} - -// GetNamedAudioSource attempts to find an audio device by the given name. -func GetNamedAudioSource( - name string, - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[wave.Audio], error) { - d, selectedMedia, err := getUserAudioDriver(constraints, &name, logger) - if err != nil { - return nil, err - } - return newAudioSourceFromDriver(d, selectedMedia, logger) -} - -// GetPatternedAudioSource attempts to find an audio device by the given label pattern. -func GetPatternedAudioSource( - labelPattern *regexp.Regexp, - constraints mediadevices.MediaStreamConstraints, - logger golog.Logger, -) (MediaSource[wave.Audio], error) { - d, selectedMedia, err := getUserAudioDriverPattern(constraints, labelPattern, logger) - if err != nil { - return nil, err - } - return newAudioSourceFromDriver(d, selectedMedia, logger) -} - -// DeviceInfo describes a driver. -type DeviceInfo struct { - ID string - Labels []string - Properties []prop.Media - Priority driver.Priority - Error error -} - -// QueryVideoDevices lists all known video devices (not a screen). -func QueryVideoDevices() []DeviceInfo { - return getDriverInfo(driver.GetManager().Query(getVideoFilterBase()), true) -} - -// QueryScreenDevices lists all known screen devices. -func QueryScreenDevices() []DeviceInfo { - return getDriverInfo(driver.GetManager().Query(getScreenFilterBase()), true) -} - -// QueryAudioDevices lists all known audio devices. -func QueryAudioDevices() []DeviceInfo { - return getDriverInfo(driver.GetManager().Query(getAudioFilterBase()), true) -} - -func getDriverInfo(drivers []driver.Driver, useSep bool) []DeviceInfo { - infos := make([]DeviceInfo, len(drivers)) - for i, d := range drivers { - if d.Status() == driver.StateClosed { - if err := d.Open(); err != nil { - infos[i].Error = err - } else { - defer func() { - infos[i].Error = d.Close() - }() - } - } - infos[i].ID = d.ID() - infos[i].Labels = getDriverLabels(d, useSep) - infos[i].Properties = d.Properties() - infos[i].Priority = d.Info().Priority - } - return infos -} - -// QueryScreenDevicesLabels lists all known screen devices. -func QueryScreenDevicesLabels() []string { - return getDriversLabels(driver.GetManager().Query(getScreenFilterBase()), false) -} - -// QueryVideoDeviceLabels lists all known video devices (not a screen). -func QueryVideoDeviceLabels() []string { - return getDriversLabels(driver.GetManager().Query(getVideoFilterBase()), true) -} - -// QueryAudioDeviceLabels lists all known audio devices. -func QueryAudioDeviceLabels() []string { - return getDriversLabels(driver.GetManager().Query(getAudioFilterBase()), true) -} - -func getDriversLabels(drivers []driver.Driver, useSep bool) []string { - var labels []string - for _, d := range drivers { - labels = append(labels, getDriverLabels(d, useSep)...) - } - return labels -} - -func getDriverLabels(d driver.Driver, useSep bool) []string { - if !useSep { - return []string{d.Info().Label} - } - return strings.Split(d.Info().Label, camera.LabelSeparator) -} - -func getScreenDriver( - constraints mediadevices.MediaStreamConstraints, - label *string, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - var videoConstraints mediadevices.MediaTrackConstraints - if constraints.Video != nil { - constraints.Video(&videoConstraints) - } - return selectScreen(videoConstraints, label, logger) -} - -func getScreenDriverPattern( - constraints mediadevices.MediaStreamConstraints, - labelPattern *regexp.Regexp, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - var videoConstraints mediadevices.MediaTrackConstraints - if constraints.Video != nil { - constraints.Video(&videoConstraints) - } - return selectScreenPattern(videoConstraints, labelPattern, logger) -} - -func getUserVideoDriver( - constraints mediadevices.MediaStreamConstraints, - label *string, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - var videoConstraints mediadevices.MediaTrackConstraints - if constraints.Video != nil { - constraints.Video(&videoConstraints) - } - return selectVideo(videoConstraints, label, logger) -} - -func getUserVideoDriverPattern( - constraints mediadevices.MediaStreamConstraints, - labelPattern *regexp.Regexp, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - var videoConstraints mediadevices.MediaTrackConstraints - if constraints.Video != nil { - constraints.Video(&videoConstraints) - } - return selectVideoPattern(videoConstraints, labelPattern, logger) -} - -func newVideoSourceFromDriver( - videoDriver driver.Driver, - mediaProp prop.Media, -) (MediaSource[image.Image], error) { - recorder, ok := videoDriver.(driver.VideoRecorder) - if !ok { - return nil, errors.New("driver not a driver.VideoRecorder") - } - - if ok, err := driver.IsAvailable(videoDriver); !errors.Is(err, availability.ErrUnimplemented) && !ok { - return nil, errors.Wrap(err, "video driver not available") - } else if driverStatus := videoDriver.Status(); driverStatus != driver.StateClosed { - return nil, errors.New("video driver in use") - } else if err := videoDriver.Open(); err != nil { - return nil, errors.Wrap(err, "cannot open video driver") - } - - mediaProp.DiscardFramesOlderThan = time.Second - reader, err := recorder.VideoRecord(mediaProp) - if err != nil { - return nil, err - } - return newMediaSource[image.Image](videoDriver, mediaReaderFuncNoCtx[image.Image](reader.Read), mediaProp.Video), nil -} - -func getUserAudioDriver( - constraints mediadevices.MediaStreamConstraints, - label *string, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - var audioConstraints mediadevices.MediaTrackConstraints - if constraints.Audio != nil { - constraints.Audio(&audioConstraints) - } - return selectAudio(audioConstraints, label, logger) -} - -func getUserAudioDriverPattern( - constraints mediadevices.MediaStreamConstraints, - labelPattern *regexp.Regexp, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - var audioConstraints mediadevices.MediaTrackConstraints - if constraints.Audio != nil { - constraints.Audio(&audioConstraints) - } - return selectVideoPattern(audioConstraints, labelPattern, logger) -} - -func newAudioSourceFromDriver( - audioDriver driver.Driver, - mediaProp prop.Media, - logger golog.Logger, -) (MediaSource[wave.Audio], error) { - recorder, ok := audioDriver.(driver.AudioRecorder) - if !ok { - return nil, errors.New("driver not a driver.AudioRecorder") - } - - if driverStatus := audioDriver.Status(); driverStatus != driver.StateClosed { - logger.Warnw("audio driver is not closed, attempting to close and reopen", "status", driverStatus) - if err := audioDriver.Close(); err != nil { - logger.Errorw("error closing driver", "error", err) - } - } - if err := audioDriver.Open(); err != nil { - return nil, err - } - reader, err := recorder.AudioRecord(mediaProp) - if err != nil { - return nil, err - } - return newMediaSource[wave.Audio](audioDriver, mediaReaderFuncNoCtx[wave.Audio](reader.Read), mediaProp.Audio), nil -} - -func labelFilter(target string, useSep bool) driver.FilterFn { - return driver.FilterFn(func(d driver.Driver) bool { - if !useSep { - return d.Info().Label == target - } - labels := strings.Split(d.Info().Label, camera.LabelSeparator) - for _, label := range labels { - if label == target { - return true - } - } - return false - }) -} - -func labelFilterPattern(labelPattern *regexp.Regexp, useSep bool) driver.FilterFn { - return driver.FilterFn(func(d driver.Driver) bool { - if !useSep { - return labelPattern.MatchString(d.Info().Label) - } - labels := strings.Split(d.Info().Label, camera.LabelSeparator) - for _, label := range labels { - if labelPattern.MatchString(label) { - return true - } - } - return false - }) -} - -func selectVideo( - constraints mediadevices.MediaTrackConstraints, - label *string, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - return selectBestDriver(getVideoFilterBase(), getVideoFilter(label), constraints, logger) -} - -func selectVideoPattern( - constraints mediadevices.MediaTrackConstraints, - labelPattern *regexp.Regexp, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - return selectBestDriver(getVideoFilterBase(), getVideoFilterPattern(labelPattern), constraints, logger) -} - -func selectScreen( - constraints mediadevices.MediaTrackConstraints, - label *string, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - return selectBestDriver(getScreenFilterBase(), getScreenFilter(label), constraints, logger) -} - -func selectScreenPattern( - constraints mediadevices.MediaTrackConstraints, - labelPattern *regexp.Regexp, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - return selectBestDriver(getScreenFilterBase(), getScreenFilterPattern(labelPattern), constraints, logger) -} - -func selectAudio( - constraints mediadevices.MediaTrackConstraints, - label *string, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - return selectBestDriver(getAudioFilterBase(), getAudioFilter(label), constraints, logger) -} - -func getVideoFilterBase() driver.FilterFn { - typeFilter := driver.FilterVideoRecorder() - notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen)) - return driver.FilterAnd(typeFilter, notScreenFilter) -} - -func getVideoFilter(label *string) driver.FilterFn { - filter := getVideoFilterBase() - if label != nil { - filter = driver.FilterAnd(filter, labelFilter(*label, true)) - } - return filter -} - -func getVideoFilterPattern(labelPattern *regexp.Regexp) driver.FilterFn { - filter := getVideoFilterBase() - filter = driver.FilterAnd(filter, labelFilterPattern(labelPattern, true)) - return filter -} - -func getScreenFilterBase() driver.FilterFn { - typeFilter := driver.FilterVideoRecorder() - screenFilter := driver.FilterDeviceType(driver.Screen) - return driver.FilterAnd(typeFilter, screenFilter) -} - -func getScreenFilter(label *string) driver.FilterFn { - filter := getScreenFilterBase() - if label != nil { - filter = driver.FilterAnd(filter, labelFilter(*label, true)) - } - return filter -} - -func getScreenFilterPattern(labelPattern *regexp.Regexp) driver.FilterFn { - filter := getScreenFilterBase() - filter = driver.FilterAnd(filter, labelFilterPattern(labelPattern, true)) - return filter -} - -func getAudioFilterBase() driver.FilterFn { - return driver.FilterAudioRecorder() -} - -func getAudioFilter(label *string) driver.FilterFn { - filter := getAudioFilterBase() - if label != nil { - filter = driver.FilterAnd(filter, labelFilter(*label, true)) - } - return filter -} - -// select implements SelectSettings algorithm. -// Reference: https://w3c.github.io/mediacapture-main/#dfn-selectsettings -func selectBestDriver( - baseFilter driver.FilterFn, - filter driver.FilterFn, - constraints mediadevices.MediaTrackConstraints, - logger golog.Logger, -) (driver.Driver, prop.Media, error) { - var bestDriver driver.Driver - var bestProp prop.Media - minFitnessDist := math.Inf(1) - - baseDrivers := driver.GetManager().Query(baseFilter) - logger.Debugw("before specific filter, we found the following drivers", "count", len(baseDrivers)) - for _, d := range baseDrivers { - logger.Debugw(d.Info().Label, "priority", float32(d.Info().Priority), "type", d.Info().DeviceType) - } - - driverProperties := queryDriverProperties(filter, logger) - if len(driverProperties) == 0 { - logger.Debugw("found no drivers matching filter") - } else { - logger.Debugw("found drivers matching specific filter", "count", len(driverProperties)) - } - for d, props := range driverProperties { - priority := float64(d.Info().Priority) - logger.Debugw( - "considering driver", - "label", d.Info().Label, - "priority", priority) - for _, p := range props { - fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p) - if !ok { - logger.Debugw("driver does not satisfy any constraints", "label", d.Info().Label) - continue - } - fitnessDistWithPriority := fitnessDist - priority - logger.Debugw( - "driver properties satisfy some constraints", - "label", d.Info().Label, - "props", p, - "distance", fitnessDist, - "distance_with_priority", fitnessDistWithPriority) - if fitnessDistWithPriority < minFitnessDist { - minFitnessDist = fitnessDistWithPriority - bestDriver = d - bestProp = p - } - } - } - - if bestDriver == nil { - return nil, prop.Media{}, ErrNotFound - } - - logger.Debugw("winning driver", "label", bestDriver.Info().Label, "props", bestProp) - selectedMedia := prop.Media{} - selectedMedia.MergeConstraints(constraints.MediaConstraints) - selectedMedia.Merge(bestProp) - return bestDriver, selectedMedia, nil -} - -func queryDriverProperties( - filter driver.FilterFn, - logger golog.Logger, -) map[driver.Driver][]prop.Media { - var needToClose []driver.Driver - drivers := driver.GetManager().Query(filter) - m := make(map[driver.Driver][]prop.Media) - - for _, d := range drivers { - var status string - isAvailable, err := driver.IsAvailable(d) - if errors.Is(err, availability.ErrUnimplemented) { - s := d.Status() - status = string(s) - isAvailable = s == driver.StateClosed - } else if err != nil { - status = err.Error() - } - - if isAvailable { - err := d.Open() - if err != nil { - logger.Debugw("error opening driver for querying", "error", err) - // Skip this driver if we failed to open because we can't get the properties - continue - } - needToClose = append(needToClose, d) - m[d] = d.Properties() - } else { - logger.Debugw("driver not available", "name", d.Info().Name, "label", d.Info().Label, "status", status) - } - } - - for _, d := range needToClose { - // Since it was closed, we should close it to avoid a leak - if err := d.Close(); err != nil { - logger.Errorw("error closing driver", "error", err) - } - } - - return m -} diff --git a/gostream/shell.nix b/gostream/shell.nix deleted file mode 100644 index 33621f0ee97..00000000000 --- a/gostream/shell.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ pkgs ? import (fetchTarball - "https://github.com/NixOS/nixpkgs/archive/54ce16370bc03831b7d225b64a1ff82b383e0242.tar.gz") - { } }: - -pkgs.mkShell { - buildInputs = - [ pkgs.which pkgs.htop pkgs.go pkgs.nodejs pkgs.pkg-config pkgs.libvpx pkgs.x264 pkgs.libopus ] - ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ - pkgs.darwin.apple_sdk.frameworks.AVFoundation - pkgs.darwin.apple_sdk.frameworks.CoreMedia - ]; - - outputs = [ "out" "doc" "dev" ]; -} diff --git a/gostream/source_stream_utils.go b/gostream/source_stream_utils.go deleted file mode 100644 index 720a6c227a6..00000000000 --- a/gostream/source_stream_utils.go +++ /dev/null @@ -1,102 +0,0 @@ -package gostream - -import ( - "context" - - "github.com/edaniels/golog" - "go.viam.com/utils" -) - -// StreamVideoSource streams the given video source to the stream forever until context signals cancellation. -func StreamVideoSource(ctx context.Context, vs VideoSource, stream Stream) error { - return streamMediaSource(ctx, vs, stream, func(ctx context.Context, frameErr error) { - golog.Global().Debugw("error getting frame", "error", frameErr) - }, stream.InputVideoFrames) -} - -// StreamAudioSource streams the given video source to the stream forever until context signals cancellation. -func StreamAudioSource(ctx context.Context, as AudioSource, stream Stream) error { - return streamMediaSource(ctx, as, stream, func(ctx context.Context, frameErr error) { - golog.Global().Debugw("error getting frame", "error", frameErr) - }, stream.InputAudioChunks) -} - -// StreamVideoSourceWithErrorHandler streams the given video source to the stream forever -// until context signals cancellation, frame errors are sent via the error handler. -func StreamVideoSourceWithErrorHandler( - ctx context.Context, vs VideoSource, stream Stream, errHandler ErrorHandler, -) error { - return streamMediaSource(ctx, vs, stream, errHandler, stream.InputVideoFrames) -} - -// StreamAudioSourceWithErrorHandler streams the given audio source to the stream forever -// until context signals cancellation, audio errors are sent via the error handler. -func StreamAudioSourceWithErrorHandler( - ctx context.Context, as AudioSource, stream Stream, errHandler ErrorHandler, -) error { - return streamMediaSource(ctx, as, stream, errHandler, stream.InputAudioChunks) -} - -// streamMediaSource will stream a source of media forever to the stream until the given context tells it to cancel. -func streamMediaSource[T, U any]( - ctx context.Context, - ms MediaSource[T], - stream Stream, - errHandler ErrorHandler, - inputChan func(props U) (chan<- MediaReleasePair[T], error), -) error { - streamLoop := func() error { - readyCh, readyCtx := stream.StreamingReady() - select { - case <-ctx.Done(): - return ctx.Err() - case <-readyCh: - } - var props U - if provider, ok := ms.(MediaPropertyProvider[U]); ok { - var err error - props, err = provider.MediaProperties(ctx) - if err != nil { - golog.Global().Debugw("no properties found for media; will assume empty", "error", err) - } - } else { - golog.Global().Debug("no properties found for media; will assume empty") - } - input, err := inputChan(props) - if err != nil { - return err - } - mediaStream, err := ms.Stream(ctx, errHandler) - if err != nil { - return err - } - defer func() { - utils.UncheckedError(mediaStream.Close(ctx)) - }() - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-readyCtx.Done(): - return nil - default: - } - media, release, err := mediaStream.Next(ctx) - if err != nil { - continue - } - select { - case <-ctx.Done(): - return ctx.Err() - case <-readyCtx.Done(): - return nil - case input <- MediaReleasePair[T]{media, release}: - } - } - } - for { - if err := streamLoop(); err != nil { - return err - } - } -} diff --git a/gostream/stream.go b/gostream/stream.go deleted file mode 100644 index 7a75deda346..00000000000 --- a/gostream/stream.go +++ /dev/null @@ -1,417 +0,0 @@ -// Package gostream implements a simple server for serving video streams over WebRTC. -package gostream - -import ( - "context" - "errors" - "image" - "sync" - "time" - - "github.com/edaniels/golog" - "github.com/google/uuid" - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "github.com/pion/rtp" - "github.com/pion/webrtc/v3" - "go.viam.com/utils" - - "go.viam.com/rdk/gostream/codec" - "go.viam.com/rdk/rimage" - utils2 "go.viam.com/rdk/utils" -) - -// A Stream is sink that accepts any image frames for the purpose -// of displaying in a WebRTC video track. -type Stream interface { - internalStream - - Name() string - - // Start starts processing frames. - Start() - WriteRTP(pkt *rtp.Packet) error - - // Ready signals that there is at least one client connected and that - // streams are ready for input. The returned context should be used for - // signaling that streaming is no longer ready. - StreamingReady() (<-chan struct{}, context.Context) - - InputVideoFrames(props prop.Video) (chan<- MediaReleasePair[image.Image], error) - - InputAudioChunks(props prop.Audio) (chan<- MediaReleasePair[wave.Audio], error) - - // Stop stops further processing of frames. - Stop() -} - -type internalStream interface { - VideoTrackLocal() (webrtc.TrackLocal, bool) - AudioTrackLocal() (webrtc.TrackLocal, bool) -} - -// MediaReleasePair associates a media with a corresponding -// function to release its resources once the receiver of a -// pair is finished with the media. -type MediaReleasePair[T any] struct { - Media T - Release func() -} - -// NewStream returns a newly configured stream that can begin to handle -// new connections. -func NewStream(config StreamConfig) (Stream, error) { - logger := config.Logger - if logger == nil { - logger = golog.Global() - } - if config.VideoEncoderFactory == nil && config.AudioEncoderFactory == nil { - return nil, errors.New("at least one audio or video encoder factory must be set") - } - if config.TargetFrameRate == 0 { - config.TargetFrameRate = codec.DefaultKeyFrameInterval - } - - name := config.Name - if name == "" { - name = uuid.NewString() - } - - var trackLocal *trackLocalStaticSample - if config.VideoEncoderFactory != nil { - trackLocal = newVideoTrackLocalStaticSample( - webrtc.RTPCodecCapability{MimeType: config.VideoEncoderFactory.MIMEType()}, - "video", - name, - ) - } - - var audioTrackLocal *trackLocalStaticSample - if config.AudioEncoderFactory != nil { - audioTrackLocal = newAudioTrackLocalStaticSample( - webrtc.RTPCodecCapability{MimeType: config.AudioEncoderFactory.MIMEType()}, - "audio", - name, - ) - } - - ctx, cancelFunc := context.WithCancel(context.Background()) - bs := &basicStream{ - name: name, - config: config, - streamingReadyCh: make(chan struct{}), - - videoTrackLocal: trackLocal, - inputImageChan: make(chan MediaReleasePair[image.Image]), - outputVideoChan: make(chan []byte), - - audioTrackLocal: audioTrackLocal, - inputAudioChan: make(chan MediaReleasePair[wave.Audio]), - outputAudioChan: make(chan []byte), - - logger: logger, - shutdownCtx: ctx, - shutdownCtxCancel: cancelFunc, - } - - return bs, nil -} - -type basicStream struct { - mu sync.RWMutex - name string - config StreamConfig - started bool - streamingReadyCh chan struct{} - - videoTrackLocal *trackLocalStaticSample - inputImageChan chan MediaReleasePair[image.Image] - outputVideoChan chan []byte - videoEncoder codec.VideoEncoder - - audioTrackLocal *trackLocalStaticSample - inputAudioChan chan MediaReleasePair[wave.Audio] - outputAudioChan chan []byte - audioEncoder codec.AudioEncoder - - // audioLatency specifies how long in between audio samples. This must be guaranteed - // by all streamed audio. - audioLatency time.Duration - audioLatencySet bool - - shutdownCtx context.Context - shutdownCtxCancel func() - activeBackgroundWorkers sync.WaitGroup - logger golog.Logger -} - -func (bs *basicStream) Name() string { - return bs.name -} - -func (bs *basicStream) Start() { - bs.mu.Lock() - defer bs.mu.Unlock() - - if bs.started { - return - } - bs.started = true - close(bs.streamingReadyCh) - bs.activeBackgroundWorkers.Add(4) - utils.ManagedGo(bs.processInputFrames, bs.activeBackgroundWorkers.Done) - utils.ManagedGo(bs.processOutputFrames, bs.activeBackgroundWorkers.Done) - utils.ManagedGo(bs.processInputAudioChunks, bs.activeBackgroundWorkers.Done) - utils.ManagedGo(bs.processOutputAudioChunks, bs.activeBackgroundWorkers.Done) -} - -func (bs *basicStream) WriteRTP(pkt *rtp.Packet) error { - return bs.videoTrackLocal.rtpTrack.WriteRTP(pkt) -} - -func (bs *basicStream) Stop() { - bs.mu.Lock() - defer bs.mu.Unlock() - - if !bs.started { - close(bs.streamingReadyCh) - } - - bs.started = false - bs.shutdownCtxCancel() - bs.activeBackgroundWorkers.Wait() - if bs.audioEncoder != nil { - bs.audioEncoder.Close() - } - if bs.videoEncoder != nil { - if err := bs.videoEncoder.Close(); err != nil { - bs.logger.Error(err) - } - } - - // reset - bs.outputVideoChan = make(chan []byte) - bs.outputAudioChan = make(chan []byte) - ctx, cancelFunc := context.WithCancel(context.Background()) - bs.shutdownCtx = ctx - bs.shutdownCtxCancel = cancelFunc - bs.streamingReadyCh = make(chan struct{}) -} - -func (bs *basicStream) StreamingReady() (<-chan struct{}, context.Context) { - bs.mu.RLock() - defer bs.mu.RUnlock() - return bs.streamingReadyCh, bs.shutdownCtx -} - -func (bs *basicStream) InputVideoFrames(props prop.Video) (chan<- MediaReleasePair[image.Image], error) { - if bs.config.VideoEncoderFactory == nil { - return nil, errors.New("no video in stream") - } - return bs.inputImageChan, nil -} - -func (bs *basicStream) InputAudioChunks(props prop.Audio) (chan<- MediaReleasePair[wave.Audio], error) { - if bs.config.AudioEncoderFactory == nil { - return nil, errors.New("no audio in stream") - } - bs.mu.Lock() - if bs.audioLatencySet && bs.audioLatency != props.Latency { - return nil, errors.New("cannot stream audio source with different latencies") - } - bs.audioLatencySet = true - bs.audioLatency = props.Latency - bs.mu.Unlock() - return bs.inputAudioChan, nil -} - -func (bs *basicStream) VideoTrackLocal() (webrtc.TrackLocal, bool) { - return bs.videoTrackLocal, bs.videoTrackLocal != nil -} - -func (bs *basicStream) AudioTrackLocal() (webrtc.TrackLocal, bool) { - return bs.audioTrackLocal, bs.audioTrackLocal != nil -} - -func (bs *basicStream) processInputFrames() { - frameLimiterDur := time.Second / time.Duration(bs.config.TargetFrameRate) - defer close(bs.outputVideoChan) - var dx, dy int - ticker := time.NewTicker(frameLimiterDur) - defer ticker.Stop() - for { - select { - case <-bs.shutdownCtx.Done(): - return - default: - } - select { - case <-bs.shutdownCtx.Done(): - return - case <-ticker.C: - } - var framePair MediaReleasePair[image.Image] - select { - case framePair = <-bs.inputImageChan: - case <-bs.shutdownCtx.Done(): - return - } - if framePair.Media == nil { - continue - } - var initErr bool - func() { - if framePair.Release != nil { - defer framePair.Release() - } - - var encodedFrame []byte - - if frame, ok := framePair.Media.(*rimage.LazyEncodedImage); ok && frame.MIMEType() == utils2.MimeTypeH264 { - encodedFrame = frame.RawData() // nothing to do; already encoded - } else { - bounds := framePair.Media.Bounds() - newDx, newDy := bounds.Dx(), bounds.Dy() - if bs.videoEncoder == nil || dx != newDx || dy != newDy { - dx, dy = newDx, newDy - bs.logger.Infow("detected new image bounds", "width", dx, "height", dy) - - if err := bs.initVideoCodec(dx, dy); err != nil { - bs.logger.Error(err) - initErr = true - return - } - } - - // thread-safe because the size is static - var err error - encodedFrame, err = bs.videoEncoder.Encode(bs.shutdownCtx, framePair.Media) - if err != nil { - bs.logger.Error(err) - return - } - } - - if encodedFrame != nil { - select { - case <-bs.shutdownCtx.Done(): - return - case bs.outputVideoChan <- encodedFrame: - } - } - }() - if initErr { - return - } - } -} - -func (bs *basicStream) processInputAudioChunks() { - defer close(bs.outputAudioChan) - var samplingRate, channels int - for { - select { - case <-bs.shutdownCtx.Done(): - return - default: - } - var audioChunkPair MediaReleasePair[wave.Audio] - select { - case audioChunkPair = <-bs.inputAudioChan: - case <-bs.shutdownCtx.Done(): - return - } - if audioChunkPair.Media == nil { - continue - } - var initErr bool - func() { - if audioChunkPair.Release != nil { - defer audioChunkPair.Release() - } - - info := audioChunkPair.Media.ChunkInfo() - newSamplingRate, newChannels := info.SamplingRate, info.Channels - if samplingRate != newSamplingRate || channels != newChannels { - samplingRate, channels = newSamplingRate, newChannels - bs.logger.Infow("detected new audio info", "sampling_rate", samplingRate, "channels", channels) - - bs.audioTrackLocal.setAudioLatency(bs.audioLatency) - if err := bs.initAudioCodec(samplingRate, channels); err != nil { - bs.logger.Error(err) - initErr = true - return - } - } - - encodedChunk, ready, err := bs.audioEncoder.Encode(bs.shutdownCtx, audioChunkPair.Media) - if err != nil { - bs.logger.Error(err) - return - } - if ready && encodedChunk != nil { - select { - case <-bs.shutdownCtx.Done(): - return - case bs.outputAudioChan <- encodedChunk: - } - } - }() - if initErr { - return - } - } -} - -func (bs *basicStream) processOutputFrames() { - framesSent := 0 - for outputFrame := range bs.outputVideoChan { - select { - case <-bs.shutdownCtx.Done(): - return - default: - } - now := time.Now() - if err := bs.videoTrackLocal.WriteData(outputFrame); err != nil { - bs.logger.Errorw("error writing frame", "error", err) - } - framesSent++ - if Debug { - bs.logger.Debugw("wrote sample", "frames_sent", framesSent, "write_time", time.Since(now)) - } - } -} - -func (bs *basicStream) processOutputAudioChunks() { - chunksSent := 0 - for outputChunk := range bs.outputAudioChan { - select { - case <-bs.shutdownCtx.Done(): - return - default: - } - now := time.Now() - if err := bs.audioTrackLocal.WriteData(outputChunk); err != nil { - bs.logger.Errorw("error writing audio chunk", "error", err) - } - chunksSent++ - if Debug { - bs.logger.Debugw("wrote sample", "chunks_sent", chunksSent, "write_time", time.Since(now)) - } - } -} - -func (bs *basicStream) initVideoCodec(width, height int) error { - var err error - bs.videoEncoder, err = bs.config.VideoEncoderFactory.New(width, height, bs.config.TargetFrameRate, bs.logger) - return err -} - -func (bs *basicStream) initAudioCodec(sampleRate, channelCount int) error { - var err error - if bs.audioEncoder != nil { - bs.audioEncoder.Close() - } - bs.audioEncoder, err = bs.config.AudioEncoderFactory.New(sampleRate, channelCount, bs.audioLatency, bs.logger) - return err -} diff --git a/gostream/stream_cgo.go b/gostream/stream_cgo.go deleted file mode 100644 index 2b26fe2bb85..00000000000 --- a/gostream/stream_cgo.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !no_cgo - -package gostream - -import ( - // comment for linter. - _ "github.com/pion/mediadevices/pkg/driver/microphone" -) diff --git a/gostream/stream_config.go b/gostream/stream_config.go deleted file mode 100644 index 0b064e74b0e..00000000000 --- a/gostream/stream_config.go +++ /dev/null @@ -1,19 +0,0 @@ -package gostream - -import ( - "github.com/edaniels/golog" - - "go.viam.com/rdk/gostream/codec" -) - -// A StreamConfig describes how a Stream should be managed. -type StreamConfig struct { - Name string - VideoEncoderFactory codec.VideoEncoderFactory - AudioEncoderFactory codec.AudioEncoderFactory - - // TargetFrameRate will hint to the stream to try to maintain this frame rate. - TargetFrameRate int - - Logger golog.Logger -} diff --git a/gostream/stream_test.go b/gostream/stream_test.go deleted file mode 100644 index 2cbc1928b28..00000000000 --- a/gostream/stream_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package gostream - -import ( - "context" - "flag" - "image" - "testing" - "time" - - "go.viam.com/test" - "golang.org/x/time/rate" -) - -func init() { - testing.Init() - _ = flag.Set("test.benchtime", "100x") -} - -type reader struct { - img image.Image - limiter *rate.Limiter -} - -func newReader(fps float64) *reader { - limiter := rate.NewLimiter(rate.Limit(fps), 1) - return &reader{ - img: image.Image(image.Rect(0, 0, 0, 0)), - limiter: limiter, - } -} - -func (r *reader) Close(_ context.Context) error { return nil } -func (r *reader) Read(_ context.Context) (image.Image, func(), error) { - _ = r.limiter.Wait(context.Background()) - return r.img, func() {}, nil -} - -func stream(ctx context.Context, b *testing.B, s VideoStream) { - b.Helper() - for { - select { - case <-ctx.Done(): - return - default: - _, _, err := s.Next(context.Background()) - test.That(b, err, test.ShouldBeNil) - } - } -} - -const SecondNs = 1000000000.0 // second in nanoseconds - -func incrementAverage(avgOld, valNew, sizeNew float64) float64 { - avgNew := (avgOld) + (valNew-avgOld)/sizeNew - return avgNew -} - -func BenchmarkStream_30FPS(b *testing.B) { - r := newReader(30) - s := NewEmbeddedVideoStreamFromReader(r) - - var avgNs float64 - var count int64 - b.ResetTimer() - for i := 0; i < b.N; i++ { - start := time.Now() - - b.StartTimer() - _, _, err := s.Next(context.Background()) - b.StopTimer() - - test.That(b, err, test.ShouldBeNil) - count++ - elapsedNs := time.Since(start).Nanoseconds() - avgNs += (float64(elapsedNs) - avgNs) / float64(count) - avgNs = incrementAverage(avgNs, float64(elapsedNs), float64(count)) - } - - b.ReportMetric(SecondNs/avgNs, "fps") -} - -func BenchmarkStream_60FPS(b *testing.B) { - r := newReader(60) - s := NewEmbeddedVideoStreamFromReader(r) - - var avgNs float64 - var count int64 - b.ResetTimer() - for i := 0; i < b.N; i++ { - start := time.Now() - - b.StartTimer() - _, _, err := s.Next(context.Background()) - b.StopTimer() - - test.That(b, err, test.ShouldBeNil) - count++ - elapsedNs := time.Since(start).Nanoseconds() - avgNs += (float64(elapsedNs) - avgNs) / float64(count) - avgNs = incrementAverage(avgNs, float64(elapsedNs), float64(count)) - } - - b.ReportMetric(SecondNs/avgNs, "fps") -} - -func BenchmarkStream_30FPS_2Streams(b *testing.B) { - r := newReader(30) - s := NewEmbeddedVideoStreamFromReader(r) - ctx, cancel := context.WithCancel(context.Background()) - - go stream(ctx, b, NewEmbeddedVideoStreamFromReader(r)) - - var avgNs float64 - var count int64 - b.ResetTimer() - for i := 0; i < b.N; i++ { - start := time.Now() - - b.StartTimer() - _, _, err := s.Next(context.Background()) - b.StopTimer() - - test.That(b, err, test.ShouldBeNil) - count++ - elapsedNs := time.Since(start).Nanoseconds() - avgNs += (float64(elapsedNs) - avgNs) / float64(count) - avgNs = incrementAverage(avgNs, float64(elapsedNs), float64(count)) - } - - cancel() - b.ReportMetric(SecondNs/avgNs, "fps") -} - -func BenchmarkStream_30FPS_3Streams(b *testing.B) { - r := newReader(30) - s := NewEmbeddedVideoStreamFromReader(r) - ctx, cancel := context.WithCancel(context.Background()) - - go stream(ctx, b, NewEmbeddedVideoStreamFromReader(r)) - go stream(ctx, b, NewEmbeddedVideoStreamFromReader(r)) - - var avgNs float64 - var count int64 - b.ResetTimer() - for i := 0; i < b.N; i++ { - start := time.Now() - - b.StartTimer() - _, _, err := s.Next(context.Background()) - b.StopTimer() - - test.That(b, err, test.ShouldBeNil) - count++ - elapsedNs := time.Since(start).Nanoseconds() - avgNs += (float64(elapsedNs) - avgNs) / float64(count) - avgNs = incrementAverage(avgNs, float64(elapsedNs), float64(count)) - } - - cancel() - b.ReportMetric(SecondNs/avgNs, "fps") -} diff --git a/gostream/swapper.go b/gostream/swapper.go deleted file mode 100644 index cdc2a5329af..00000000000 --- a/gostream/swapper.go +++ /dev/null @@ -1,176 +0,0 @@ -package gostream - -import ( - "context" - "image" - "sync" - - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "github.com/pkg/errors" - "go.viam.com/utils" -) - -type ( - // A HotSwappableMediaSource allows for continuous streaming of media of - // swappable underlying media sources. - HotSwappableMediaSource[T, U any] interface { - MediaSource[T] - MediaPropertyProvider[U] - Swap(src MediaSource[T]) - } - - // A HotSwappableVideoSource allows for continuous streaming of video of - // swappable underlying video sources. - HotSwappableVideoSource = HotSwappableMediaSource[image.Image, prop.Video] - - // A HotSwappableAudioSource allows for continuous streaming of audio of - // swappable underlying audio sources. - HotSwappableAudioSource = HotSwappableMediaSource[wave.Audio, prop.Audio] -) - -type hotSwappableMediaSource[T, U any] struct { - mu sync.RWMutex - src MediaSource[T] - cancelCtx context.Context - cancel func() -} - -// NewHotSwappableMediaSource returns a hot swappable media source. -func NewHotSwappableMediaSource[T, U any](src MediaSource[T]) HotSwappableMediaSource[T, U] { - swapper := &hotSwappableMediaSource[T, U]{} - swapper.Swap(src) - return swapper -} - -// NewHotSwappableVideoSource returns a hot swappable video source. -func NewHotSwappableVideoSource(src VideoSource) HotSwappableVideoSource { - return NewHotSwappableMediaSource[image.Image, prop.Video](src) -} - -// NewHotSwappableAudioSource returns a hot swappable audio source. -func NewHotSwappableAudioSource(src AudioSource) HotSwappableAudioSource { - return NewHotSwappableMediaSource[wave.Audio, prop.Audio](src) -} - -var errSwapperClosed = errors.New("hot swapper closed or uninitialized") - -// Stream returns a stream that is tolerant to the underlying media source changing. -func (swapper *hotSwappableMediaSource[T, U]) Stream( - ctx context.Context, - errHandlers ...ErrorHandler, -) (MediaStream[T], error) { - swapper.mu.RLock() - defer swapper.mu.RUnlock() - - if swapper.src == nil { - return nil, errSwapperClosed - } - - stream := &hotSwappableMediaSourceStream[T, U]{ - parent: swapper, - errHandlers: errHandlers, - cancelCtx: swapper.cancelCtx, - } - stream.mu.Lock() - defer stream.mu.Unlock() - if err := stream.init(ctx); err != nil { - return nil, err - } - - return stream, nil -} - -// Swap replaces the underlying media source with the given one and signals to all -// streams that a new source is available. -func (swapper *hotSwappableMediaSource[T, U]) Swap(newSrc MediaSource[T]) { - swapper.mu.Lock() - defer swapper.mu.Unlock() - if swapper.src == newSrc { - // they are the same so lets not cause any interruptions. - return - } - - if swapper.cancel != nil { - swapper.cancel() - } - - swapper.src = newSrc - cancelCtx, cancel := context.WithCancel(context.Background()) - swapper.cancelCtx = cancelCtx - swapper.cancel = cancel -} - -// MediaProperties attempts to return media properties for the source, if they exist. -func (swapper *hotSwappableMediaSource[T, U]) MediaProperties(ctx context.Context) (U, error) { - swapper.mu.RLock() - defer swapper.mu.RUnlock() - - var zero U - if swapper.src == nil { - return zero, errSwapperClosed - } - - if provider, ok := swapper.src.(MediaPropertyProvider[U]); ok { - return provider.MediaProperties(ctx) - } - return zero, nil -} - -// Close unsets the underlying media source and signals all streams to close. -func (swapper *hotSwappableMediaSource[T, U]) Close(ctx context.Context) error { - swapper.Swap(nil) - return nil -} - -type hotSwappableMediaSourceStream[T, U any] struct { - mu sync.Mutex - parent *hotSwappableMediaSource[T, U] - errHandlers []ErrorHandler - stream MediaStream[T] - cancelCtx context.Context -} - -func (cs *hotSwappableMediaSourceStream[T, U]) init(ctx context.Context) error { - var err error - if cs.stream != nil { - utils.UncheckedError(cs.stream.Close(ctx)) - cs.stream = nil - } - cs.parent.mu.RLock() - defer cs.parent.mu.RUnlock() - cs.cancelCtx = cs.parent.cancelCtx - if cs.parent.src == nil { - return errSwapperClosed - } - cs.stream, err = cs.parent.src.Stream(ctx, cs.errHandlers...) - return err -} - -func (cs *hotSwappableMediaSourceStream[T, U]) checkStream(ctx context.Context) error { - if cs.stream != nil && cs.cancelCtx.Err() == nil { - return nil - } - return cs.init(ctx) -} - -func (cs *hotSwappableMediaSourceStream[T, U]) Next(ctx context.Context) (T, func(), error) { - cs.mu.Lock() - defer cs.mu.Unlock() - - if err := cs.checkStream(ctx); err != nil { - var zero T - return zero, nil, err - } - return cs.stream.Next(ctx) -} - -func (cs *hotSwappableMediaSourceStream[T, U]) Close(ctx context.Context) error { - cs.mu.Lock() - defer cs.mu.Unlock() - - if cs.stream == nil { - return nil - } - return cs.stream.Close(ctx) -} diff --git a/gostream/templates/index.html b/gostream/templates/index.html deleted file mode 100644 index 7d7227a85bd..00000000000 --- a/gostream/templates/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - gostream - - - - - diff --git a/gostream/tools/tools.go b/gostream/tools/tools.go deleted file mode 100644 index 2571cc78bbe..00000000000 --- a/gostream/tools/tools.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build tools -// +build tools - -// Package tools defines helper build time tooling needed by the codebase. -package tools - -import ( - _ "github.com/bufbuild/buf/cmd/buf" - _ "github.com/edaniels/golinters/cmd/combined" - _ "github.com/golangci/golangci-lint/cmd/golangci-lint" - _ "github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt" -) diff --git a/gostream/video.go b/gostream/video.go deleted file mode 100644 index 867a99ece14..00000000000 --- a/gostream/video.go +++ /dev/null @@ -1,58 +0,0 @@ -package gostream - -import ( - "context" - "image" - - "github.com/pion/mediadevices/pkg/driver" - "github.com/pion/mediadevices/pkg/prop" -) - -type ( - // An VideoReader is anything that can read and recycle video data. - VideoReader = MediaReader[image.Image] - - // An VideoReaderFunc is a helper to turn a function into an VideoReader. - VideoReaderFunc = MediaReaderFunc[image.Image] - - // A VideoSource is responsible for producing images when requested. A source - // should produce the image as quickly as possible and introduce no rate limiting - // of its own as that is handled internally. - VideoSource = MediaSource[image.Image] - - // An VideoStream streams video forever until closed. - VideoStream = MediaStream[image.Image] - - // VideoPropertyProvider providers information about a video source. - VideoPropertyProvider = MediaPropertyProvider[prop.Video] -) - -// NewVideoSource instantiates a new video source. -func NewVideoSource(r VideoReader, p prop.Video) VideoSource { - return newMediaSource(nil, r, p) -} - -// NewVideoSourceForDriver instantiates a new video source and references the given driver. -func NewVideoSourceForDriver(d driver.Driver, r VideoReader, p prop.Video) VideoSource { - return newMediaSource(d, r, p) -} - -// ReadImage gets a single image from a video source. Using this has less of a guarantee -// than VideoSource.Stream that the Nth image follows the N-1th image. -func ReadImage(ctx context.Context, source VideoSource) (image.Image, func(), error) { - return ReadMedia(ctx, source) -} - -// NewEmbeddedVideoStream returns a video stream from a video source that is -// intended to be embedded/composed by another source. It defers the creation -// of its stream. -func NewEmbeddedVideoStream(src VideoSource) VideoStream { - return NewEmbeddedMediaStream[image.Image, prop.Video](src) -} - -// NewEmbeddedVideoStreamFromReader returns a video stream from a video reader that is -// intended to be embedded/composed by another source. It defers the creation -// of its stream. -func NewEmbeddedVideoStreamFromReader(reader VideoReader) VideoStream { - return NewEmbeddedMediaStreamFromReader(reader, prop.Video{}) -} diff --git a/gostream/video_test.go b/gostream/video_test.go deleted file mode 100644 index d972e59e1b0..00000000000 --- a/gostream/video_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package gostream_test - -import ( - "context" - "image" - "testing" - - "github.com/pion/mediadevices/pkg/driver" - "github.com/pion/mediadevices/pkg/prop" - "go.viam.com/test" - - "go.viam.com/rdk/gostream" -) - -func TestReaderClose(t *testing.T) { - d := newFakeDriver("/dev/fake") - - vrc1 := gostream.NewVideoSourceForDriver(d, newFakeReader(), prop.Video{}) - vrc2 := gostream.NewVideoSourceForDriver(d, newFakeReader(), prop.Video{}) - - if closedCount := d.(*fakeDriver).closedCount; closedCount != 0 { - t.Fatalf("expected driver to be open, but was closed %d times", closedCount) - } - - test.That(t, vrc1.Close(context.Background()), test.ShouldHaveSameTypeAs, &gostream.DriverInUseError{}) - test.That(t, d.(*fakeDriver).closedCount, test.ShouldEqual, 0) - - test.That(t, vrc2.Close(context.Background()), test.ShouldBeNil) - test.That(t, d.(*fakeDriver).closedCount, test.ShouldEqual, 1) -} - -// fakeDriver is a driver has a label and keeps track of how many times it is closed. -type fakeDriver struct { - label string - closedCount int -} - -func (d *fakeDriver) Open() error { return nil } -func (d *fakeDriver) Properties() []prop.Media { return []prop.Media{} } -func (d *fakeDriver) ID() string { return d.label } -func (d *fakeDriver) Info() driver.Info { return driver.Info{Label: d.label} } -func (d *fakeDriver) Status() driver.State { return "FakeState" } - -func (d *fakeDriver) Close() error { - d.closedCount++ - return nil -} - -func newFakeDriver(label string) driver.Driver { - return &fakeDriver{label: label} -} - -// fakeReader is a reader that always returns a pixel-sized canvas. -type fakeReader struct{} - -func (r *fakeReader) Read(_ context.Context) (img image.Image, release func(), err error) { - return image.NewNRGBA(image.Rect(0, 0, 1, 1)), func() {}, nil -} - -func (r *fakeReader) Close(_ context.Context) error { - return nil -} - -func newFakeReader() gostream.MediaReader[image.Image] { - return &fakeReader{} -} diff --git a/gostream/video_transforms.go b/gostream/video_transforms.go deleted file mode 100644 index a6ffe5f5c4d..00000000000 --- a/gostream/video_transforms.go +++ /dev/null @@ -1,48 +0,0 @@ -package gostream - -import ( - "context" - "image" - - "github.com/disintegration/imaging" - "github.com/pion/mediadevices/pkg/prop" - "go.uber.org/multierr" -) - -type resizeVideoSource struct { - src VideoSource - stream VideoStream - width, height int -} - -// NewResizeVideoSource returns a source that resizes images to the set dimensions. -func NewResizeVideoSource(src VideoSource, width, height int) VideoSource { - rvs := &resizeVideoSource{ - src: src, - stream: NewEmbeddedVideoStream(src), - width: width, - height: height, - } - return NewVideoSource(rvs, prop.Video{ - Width: rvs.width, - Height: rvs.height, - }) -} - -// Read returns a resized image to Width x Height dimensions. -func (rvs resizeVideoSource) Read(ctx context.Context) (image.Image, func(), error) { - img, release, err := rvs.stream.Next(ctx) - if err != nil { - return nil, nil, err - } - if release != nil { - defer release() - } - - return imaging.Resize(img, rvs.width, rvs.height, imaging.NearestNeighbor), func() {}, nil -} - -// Close closes the underlying source. -func (rvs resizeVideoSource) Close(ctx context.Context) error { - return multierr.Combine(rvs.stream.Close(ctx), rvs.src.Close(ctx)) -} diff --git a/gostream/webrtc_track.go b/gostream/webrtc_track.go deleted file mode 100644 index b09015f9d40..00000000000 --- a/gostream/webrtc_track.go +++ /dev/null @@ -1,354 +0,0 @@ -package gostream - -import ( - "math" - "strings" - "sync" - "time" - - "github.com/pion/rtp" - "github.com/pion/rtp/codecs" - "github.com/pion/webrtc/v3" - "go.uber.org/multierr" -) - -// Adapted from https://github.com/pion/webrtc/blob/master/track_local_static.go -// TODO(https://github.com/edaniels/gostream/issues/4): go through these comments -// and write them in your own words so that it's consistent and you understand -// what's going on here. - -// trackBinding is a single bind for a Track -// Bind can be called multiple times, this stores the -// result for a single bind call so that it can be used when writing. -type trackBinding struct { - id string - ssrc webrtc.SSRC - payloadType webrtc.PayloadType - writeStream webrtc.TrackLocalWriter -} - -// trackLocalStaticRTP is a TrackLocal that has a pre-set codec and accepts RTP Packets. -// If you wish to send a media.Sample use trackLocalStaticSample. -type trackLocalStaticRTP struct { - mu sync.RWMutex - bindings []trackBinding - codec webrtc.RTPCodecCapability - id, rid, streamID string -} - -// newtrackLocalStaticRTP returns a trackLocalStaticRTP. -func newtrackLocalStaticRTP(c webrtc.RTPCodecCapability, id, streamID string) *trackLocalStaticRTP { - return &trackLocalStaticRTP{ - codec: c, - bindings: []trackBinding{}, - id: id, - streamID: streamID, - } -} - -// Bind is called by the PeerConnection after negotiation is complete -// This asserts that the code requested is supported by the remote peer. -// If so it setups all the state (SSRC and PayloadType) to have a call. -func (s *trackLocalStaticRTP) Bind(t webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) { - s.mu.Lock() - defer s.mu.Unlock() - - parameters := webrtc.RTPCodecParameters{RTPCodecCapability: s.codec} - if codec, err := codecParametersFuzzySearch(parameters, t.CodecParameters()); err == nil { - s.bindings = append(s.bindings, trackBinding{ - ssrc: t.SSRC(), - payloadType: codec.PayloadType, - writeStream: t.WriteStream(), - id: t.ID(), - }) - return codec, nil - } - - return webrtc.RTPCodecParameters{}, webrtc.ErrUnsupportedCodec -} - -// Unbind implements the teardown logic when the track is no longer needed. This happens -// because a track has been stopped. -func (s *trackLocalStaticRTP) Unbind(t webrtc.TrackLocalContext) error { - s.mu.Lock() - defer s.mu.Unlock() - - for i := range s.bindings { - if s.bindings[i].id == t.ID() { - s.bindings[i] = s.bindings[len(s.bindings)-1] - s.bindings = s.bindings[:len(s.bindings)-1] - return nil - } - } - - return webrtc.ErrUnbindFailed -} - -// ID is the unique identifier for this Track. This should be unique for the -// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' -// and StreamID would be 'desktop' or 'webcam'. -func (s *trackLocalStaticRTP) ID() string { return s.id } - -// RID is the RTP stream identifier. -func (s *trackLocalStaticRTP) RID() string { return s.rid } - -// StreamID is the group this track belongs too. This must be unique. -func (s *trackLocalStaticRTP) StreamID() string { return s.streamID } - -// Kind controls if this TrackLocal is audio or video. -func (s *trackLocalStaticRTP) Kind() webrtc.RTPCodecType { - switch { - case strings.HasPrefix(s.codec.MimeType, "audio/"): - return webrtc.RTPCodecTypeAudio - case strings.HasPrefix(s.codec.MimeType, "video/"): - return webrtc.RTPCodecTypeVideo - default: - return webrtc.RTPCodecType(0) - } -} - -// Codec gets the Codec of the track. -func (s *trackLocalStaticRTP) Codec() webrtc.RTPCodecCapability { - return s.codec -} - -// WriteRTP writes a RTP Packet to the trackLocalStaticRTP -// If one PeerConnection fails the packets will still be sent to -// all PeerConnections. The error message will contain the ID of the failed -// PeerConnections so you can remove them. -func (s *trackLocalStaticRTP) WriteRTP(p *rtp.Packet) error { - s.mu.RLock() - defer s.mu.RUnlock() - - writeErrs := []error{} - outboundPacket := *p - - for _, b := range s.bindings { - outboundPacket.Header.SSRC = uint32(b.ssrc) - outboundPacket.Header.PayloadType = uint8(b.payloadType) - if _, err := b.writeStream.WriteRTP(&outboundPacket.Header, outboundPacket.Payload); err != nil { - writeErrs = append(writeErrs, err) - } - } - - return multierr.Combine(writeErrs...) -} - -// Write writes a RTP Packet as a buffer to the trackLocalStaticRTP -// If one PeerConnection fails the packets will still be sent to -// all PeerConnections. The error message will contain the ID of the failed -// PeerConnections so you can remove them. -func (s *trackLocalStaticRTP) Write(b []byte) (n int, err error) { - packet := &rtp.Packet{} - if err = packet.Unmarshal(b); err != nil { - return 0, err - } - - return len(b), s.WriteRTP(packet) -} - -// trackLocalStaticSample is a TrackLocal that has a pre-set codec and accepts Samples. -// If you wish to send a RTP Packet use trackLocalStaticRTP. -type trackLocalStaticSample struct { - packetizer rtp.Packetizer - rtpTrack *trackLocalStaticRTP - sampler samplerFunc - isAudio bool - clockRate uint32 - audioLatency time.Duration -} - -// newVideoTrackLocalStaticSample returns a trackLocalStaticSample for video. -func newVideoTrackLocalStaticSample(c webrtc.RTPCodecCapability, id, streamID string) *trackLocalStaticSample { - return &trackLocalStaticSample{ - rtpTrack: newtrackLocalStaticRTP(c, id, streamID), - } -} - -// newAudioTrackLocalStaticSample returns a trackLocalStaticSample for audio. -func newAudioTrackLocalStaticSample( - c webrtc.RTPCodecCapability, - id, streamID string, -) *trackLocalStaticSample { - return &trackLocalStaticSample{ - rtpTrack: newtrackLocalStaticRTP(c, id, streamID), - isAudio: true, - } -} - -// ID is the unique identifier for this Track. This should be unique for the -// stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' -// and StreamID would be 'desktop' or 'webcam'. -func (s *trackLocalStaticSample) ID() string { return s.rtpTrack.ID() } - -// StreamID is the group this track belongs too. This must be unique. -func (s *trackLocalStaticSample) StreamID() string { return s.rtpTrack.StreamID() } - -// RID is the RTP stream identifier. -func (s *trackLocalStaticSample) RID() string { return s.rtpTrack.RID() } - -// Kind controls if this TrackLocal is audio or video. -func (s *trackLocalStaticSample) Kind() webrtc.RTPCodecType { return s.rtpTrack.Kind() } - -// Codec gets the Codec of the track. -func (s *trackLocalStaticSample) Codec() webrtc.RTPCodecCapability { - return s.rtpTrack.Codec() -} - -const rtpOutboundMTU = 1200 - -// Bind is called by the PeerConnection after negotiation is complete -// This asserts that the code requested is supported by the remote peer. -// If so it setups all the state (SSRC and PayloadType) to have a call. -func (s *trackLocalStaticSample) Bind(t webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) { - codec, err := s.rtpTrack.Bind(t) - if err != nil { - return codec, err - } - - s.rtpTrack.mu.Lock() - defer s.rtpTrack.mu.Unlock() - - // We only need one packetizer. But isn't that confusing with other clock rates - // from other codecs? - if s.packetizer != nil { - return codec, nil - } - - payloader, err := payloaderForCodec(codec.RTPCodecCapability) - if err != nil { - return codec, err - } - - // TODO(erd): I think we need to do this for each bind - s.packetizer = rtp.NewPacketizer( - rtpOutboundMTU, - uint8(codec.PayloadType), - uint32(t.SSRC()), - payloader, - rtp.NewRandomSequencer(), - codec.ClockRate, - ) - - s.clockRate = codec.RTPCodecCapability.ClockRate - return codec, nil -} - -func (s *trackLocalStaticSample) setAudioLatency(latency time.Duration) { - s.rtpTrack.mu.Lock() - defer s.rtpTrack.mu.Unlock() - s.audioLatency = latency -} - -// Unbind implements the teardown logic when the track is no longer needed. This happens -// because a track has been stopped. -func (s *trackLocalStaticSample) Unbind(t webrtc.TrackLocalContext) error { - return s.rtpTrack.Unbind(t) -} - -// WriteData writes already encoded data to the trackLocalStaticSample -// If one PeerConnection fails the packets will still be sent to -// all PeerConnections. The error message will contain the ID of the failed -// PeerConnections so you can remove them. -func (s *trackLocalStaticSample) WriteData(frame []byte) error { - s.rtpTrack.mu.Lock() - p := s.packetizer - if p == nil { - s.rtpTrack.mu.Unlock() - return nil - } - if s.isAudio && s.audioLatency == 0 { - return nil - } - sampler := s.sampler - if sampler == nil { - if s.isAudio { - s.sampler = newAudioSampler(s.clockRate, s.audioLatency) - } else { - s.sampler = newVideoSampler(s.clockRate) - } - } - - s.rtpTrack.mu.Unlock() - - if s.sampler == nil { - return nil - } - samples := s.sampler() - packets := p.Packetize(frame, samples) - - writeErrs := []error{} - for _, p := range packets { - if err := s.rtpTrack.WriteRTP(p); err != nil { - writeErrs = append(writeErrs, err) - } - } - - return multierr.Combine(writeErrs...) -} - -// Do a fuzzy find for a codec in the list of codecs -// Used for lookup up a codec in an existing list to find a match. -func codecParametersFuzzySearch(needle webrtc.RTPCodecParameters, haystack []webrtc.RTPCodecParameters) (webrtc.RTPCodecParameters, error) { - // First attempt to match on MimeType + SDPFmtpLine - for _, c := range haystack { - if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) && - c.RTPCodecCapability.SDPFmtpLine == needle.RTPCodecCapability.SDPFmtpLine { - return c, nil - } - } - - // Fallback to just MimeType - for _, c := range haystack { - if strings.EqualFold(c.RTPCodecCapability.MimeType, needle.RTPCodecCapability.MimeType) { - return c, nil - } - } - - return webrtc.RTPCodecParameters{}, webrtc.ErrCodecNotFound -} - -func payloaderForCodec(codec webrtc.RTPCodecCapability) (rtp.Payloader, error) { - switch strings.ToLower(codec.MimeType) { - case strings.ToLower(webrtc.MimeTypeH264): - return &codecs.H264Payloader{}, nil - case strings.ToLower(webrtc.MimeTypeOpus): - return &codecs.OpusPayloader{}, nil - case strings.ToLower(webrtc.MimeTypeVP8): - return &codecs.VP8Payloader{}, nil - case strings.ToLower(webrtc.MimeTypeVP9): - return &codecs.VP9Payloader{}, nil - case strings.ToLower(webrtc.MimeTypeG722): - return &codecs.G722Payloader{}, nil - case strings.ToLower(webrtc.MimeTypePCMU), strings.ToLower(webrtc.MimeTypePCMA): - return &codecs.G711Payloader{}, nil - default: - return nil, webrtc.ErrNoPayloaderForCodec - } -} - -type samplerFunc func() uint32 - -// newVideoSampler creates a video sampler that uses the actual video frame rate and -// the codec's clock rate to come up with a duration for each sample. -func newVideoSampler(clockRate uint32) samplerFunc { - clockRateFloat := float64(clockRate) - lastTimestamp := time.Now() - - return samplerFunc(func() uint32 { - now := time.Now() - duration := now.Sub(lastTimestamp).Seconds() - samples := uint32(math.Round(clockRateFloat * duration)) - lastTimestamp = now - return samples - }) -} - -// newAudioSampler creates a audio sampler that uses a fixed latency and -// the codec's clock rate to come up with a duration for each sample. -func newAudioSampler(clockRate uint32, latency time.Duration) samplerFunc { - samples := uint32(math.Round(float64(clockRate) * latency.Seconds())) - return samplerFunc(func() uint32 { - return samples - }) -} diff --git a/grpc/dial_test.go b/grpc/dial_test.go deleted file mode 100644 index 4da8312e13d..00000000000 --- a/grpc/dial_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package grpc - -import ( - "testing" - - "go.viam.com/test" -) - -func TestInferSignalingServerAddress(t *testing.T) { - tests := []struct { - domain string - expectedAddress string - isSecure bool - wasFound bool - }{ - {"unknownDomain", "", false, false}, - {"xyz.viam.cloud", "app.viam.com:443", true, true}, - {"abc.xyz.viam.cloud", "app.viam.com:443", true, true}, - {"xyz.robot.viaminternal", "app.viaminternal:8089", true, true}, - {"xyz.viamstg.cloud", "app.viam.dev:443", true, true}, - } - - for _, input := range tests { - address, secure, ok := InferSignalingServerAddress(input.domain) - - test.That(t, ok, test.ShouldEqual, input.wasFound) - test.That(t, address, test.ShouldEqual, input.expectedAddress) - test.That(t, secure, test.ShouldEqual, input.isSecure) - } -} diff --git a/internal/cloud/service_test.go b/internal/cloud/service_test.go deleted file mode 100644 index a5365492011..00000000000 --- a/internal/cloud/service_test.go +++ /dev/null @@ -1,289 +0,0 @@ -package cloud_test - -import ( - "context" - "errors" - "fmt" - "testing" - - "go.viam.com/test" - echopb "go.viam.com/utils/proto/rpc/examples/echo/v1" - "go.viam.com/utils/rpc" - echoserver "go.viam.com/utils/rpc/examples/echo/server" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/internal/cloud" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -func TestNotCloudManaged(t *testing.T) { - logger := logging.NewTestLogger(t) - svc := cloud.NewCloudConnectionService(nil, logger) - _, _, err := svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldEqual, cloud.ErrNotCloudManaged) - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) - _, _, err = svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldEqual, cloud.ErrNotCloudManaged) -} - -func TestCloudManaged(t *testing.T) { - logger := logging.NewTestLogger(t) - - server, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - test.That(t, server.RegisterServiceServer( - context.Background(), - &echopb.EchoService_ServiceDesc, - &echoserver.Server{}, - echopb.RegisterEchoServiceHandlerFromEndpoint, - ), test.ShouldBeNil) - - test.That(t, server.Start(), test.ShouldBeNil) - defer func() { - test.That(t, server.Stop(), test.ShouldBeNil) - }() - - addr := server.InternalAddr().String() - - conf := &config.Cloud{ - AppAddress: fmt.Sprintf("http://%s", addr), - } - - svc := cloud.NewCloudConnectionService(conf, logger) - id, conn1, err := svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id, test.ShouldBeEmpty) - test.That(t, conn1, test.ShouldNotBeNil) - - id2, conn2, err := svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id2, test.ShouldBeEmpty) - test.That(t, conn2, test.ShouldNotBeNil) - test.That(t, conn2, test.ShouldNotEqual, conn1) - - echoClient1 := echopb.NewEchoServiceClient(conn1) - echoClient2 := echopb.NewEchoServiceClient(conn2) - - resp, err := echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Message, test.ShouldEqual, "hello") - - conn2.Close() - - // technically conn2 is open via ref count - resp, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Message, test.ShouldEqual, "hello") - test.That(t, conn1.Close(), test.ShouldBeNil) - - // now both are closed - _, err = echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - - id3, conn3, err := svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id3, test.ShouldBeEmpty) - test.That(t, conn3, test.ShouldNotBeNil) - test.That(t, conn3, test.ShouldNotEqual, conn2) - - _, err = echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - - echoClient3 := echopb.NewEchoServiceClient(conn3) - resp, err = echoClient3.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Message, test.ShouldEqual, "hello") - - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) - _, err = echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient3.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - test.That(t, conn2.Close(), test.ShouldBeNil) - - _, _, err = svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeError, errors.New("service closed")) -} - -func TestCloudManagedWithAuth(t *testing.T) { - logger := logging.NewTestLogger(t) - - server, err := rpc.NewServer( - logger.AsZap(), - rpc.WithAuthHandler( - utils.CredentialsTypeRobotSecret, - rpc.MakeSimpleMultiAuthHandler([]string{"foo"}, []string{"bar"}), - ), - ) - test.That(t, err, test.ShouldBeNil) - - test.That(t, server.RegisterServiceServer( - context.Background(), - &echopb.EchoService_ServiceDesc, - &echoserver.Server{}, - echopb.RegisterEchoServiceHandlerFromEndpoint, - ), test.ShouldBeNil) - - test.That(t, server.Start(), test.ShouldBeNil) - defer func() { - test.That(t, server.Stop(), test.ShouldBeNil) - }() - - addr := server.InternalAddr().String() - - conf := &config.Cloud{ - AppAddress: fmt.Sprintf("http://%s", addr), - } - - svc := cloud.NewCloudConnectionService(conf, logger) - id, conn1, err := svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id, test.ShouldBeEmpty) - test.That(t, conn1, test.ShouldNotBeNil) - - id2, conn2, err := svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id2, test.ShouldBeEmpty) - test.That(t, conn2, test.ShouldNotBeNil) - test.That(t, conn2, test.ShouldNotEqual, conn1) - - echoClient := echopb.NewEchoServiceClient(conn1) - _, err = echoClient.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Unauthenticated) - - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) - - conf = &config.Cloud{ - AppAddress: fmt.Sprintf("http://%s", addr), - ID: "foo", - Secret: "bar", - } - - svc = cloud.NewCloudConnectionService(conf, logger) - id, conn1, err = svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id, test.ShouldEqual, "foo") - test.That(t, conn1, test.ShouldNotBeNil) - - id2, conn2, err = svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id2, test.ShouldEqual, "foo") - test.That(t, conn2, test.ShouldNotBeNil) - test.That(t, conn2, test.ShouldNotEqual, conn1) - - echoClient1 := echopb.NewEchoServiceClient(conn1) - echoClient2 := echopb.NewEchoServiceClient(conn2) - - resp, err := echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Message, test.ShouldEqual, "hello") - - conn2.Close() - - // technically conn2 is open via ref count - resp, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Message, test.ShouldEqual, "hello") - test.That(t, conn1.Close(), test.ShouldBeNil) - - // now both are closed - _, err = echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - - id3, conn3, err := svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, id3, test.ShouldEqual, "foo") - test.That(t, conn3, test.ShouldNotBeNil) - test.That(t, conn3, test.ShouldNotEqual, conn2) - - _, err = echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - - echoClient3 := echopb.NewEchoServiceClient(conn3) - resp, err = echoClient3.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Message, test.ShouldEqual, "hello") - - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) - _, err = echoClient1.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient2.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - _, err = echoClient3.Echo(context.Background(), &echopb.EchoRequest{ - Message: "hello", - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, status.Code(err), test.ShouldEqual, codes.Canceled) - test.That(t, conn2.Close(), test.ShouldBeNil) - - _, _, err = svc.AcquireConnection(context.Background()) - test.That(t, err, test.ShouldBeError, errors.New("service closed")) -} diff --git a/logging/impl_test.go b/logging/impl_test.go deleted file mode 100644 index 29740e41beb..00000000000 --- a/logging/impl_test.go +++ /dev/null @@ -1,300 +0,0 @@ -package logging - -import ( - "bytes" - "context" - "fmt" - "reflect" - "strconv" - "strings" - "testing" - - "go.viam.com/test" -) - -type BasicStruct struct { - X int - y string - z string -} - -type User struct { - Name string -} - -type StructWithStruct struct { - x int - Y User - z string -} - -type StructWithAnonymousStruct struct { - x int - Y struct { - Y1 string - } - Z string -} - -// A custom `test.That` assertion function that takes the log line as a third argument for -// outputting a better message and a fourth for outputting the log line being tested. -func EqualsWithLogLine(actual interface{}, otherArgs ...interface{}) string { - if len(otherArgs) != 3 { - panic("EqualsWithMessage requires 4 inputs: actual, expected, message, log line.") - } - - expected := otherArgs[0] - message := otherArgs[1] - logLine := otherArgs[2] - if reflect.DeepEqual(actual, expected) { - return "" - } - - // A modified version of `assertions.ShouldEqual`. - return fmt.Sprintf("Expected: '%v'\nActual: '%v'\n(Should be equal)\nMessage: %v\nLog line: `%v`", expected, actual, message, logLine) -} - -// assertLogMatches will fuzzy match log lines. Notably, this checks the time format, but ignores -// the exact time. And it expects a match on the filename, but the exact line number can be wrong. -func assertLogMatches(t *testing.T, actual *bytes.Buffer, expected string) { - // `Helper` will result in test failures being associated with the callers line number. It's - // more useful to report which `assertLogMatches` call failed rather than which assertion - // inside this function. Maybe. - t.Helper() - - output, err := actual.ReadString('\n') - test.That(t, err, test.ShouldBeNil) - - actualTrimmed := strings.TrimSuffix(output, "\n") - actualParts := strings.Split(actualTrimmed, "\t") - expectedParts := strings.Split(expected, "\t") - partsIdx := 0 - - // Example log: - // 2023-10-30T09:12:09.459Z ERROR impl logging/impl_test.go:200 Errorw log {"traceKey":"foobar","key":"value"} - // ^1 ^2 ^3 ^4 ^5 ^6 - // Date Level Name File/Line Message Structured Data - - // Use the length of the first string as a weak verification of checking that the result looks like a date. - test.That(t, len(actualParts[partsIdx]), EqualsWithLogLine, len(expectedParts[partsIdx]), "Date length mismatch", actualTrimmed) - - // Log level. - partsIdx++ - test.That(t, actualParts[partsIdx], EqualsWithLogLine, expectedParts[partsIdx], "Log level mismatch", actualTrimmed) - - // Logger name. - partsIdx++ - test.That(t, actualParts[partsIdx], EqualsWithLogLine, expectedParts[partsIdx], "Logger name mismatch", actualTrimmed) - - // Filename:line_number. - partsIdx++ - actualFilename, actualLineNumber, found := strings.Cut(actualParts[partsIdx], ":") - test.That(t, found, EqualsWithLogLine, true, "Missing colon on test output", actualTrimmed) - - // Verify the filename matches exactly. - expectedFilename, _, found := strings.Cut(expectedParts[partsIdx], ":") - test.That(t, found, EqualsWithLogLine, true, "Missing colon on expected output", expected) - test.That(t, actualFilename, EqualsWithLogLine, expectedFilename, "Filename mismatch", actualTrimmed) - // Verify the line number is in fact a number, but no more. - _, err = strconv.Atoi(actualLineNumber) - test.That(t, err, EqualsWithLogLine, nil, "Line number is not a number", actualTrimmed) - - // Log message. - partsIdx++ - test.That(t, actualParts[partsIdx], EqualsWithLogLine, expectedParts[partsIdx], "Log message mismatch", actualTrimmed) - - // Structured logging with the "w" API. E.g: `Debugw` has an extra tab delimited output. - test.That(t, len(actualParts), EqualsWithLogLine, len(expectedParts), "Structured log mismatch", actualTrimmed) - if len(actualParts) == partsIdx+1 { - // We hit the end of the list. - return - } - - partsIdx++ - test.That(t, actualParts[partsIdx], EqualsWithLogLine, expectedParts[partsIdx], "Structured log mismatch", actualTrimmed) -} - -// This test asserts our logger matches the output produced by the following zap config: -// -// zap := zap.Must(zap.Config{ -// Level: zap.NewAtomicLevelAt(zap.InfoLevel), -// Encoding: "console", -// EncoderConfig: zapcore.EncoderConfig{ -// TimeKey: "ts", -// LevelKey: "level", -// NameKey: "logger", -// CallerKey: "caller", -// FunctionKey: zapcore.OmitKey, -// MessageKey: "msg", -// StacktraceKey: "stacktrace", -// LineEnding: zapcore.DefaultLineEnding, -// EncodeLevel: zapcore.CapitalLevelEncoder, -// EncodeTime: zapcore.ISO8601TimeEncoder, -// EncodeDuration: zapcore.StringDurationEncoder, -// EncodeCaller: zapcore.ShortCallerEncoder, -// }, -// DisableStacktrace: true, -// OutputPaths: []string{"stdout"}, -// ErrorOutputPaths: []string{"stderr"}, -// }.Build()).Sugar() -// -// E.g: -// -// 2023-10-30T09:12:09.459-0400 INFO logging/impl_test.go:87 zap Info log -func TestConsoleOutputFormat(t *testing.T) { - // A logger object that will write to the `notStdout` buffer. - notStdout := &bytes.Buffer{} - impl := &impl{ - name: "impl", - level: NewAtomicLevelAt(DEBUG), - appenders: []Appender{NewWriterAppender(notStdout)}, - testHelper: func() {}, - } - - impl.Info("impl Info log") - // Note the use of tabs between the date, level, file location and log line. The - // `assertLogMatches` helper will also deal with the changes to the time/line number. - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z INFO impl logging/impl_test.go:67 impl Info log`) - - // Using `Infof` substitutes the tail arguments into the leading template string input. - impl.Infof("impl %s log", "infof") - assertLogMatches(t, notStdout, - `2023-10-30T09:45:20.764Z INFO impl logging/impl_test.go:131 impl infof log`) - - // Using `Infow` turns the tail arguments into a map for structured logging. - impl.Infow("impl logw", "key", "value") - assertLogMatches(t, notStdout, - `2023-10-30T13:19:45.806Z INFO impl logging/impl_test.go:132 impl logw {"key":"value"}`) - - // A few examples of structs. - impl.Infow("impl logw", "key", "val", "StructWithAnonymousStruct", StructWithAnonymousStruct{1, struct{ Y1 string }{"y1"}, "foo"}) - assertLogMatches(t, notStdout, - //nolint:lll - `2023-10-31T14:25:10.239Z INFO impl logging/impl_test.go:148 impl logw {"key":"val","StructWithAnonymousStruct":{"Y":{"Y1":"y1"},"Z":"foo"}}`) - - impl.Infow("StructWithStruct", "key", "val", "StructWithStruct", StructWithStruct{1, User{"alice"}, "foo"}) - assertLogMatches(t, notStdout, - `2023-10-31T14:25:21.095Z INFO impl logging/impl_test.go:153 StructWithStruct {"key":"val","StructWithStruct":{"Y":{"Name":"alice"}}}`) - - impl.Infow("BasicStruct", "implOneKey", "1val", "BasicStruct", BasicStruct{1, "alice", "foo"}) - assertLogMatches(t, notStdout, - `2023-10-31T14:25:29.927Z INFO impl logging/impl_test.go:157 BasicStruct {"implOneKey":"1val","BasicStruct":{"X":1}}`) - - // Define a completely anonymous struct. - anonymousTypedValue := struct { - x int - y struct { - Y1 string - } - Z string - }{1, struct{ Y1 string }{"y1"}, "z"} - - // Even though `y.Y1` is public, it is not included in the output. It isn't a rule that must be - // excluded. This is tested just as a description of the current behavior. - impl.Infow("impl logw", "key", "val", "anonymous struct", anonymousTypedValue) - assertLogMatches(t, notStdout, - `2023-10-31T14:25:39.320Z INFO impl logging/impl_test.go:172 impl logw {"key":"val","anonymous struct":{"Z":"z"}}`) - - // Represent a struct as a string using `fmt.Sprintf`. - impl.Infow("impl logw", "key", "val", "fmt.Sprintf", fmt.Sprintf("%+v", anonymousTypedValue)) - assertLogMatches(t, notStdout, - `2023-10-31T14:25:49.124Z INFO impl logging/impl_test.go:177 impl logw {"key":"val","fmt.Sprintf":"{x:1 y:{Y1:y1} Z:z}"}`) -} - -func TestContextLogging(t *testing.T) { - ctxNoDebug := context.Background() - - // A logger object that will write to the `notStdout` buffer. - notStdout := &bytes.Buffer{} - // The default log level is error. - logger := &impl{ - name: "impl", - level: NewAtomicLevelAt(ERROR), - appenders: []Appender{NewWriterAppender(notStdout)}, - testHelper: func() {}, - } - - logger.CDebug(ctxNoDebug, "Debug log") - test.That(t, notStdout.Len(), test.ShouldEqual, 0) - - traceKey := "foobar" - ctxWithDebug := EnableDebugModeWithKey(ctxNoDebug, traceKey) - logger.CDebug(ctxWithDebug, "Debug log") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z DEBUG impl logging/impl_test.go:200 Debug log {"traceKey":"foobar"}`) - - logger.CDebugf(ctxWithDebug, "Debugf log %v", "Debugf") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z DEBUG impl logging/impl_test.go:200 Debugf log Debugf {"traceKey":"foobar"}`) - - logger.CDebugw(ctxWithDebug, "Debugw log", "key", "value") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z DEBUG impl logging/impl_test.go:200 Debugw log {"traceKey":"foobar","key":"value"}`) - - // Run the same battery of tests on the "Info" loggers. - logger.CInfo(ctxNoDebug, "Info log") - test.That(t, notStdout.Len(), test.ShouldEqual, 0) - - logger.CInfo(ctxWithDebug, "Info log") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z INFO impl logging/impl_test.go:200 Info log {"traceKey":"foobar"}`) - - logger.CInfof(ctxWithDebug, "Infof log %v", "Infof") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z INFO impl logging/impl_test.go:200 Infof log Infof {"traceKey":"foobar"}`) - - logger.CInfow(ctxWithDebug, "Infow log", "key", "value") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z INFO impl logging/impl_test.go:200 Infow log {"traceKey":"foobar","key":"value"}`) - - // Run the same battery of tests on the "Warn" loggers. - logger.CWarn(ctxNoDebug, "Warn log") - test.That(t, notStdout.Len(), test.ShouldEqual, 0) - - logger.CWarn(ctxWithDebug, "Warn log") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z WARN impl logging/impl_test.go:200 Warn log {"traceKey":"foobar"}`) - - logger.CWarnf(ctxWithDebug, "Warnf log %v", "Warnf") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z WARN impl logging/impl_test.go:200 Warnf log Warnf {"traceKey":"foobar"}`) - - logger.CWarnw(ctxWithDebug, "Warnw log", "key", "value") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z WARN impl logging/impl_test.go:200 Warnw log {"traceKey":"foobar","key":"value"}`) - - // Run the same calls on the "CError*" loggers. Because "Error" is the log level, the context - // isn't needed for logging. But we continue to assert that the `traceKey` is included. - logger.CError(ctxWithDebug, "Error log") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z ERROR impl logging/impl_test.go:200 Error log {"traceKey":"foobar"}`) - - logger.CErrorf(ctxWithDebug, "Errorf log %v", "Errorf") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z ERROR impl logging/impl_test.go:200 Errorf log Errorf {"traceKey":"foobar"}`) - - logger.CErrorw(ctxWithDebug, "Errorw log", "key", "value") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z ERROR impl logging/impl_test.go:200 Errorw log {"traceKey":"foobar","key":"value"}`) -} - -func TestSublogger(t *testing.T) { - // A logger object that will write to the `notStdout` buffer. - notStdout := &bytes.Buffer{} - logger := &impl{ - name: "impl", - level: NewAtomicLevelAt(DEBUG), - appenders: []Appender{NewWriterAppender(notStdout)}, - testHelper: func() {}, - } - - logger.Info("info log") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z INFO impl logging/impl_test.go:67 info log`) - - subLogger := logger.Sublogger("sub") - subLogger.Info("info log") - assertLogMatches(t, notStdout, - `2023-10-30T09:12:09.459Z INFO impl.sub logging/impl_test.go:67 info log`) -} diff --git a/logging/logging_test.go b/logging/logging_test.go deleted file mode 100644 index adef99cce48..00000000000 --- a/logging/logging_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package logging - -import ( - "encoding/json" - "testing" - - "go.viam.com/test" -) - -func TestRoundTrip(t *testing.T) { - for _, level := range []Level{DEBUG, INFO, WARN, ERROR} { - serialzied := level.String() - parsed, err := LevelFromString(serialzied) - test.That(t, err, test.ShouldBeNil) - test.That(t, parsed, test.ShouldEqual, level) - } -} - -func TestJSONRoundTrip(t *testing.T) { - type AllLevelStruct struct { - Debug Level - Info Level - Warn Level - Error Level - } - - levels := AllLevelStruct{DEBUG, INFO, WARN, ERROR} - - serialized, err := json.Marshal(levels) - test.That(t, err, test.ShouldBeNil) - - var parsed AllLevelStruct - json.Unmarshal(serialized, &parsed) - test.That(t, levels, test.ShouldResemble, parsed) -} - -func TestJSONErrors(t *testing.T) { - var level Level - err := json.Unmarshal([]byte(`{}`), &level) - test.That(t, err, test.ShouldNotBeNil) - err = json.Unmarshal([]byte(`Debug"`), &level) - test.That(t, err, test.ShouldNotBeNil) - err = json.Unmarshal([]byte(`"not a level"`), &level) - test.That(t, err, test.ShouldNotBeNil) -} diff --git a/logging/net_appender_test.go b/logging/net_appender_test.go deleted file mode 100644 index d0a3b007b59..00000000000 --- a/logging/net_appender_test.go +++ /dev/null @@ -1,231 +0,0 @@ -package logging - -import ( - "context" - "errors" - "fmt" - "net" - "sync" - "testing" - "time" - - apppb "go.viam.com/api/app/v1" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" -) - -func TestNetLoggerQueueOperations(t *testing.T) { - t.Run("test addBatchToQueue", func(t *testing.T) { - queueSize := 10 - nl := NetAppender{ - maxQueueSize: queueSize, - } - - nl.addBatchToQueue(make([]*commonpb.LogEntry, queueSize-1)) - test.That(t, nl.queueSize(), test.ShouldEqual, queueSize-1) - - nl.addBatchToQueue(make([]*commonpb.LogEntry, 2)) - test.That(t, nl.queueSize(), test.ShouldEqual, queueSize) - - nl.addBatchToQueue(make([]*commonpb.LogEntry, queueSize+1)) - test.That(t, nl.queueSize(), test.ShouldEqual, queueSize) - }) - - t.Run("test addToQueue", func(t *testing.T) { - queueSize := 2 - nl := NetAppender{ - maxQueueSize: queueSize, - } - - nl.addToQueue(&commonpb.LogEntry{}) - test.That(t, nl.queueSize(), test.ShouldEqual, 1) - - nl.addToQueue(&commonpb.LogEntry{}) - test.That(t, nl.queueSize(), test.ShouldEqual, queueSize) - - nl.addToQueue(&commonpb.LogEntry{}) - test.That(t, nl.queueSize(), test.ShouldEqual, queueSize) - }) -} - -type mockRobotService struct { - apppb.UnimplementedRobotServiceServer - expectedID string - - logsMu sync.Mutex - logFailForSizeCount int - logs []*commonpb.LogEntry - logBatches [][]*commonpb.LogEntry -} - -func (ms *mockRobotService) Log(ctx context.Context, req *apppb.LogRequest) (*apppb.LogResponse, error) { - if ms.expectedID != req.Id { - return nil, fmt.Errorf("expected id %q but got %q", ms.expectedID, req.Id) - } - ms.logsMu.Lock() - defer ms.logsMu.Unlock() - if ms.logFailForSizeCount > 0 { - ms.logFailForSizeCount -= len(req.Logs) - return &apppb.LogResponse{}, errors.New("not right now") - } - ms.logs = append(ms.logs, req.Logs...) - ms.logBatches = append(ms.logBatches, req.Logs) - return &apppb.LogResponse{}, nil -} - -type serverForRobotLogger struct { - service *mockRobotService - cloudConfig *CloudConfig - stop func() error -} - -func makeServerForRobotLogger(t *testing.T) serverForRobotLogger { - logger := NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - robotService := &mockRobotService{expectedID: "abc-123"} - test.That(t, rpcServer.RegisterServiceServer( - context.Background(), - &apppb.RobotService_ServiceDesc, - robotService, - apppb.RegisterRobotServiceHandlerFromEndpoint, - ), test.ShouldBeNil) - - go rpcServer.Serve(listener) - config := &CloudConfig{ - AppAddress: fmt.Sprintf("http://%s", listener.Addr().String()), - ID: robotService.expectedID, - } - return serverForRobotLogger{robotService, config, rpcServer.Stop} -} - -func TestNetLoggerBatchWrites(t *testing.T) { - server := makeServerForRobotLogger(t) - defer server.stop() - - netAppender, err := NewNetAppender(server.cloudConfig) - test.That(t, err, test.ShouldBeNil) - - logger := NewDebugLogger("test logger") - // The stdout appender is not necessary for test correctness. But it does provide information in - // the output w.r.t the injected grpc errors. - logger.AddAppender(netAppender) - - for i := 0; i < writeBatchSize+1; i++ { - logger.Info("Some-info") - } - - netAppender.sync() - netAppender.Close() - - server.service.logsMu.Lock() - defer server.service.logsMu.Unlock() - test.That(t, server.service.logBatches, test.ShouldHaveLength, 2) - test.That(t, server.service.logBatches[0], test.ShouldHaveLength, 100) - test.That(t, server.service.logBatches[1], test.ShouldHaveLength, 1) - for i := 0; i < writeBatchSize+1; i++ { - test.That(t, server.service.logs[i].Message, test.ShouldEqual, "Some-info") - } -} - -func TestNetLoggerBatchFailureAndRetry(t *testing.T) { - server := makeServerForRobotLogger(t) - defer server.stop() - - netAppender, err := NewNetAppender(server.cloudConfig) - test.That(t, err, test.ShouldBeNil) - logger := NewDebugLogger("test logger") - // The stdout appender is not necessary for test correctness. But it does provide information in - // the output w.r.t the injected grpc errors. - logger.AddAppender(netAppender) - - // This test will first log 10 "Some-info" logs. Followed by a single "New info" log. - numLogs := 11 - - // Inject a failure into the server handling `Log` requests. - server.service.logsMu.Lock() - server.service.logFailForSizeCount = numLogs - server.service.logsMu.Unlock() - - for i := 0; i < numLogs-1; i++ { - logger.Info("Some-info") - } - - // This test requires at least three syncs for the logs to be guaranteed received by the - // server. Once the log queue is full of size ten batches, the first sync will decrement - // `logFailForSizeCount` to 1 and return an error. The second will decrement it to a negative - // value and return an error. The third will succeed. - // - // This test depends on the `Close` method performing a `Sync`. - // - // The `netAppender` also has a background worker syncing on its own cadence. This complicates - // exactly which syncs do what work and which ones return errors. - netAppender.sync() - - logger.Info("New info") - - netAppender.sync() - netAppender.Close() - - server.service.logsMu.Lock() - defer server.service.logsMu.Unlock() - test.That(t, server.service.logs, test.ShouldHaveLength, numLogs) - for i := 0; i < numLogs-1; i++ { - test.That(t, server.service.logs[i].Message, test.ShouldEqual, "Some-info") - } - test.That(t, server.service.logs[numLogs-1].Message, test.ShouldEqual, "New info") -} - -func TestNetLoggerOverflowDuringWrite(t *testing.T) { - // Lower defaultMaxQueueSize for test. - originalDefaultMaxQueueSize := defaultMaxQueueSize - defaultMaxQueueSize = 10 - defer func() { - defaultMaxQueueSize = originalDefaultMaxQueueSize - }() - - server := makeServerForRobotLogger(t) - defer server.stop() - - netAppender, err := NewNetAppender(server.cloudConfig) - test.That(t, err, test.ShouldBeNil) - logger := NewDebugLogger("test logger") - logger.AddAppender(netAppender) - - // Lock server logsMu to mimic network latency for log syncing. Inject max - // number of logs into netAppender queue. Wait for a Sync: syncOnce should - // read the created, injected batch, send it to the server, and hang on - // receiving a non-nil err. - server.service.logsMu.Lock() - for i := 0; i < defaultMaxQueueSize; i++ { - netAppender.addToQueue(&commonpb.LogEntry{Message: fmt.Sprint(i)}) - } - - // Sleep to ensure syncOnce happens (normally every 100ms) and hangs in - // receiving non-nil error from write to remote. - time.Sleep(300 * time.Millisecond) - - // This "10" log should "overflow" the netAppender queue and remove the "0" - // (oldest) log. syncOnce should sense that an overflow occurred and only - // remove "1"-"9" from the queue. - logger.Info("10") - server.service.logsMu.Unlock() - - // Close net appender to cause final syncOnce that sends batch of logs after - // overflow was accounted for: ["10"]. - netAppender.Close() - - // Server should have received logs with Messages: ["0", "1", "2", "3", "4", - // "5", "6", "7", "8", "9", "10"]. - server.service.logsMu.Lock() - defer server.service.logsMu.Unlock() - test.That(t, server.service.logs, test.ShouldHaveLength, 11) - for i := 0; i < 11; i++ { - // First batch of "0"-"10". - test.That(t, server.service.logs[i].Message, test.ShouldEqual, fmt.Sprint(i)) - } -} diff --git a/logging/proto_conversions_test.go b/logging/proto_conversions_test.go deleted file mode 100644 index a7a75bc1e82..00000000000 --- a/logging/proto_conversions_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package logging - -import ( - "math" - "testing" - "time" - - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "go.viam.com/test" -) - -func TestFieldConversion(t *testing.T) { - t.Parallel() - - testTime, err := time.Parse(DefaultTimeFormatStr, "2024-03-01T11:39:40.897Z") - test.That(t, err, test.ShouldBeNil) - - type s struct { - Field1 string - Field2 bool - } - testStruct := &s{"foo", true} - - testCases := []struct { - field zap.Field - expectedVal any - }{ - { - field: zap.Field{ - Key: "boolean", - Type: zapcore.BoolType, - Integer: 0, - }, - expectedVal: false, - }, - { - field: zap.Field{ - Key: "duration", - Type: zapcore.DurationType, - Integer: time.Hour.Nanoseconds(), - }, - expectedVal: time.Hour, - }, - { - field: zap.Field{ - Key: "float64", - Type: zapcore.Float64Type, - Integer: 4608218246714312622, - }, - expectedVal: float64(1.23), - }, - { - field: zap.Field{ - Key: "big float64", - Type: zapcore.Float64Type, - Integer: int64(math.Float64bits(math.MaxFloat64)), - }, - expectedVal: math.MaxFloat64, - }, - { - field: zap.Field{ - Key: "float32", - Type: zapcore.Float32Type, - Integer: 1068037571, - }, - expectedVal: float32(1.32), - }, - { - field: zap.Field{ - Key: "big float32", - Type: zapcore.Float32Type, - Integer: int64(math.Float32bits(math.MaxFloat32)), - }, - expectedVal: float32(math.MaxFloat32), - }, - { - field: zap.Field{ - Key: "int", - Type: zapcore.Int64Type, - Integer: -1234567891011, - }, - expectedVal: int64(-1234567891011), - }, - { - field: zap.Field{ - Key: "uint", - Type: zapcore.Uint64Type, - Integer: 1234567891011, - }, - expectedVal: uint64(1234567891011), - }, - { - field: zap.Field{ - Key: "string", - Type: zapcore.StringType, - String: "foobar", - }, - expectedVal: "foobar", - }, - { - field: zap.Field{ - Key: "error", - Type: zapcore.ErrorType, - String: "error message", - }, - // Error types retain only their message. Stacks are contained in log.Stack. - expectedVal: "error message", - }, - { - field: zap.Field{ - Key: "time", - Type: zapcore.TimeType, - Interface: time.Local, - Integer: testTime.UnixNano(), - }, - // Ensure that UTC is used instead of the Local location from original time. - expectedVal: testTime.In(time.UTC), - }, - { - field: zap.Field{ - Key: "struct", - Type: zapcore.ReflectType, - Interface: testStruct, - }, - // Types of structs cannot actually be preserved; we convert to - // map[string]interface{}. - expectedVal: map[string]interface{}{"Field1": "foo", "Field2": true}, - }, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.field.Key, func(t *testing.T) { - t.Parallel() - - field, err := FieldToProto(tc.field) - test.That(t, err, test.ShouldBeNil) - - key, val, err := FieldKeyAndValueFromProto(field) - test.That(t, err, test.ShouldBeNil) - test.That(t, key, test.ShouldEqual, tc.field.Key) - test.That(t, val, test.ShouldResemble, tc.expectedVal) - }) - } -} diff --git a/ml/inference/tflite_test.go b/ml/inference/tflite_test.go deleted file mode 100644 index 0115ed74bb7..00000000000 --- a/ml/inference/tflite_test.go +++ /dev/null @@ -1,177 +0,0 @@ -//go:build !no_tflite - -package inference - -import ( - "testing" - - tflite "github.com/mattn/go-tflite" - "go.viam.com/test" - "go.viam.com/utils/artifact" - "gorgonia.org/tensor" - - "go.viam.com/rdk/ml" -) - -type fakeInterpreter struct{} - -func (fI *fakeInterpreter) AllocateTensors() tflite.Status { - return tflite.OK -} - -func (fI *fakeInterpreter) Invoke() tflite.Status { - return tflite.OK -} - -func (fI *fakeInterpreter) GetOutputTensorCount() int { - return 1 -} - -func (fI *fakeInterpreter) GetInputTensorCount() int { - return 1 -} - -func (fI *fakeInterpreter) GetOutputTensor(i int) *tflite.Tensor { - return &tflite.Tensor{} -} - -func (fI *fakeInterpreter) GetInputTensor(i int) *tflite.Tensor { - return &tflite.Tensor{} -} - -func (fI *fakeInterpreter) Delete() {} - -var goodOptions *tflite.InterpreterOptions = &tflite.InterpreterOptions{} - -func goodGetInfo(i Interpreter) *TFLiteInfo { - return &TFLiteInfo{} -} - -// TestLoadModel uses a real tflite model to test loading. -func TestLoadModel(t *testing.T) { - tfliteModelPath := artifact.MustPath("ml/inference/model_with_metadata.tflite") - loader, err := NewDefaultTFLiteModelLoader() - test.That(t, err, test.ShouldBeNil) - tfliteStruct, err := loader.Load(tfliteModelPath) - test.That(t, tfliteStruct, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - structInfo := tfliteStruct.Info - test.That(t, structInfo, test.ShouldNotBeNil) - - h := structInfo.InputHeight - w := structInfo.InputWidth - c := structInfo.InputChannels - test.That(t, h, test.ShouldEqual, 640) - test.That(t, w, test.ShouldEqual, 640) - test.That(t, c, test.ShouldEqual, 3) - test.That(t, structInfo.InputTensorType, test.ShouldEqual, "Float32") - test.That(t, structInfo.InputTensorCount, test.ShouldEqual, 1) - test.That(t, structInfo.OutputTensorCount, test.ShouldEqual, 4) - test.That(t, structInfo.OutputTensorTypes, test.ShouldResemble, []string{"Float32", "Float32", "Float32", "Float32"}) - - buf := make([]float32, c*h*w) - tensors := ml.Tensors{"serving_default_input:0": tensor.New(tensor.WithShape(h, w, c), tensor.WithBacking(buf))} - outTensors, err := tfliteStruct.Infer(tensors) - test.That(t, err, test.ShouldBeNil) - test.That(t, outTensors, test.ShouldNotBeNil) - test.That(t, len(outTensors), test.ShouldEqual, 4) - - tfliteStruct.Close() -} - -func TestLoadRealBadPath(t *testing.T) { - loader, err := NewDefaultTFLiteModelLoader() - test.That(t, err, test.ShouldBeNil) - tfliteStruct, err := loader.Load("67387030-86d5-4eb7-a086-020bd03552cb") - test.That(t, tfliteStruct, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, FailedToLoadError("model")) -} - -const badPath string = "bad path" - -func TestLoadTFLiteStruct(t *testing.T) { - goodInterpreterLoader := func(model *tflite.Model, options *tflite.InterpreterOptions) (Interpreter, error) { - return &fakeInterpreter{}, nil - } - - loader := &TFLiteModelLoader{ - newModelFromFile: modelLoader, - newInterpreter: goodInterpreterLoader, - interpreterOptions: goodOptions, - getInfo: goodGetInfo, - } - - tfStruct, err := loader.Load("random path") - test.That(t, err, test.ShouldBeNil) - test.That(t, tfStruct, test.ShouldNotBeNil) - test.That(t, tfStruct.model, test.ShouldNotBeNil) - test.That(t, tfStruct.interpreter, test.ShouldNotBeNil) - test.That(t, tfStruct.interpreterOptions, test.ShouldNotBeNil) - - tfStruct, err = loader.Load(badPath) - test.That(t, err, test.ShouldBeError, FailedToLoadError("model")) - test.That(t, tfStruct, test.ShouldBeNil) -} - -func TestMetadataReader(t *testing.T) { - val, err := getTFLiteMetadataBytes(badPath) - test.That(t, err, test.ShouldBeError) - test.That(t, val, test.ShouldBeNil) -} - -func TestBadInterpreter(t *testing.T) { - badInterpreter := func(model *tflite.Model, options *tflite.InterpreterOptions) (Interpreter, error) { - return nil, FailedToLoadError("interpreter") - } - - loader := &TFLiteModelLoader{ - newModelFromFile: modelLoader, - newInterpreter: badInterpreter, - interpreterOptions: goodOptions, - getInfo: goodGetInfo, - } - - tfStruct, err := loader.Load("ok path") - test.That(t, err, test.ShouldBeError, FailedToLoadError("interpreter")) - test.That(t, tfStruct, test.ShouldBeNil) -} - -func TestHasMetadata(t *testing.T) { - tfliteModelPath := artifact.MustPath("ml/inference/model_with_metadata.tflite") - loader, err := NewDefaultTFLiteModelLoader() - test.That(t, err, test.ShouldBeNil) - tfliteStruct, err := loader.Load(tfliteModelPath) - test.That(t, tfliteStruct, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - meta, err := tfliteStruct.Metadata() - test.That(t, err, test.ShouldBeNil) - test.That(t, meta, test.ShouldNotBeNil) - test.That(t, meta.SubgraphMetadata[0].OutputTensorGroups[0].TensorNames, test.ShouldResemble, []string{"location", "category", "score"}) - - tfliteStruct.Close() -} - -func TestNoMetadata(t *testing.T) { - tfliteModelPath := artifact.MustPath("ml/inference/fizzbuzz_model.tflite") - loader, err := NewDefaultTFLiteModelLoader() - test.That(t, err, test.ShouldBeNil) - tfliteStruct, err := loader.Load(tfliteModelPath) - test.That(t, tfliteStruct, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - fizzMeta, err := tfliteStruct.Metadata() - test.That(t, err, test.ShouldBeError, MetadataDoesNotExistError()) - test.That(t, fizzMeta, test.ShouldBeNil) - - tfliteStruct.Close() -} - -func modelLoader(path string) *tflite.Model { - if path == badPath { - return nil - } - - return &tflite.Model{} -} diff --git a/ml/verify_main_test.go b/ml/verify_main_test.go deleted file mode 100644 index f5694a27c8e..00000000000 --- a/ml/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package ml - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/module/modmanager/manager_test.go b/module/modmanager/manager_test.go deleted file mode 100644 index 672d960f0f1..00000000000 --- a/module/modmanager/manager_test.go +++ /dev/null @@ -1,1348 +0,0 @@ -package modmanager - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sync/atomic" - "testing" - "time" - - "github.com/pion/rtp" - "go.uber.org/zap/zaptest/observer" - v1 "go.viam.com/api/module/v1" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/camera" - fakeCamera "go.viam.com/rdk/components/camera/fake" - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - modlib "go.viam.com/rdk/module" - modmanageroptions "go.viam.com/rdk/module/modmanager/options" - "go.viam.com/rdk/module/modmaninterface" - "go.viam.com/rdk/resource" - rtestutils "go.viam.com/rdk/testutils" - rutils "go.viam.com/rdk/utils" -) - -func setupModManager( - t *testing.T, - ctx context.Context, - parentAddr string, - logger logging.Logger, - options modmanageroptions.Options, -) modmaninterface.ModuleManager { - t.Helper() - mgr := NewManager(ctx, parentAddr, logger, options) - t.Cleanup(func() { - // Wait for module recovery processes here because modmanager.Close does not. - // Do so by grabbing a copy of the modules and then waiting after - // mgr.Close() completes, which cancels all contexts relating to module - // recovery. - mMgr, ok := mgr.(*Manager) - test.That(t, ok, test.ShouldBeTrue) - modules := []*module{} - mMgr.modules.Range(func(_ string, mod *module) bool { - modules = append(modules, mod) - return true - }) - test.That(t, mgr.Close(ctx), test.ShouldBeNil) - for _, mod := range modules { - if mod != nil { - func() { - // Wait for module recovery processes to complete. - mod.inRecoveryLock.Lock() - defer mod.inRecoveryLock.Unlock() - }() - } - } - }) - return mgr -} - -func TestModManagerFunctions(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Precompile module copies to avoid timeout issues when building takes too long. - modPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") - modPath2 := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") - - myCounterModel := resource.NewModel("acme", "demo", "mycounter") - rNameCounter1 := resource.NewName(generic.API, "counter1") - cfgCounter1 := resource.Config{ - Name: "counter1", - API: generic.API, - Model: myCounterModel, - } - _, err := cfgCounter1.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - t.Log("test Helpers") - viamHomeTemp := t.TempDir() - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false, ViamHomeDir: viamHomeTemp}) - - mod := &module{ - cfg: config.Module{ - Name: "test", - ExePath: modPath, - Type: config.ModuleTypeRegistry, - ModuleID: "new:york", - Environment: map[string]string{"SMART": "MACHINES"}, - }, - dataDir: "module-data-dir", - logger: logger, - } - - err = mod.startProcess(ctx, parentAddr, nil, logger, viamHomeTemp) - test.That(t, err, test.ShouldBeNil) - - err = mod.dial() - test.That(t, err, test.ShouldBeNil) - - err = mod.checkReady(ctx, parentAddr, logger) - test.That(t, err, test.ShouldBeNil) - - mod.registerResources(mgr, logger) - reg, ok := resource.LookupRegistration(generic.API, myCounterModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, reg, test.ShouldNotBeNil) - test.That(t, reg.Constructor, test.ShouldNotBeNil) - - mod.deregisterResources() - _, ok = resource.LookupRegistration(generic.API, myCounterModel) - test.That(t, ok, test.ShouldBeFalse) - - test.That(t, mod.process.Stop(), test.ShouldBeNil) - - modEnv := mod.getFullEnvironment(viamHomeTemp) - test.That(t, modEnv["VIAM_HOME"], test.ShouldEqual, viamHomeTemp) - test.That(t, modEnv["VIAM_MODULE_DATA"], test.ShouldEqual, "module-data-dir") - test.That(t, modEnv["VIAM_MODULE_ID"], test.ShouldEqual, "new:york") - test.That(t, modEnv["SMART"], test.ShouldEqual, "MACHINES") - - // Test that VIAM_MODULE_ID is unset for local modules - mod.cfg.Type = config.ModuleTypeLocal - modEnv = mod.getFullEnvironment(viamHomeTemp) - _, ok = modEnv["VIAM_MODULE_ID"] - test.That(t, ok, test.ShouldBeFalse) - - // Make a copy of addr and client to test that connections are properly remade - oldAddr := mod.addr - oldClient := mod.client - - utils.UncheckedError(mod.startProcess(ctx, parentAddr, nil, logger, viamHomeTemp)) - err = mod.dial() - test.That(t, err, test.ShouldBeNil) - - // make sure mod.addr has changed - test.That(t, mod.addr, test.ShouldNotEqual, oldAddr) - // check that we're still able to use the old client - _, err = oldClient.Ready(ctx, &v1.ReadyRequest{ParentAddress: parentAddr}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, mod.process.Stop(), test.ShouldBeNil) - - t.Log("test AddModule") - mgr = setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false}) - test.That(t, err, test.ShouldBeNil) - - modCfg := config.Module{ - Name: "simple-module", - ExePath: modPath, - } - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - reg, ok = resource.LookupRegistration(generic.API, myCounterModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, reg.Constructor, test.ShouldNotBeNil) - - t.Log("test Provides") - ok = mgr.Provides(cfgCounter1) - test.That(t, ok, test.ShouldBeTrue) - - cfg2 := resource.Config{ - API: motor.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - } - ok = mgr.Provides(cfg2) - test.That(t, ok, test.ShouldBeFalse) - - t.Log("test AddResource") - counter, err := mgr.AddResource(ctx, cfgCounter1, nil) - test.That(t, err, test.ShouldBeNil) - - ret, err := counter.DoCommand(ctx, map[string]interface{}{"command": "get"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret["total"], test.ShouldEqual, 0) - - t.Log("test IsModularResource") - ok = mgr.IsModularResource(rNameCounter1) - test.That(t, ok, test.ShouldBeTrue) - - ok = mgr.IsModularResource(resource.NewName(generic.API, "missing")) - test.That(t, ok, test.ShouldBeFalse) - - t.Log("test ValidateConfig") - // ValidateConfig for cfgCounter1 will not actually call any Validate functionality, - // as the mycounter model does not have a configuration object with Validate. - // Assert that ValidateConfig does not fail in this case (allows unimplemented - // validation). - deps, err := mgr.ValidateConfig(ctx, cfgCounter1) - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldBeNil) - - t.Log("test ReconfigureResource") - // Reconfigure should replace the proxied object, resetting the counter - ret, err = counter.DoCommand(ctx, map[string]interface{}{"command": "add", "value": 73}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret["total"], test.ShouldEqual, 73) - - err = mgr.ReconfigureResource(ctx, cfgCounter1, nil) - test.That(t, err, test.ShouldBeNil) - - ret, err = counter.DoCommand(ctx, map[string]interface{}{"command": "get"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret["total"], test.ShouldEqual, 0) - - t.Log("test RemoveResource") - err = mgr.RemoveResource(ctx, rNameCounter1) - test.That(t, err, test.ShouldBeNil) - - ok = mgr.IsModularResource(rNameCounter1) - test.That(t, ok, test.ShouldBeFalse) - - err = mgr.RemoveResource(ctx, rNameCounter1) - test.That(t, err, test.ShouldNotBeNil) - - _, err = counter.DoCommand(ctx, map[string]interface{}{"command": "get"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - t.Log("test ReconfigureModule") - // Re-add counter1. - _, err = mgr.AddResource(ctx, cfgCounter1, nil) - test.That(t, err, test.ShouldBeNil) - // Add 24 to counter and ensure 'total' gets reset after reconfiguration. - ret, err = counter.DoCommand(ctx, map[string]interface{}{"command": "add", "value": 24}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ret["total"], test.ShouldEqual, 24) - - // Change underlying binary path of module to be a different copy of the same module - modCfg.ExePath = modPath2 - - // Reconfigure module with new ExePath. - orphanedResourceNames, err := mgr.Reconfigure(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, orphanedResourceNames, test.ShouldResemble, []resource.Name{rNameCounter1}) - - t.Log("test RemoveModule") - orphanedResourceNames, err = mgr.Remove("simple-module") - test.That(t, err, test.ShouldBeNil) - test.That(t, orphanedResourceNames, test.ShouldBeNil) - - ok = mgr.IsModularResource(rNameCounter1) - test.That(t, ok, test.ShouldBeFalse) - _, err = counter.DoCommand(ctx, map[string]interface{}{"command": "get"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not connected") - - err = counter.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - t.Log("test UntrustedEnv") - mgr = setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: true}) - - modCfg = config.Module{ - Name: "simple-module", - ExePath: modPath, - } - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldEqual, errModularResourcesDisabled) - - t.Log("test empty dir for CleanModuleDataDirectory") - mgr = setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false, ViamHomeDir: ""}) - err = mgr.CleanModuleDataDirectory() - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "cannot clean a root level module data directory") - - t.Log("test CleanModuleDataDirectory") - viamHomeTemp = t.TempDir() - robotCloudID := "a-b-c-d" - expectedDataDir := filepath.Join(viamHomeTemp, parentModuleDataFolderName, robotCloudID) - mgr = setupModManager( - t, - ctx, - parentAddr, - logger, - modmanageroptions.Options{UntrustedEnv: false, ViamHomeDir: viamHomeTemp, RobotCloudID: robotCloudID}, - ) - // check that premature clean is okay - err = mgr.CleanModuleDataDirectory() - test.That(t, err, test.ShouldBeNil) - // create a module and add it to the modmanager - modCfg = config.Module{ - Name: "simple-module", - ExePath: modPath, - } - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - // check that we created the expected directory - moduleDataDir := filepath.Join(expectedDataDir, modCfg.Name) - _, err = os.Stat(moduleDataDir) - test.That(t, err, test.ShouldBeNil) - // make unwanted / unexpected directory - litterDataDir := filepath.Join(expectedDataDir, "litter") - err = os.MkdirAll(litterDataDir, os.ModePerm) - test.That(t, err, test.ShouldBeNil) - // clean - err = mgr.CleanModuleDataDirectory() - test.That(t, err, test.ShouldBeNil) - // check that the module directory still exists - _, err = os.Stat(moduleDataDir) - test.That(t, err, test.ShouldBeNil) - // check that the litter directory is removed - _, err = os.Stat(litterDataDir) - test.That(t, err, test.ShouldBeError) - // remove the module and verify that the entire directory is removed - _, err = mgr.Remove("simple-module") - test.That(t, err, test.ShouldBeNil) - // clean - err = mgr.CleanModuleDataDirectory() - test.That(t, err, test.ShouldBeNil) - _, err = os.Stat(expectedDataDir) - test.That(t, err, test.ShouldBeError) -} - -func TestModManagerValidation(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Precompile module to avoid timeout issues when building takes too long. - modPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - - myBaseModel := resource.NewModel("acme", "demo", "mybase") - cfgMyBase1 := resource.Config{ - Name: "mybase1", - API: base.API, - Model: myBaseModel, - Attributes: map[string]interface{}{ - "motorL": "motor1", - "motorR": "motor2", - }, - } - _, err := cfgMyBase1.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - // cfgMyBase2 is missing required attributes "motorL" and "motorR" and should - // cause module Validation error. - cfgMyBase2 := resource.Config{ - Name: "mybase2", - API: base.API, - Model: myBaseModel, - } - _, err = cfgMyBase2.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - t.Log("adding complex module") - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false}) - - modCfg := config.Module{ - Name: "complex-module", - ExePath: modPath, - } - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - reg, ok := resource.LookupRegistration(base.API, myBaseModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, reg.Constructor, test.ShouldNotBeNil) - - t.Log("test ValidateConfig") - deps, err := mgr.ValidateConfig(ctx, cfgMyBase1) - test.That(t, err, test.ShouldBeNil) - test.That(t, deps, test.ShouldNotBeNil) - test.That(t, deps[0], test.ShouldResemble, "motor1") - test.That(t, deps[1], test.ShouldResemble, "motor2") - - _, err = mgr.ValidateConfig(ctx, cfgMyBase2) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, - `rpc error: code = Unknown desc = error validating resource: expected "motorL" attribute for mybase "mybase2"`) - - // Test that ValidateConfig respects validateConfigTimeout by artificially - // lowering it to an impossibly small duration. - validateConfigTimeout = 1 * time.Nanosecond - _, err = mgr.ValidateConfig(ctx, cfgMyBase1) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, - "rpc error: code = DeadlineExceeded desc = context deadline exceeded") -} - -func TestModuleReloading(t *testing.T) { - ctx := context.Background() - - // Lower global timeout early to avoid race with actual restart code. - defer func(oriOrigVal time.Duration) { - oueRestartInterval = oriOrigVal - }(oueRestartInterval) - oueRestartInterval = 10 * time.Millisecond - - myHelperModel := resource.NewModel("rdk", "test", "helper") - rNameMyHelper := generic.Named("myhelper") - cfgMyHelper := resource.Config{ - Name: "myhelper", - API: generic.API, - Model: myHelperModel, - } - _, err := cfgMyHelper.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - modCfg := config.Module{Name: "test-module"} - - t.Run("successful restart", func(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - - // Precompile module to avoid timeout issues when building takes too long. - modCfg.ExePath = rtestutils.BuildTempModule(t, "module/testmodule") - - // This test neither uses a resource manager nor asserts anything about - // the existence of resources in the graph. Use a dummy - // RemoveOrphanedResources function so orphaned resource logic does not - // panic. - var dummyRemoveOrphanedResourcesCallCount atomic.Uint64 - dummyRemoveOrphanedResources := func(context.Context, []resource.Name) { - dummyRemoveOrphanedResourcesCallCount.Add(1) - } - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - RemoveOrphanedResources: dummyRemoveOrphanedResources, - }) - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - // Add helper resource and ensure "echo" works correctly. - h, err := mgr.AddResource(ctx, cfgMyHelper, nil) - test.That(t, err, test.ShouldBeNil) - ok := mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeTrue) - - resp, err := h.DoCommand(ctx, map[string]interface{}{"command": "echo"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["command"], test.ShouldEqual, "echo") - - // Run 'kill_module' command through helper resource to cause module to exit - // with error. Assert that after module is restarted, helper is modularly - // managed again and remains functional. - _, err = h.DoCommand(ctx, map[string]interface{}{"command": "kill_module"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "rpc error") - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterMessageSnippet("Module successfully restarted").Len(), - test.ShouldEqual, 1) - }) - - ok = mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeTrue) - resp, err = h.DoCommand(ctx, map[string]interface{}{"command": "echo"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["command"], test.ShouldEqual, "echo") - - // Assert that logs reflect that test-module crashed and there were no - // errors during restart. - test.That(t, logs.FilterMessageSnippet("Module has unexpectedly exited").Len(), - test.ShouldEqual, 1) - test.That(t, logs.FilterMessageSnippet("Error while restarting crashed module").Len(), - test.ShouldEqual, 0) - - // Assert that RemoveOrphanedResources was called once. - test.That(t, dummyRemoveOrphanedResourcesCallCount.Load(), test.ShouldEqual, 1) - }) - t.Run("unsuccessful restart", func(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - - // Precompile module to avoid timeout issues when building takes too long. - modCfg.ExePath = rtestutils.BuildTempModule(t, "module/testmodule") - - // This test neither uses a resource manager nor asserts anything about - // the existence of resources in the graph. Use a dummy - // RemoveOrphanedResources function so orphaned resource logic does not - // panic. - var dummyRemoveOrphanedResourcesCallCount atomic.Uint64 - dummyRemoveOrphanedResources := func(context.Context, []resource.Name) { - dummyRemoveOrphanedResourcesCallCount.Add(1) - } - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - RemoveOrphanedResources: dummyRemoveOrphanedResources, - }) - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - // Add helper resource and ensure "echo" works correctly. - h, err := mgr.AddResource(ctx, cfgMyHelper, nil) - test.That(t, err, test.ShouldBeNil) - ok := mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeTrue) - - resp, err := h.DoCommand(ctx, map[string]interface{}{"command": "echo"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["command"], test.ShouldEqual, "echo") - - // Remove testmodule binary, so process cannot be successfully restarted - // after crash. - err = os.Remove(modCfg.ExePath) - test.That(t, err, test.ShouldBeNil) - - // Run 'kill_module' command through helper resource to cause module to - // exit with error. Assert that after three restart errors occur, helper is - // not modularly managed and commands return error. - _, err = h.DoCommand(ctx, map[string]interface{}{"command": "kill_module"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "rpc error") - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterMessageSnippet("Error while restarting crashed module").Len(), - test.ShouldEqual, 3) - }) - - ok = mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeFalse) - _, err = h.DoCommand(ctx, map[string]interface{}{"command": "echo"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not connected") - - // Assert that logs reflect that test-module crashed and was not - // successfully restarted. - test.That(t, logs.FilterMessageSnippet("Module has unexpectedly exited").Len(), - test.ShouldEqual, 1) - test.That(t, logs.FilterMessageSnippet("Module successfully restarted").Len(), - test.ShouldEqual, 0) - - // Assert that RemoveOrphanedResources was called once. - test.That(t, dummyRemoveOrphanedResourcesCallCount.Load(), test.ShouldEqual, 1) - }) - t.Run("do not restart if context canceled", func(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - - // Precompile module to avoid timeout issues when building takes too long. - modCfg.ExePath = rtestutils.BuildTempModule(t, "module/testmodule") - - // This test neither uses a resource manager nor asserts anything about - // the existence of resources in the graph. Use a dummy - // RemoveOrphanedResources function so orphaned resource logic does not - // panic. - var dummyRemoveOrphanedResourcesCallCount atomic.Uint64 - dummyRemoveOrphanedResources := func(context.Context, []resource.Name) { - dummyRemoveOrphanedResourcesCallCount.Add(1) - } - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - RemoveOrphanedResources: dummyRemoveOrphanedResources, - }) - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - // Add helper resource and ensure "echo" works correctly. - h, err := mgr.AddResource(ctx, cfgMyHelper, nil) - test.That(t, err, test.ShouldBeNil) - ok := mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeTrue) - - mgr.(*Manager).restartCtxCancel() - - // Run 'kill_module' command through helper resource to cause module to - // exit with error. Assert that we do not restart the module if context is cancelled. - _, err = h.DoCommand(ctx, map[string]interface{}{"command": "kill_module"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "rpc error") - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterMessageSnippet("Will not attempt to restart crashed module").Len(), - test.ShouldEqual, 1) - }) - - ok = mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeFalse) - _, err = h.DoCommand(ctx, map[string]interface{}{"command": "echo"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not connected") - - // Assert that logs reflect that test-module crashed and was not - // successfully restarted. - test.That(t, logs.FilterMessageSnippet("Module has unexpectedly exited").Len(), - test.ShouldEqual, 1) - test.That(t, logs.FilterMessageSnippet("Module successfully restarted").Len(), - test.ShouldEqual, 0) - - // Assert that RemoveOrphanedResources was called once. - test.That(t, dummyRemoveOrphanedResourcesCallCount.Load(), test.ShouldEqual, 1) - }) - t.Run("timed out module process is stopped", func(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - - modCfg.ExePath = rutils.ResolveFile("module/testmodule/fakemodule.sh") - - // Lower module startup timeout to avoid waiting for 5 mins. - t.Setenv(rutils.ModuleStartupTimeoutEnvVar, "10ms") - - // This test neither uses a resource manager nor asserts anything about - // the existence of resources in the graph. Use a dummy - // RemoveOrphanedResources function so orphaned resource logic does not - // panic. - dummyRemoveOrphanedResources := func(context.Context, []resource.Name) {} - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - RemoveOrphanedResources: dummyRemoveOrphanedResources, - }) - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, - "module test-module timed out after 10ms during startup") - - // Assert that number of "fakemodule is running" messages does not increase - // over time (the process was stopped). - msgNum := logs.FilterMessageSnippet("fakemodule is running").Len() - time.Sleep(100 * time.Millisecond) - test.That(t, logs.FilterMessageSnippet("fakemodule is running").Len(), test.ShouldEqual, msgNum) - - // Assert that manager removes module. - test.That(t, len(mgr.Configs()), test.ShouldEqual, 0) - }) - - t.Run("cancelled module process is stopped", func(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - - modCfg.ExePath = rutils.ResolveFile("module/testmodule/fakemodule.sh") - - // Lower module startup timeout to avoid waiting for 5 mins. - t.Setenv(rutils.ModuleStartupTimeoutEnvVar, "30s") - - // This test neither uses a resource manager nor asserts anything about - // the existence of resources in the graph. Use a dummy - // RemoveOrphanedResources function so orphaned resource logic does not - // panic. - ctx, cancel := context.WithCancel(ctx) - cancel() - dummyRemoveOrphanedResources := func(context.Context, []resource.Name) {} - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - RemoveOrphanedResources: dummyRemoveOrphanedResources, - }) - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "context canceled") - - // Assert that number of "fakemodule is running" messages does not increase - // over time (the process was stopped). - msgNum := logs.FilterMessageSnippet("fakemodule is running").Len() - time.Sleep(100 * time.Millisecond) - test.That(t, logs.FilterMessageSnippet("fakemodule is running").Len(), test.ShouldEqual, msgNum) - - // Assert that manager removes module. - test.That(t, len(mgr.Configs()), test.ShouldEqual, 0) - }) -} - -func TestDebugModule(t *testing.T) { - ctx := context.Background() - - // Precompile module to avoid timeout issues when building takes too long. - modPath := rtestutils.BuildTempModule(t, "module/testmodule") - - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - testCases := []struct { - name string - managerDebugEnabled bool - moduleLogLevel string - debugStatementExpected bool - }{ - { - "manager false debug/module empty log", - false, - "", - false, - }, - { - "manager false debug/module info log", - false, - "info", - false, - }, - { - "manager false debug/module debug log", - false, - "debug", - true, - }, - { - "manager true debug/module empty log", - true, - "", - true, - }, - { - "manager true debug/module info log", - true, - "info", - false, - }, - { - "manager true debug/module debug log", - true, - "debug", - true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var logger logging.Logger - var logs *observer.ObservedLogs - if tc.managerDebugEnabled { - logger, logs = logging.NewObservedTestLogger(t) - } else { - logger, logs = rtestutils.NewInfoObservedTestLogger(t) - } - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false}) - - modCfg := config.Module{ - Name: "test-module", - ExePath: modPath, - LogLevel: tc.moduleLogLevel, - } - - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - if tc.debugStatementExpected { - testutils.WaitForAssertion(t, func(tb testing.TB) { - test.That(tb, logs.FilterMessageSnippet("debug mode enabled").Len(), - test.ShouldEqual, 1) - }) - return - } - - // Assert that after two seconds, "debug mode enabled" debug log is not - // printed by testmodule - time.Sleep(2 * time.Second) - test.That(t, logs.FilterMessageSnippet("debug mode enabled").Len(), - test.ShouldEqual, 0) - }) - } -} - -func TestModuleMisc(t *testing.T) { - ctx := context.Background() - - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - // Build the testmodule - modPath := rtestutils.BuildTempModule(t, "module/testmodule") - modCfg := config.Module{ - Name: "test-module", - ExePath: modPath, - Type: config.ModuleTypeLocal, - } - - testViamHomeDir := t.TempDir() - // Add a component that uses the module - myHelperModel := resource.NewModel("rdk", "test", "helper") - rNameMyHelper := generic.Named("myhelper") - cfgMyHelper := resource.Config{ - Name: "myhelper", - API: generic.API, - Model: myHelperModel, - } - - t.Run("data directory fullstack", func(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - ViamHomeDir: testViamHomeDir, - }) - // Test that cleaning the data directory before it has been created does not produce log messages - err = mgr.CleanModuleDataDirectory() - test.That(t, err, test.ShouldBeNil) - test.That(t, logs.FilterMessageSnippet("Removing module data").Len(), test.ShouldEqual, 0) - - // Add the module - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - _, err = cfgMyHelper.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - h, err := mgr.AddResource(ctx, cfgMyHelper, nil) - test.That(t, err, test.ShouldBeNil) - ok := mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeTrue) - - // Create a file in the modules data directory and then verify that it was written - resp, err := h.DoCommand(ctx, map[string]interface{}{ - "command": "write_data_file", - "filename": "data.txt", - "contents": "hello, world!", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - dataFullPath, ok := resp["fullpath"].(string) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, dataFullPath, test.ShouldEqual, filepath.Join(testViamHomeDir, "module-data", "local", "test-module", "data.txt")) - dataFileContents, err := os.ReadFile(dataFullPath) - test.That(t, err, test.ShouldBeNil) - test.That(t, string(dataFileContents), test.ShouldEqual, "hello, world!") - - err = mgr.RemoveResource(ctx, rNameMyHelper) - test.That(t, err, test.ShouldBeNil) - _, err = mgr.Remove("test-module") - test.That(t, err, test.ShouldBeNil) - // test that the data directory is cleaned up - err = mgr.CleanModuleDataDirectory() - test.That(t, err, test.ShouldBeNil) - test.That(t, logs.FilterMessageSnippet("Removing module data").Len(), test.ShouldEqual, 1) - _, err = os.Stat(filepath.Join(testViamHomeDir, "module-data", "local")) - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "no such file or directory") - }) - t.Run("module working directory", func(t *testing.T) { - logger := logging.NewTestLogger(t) - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - ViamHomeDir: testViamHomeDir, - }) - - // Add the module with a user-specified VIAM_MODULE_ROOT - modCfg := config.Module{ - Name: "test-module", - ExePath: modPath, - Environment: map[string]string{"VIAM_MODULE_ROOT": "/"}, - Type: config.ModuleTypeLocal, - } - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - _, err = cfgMyHelper.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - h, err := mgr.AddResource(ctx, cfgMyHelper, nil) - test.That(t, err, test.ShouldBeNil) - ok := mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeTrue) - - // Create a file in the modules data directory and then verify that it was written - resp, err := h.DoCommand(ctx, map[string]interface{}{ - "command": "get_working_directory", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - modWorkingDirectory, ok := resp["path"].(string) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, modWorkingDirectory, test.ShouldEqual, "/") - }) - t.Run("module working directory fallback", func(t *testing.T) { - logger := logging.NewTestLogger(t) - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - ViamHomeDir: testViamHomeDir, - }) - // Add the module - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - _, err = cfgMyHelper.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - h, err := mgr.AddResource(ctx, cfgMyHelper, nil) - test.That(t, err, test.ShouldBeNil) - ok := mgr.IsModularResource(rNameMyHelper) - test.That(t, ok, test.ShouldBeTrue) - - // Create a file in the modules data directory and then verify that it was written - resp, err := h.DoCommand(ctx, map[string]interface{}{ - "command": "get_working_directory", - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - modWorkingDirectory, ok := resp["path"].(string) - test.That(t, ok, test.ShouldBeTrue) - // MacOS prepends "/private/" to the filepath so we check the end of the path to account for this - // i.e. '/private/var/folders/p1/nl3sq7jn5nx8tfkdwpz2_g7r0000gn/T/TestModuleMisc1764175663/002' - test.That(t, modWorkingDirectory, test.ShouldEndWith, filepath.Dir(modPath)) - }) -} - -func TestTwoModulesRestart(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - - modCfgs := []config.Module{ - { - Name: "test-module", - ExePath: rtestutils.BuildTempModule(t, "module/testmodule"), - Type: config.ModuleTypeLocal, - }, - { - Name: "test-module2", - ExePath: rtestutils.BuildTempModule(t, "module/testmodule2"), - Type: config.ModuleTypeLocal, - }, - } - - // Lower global timeout early to avoid race with actual restart code. - defer func(oriOrigVal time.Duration) { - oueRestartInterval = oriOrigVal - }(oueRestartInterval) - oueRestartInterval = 10 * time.Millisecond - - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - var dummyRemoveOrphanedResourcesCallCount atomic.Uint64 - dummyRemoveOrphanedResources := func(context.Context, []resource.Name) { - dummyRemoveOrphanedResourcesCallCount.Add(1) - } - mgr := setupModManager(t, ctx, parentAddr, logger, modmanageroptions.Options{ - UntrustedEnv: false, - RemoveOrphanedResources: dummyRemoveOrphanedResources, - }) - err = mgr.Add(ctx, modCfgs...) - test.That(t, err, test.ShouldBeNil) - - // Add resources and ensure "echo" works correctly. - models := map[string]resource.Model{ - "myhelper": resource.NewModel("rdk", "test", "helper"), - "myhelper2": resource.NewModel("rdk", "test", "helper2"), - } - for name, model := range models { - resName := generic.Named(name) - resCfg := resource.Config{ - Name: name, - API: generic.API, - Model: model, - } - _, err = resCfg.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - res, err := mgr.AddResource(ctx, resCfg, nil) - test.That(t, err, test.ShouldBeNil) - ok := mgr.IsModularResource(resName) - test.That(t, ok, test.ShouldBeTrue) - - resp, err := res.DoCommand(ctx, map[string]interface{}{"command": "echo"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["command"], test.ShouldEqual, "echo") - - // Run 'kill_module' command through helper resource to cause module to exit - // with error. Assert that after module is restarted, helper is modularly - // managed again and remains functional. - _, err = res.DoCommand(ctx, map[string]interface{}{"command": "kill_module"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "rpc error") - } - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterMessageSnippet("Module resources successfully re-added after module restart").Len(), - test.ShouldEqual, 2) - }) - - // Assert that logs reflect that test-module crashed and there were no - // errors during restart. - test.That(t, logs.FilterMessageSnippet("Module has unexpectedly exited").Len(), - test.ShouldEqual, 2) - test.That(t, logs.FilterMessageSnippet("Error while restarting crashed module").Len(), - test.ShouldEqual, 0) - - // Assert that RemoveOrphanedResources was called once for each module. - test.That(t, dummyRemoveOrphanedResourcesCallCount.Load(), test.ShouldEqual, 2) -} - -var ( - Green = "\033[32m" - Reset = "\033[0m" -) - -// this helps make the test case much easier to read. -func greenLog(t *testing.T, msg string) { - t.Log(Green + msg + Reset) -} - -func TestRTPPassthrough(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Precompile module copies to avoid timeout issues when building takes too long. - modPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/rtppassthrough") - modPath2 := rtestutils.BuildTempModule(t, "examples/customresources/demos/rtppassthrough") - - // configs - noPassConf := resource.Config{ - Name: "no_rtp_passthrough_camera", - API: camera.API, - Model: resource.NewModel("acme", "camera", "fake"), - } - _, err := noPassConf.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - passConf := resource.Config{ - Name: "rtp_passthrough_camera", - API: camera.API, - Model: resource.NewModel("acme", "camera", "fake"), - Attributes: map[string]interface{}{"rtp_passthrough": true}, - } - _, err = passConf.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // robot config - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - greenLog(t, "test AddModule") - mgr := NewManager(ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false}) - test.That(t, err, test.ShouldBeNil) - - // add module executable - modCfg := config.Module{ - Name: "rtp-passthrough-module", - ExePath: modPath, - } - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - // confirm registered - reg, ok := resource.LookupRegistration(camera.API, passConf.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, reg.Constructor, test.ShouldNotBeNil) - - greenLog(t, "Camera that does not support rtp_passthrough returns errors from rtppassthrough.Source methods") - noPassCam, err := mgr.AddResource(ctx, noPassConf, nil) - test.That(t, err, test.ShouldBeNil) - - noPassSource, ok := noPassCam.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - - subscribeRTPTimeout := time.Second * 5 - subCtx, subCancelFn := context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub, err := noPassSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) { - t.Log("should not happen") - t.FailNow() - }) - subCancelFn() - test.That(t, err, test.ShouldBeError) - test.That(t, err.Error(), test.ShouldContainSubstring, fakeCamera.ErrRTPPassthroughNotEnabled.Error()) - test.That(t, sub, test.ShouldResemble, rtppassthrough.NilSubscription) - - err = noPassSource.Unsubscribe(context.Background(), sub.ID) - test.That(t, err, test.ShouldBeError) - test.That(t, err, test.ShouldBeError, camera.ErrUnknownSubscriptionID) - - greenLog(t, "Camera that supports rtp_passthrough") - passCam, err := mgr.AddResource(ctx, passConf, nil) - test.That(t, err, test.ShouldBeNil) - - passSource, ok := passCam.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - - // SubscribeRTP succeeds - calledCtx, calledFn := context.WithCancel(context.Background()) - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub, err = passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) { - test.That(t, len(pkts), test.ShouldBeGreaterThan, 0) - calledFn() - }) - subCancelFn() - test.That(t, err, test.ShouldBeNil) - test.That(t, sub.ID, test.ShouldNotResemble, rtppassthrough.NilSubscription) - test.That(t, sub.Terminated.Err(), test.ShouldBeNil) - <-calledCtx.Done() - - // Unsubscribe succeeds and terminates the subscription - greenLog(t, "Unsubscribe immediately terminates the relevant subscription") - err = passSource.Unsubscribe(context.Background(), sub.ID) - test.That(t, err, test.ShouldBeNil) - test.That(t, sub.Terminated.Err(), test.ShouldBeError, context.Canceled) - - greenLog(t, "The first SubscribeRTP call receives rtp packets") - calledCtx1, calledFn1 := context.WithCancel(context.Background()) - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub1, err := passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) { - test.That(t, len(pkts), test.ShouldBeGreaterThan, 0) - calledFn1() - }) - subCancelFn() - test.That(t, err, test.ShouldBeNil) - - greenLog(t, "The second SubscribeRTP call receives rtp packets concurrently") - calledCtx2, calledFn2 := context.WithCancel(context.Background()) - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub2, err := passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) { - test.That(t, len(pkts), test.ShouldBeGreaterThan, 0) - calledFn2() - }) - subCancelFn() - test.That(t, err, test.ShouldBeNil) - <-calledCtx1.Done() - <-calledCtx2.Done() - test.That(t, sub1.Terminated.Err(), test.ShouldBeNil) - test.That(t, sub2.Terminated.Err(), test.ShouldBeNil) - - greenLog(t, "camera.Close immediately terminates all subscriptions") - err = passCam.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, sub1.Terminated.Err(), test.ShouldBeError, context.Canceled) - test.That(t, sub2.Terminated.Err(), test.ShouldBeError, context.Canceled) - - // reset passthrough - err = mgr.RemoveResource(ctx, passConf.ResourceName()) - test.That(t, err, test.ShouldBeNil) - - passCam, err = mgr.AddResource(ctx, passConf, nil) - test.That(t, err, test.ShouldBeNil) - - passSource, ok = passCam.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - - greenLog(t, "RemoveResource eventually terminates all subscriptions") - // create 2 sub - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub1, err = passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) {}) - test.That(t, err, test.ShouldBeNil) - subCancelFn() - - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub2, err = passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) {}) - test.That(t, err, test.ShouldBeNil) - subCancelFn() - - test.That(t, sub1.Terminated.Err(), test.ShouldBeNil) - test.That(t, sub2.Terminated.Err(), test.ShouldBeNil) - - // remove resource - err = mgr.RemoveResource(ctx, passConf.ResourceName()) - test.That(t, err, test.ShouldBeNil) - - // subs are canceled - test.That(t, utils.SelectContextOrWait(sub1.Terminated, time.Second), test.ShouldBeFalse) - test.That(t, utils.SelectContextOrWait(sub2.Terminated, time.Second), test.ShouldBeFalse) - test.That(t, sub1.Terminated.Err(), test.ShouldBeError, context.Canceled) - test.That(t, sub2.Terminated.Err(), test.ShouldBeError, context.Canceled) - - // reset passthrough - passCam, err = mgr.AddResource(ctx, passConf, nil) - test.That(t, err, test.ShouldBeNil) - - passSource, ok = passCam.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - - // NOTE: This test relies on the model's Close() method hanlding terminating all subscriptions - greenLog(t, "ReconfigureResource eventually terminates all subscriptions when the new model doesn't impelement Reconfigure") - // create a sub - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub, err = passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) {}) - subCancelFn() - test.That(t, err, test.ShouldBeNil) - - test.That(t, sub.Terminated.Err(), test.ShouldBeNil) - - // reconfigure - err = mgr.ReconfigureResource(ctx, passConf, nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, utils.SelectContextOrWait(sub.Terminated, time.Second), test.ShouldBeFalse) - test.That(t, sub.Terminated.Err(), test.ShouldBeError, context.Canceled) - - greenLog(t, "replacing a module binary eventually cancels subscriptions") - // add a subscription - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub, err = passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) {}) - subCancelFn() - test.That(t, err, test.ShouldBeNil) - test.That(t, sub.Terminated.Err(), test.ShouldBeNil) - - // Change underlying binary path of module to be a different copy of the same module - modCfg.ExePath = modPath2 - - // Reconfigure module with new ExePath. - orphanedResourceNames, err := mgr.Reconfigure(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(orphanedResourceNames), test.ShouldEqual, 2) - test.That(t, orphanedResourceNames, test.ShouldContain, noPassConf.ResourceName()) - test.That(t, orphanedResourceNames, test.ShouldContain, passConf.ResourceName()) - // the subscription from the previous module instance should be terminated - test.That(t, utils.SelectContextOrWait(sub.Terminated, time.Second), test.ShouldBeFalse) - test.That(t, sub.Terminated.Err(), test.ShouldBeError, context.Canceled) - - greenLog(t, "modmanager Close eventually cancels subscriptions") - // reset passthrough - passCam, err = mgr.AddResource(ctx, passConf, nil) - test.That(t, err, test.ShouldBeNil) - passSource, ok = passCam.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - - // add a subscription - subCtx, subCancelFn = context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub, err = passSource.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) {}) - test.That(t, err, test.ShouldBeNil) - subCancelFn() - test.That(t, sub.Terminated.Err(), test.ShouldBeNil) - - // close the mod manager - err = mgr.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - // the subscription should be terminated - test.That(t, utils.SelectContextOrWait(sub.Terminated, time.Second), test.ShouldBeFalse) - test.That(t, sub.Terminated.Err(), test.ShouldBeError, context.Canceled) -} - -func TestAddStreamMaxTrackErr(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Precompile module copies to avoid timeout issues when building takes too long. - modPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/rtppassthrough") - logger.Info(modPath) - - api := camera.API - model := resource.NewModel("acme", "camera", "fake") - confs := []resource.Config{} - for i := 0; i < 10; i++ { - conf := resource.Config{ - Name: fmt.Sprintf("fake-%d", i), - API: api, - Model: model, - Attributes: map[string]interface{}{"rtp_passthrough": true}, - } - _, err := conf.Validate("test", resource.APITypeComponentName) - confs = append(confs, conf) - test.That(t, err, test.ShouldBeNil) - } - - parentAddr, err := modlib.CreateSocketAddress(t.TempDir(), "parent") - test.That(t, err, test.ShouldBeNil) - fakeRobot := rtestutils.MakeRobotForModuleLogging(t, parentAddr) - defer func() { - test.That(t, fakeRobot.Stop(), test.ShouldBeNil) - }() - - greenLog(t, "test AddModule") - mgr := NewManager(ctx, parentAddr, logger, modmanageroptions.Options{UntrustedEnv: false}) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, mgr.Close(ctx), test.ShouldBeNil) - }() - // close the mod manager - - modCfg := config.Module{ - Name: "rtp-passthrough-module", - ExePath: modPath, - } - err = mgr.Add(ctx, modCfg) - test.That(t, err, test.ShouldBeNil) - - reg, ok := resource.LookupRegistration(camera.API, model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, reg.Constructor, test.ShouldNotBeNil) - - greenLog(t, "add 10 cameras") - cams := []resource.Resource{} - for _, conf := range confs { - cam, err := mgr.AddResource(ctx, conf, nil) - test.That(t, err, test.ShouldBeNil) - cams = append(cams, cam) - } - - sources := []rtppassthrough.Source{} - for _, cam := range cams { - source, ok := cam.(rtppassthrough.Source) - test.That(t, ok, test.ShouldBeTrue) - sources = append(sources, source) - } - - first9Sources := sources[1:] - - test.That(t, len(first9Sources), test.ShouldEqual, 9) - - greenLog(t, "the first 9's SubscribeRTP calls succeed") - subscribeRTPTimeout := time.Second * 5 - for _, source := range first9Sources { - calledCtx, calledFn := context.WithCancel(context.Background()) - // SubscribeRTP succeeds - subCtx, subCancelFn := context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub, err := source.SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) { - test.That(t, len(pkts), test.ShouldBeGreaterThan, 0) - calledFn() - }) - subCancelFn() - test.That(t, err, test.ShouldBeNil) - test.That(t, sub.ID, test.ShouldNotResemble, rtppassthrough.NilSubscription) - test.That(t, sub.Terminated.Err(), test.ShouldBeNil) - <-calledCtx.Done() - } - - greenLog(t, "the 10th returns an error") - subCtx, subCancelFn := context.WithTimeout(context.Background(), subscribeRTPTimeout) - sub, err := sources[0].SubscribeRTP(subCtx, 512, func(pkts []*rtp.Packet) { - t.Log("should not happen") - t.FailNow() - }) - subCancelFn() - test.That(t, err, test.ShouldBeError) - test.That(t, err.Error(), test.ShouldContainSubstring, "only 9 WebRTC tracks are supported per peer connection") - test.That(t, sub, test.ShouldResemble, rtppassthrough.NilSubscription) -} diff --git a/module/module.go b/module/module.go index 291e70d848c..3eb8571a73e 100644 --- a/module/module.go +++ b/module/module.go @@ -15,21 +15,17 @@ import ( grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/jhump/protoreflect/desc" "github.com/jhump/protoreflect/grpcreflect" - "github.com/pion/rtp" "github.com/pion/webrtc/v3" "github.com/pkg/errors" - "go.opencensus.io/trace" "go.uber.org/multierr" pb "go.viam.com/api/module/v1" robotpb "go.viam.com/api/robot/v1" streampb "go.viam.com/api/stream/v1" "go.viam.com/utils" "go.viam.com/utils/rpc" - "golang.org/x/exp/maps" "google.golang.org/grpc" reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - "go.viam.com/rdk/components/camera/rtppassthrough" "go.viam.com/rdk/config" rgrpc "go.viam.com/rdk/grpc" "go.viam.com/rdk/logging" @@ -157,12 +153,6 @@ func NewHandlerMapFromProto(ctx context.Context, pMap *pb.HandlerMap, conn rpc.C return hMap, errs } -type peerResourceState struct { - // NOTE As I'm only suppporting video to start this will always be a single element - // once we add audio we will need to make this a slice / map - subID rtppassthrough.SubscriptionID -} - // Module represents an external resource module that services components/services. type Module struct { shutdownCtx context.Context @@ -171,8 +161,6 @@ type Module struct { server rpc.Server logger logging.Logger mu sync.Mutex - activeResourceStreams map[resource.Name]peerResourceState - streamSourceByName map[resource.Name]rtppassthrough.Source operations *operation.Manager ready bool addr string @@ -208,18 +196,16 @@ func NewModule(ctx context.Context, address string, logger logging.Logger) (*Mod cancelCtx, cancel := context.WithCancel(context.Background()) m := &Module{ - shutdownCtx: cancelCtx, - shutdownFn: cancel, - logger: logger, - addr: address, - operations: opMgr, - streamSourceByName: map[resource.Name]rtppassthrough.Source{}, - activeResourceStreams: map[resource.Name]peerResourceState{}, - server: NewServer(opts...), - ready: true, - handlers: HandlerMap{}, - collections: map[resource.API]resource.APIResourceCollection[resource.Resource]{}, - resLoggers: map[resource.Resource]logging.Logger{}, + shutdownCtx: cancelCtx, + shutdownFn: cancel, + logger: logger, + addr: address, + operations: opMgr, + server: NewServer(opts...), + ready: true, + handlers: HandlerMap{}, + collections: map[resource.API]resource.APIResourceCollection[resource.Resource]{}, + resLoggers: map[resource.Resource]logging.Logger{}, } if err := m.server.RegisterServiceServer(ctx, &pb.ModuleService_ServiceDesc, m); err != nil { return nil, err @@ -472,11 +458,6 @@ func (m *Module) AddResource(ctx context.Context, req *pb.AddResourceRequest) (* return nil, err } - var passthroughSource rtppassthrough.Source - if p, ok := res.(rtppassthrough.Source); ok { - passthroughSource = p - } - m.mu.Lock() defer m.mu.Unlock() coll, ok := m.collections[conf.API] @@ -493,9 +474,6 @@ func (m *Module) AddResource(ctx context.Context, req *pb.AddResourceRequest) (* m.resLoggers[res] = resLogger // add the video stream resources upon creation - if passthroughSource != nil { - m.streamSourceByName[res.Name()] = passthroughSource - } return &pb.AddResourceResponse{}, nil } @@ -560,7 +538,6 @@ func (m *Module) ReconfigureResource(ctx context.Context, req *pb.ReconfigureRes m.logger.Error(err) } - delete(m.activeResourceStreams, res.Name()) resInfo, ok := resource.LookupRegistration(conf.API, conf.Model) if !ok { return nil, errors.Errorf("do not know how to construct %q", conf.API) @@ -573,14 +550,6 @@ func (m *Module) ReconfigureResource(ctx context.Context, req *pb.ReconfigureRes if err != nil { return nil, err } - var passthroughSource rtppassthrough.Source - if p, ok := newRes.(rtppassthrough.Source); ok { - passthroughSource = p - } - - if passthroughSource != nil { - m.streamSourceByName[res.Name()] = passthroughSource - } return &pb.ReconfigureResourceResponse{}, coll.ReplaceOne(conf.ResourceName(), newRes) } @@ -639,9 +608,6 @@ func (m *Module) RemoveResource(ctx context.Context, req *pb.RemoveResourceReque m.logger.Error(err) } - delete(m.streamSourceByName, res.Name()) - delete(m.activeResourceStreams, res.Name()) - return &pb.RemoveResourceResponse{}, coll.Remove(name) } @@ -710,13 +676,7 @@ func (m *Module) OperationManager() *operation.Manager { // ListStreams lists the streams. func (m *Module) ListStreams(ctx context.Context, req *streampb.ListStreamsRequest) (*streampb.ListStreamsResponse, error) { - _, span := trace.StartSpan(ctx, "module::module::ListStreams") - defer span.End() - names := make([]string, 0, len(m.streamSourceByName)) - for _, n := range maps.Keys(m.streamSourceByName) { - names = append(names, n.String()) - } - return &streampb.ListStreamsResponse{Names: names}, nil + return &streampb.ListStreamsResponse{}, nil } // AddStream adds a stream. @@ -729,114 +689,11 @@ func (m *Module) ListStreams(ctx context.Context, req *streampb.ListStreamsReque // 6. A webrtc track is unable to be created // 7. Adding the track to the peer connection fails. func (m *Module) AddStream(ctx context.Context, req *streampb.AddStreamRequest) (*streampb.AddStreamResponse, error) { - ctx, span := trace.StartSpan(ctx, "module::module::AddStream") - defer span.End() - name, err := resource.NewFromString(req.GetName()) - if err != nil { - return nil, err - } - m.mu.Lock() - defer m.mu.Unlock() - if m.pc == nil { - return nil, errors.New("module has no peer connection") - } - vcss, ok := m.streamSourceByName[name] - if !ok { - err := errors.New("unknown stream for resource") - m.logger.CWarnw(ctx, err.Error(), "name", name, "streamSourceByName", fmt.Sprintf("%#v", m.streamSourceByName)) - return nil, err - } - - if _, ok = m.activeResourceStreams[name]; ok { - m.logger.CWarnw(ctx, "AddStream called with when there is already a stream for peer connection. NoOp", "name", name) - return &streampb.AddStreamResponse{}, nil - } - - if len(m.activeResourceStreams) >= maxSupportedWebRTCTRacks { - return nil, errMaxSupportedWebRTCTrackLimit - } - - tlsRTP, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/H264"}, "video", name.String()) - if err != nil { - return nil, errors.Wrap(err, "error creating a new TrackLocalStaticRTP") - } - - sub, err := vcss.SubscribeRTP(ctx, rtpBufferSize, func(pkts []*rtp.Packet) { - for _, pkt := range pkts { - if err := tlsRTP.WriteRTP(pkt); err != nil { - m.logger.CWarnw(ctx, "SubscribeRTP callback function WriteRTP", "err", err) - } - } - }) - if err != nil { - return nil, errors.Wrap(err, "error setting up stream subscription") - } - - m.logger.CDebugw(ctx, "AddStream calling AddTrack", "name", name, "subID", sub.ID.String()) - sender, err := m.pc.AddTrack(tlsRTP) - if err != nil { - err = errors.Wrap(err, "error adding track") - if unsubErr := vcss.Unsubscribe(ctx, sub.ID); unsubErr != nil { - return nil, multierr.Combine(err, unsubErr) - } - return nil, err - } - - removeTrackOnSubTerminate := func() { - defer m.logger.Debugw("RemoveTrack called on ", "name", name, "subID", sub.ID.String()) - // wait until either the module is shutting down, or the subscription terminates - var msg string - select { - case <-sub.Terminated.Done(): - msg = "rtp_passthrough subscription expired, calling RemoveTrack" - case <-m.shutdownCtx.Done(): - msg = "module closing calling RemoveTrack" - } - // remove the track from the peer connection so that viam-server clients know that the stream has terminated - m.mu.Lock() - defer m.mu.Unlock() - m.logger.Debugw(msg, "name", name, "subID", sub.ID.String()) - delete(m.activeResourceStreams, name) - if err := m.pc.RemoveTrack(sender); err != nil { - m.logger.Warnf("RemoveTrack returned error", "name", name, "subID", sub.ID.String(), "err", err) - } - } - m.activeBackgroundWorkers.Add(1) - utils.ManagedGo(removeTrackOnSubTerminate, m.activeBackgroundWorkers.Done) - - m.activeResourceStreams[name] = peerResourceState{subID: sub.ID} return &streampb.AddStreamResponse{}, nil } // RemoveStream removes a stream. func (m *Module) RemoveStream(ctx context.Context, req *streampb.RemoveStreamRequest) (*streampb.RemoveStreamResponse, error) { - ctx, span := trace.StartSpan(ctx, "module::module::RemoveStream") - defer span.End() - name, err := resource.NewFromString(req.GetName()) - if err != nil { - return nil, err - } - m.mu.Lock() - defer m.mu.Unlock() - if m.pc == nil { - return nil, errors.New("module has no peer connection") - } - vcss, ok := m.streamSourceByName[name] - if !ok { - return nil, errors.Errorf("unknown stream for resource %s", name) - } - - prs, ok := m.activeResourceStreams[name] - if !ok { - return nil, errors.Errorf("stream %s is not active", name) - } - - if err := vcss.Unsubscribe(ctx, prs.subID); err != nil { - m.logger.CWarnw(ctx, "RemoveStream > Unsubscribe", "name", name, "subID", prs.subID.String(), "err", err) - return nil, err - } - - delete(m.activeResourceStreams, name) return &streampb.RemoveStreamResponse{}, nil } diff --git a/module/module_interceptors_test.go b/module/module_interceptors_test.go deleted file mode 100644 index 444b45c8381..00000000000 --- a/module/module_interceptors_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package module_test - -import ( - "context" - "fmt" - "os" - "runtime" - "testing" - - "github.com/google/uuid" - commonpb "go.viam.com/api/common/v1" - genericpb "go.viam.com/api/component/generic/v1" - robotpb "go.viam.com/api/robot/v1" - "go.viam.com/test" - goutils "go.viam.com/utils" - "go.viam.com/utils/testutils" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - rtestutils "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/robottestutils" -) - -func TestOpID(t *testing.T) { - ctx := context.Background() - - if runtime.GOARCH == "arm" { - t.Skip("skipping on 32-bit ARM -- subprocess build warnings cause failure") - } - logger, logObserver := logging.NewObservedTestLogger(t) - - var port int - var success bool - for portTryNum := 0; portTryNum < 10; portTryNum++ { - cfgFilename, p, err := makeConfig(t, logger) - port = p - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, os.Remove(cfgFilename), test.ShouldBeNil) - }() - - server := robottestutils.ServerAsSeparateProcess(t, cfgFilename, logger) - err = server.Start(context.Background()) - test.That(t, err, test.ShouldBeNil) - - if success = robottestutils.WaitForServing(logObserver, port); success { - defer func() { - test.That(t, server.Stop(), test.ShouldBeNil) - }() - break - } - logger.Infow("Port in use. Restarting on new port.", "port", port, "err", err) - server.Stop() - continue - } - test.That(t, success, test.ShouldBeTrue) - - conn, err := robottestutils.Connect(port) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, conn.Close(), test.ShouldBeNil) - }() - rc := robotpb.NewRobotServiceClient(conn) - gc := genericpb.NewGenericServiceClient(conn) - - opIDOutgoing := uuid.New().String() - var opIDIncoming string - md := metadata.New(map[string]string{"opid": opIDOutgoing}) - mdCtx := metadata.NewOutgoingContext(ctx, md) - - // Do this twice, once with no opID set, and a second with a set opID. - for name, cCtx := range map[string]context.Context{"default context": ctx, "context with opid set": mdCtx} { - t.Run(name, func(t *testing.T) { - syncChan := make(chan string) - // in the background, run a operation that sleeps for one second, and capture it's header - go func() { - cmd, err := structpb.NewStruct(map[string]interface{}{"command": "sleep"}) - test.That(t, err, test.ShouldBeNil) - - var hdr metadata.MD - _, err = gc.DoCommand(cCtx, &commonpb.DoCommandRequest{Name: "helper1", Command: cmd}, grpc.Header(&hdr)) - test.That(t, err, test.ShouldBeNil) - - test.That(t, hdr["opid"], test.ShouldHaveLength, 1) - origOpID, err := uuid.Parse(hdr["opid"][0]) - test.That(t, err, test.ShouldBeNil) - syncChan <- origOpID.String() - }() - - // directly get the operations list from the parent server, naively waiting for it to be non-zero - var parentOpList, modOpList []string - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - resp, err := rc.GetOperations(ctx, &robotpb.GetOperationsRequest{}) - test.That(tb, err, test.ShouldBeNil) - opList := resp.GetOperations() - test.That(tb, len(opList), test.ShouldBeGreaterThan, 0) - for _, op := range opList { - parentOpList = append(parentOpList, op.Id) - } - }) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - // as soon as we see the op in the parent, check the operations in the module - cmd, err := structpb.NewStruct(map[string]interface{}{"command": "get_ops"}) - test.That(tb, err, test.ShouldBeNil) - resp, err := gc.DoCommand(ctx, &commonpb.DoCommandRequest{Name: "helper1", Command: cmd}) - test.That(tb, err, test.ShouldBeNil) - ret, ok := resp.GetResult().AsMap()["ops"] - test.That(tb, ok, test.ShouldBeTrue) - retList, ok := ret.([]interface{}) - test.That(tb, ok, test.ShouldBeTrue) - test.That(tb, len(retList), test.ShouldBeGreaterThan, 1) - for _, v := range retList { - val, ok := v.(string) - test.That(tb, ok, test.ShouldBeTrue) - modOpList = append(modOpList, val) - } - test.That(tb, len(modOpList), test.ShouldBeGreaterThan, 1) - }) - - // wait for the original call to sleep and parse its header for the opID - commandOpID := <-syncChan - - // then make sure the initial opID showed up in both parent and module correctly - test.That(t, commandOpID, test.ShouldBeIn, parentOpList) - test.That(t, commandOpID, test.ShouldBeIn, modOpList) - opIDIncoming = commandOpID - - // lastly, and only for the second iteration, make sure the intentionally set, outgoing opID was used - if name != "default context" { - test.That(t, opIDIncoming, test.ShouldEqual, opIDOutgoing) - } - }) - } -} - -func makeConfig(t *testing.T, logger logging.Logger) (string, int, error) { - // Precompile module to avoid timeout issues when building takes too long. - modPath := rtestutils.BuildTempModule(t, "module/testmodule") - - port, err := goutils.TryReserveRandomPort() - if err != nil { - return "", 0, err - } - - cfg := config.Config{ - Modules: []config.Module{{ - Name: "TestModule", - ExePath: modPath, - }}, - Network: config.NetworkConfig{ - NetworkConfigData: config.NetworkConfigData{ - BindAddress: fmt.Sprintf("localhost:%d", port), - }, - }, - Components: []resource.Config{{ - API: generic.API, - Model: resource.NewModel("rdk", "test", "helper"), - Name: "helper1", - }}, - } - cfgFilename, err := robottestutils.MakeTempConfig(t, &cfg, logger) - return cfgFilename, port, err -} diff --git a/module/module_test.go b/module/module_test.go deleted file mode 100644 index b91dfe42761..00000000000 --- a/module/module_test.go +++ /dev/null @@ -1,762 +0,0 @@ -package module_test - -import ( - "context" - "errors" - "fmt" - "path/filepath" - "strings" - "testing" - - grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry" - v1 "go.viam.com/api/app/v1" - pb "go.viam.com/api/module/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - "go.viam.com/rdk/examples/customresources/models/mybase" - "go.viam.com/rdk/examples/customresources/models/mygizmo" - "go.viam.com/rdk/examples/customresources/models/mysum" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" - "go.viam.com/rdk/resource" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/services/datamanager" - "go.viam.com/rdk/services/shell" - "go.viam.com/rdk/testutils/inject" - rutils "go.viam.com/rdk/utils" -) - -func TestAddModelFromRegistry(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Use 'foo.sock' for arbitrary module to test AddModelFromRegistry. - m, err := module.NewModule(ctx, filepath.Join(t.TempDir(), "foo.sock"), logger) - test.That(t, err, test.ShouldBeNil) - - invalidModel := resource.NewModel("non", "existent", "model") - invalidServiceAPI := resource.APINamespace("fake").WithServiceType("nonexistentservice") - invalidComponentAPI := resource.APINamespace("fake").WithComponentType("nonexistentcomponent") - - validServiceAPI := summationapi.API - validComponentAPI := gizmoapi.API - - validServiceModel := mysum.Model - validComponentModel := mygizmo.Model - - resourceError := "resource with API %s and model %s not yet registered" - testCases := []struct { - api resource.API - model resource.Model - err error - }{ - // Invalid resource APIs and models - { - invalidServiceAPI, - invalidModel, - fmt.Errorf(resourceError, invalidServiceAPI, invalidModel), - }, - { - invalidComponentAPI, - invalidModel, - fmt.Errorf(resourceError, invalidComponentAPI, invalidModel), - }, - // Valid resource APIs and invalid models - { - validServiceAPI, - invalidModel, - fmt.Errorf(resourceError, validServiceAPI, invalidModel), - }, - { - validComponentAPI, - invalidModel, - fmt.Errorf(resourceError, validComponentAPI, invalidModel), - }, - // Mixed validity resource APIs and models - { - validServiceAPI, - validComponentModel, - fmt.Errorf(resourceError, validServiceAPI, validComponentModel), - }, - { - validComponentAPI, - validServiceModel, - fmt.Errorf(resourceError, validComponentAPI, validServiceModel), - }, - // Valid resource APIs and models. - { - validServiceAPI, - validServiceModel, - nil, - }, - { - validComponentAPI, - validComponentModel, - nil, - }, - } - - for _, tc := range testCases { - tc := tc - tName := fmt.Sprintf("api: %s, model: %s", tc.api, tc.model) - t.Run(tName, func(t *testing.T) { - err := m.AddModelFromRegistry(ctx, tc.api, tc.model) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - }) - } -} - -func TestModuleFunctions(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - gizmoConf := &v1.ComponentConfig{ - Name: "gizmo1", Api: "acme:component:gizmo", Model: "acme:demo:mygizmo", - } - myBaseAttrs, err := protoutils.StructToStructPb(mybase.Config{ - LeftMotor: "motor1", - RightMotor: "motor2", - }) - test.That(t, err, test.ShouldBeNil) - myBaseConf := &v1.ComponentConfig{ - Name: "mybase1", - Api: "rdk:component:base", - Model: "acme:demo:mybase", - Attributes: myBaseAttrs, - } - // myBaseConf2 is missing required attributes "motorL" and "motorR" and should - // cause Validation error. - badMyBaseConf := &v1.ComponentConfig{ - Name: "mybase2", - Api: "rdk:component:base", - Model: "acme:demo:mybase", - } - - cfg := &config.Config{Components: []resource.Config{ - { - Name: "motor1", - API: resource.NewAPI("rdk", "component", "motor"), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{}, - }, - { - Name: "motor2", - API: resource.NewAPI("rdk", "component", "motor"), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{}, - }, - }} - - myRobot, err := robotimpl.RobotFromConfig(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - parentAddr, err := myRobot.ModuleAddress() - test.That(t, err, test.ShouldBeNil) - - addr := filepath.ToSlash(filepath.Join(filepath.Dir(parentAddr), "mod.sock")) - m, err := module.NewModule(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, m.AddModelFromRegistry(ctx, gizmoapi.API, mygizmo.Model), test.ShouldBeNil) - test.That(t, m.AddModelFromRegistry(ctx, base.API, mybase.Model), test.ShouldBeNil) - - test.That(t, m.Start(ctx), test.ShouldBeNil) - - conn, err := grpc.Dial( - "unix://"+addr, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithStreamInterceptor(grpc_retry.StreamClientInterceptor()), - grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor()), - ) - test.That(t, err, test.ShouldBeNil) - - client := pb.NewModuleServiceClient(rpc.GrpcOverHTTPClientConn{ClientConn: conn}) - - m.SetReady(false) - - resp, err := client.Ready(ctx, &pb.ReadyRequest{ParentAddress: parentAddr}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Ready, test.ShouldBeFalse) - - m.SetReady(true) - - resp, err = client.Ready(ctx, &pb.ReadyRequest{ParentAddress: parentAddr}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Ready, test.ShouldBeTrue) - - t.Run("HandlerMap", func(t *testing.T) { - // test the raw return - handlers := resp.GetHandlermap().GetHandlers() - test.That(t, "acme", test.ShouldBeIn, handlers[0].Subtype.Subtype.Namespace, handlers[1].Subtype.Subtype.Namespace) - test.That(t, "rdk", test.ShouldBeIn, handlers[0].Subtype.Subtype.Namespace, handlers[1].Subtype.Subtype.Namespace) - test.That(t, "component", test.ShouldBeIn, handlers[0].Subtype.Subtype.Type, handlers[1].Subtype.Subtype.Type) - test.That(t, "gizmo", test.ShouldBeIn, handlers[0].Subtype.Subtype.Subtype, handlers[1].Subtype.Subtype.Subtype) - test.That(t, "base", test.ShouldBeIn, handlers[0].Subtype.Subtype.Subtype, handlers[1].Subtype.Subtype.Subtype) - test.That(t, "acme:demo:mygizmo", test.ShouldBeIn, handlers[0].GetModels()[0], handlers[1].GetModels()[0]) - test.That(t, "acme:demo:mybase", test.ShouldBeIn, handlers[0].GetModels()[0], handlers[1].GetModels()[0]) - - // convert from proto - hmap, err := module.NewHandlerMapFromProto(ctx, resp.GetHandlermap(), rpc.GrpcOverHTTPClientConn{ClientConn: conn}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(hmap), test.ShouldEqual, 2) - - for k, v := range hmap { - test.That(t, k.API, test.ShouldBeIn, gizmoapi.API, base.API) - if k.API == gizmoapi.API { - test.That(t, mygizmo.Model, test.ShouldResemble, v[0]) - } else { - test.That(t, mybase.Model, test.ShouldResemble, v[0]) - } - } - - // convert back to proto - handlers2 := hmap.ToProto().GetHandlers() - test.That(t, "acme", test.ShouldBeIn, handlers2[0].Subtype.Subtype.Namespace, handlers2[1].Subtype.Subtype.Namespace) - test.That(t, "rdk", test.ShouldBeIn, handlers2[0].Subtype.Subtype.Namespace, handlers2[1].Subtype.Subtype.Namespace) - test.That(t, "component", test.ShouldBeIn, handlers2[0].Subtype.Subtype.Type, handlers2[1].Subtype.Subtype.Type) - test.That(t, "gizmo", test.ShouldBeIn, handlers2[0].Subtype.Subtype.Subtype, handlers2[1].Subtype.Subtype.Subtype) - test.That(t, "base", test.ShouldBeIn, handlers2[0].Subtype.Subtype.Subtype, handlers2[1].Subtype.Subtype.Subtype) - test.That(t, "acme:demo:mygizmo", test.ShouldBeIn, handlers2[0].GetModels()[0], handlers2[1].GetModels()[0]) - test.That(t, "acme:demo:mybase", test.ShouldBeIn, handlers2[0].GetModels()[0], handlers2[1].GetModels()[0]) - }) - - t.Run("GetParentResource", func(t *testing.T) { - motor1, err := m.GetParentResource(ctx, motor.Named("motor1")) - test.That(t, err, test.ShouldBeNil) - - rMotor, ok := motor1.(motor.Motor) - test.That(t, ok, test.ShouldBeTrue) - - err = rMotor.Stop(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - err = rMotor.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - // Test that GetParentResource will refresh resources on the parent - cfg.Components = append(cfg.Components, resource.Config{ - Name: "motor2", - API: resource.NewAPI("rdk", "component", "motor"), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{}, - }) - myRobot.Reconfigure(ctx, cfg) - _, err = m.GetParentResource(ctx, motor.Named("motor2")) - test.That(t, err, test.ShouldBeNil) - }) - - var gClient gizmoapi.Gizmo - t.Run("AddResource", func(t *testing.T) { - _, err = m.AddResource(ctx, &pb.AddResourceRequest{Config: gizmoConf}) - test.That(t, err, test.ShouldBeNil) - - gClient = gizmoapi.NewClientFromConn(rpc.GrpcOverHTTPClientConn{ClientConn: conn}, "", gizmoapi.Named("gizmo1"), logger) - - ret, err := gClient.DoOne(ctx, "test") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret, test.ShouldBeFalse) - - ret, err = gClient.DoOne(ctx, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret, test.ShouldBeTrue) - - // Test generic echo - testCmd := map[string]interface{}{"foo": "bar"} - retCmd, err := gClient.DoCommand(context.Background(), testCmd) - test.That(t, err, test.ShouldBeNil) - test.That(t, retCmd, test.ShouldResemble, testCmd) - }) - - t.Run("ReconfigureResource", func(t *testing.T) { - attrs, err := structpb.NewStruct(rutils.AttributeMap{"arg1": "test"}) - test.That(t, err, test.ShouldBeNil) - gizmoConf.Attributes = attrs - - _, err = m.ReconfigureResource(ctx, &pb.ReconfigureResourceRequest{Config: gizmoConf}) - test.That(t, err, test.ShouldBeNil) - - ret, err := gClient.DoOne(ctx, "test") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret, test.ShouldBeTrue) - - ret, err = gClient.DoOne(ctx, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, ret, test.ShouldBeFalse) - - // Test generic echo - testCmd := map[string]interface{}{"foo": "bar"} - retCmd, err := gClient.DoCommand(context.Background(), testCmd) - test.That(t, err, test.ShouldBeNil) - test.That(t, retCmd, test.ShouldResemble, testCmd) - }) - - t.Run("RemoveResource", func(t *testing.T) { - _, err = m.RemoveResource(ctx, &pb.RemoveResourceRequest{Name: gizmoConf.Api + "/" + gizmoConf.Name}) - test.That(t, err, test.ShouldBeNil) - - _, err := gClient.DoOne(ctx, "test") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - // Test generic echo - testCmd := map[string]interface{}{"foo": "bar"} - retCmd, err := gClient.DoCommand(context.Background(), testCmd) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - test.That(t, retCmd, test.ShouldBeNil) - }) - - t.Run("Validate", func(t *testing.T) { - resp, err := m.ValidateConfig(ctx, &pb.ValidateConfigRequest{Config: myBaseConf}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Dependencies, test.ShouldNotBeNil) - test.That(t, resp.Dependencies[0], test.ShouldResemble, "motor1") - test.That(t, resp.Dependencies[1], test.ShouldResemble, "motor2") - - _, err = m.ValidateConfig(ctx, &pb.ValidateConfigRequest{Config: badMyBaseConf}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldResemble, - `error validating resource: expected "motorL" attribute for mybase "mybase2"`) - }) - - err = gClient.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - err = conn.Close() - test.That(t, err, test.ShouldBeNil) - - m.Close(ctx) - - err = myRobot.Close(ctx) - test.That(t, err, test.ShouldBeNil) -} - -type MockConfig struct { - Motors []string `json:"motors"` -} - -func (c *MockConfig) Validate(path string) ([]string, error) { - if len(c.Motors) < 1 { - return nil, errors.New("required attributes 'motors' not specified or empty") - } - return c.Motors, nil -} - -// TestAttributeConversion tests that modular resource configs have attributes converted with a registered converter, -// and that validation then works on those converted attributes. -func TestAttributeConversion(t *testing.T) { - type testHarness struct { - m *module.Module - mockConf *v1.ComponentConfig - mockReconfigConf *v1.ComponentConfig - createConf1 *resource.Config - reconfigConf1 *resource.Config - reconfigConf2 *resource.Config - createDeps1 *resource.Dependencies - reconfigDeps1 *resource.Dependencies - reconfigDeps2 *resource.Dependencies - modelWithReconfigure resource.Model - } - - setupTest := func(t *testing.T) (*testHarness, func()) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - cfg := &config.Config{Components: []resource.Config{ - { - Name: "motor1", - API: resource.NewAPI("rdk", "component", "motor"), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{}, - }, - { - Name: "motor2", - API: resource.NewAPI("rdk", "component", "motor"), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{}, - }, - }} - - myRobot, err := robotimpl.RobotFromConfig(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - parentAddr, err := myRobot.ModuleAddress() - test.That(t, err, test.ShouldBeNil) - - addr := filepath.ToSlash(filepath.Join(filepath.Dir(parentAddr), "mod.sock")) - m, err := module.NewModule(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - model := resource.NewModel("inject", "demo", "shell") - modelWithReconfigure := resource.NewModel("inject", "demo", "shellWithReconfigure") - - var ( - createConf1, reconfigConf1, reconfigConf2 resource.Config - createDeps1, reconfigDeps1, reconfigDeps2 resource.Dependencies - ) - - // register the non-reconfigurable one - resource.RegisterService(shell.API, model, resource.Registration[shell.Service, *MockConfig]{ - Constructor: func( - ctx context.Context, deps resource.Dependencies, cfg resource.Config, logger logging.Logger, - ) (shell.Service, error) { - createConf1 = cfg - createDeps1 = deps - return &inject.ShellService{}, nil - }, - }) - test.That(t, m.AddModelFromRegistry(ctx, shell.API, model), test.ShouldBeNil) - - // register the reconfigurable version - resource.RegisterService(shell.API, modelWithReconfigure, resource.Registration[shell.Service, *MockConfig]{ - Constructor: func( - ctx context.Context, deps resource.Dependencies, cfg resource.Config, logger logging.Logger, - ) (shell.Service, error) { - injectable := &inject.ShellService{} - injectable.ReconfigureFunc = func(ctx context.Context, deps resource.Dependencies, cfg resource.Config) error { - reconfigConf2 = cfg - reconfigDeps2 = deps - return nil - } - reconfigConf1 = cfg - reconfigDeps1 = deps - return injectable, nil - }, - }) - test.That(t, m.AddModelFromRegistry(ctx, shell.API, modelWithReconfigure), test.ShouldBeNil) - - test.That(t, m.Start(ctx), test.ShouldBeNil) - conn, err := grpc.Dial( - "unix://"+addr, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithStreamInterceptor(grpc_retry.StreamClientInterceptor()), - grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor()), - ) - test.That(t, err, test.ShouldBeNil) - - client := pb.NewModuleServiceClient(conn) - m.SetReady(true) - readyResp, err := client.Ready(ctx, &pb.ReadyRequest{ParentAddress: parentAddr}) - test.That(t, err, test.ShouldBeNil) - test.That(t, readyResp.Ready, test.ShouldBeTrue) - - mockConf := &v1.ComponentConfig{ - Name: "mymock1", - Api: shell.API.String(), - Model: model.String(), - } - mockReconfigConf := &v1.ComponentConfig{ - Name: "mymock2", - Api: shell.API.String(), - Model: modelWithReconfigure.String(), - } - - return &testHarness{ - m: m, - mockConf: mockConf, - mockReconfigConf: mockReconfigConf, - createConf1: &createConf1, - reconfigConf1: &reconfigConf1, - reconfigConf2: &reconfigConf2, - createDeps1: &createDeps1, - reconfigDeps1: &reconfigDeps1, - reconfigDeps2: &reconfigDeps2, - modelWithReconfigure: modelWithReconfigure, - }, func() { - resource.Deregister(shell.API, model) - resource.Deregister(shell.API, modelWithReconfigure) - test.That(t, conn.Close(), test.ShouldBeNil) - m.Close(ctx) - test.That(t, myRobot.Close(ctx), test.ShouldBeNil) - } - } - - t.Run("non-reconfigurable creation", func(t *testing.T) { - ctx := context.Background() - - th, teardown := setupTest(t) - defer teardown() - - mockAttrs, err := protoutils.StructToStructPb(MockConfig{ - Motors: []string{motor.Named("motor1").String()}, - }) - test.That(t, err, test.ShouldBeNil) - - th.mockConf.Attributes = mockAttrs - - validateResp, err := th.m.ValidateConfig(ctx, &pb.ValidateConfigRequest{ - Config: th.mockConf, - }) - test.That(t, err, test.ShouldBeNil) - - deps := validateResp.Dependencies - test.That(t, deps, test.ShouldResemble, []string{"rdk:component:motor/motor1"}) - - _, err = th.m.AddResource(ctx, &pb.AddResourceRequest{ - Config: th.mockConf, - Dependencies: deps, - }) - test.That(t, err, test.ShouldBeNil) - - _, ok := (*th.createDeps1)[motor.Named("motor1")] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, th.createConf1.Attributes.StringSlice("motors"), test.ShouldResemble, []string{motor.Named("motor1").String()}) - - mc, ok := th.createConf1.ConvertedAttributes.(*MockConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, mc.Motors, test.ShouldResemble, []string{motor.Named("motor1").String()}) - }) - - t.Run("non-reconfigurable recreation", func(t *testing.T) { - ctx := context.Background() - - th, teardown := setupTest(t) - defer teardown() - - mockAttrs, err := protoutils.StructToStructPb(MockConfig{ - Motors: []string{motor.Named("motor1").String()}, - }) - test.That(t, err, test.ShouldBeNil) - - th.mockConf.Attributes = mockAttrs - - validateResp, err := th.m.ValidateConfig(ctx, &pb.ValidateConfigRequest{ - Config: th.mockConf, - }) - test.That(t, err, test.ShouldBeNil) - - deps := validateResp.Dependencies - test.That(t, deps, test.ShouldResemble, []string{"rdk:component:motor/motor1"}) - - _, err = th.m.AddResource(ctx, &pb.AddResourceRequest{ - Config: th.mockConf, - Dependencies: deps, - }) - test.That(t, err, test.ShouldBeNil) - - mockAttrs, err = protoutils.StructToStructPb(MockConfig{ - Motors: []string{motor.Named("motor2").String()}, - }) - test.That(t, err, test.ShouldBeNil) - - th.mockConf.Attributes = mockAttrs - - validateResp, err = th.m.ValidateConfig(ctx, &pb.ValidateConfigRequest{ - Config: th.mockConf, - }) - test.That(t, err, test.ShouldBeNil) - - deps = validateResp.Dependencies - test.That(t, deps, test.ShouldResemble, []string{"rdk:component:motor/motor2"}) - - _, err = th.m.ReconfigureResource(ctx, &pb.ReconfigureResourceRequest{ - Config: th.mockConf, - Dependencies: deps, - }) - test.That(t, err, test.ShouldBeNil) - - _, ok := (*th.createDeps1)[motor.Named("motor2")] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, th.createConf1.Attributes.StringSlice("motors"), test.ShouldResemble, []string{motor.Named("motor2").String()}) - - mc, ok := th.createConf1.ConvertedAttributes.(*MockConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, mc.Motors, test.ShouldResemble, []string{motor.Named("motor2").String()}) - }) - - t.Run("reconfigurable creation", func(t *testing.T) { - ctx := context.Background() - - th, teardown := setupTest(t) - defer teardown() - - mockAttrs, err := protoutils.StructToStructPb(MockConfig{ - Motors: []string{motor.Named("motor1").String()}, - }) - test.That(t, err, test.ShouldBeNil) - - th.mockReconfigConf.Attributes = mockAttrs - th.mockReconfigConf.Model = th.modelWithReconfigure.String() - - validateResp, err := th.m.ValidateConfig(ctx, &pb.ValidateConfigRequest{ - Config: th.mockReconfigConf, - }) - test.That(t, err, test.ShouldBeNil) - - deps := validateResp.Dependencies - test.That(t, deps, test.ShouldResemble, []string{"rdk:component:motor/motor1"}) - - _, err = th.m.AddResource(ctx, &pb.AddResourceRequest{ - Config: th.mockReconfigConf, - Dependencies: deps, - }) - test.That(t, err, test.ShouldBeNil) - - _, ok := (*th.reconfigDeps1)[motor.Named("motor1")] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, th.reconfigConf1.Attributes.StringSlice("motors"), test.ShouldResemble, []string{motor.Named("motor1").String()}) - - mc, ok := th.reconfigConf1.ConvertedAttributes.(*MockConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, mc.Motors, test.ShouldResemble, []string{motor.Named("motor1").String()}) - }) - - // also check that associated resource configs are processed correctly - t.Run("reconfigurable reconfiguration", func(t *testing.T) { - ctx := context.Background() - - th, teardown := setupTest(t) - defer teardown() - - mockAttrs, err := protoutils.StructToStructPb(MockConfig{ - Motors: []string{motor.Named("motor1").String()}, - }) - test.That(t, err, test.ShouldBeNil) - - th.mockReconfigConf.Attributes = mockAttrs - - // TODO(RSDK-6022): use datamanager.DataCaptureConfigs once resource.Name can be properly marshalled/unmarshalled - dummySvcCfg := rutils.AttributeMap{ - "capture_methods": []interface{}{map[string]interface{}{"name": "rdk:service:shell/mymock2", "method": "Something"}}, - } - mockServiceCfg, err := protoutils.StructToStructPb(dummySvcCfg) - test.That(t, err, test.ShouldBeNil) - - th.mockReconfigConf.ServiceConfigs = append(th.mockReconfigConf.ServiceConfigs, &v1.ResourceLevelServiceConfig{ - Type: datamanager.API.String(), - Attributes: mockServiceCfg, - }) - - th.mockReconfigConf.Model = th.modelWithReconfigure.String() - - validateResp, err := th.m.ValidateConfig(ctx, &pb.ValidateConfigRequest{ - Config: th.mockReconfigConf, - }) - test.That(t, err, test.ShouldBeNil) - - deps := validateResp.Dependencies - test.That(t, deps, test.ShouldResemble, []string{"rdk:component:motor/motor1"}) - - _, err = th.m.AddResource(ctx, &pb.AddResourceRequest{ - Config: th.mockReconfigConf, - Dependencies: deps, - }) - test.That(t, err, test.ShouldBeNil) - - mockAttrs, err = protoutils.StructToStructPb(MockConfig{ - Motors: []string{motor.Named("motor2").String()}, - }) - test.That(t, err, test.ShouldBeNil) - - th.mockReconfigConf.Attributes = mockAttrs - - dummySvcCfg2 := rutils.AttributeMap{ - "capture_methods": []interface{}{map[string]interface{}{"name": "rdk:service:shell/mymock2", "method": "Something2"}}, - } - mockServiceCfg, err = protoutils.StructToStructPb(dummySvcCfg2) - test.That(t, err, test.ShouldBeNil) - - th.mockReconfigConf.ServiceConfigs = append([]*v1.ResourceLevelServiceConfig{}, &v1.ResourceLevelServiceConfig{ - Type: datamanager.API.String(), - Attributes: mockServiceCfg, - }) - - validateResp, err = th.m.ValidateConfig(ctx, &pb.ValidateConfigRequest{ - Config: th.mockReconfigConf, - }) - test.That(t, err, test.ShouldBeNil) - - deps = validateResp.Dependencies - test.That(t, deps, test.ShouldResemble, []string{"rdk:component:motor/motor2"}) - - _, err = th.m.ReconfigureResource(ctx, &pb.ReconfigureResourceRequest{ - Config: th.mockReconfigConf, - Dependencies: deps, - }) - test.That(t, err, test.ShouldBeNil) - - _, ok := (*th.reconfigDeps2)[motor.Named("motor2")] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, th.reconfigConf2.Attributes.StringSlice("motors"), test.ShouldResemble, []string{motor.Named("motor2").String()}) - - mc, ok := th.reconfigConf2.ConvertedAttributes.(*MockConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, mc.Motors, test.ShouldResemble, []string{motor.Named("motor2").String()}) - - svcCfg := th.reconfigConf2.AssociatedResourceConfigs[0].Attributes - test.That(t, svcCfg, test.ShouldResemble, dummySvcCfg2) - - convSvcCfg, ok := th.reconfigConf2.AssociatedResourceConfigs[0].ConvertedAttributes.(*datamanager.AssociatedConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, convSvcCfg.CaptureMethods[0].Name, test.ShouldResemble, shell.Named("mymock2")) - test.That(t, convSvcCfg.CaptureMethods[0].Method, test.ShouldEqual, "Something2") - - // and as a final confirmation, check that original values weren't modified - _, ok = (*th.reconfigDeps1)[motor.Named("motor1")] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, th.reconfigConf1.Attributes.StringSlice("motors"), test.ShouldResemble, []string{motor.Named("motor1").String()}) - - mc, ok = th.reconfigConf1.ConvertedAttributes.(*MockConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, mc.Motors, test.ShouldResemble, []string{motor.Named("motor1").String()}) - - svcCfg = th.reconfigConf1.AssociatedResourceConfigs[0].Attributes - test.That(t, svcCfg, test.ShouldResemble, dummySvcCfg) - - convSvcCfg, ok = th.reconfigConf1.AssociatedResourceConfigs[0].ConvertedAttributes.(*datamanager.AssociatedConfig) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, convSvcCfg.CaptureMethods[0].Name, test.ShouldResemble, shell.Named("mymock2")) - test.That(t, convSvcCfg.CaptureMethods[0].Method, test.ShouldEqual, "Something") - }) -} - -func TestModuleSocketAddrTruncation(t *testing.T) { - // test with a short base path - path, err := module.CreateSocketAddress("/tmp", "my-cool-module") - test.That(t, err, test.ShouldBeNil) - test.That(t, path, test.ShouldEqual, "/tmp/my-cool-module.sock") - - // test exactly 103 - path, err = module.CreateSocketAddress( - "/tmp", - // 103 - len("/tmp/") - len(".sock") - strings.Repeat("a", 93), - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, path, test.ShouldHaveLength, 103) - test.That(t, path, test.ShouldEqual, - "/tmp/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.sock", - ) - - // test 104 chars - path, err = module.CreateSocketAddress( - "/tmp", - // 103 - len("/tmp/") - len(".sock") + 1 more character to trigger truncation - strings.Repeat("a", 94), - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, path, test.ShouldHaveLength, 103) - // test that creating a new socket address with the same name produces the same truncated address - test.That(t, path, test.ShouldEndWith, "-QUEUU.sock") - - // test with an extra-long base path - _, err = module.CreateSocketAddress( - // 103 - len("/a.sock") + 1 more character to trigger truncation - strings.Repeat("a", 98), - "a", - ) - test.That(t, fmt.Sprint(err), test.ShouldContainSubstring, "module socket base path") -} diff --git a/module/multiversionmodule/.gitignore b/module/multiversionmodule/.gitignore deleted file mode 100644 index 945c5774948..00000000000 --- a/module/multiversionmodule/.gitignore +++ /dev/null @@ -1 +0,0 @@ -version* diff --git a/module/multiversionmodule/module.go b/module/multiversionmodule/module.go deleted file mode 100644 index 8c7b3f584c5..00000000000 --- a/module/multiversionmodule/module.go +++ /dev/null @@ -1,116 +0,0 @@ -// Package main is a module designed to help build tests for reconfiguration logic between module versions -package main - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" - "go.viam.com/rdk/resource" -) - -var myModel = resource.NewModel("acme", "demo", "multiversionmodule") - -// VERSION is set with `-ldflags "-X main.VERSION=..."`. -var VERSION string - -// config is a simple config for a generic component. -type config struct { - // Required to be non-empty for VERSION="v2", not required otherwise - Parameter string `json:"parameter"` - // Required to be non-empty for VERSION="v3", not required otherwise - Motor string `json:"motor"` -} - -type component struct { - resource.Named - resource.TriviallyCloseable - logger logging.Logger - cfg *config -} - -// Validate validates the config depending on VERSION. -func (cfg *config) Validate(_ string) ([]string, error) { - switch VERSION { - case "v2": - if cfg.Parameter == "" { - return nil, errors.New("version 2 requires a parameter") - } - case "v3": - if cfg.Motor == "" { - return nil, errors.New("version 3 requires a motor") - } - return []string{cfg.Motor}, nil - default: - } - return make([]string, 0), nil -} - -func main() { - utils.ContextualMain(mainWithArgs, logging.NewLogger(fmt.Sprintf("MultiVersionModule-%s", VERSION))) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error { - myMod, err := module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - resource.RegisterComponent(generic.API, myModel, resource.Registration[resource.Resource, *config]{ - Constructor: newComponent, - }) - err = myMod.AddModelFromRegistry(ctx, generic.API, myModel) - if err != nil { - return err - } - - err = myMod.Start(ctx) - defer myMod.Close(ctx) - if err != nil { - return err - } - <-ctx.Done() - return nil -} - -func newComponent(_ context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (resource.Resource, error) { - newConf, err := resource.NativeConfig[*config](conf) - if err != nil { - return nil, errors.Wrap(err, "create component failed due to config parsing") - } - return &component{ - Named: conf.ResourceName().AsNamed(), - cfg: newConf, - logger: logger, - }, nil -} - -// Reconfigure swaps the config to the new conf. -func (c *component) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - newConf, err := resource.NativeConfig[*config](conf) - if err != nil { - return err - } - if VERSION == "v3" { - // Version 3 should have a motor in the deps - if _, err := motor.FromDependencies(deps, "motor1"); err != nil { - return errors.Wrapf(err, "failed to resolve motor %q for version 3", "motor1") - } - } - c.cfg = newConf - return nil -} - -// DoCommand does nothing for now. -func (c *component) DoCommand(ctx context.Context, req map[string]interface{}) (map[string]interface{}, error) { - return map[string]interface{}{}, nil -} diff --git a/module/multiversionmodule/module.json b/module/multiversionmodule/module.json deleted file mode 100644 index 0d0db6bf1c9..00000000000 --- a/module/multiversionmodule/module.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "modules": [ - { - "name": "AcmeModule", - "executable_path": "module/multiversionmodule/run_version1.sh", - "log_level": "debug" - } - ], - "components": [ - { - "name": "generic1", - "api": "rdk:component:generic", - "model": "acme:demo:multiversionmodule", - "attributes": {} - }, - { - "namespace": "rdk", - "type": "motor", - "name": "motor1", - "model": "rdk:builtin:fake", - "attributes": { - "max_rpm": 500, - "encoder": "encoder1", - "max_power_pct": 0.5, - "ticks_per_rotation": 10000 - }, - "depends_on": ["encoder1"] - }, - { - "name": "encoder1", - "type": "encoder", - "model": "fake" - } - ], - "network": { - "bind_address": ":8080" - } -} diff --git a/module/multiversionmodule/run_version1.sh b/module/multiversionmodule/run_version1.sh deleted file mode 100755 index a57db33a46c..00000000000 --- a/module/multiversionmodule/run_version1.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build -o ./version1 -ldflags "-X main.VERSION=v1" ./ -exec ./version1 $@ diff --git a/module/multiversionmodule/run_version2.sh b/module/multiversionmodule/run_version2.sh deleted file mode 100755 index 48b25936dae..00000000000 --- a/module/multiversionmodule/run_version2.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build -o ./version2 -ldflags "-X main.VERSION=v2" ./ -exec ./version2 $@ diff --git a/module/multiversionmodule/run_version3.sh b/module/multiversionmodule/run_version3.sh deleted file mode 100755 index 7e216b794d0..00000000000 --- a/module/multiversionmodule/run_version3.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build -o ./version3 -ldflags "-X main.VERSION=v3" ./ -exec ./version3 $@ diff --git a/module/testmodule/.gitignore b/module/testmodule/.gitignore deleted file mode 100644 index 6b4ec3d1b9c..00000000000 --- a/module/testmodule/.gitignore +++ /dev/null @@ -1 +0,0 @@ -testmodule diff --git a/module/testmodule/fakemodule.sh b/module/testmodule/fakemodule.sh deleted file mode 100755 index f89a7e4e668..00000000000 --- a/module/testmodule/fakemodule.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# fakemodule is a completely fake module that repeatedly echos a message. Used -# to test that modules that never start listening are stopped. - -while : -do - echo "fakemodule is running" - sleep 0.01 -done diff --git a/module/testmodule/main.go b/module/testmodule/main.go deleted file mode 100644 index 41613c83188..00000000000 --- a/module/testmodule/main.go +++ /dev/null @@ -1,277 +0,0 @@ -// Package main is a module for testing, with an inline generic component to return internal data and perform other test functions. -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" - "go.viam.com/rdk/resource" - genericservice "go.viam.com/rdk/services/generic" -) - -var ( - helperModel = resource.NewModel("rdk", "test", "helper") - otherModel = resource.NewModel("rdk", "test", "other") - testMotorModel = resource.NewModel("rdk", "test", "motor") - myMod *module.Module -) - -func main() { - utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("TestModule")) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error { - logger.Debug("debug mode enabled") - - var err error - myMod, err = module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - - resource.RegisterComponent( - generic.API, - helperModel, - resource.Registration[resource.Resource, resource.NoNativeConfig]{Constructor: newHelper}) - err = myMod.AddModelFromRegistry(ctx, generic.API, helperModel) - if err != nil { - return err - } - - resource.RegisterService( - genericservice.API, - otherModel, - resource.Registration[resource.Resource, resource.NoNativeConfig]{Constructor: newOther}) - err = myMod.AddModelFromRegistry(ctx, genericservice.API, otherModel) - if err != nil { - return err - } - - resource.RegisterComponent( - motor.API, - testMotorModel, - resource.Registration[resource.Resource, resource.NoNativeConfig]{Constructor: newTestMotor}) - err = myMod.AddModelFromRegistry(ctx, motor.API, testMotorModel) - if err != nil { - return err - } - - err = myMod.Start(ctx) - defer myMod.Close(ctx) - if err != nil { - return err - } - <-ctx.Done() - return nil -} - -func newHelper( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (resource.Resource, error) { - return &helper{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - }, nil -} - -type helper struct { - resource.Named - resource.TriviallyCloseable - logger logging.Logger - numReconfigurations int -} - -// DoCommand looks up the "real" command from the map it's passed. -func (h *helper) DoCommand(ctx context.Context, req map[string]interface{}) (map[string]interface{}, error) { - cmd, ok := req["command"] - if !ok { - return nil, errors.New("missing 'command' string") - } - - switch req["command"] { - case "sleep": - time.Sleep(time.Second * 1) - //nolint:nilnil - return nil, nil - case "get_ops": - // For testing the module's operation manager - ops := myMod.OperationManager().All() - var opsOut []string - for _, op := range ops { - opsOut = append(opsOut, op.ID.String()) - } - return map[string]interface{}{"ops": opsOut}, nil - case "echo": - // For testing module liveliness - return req, nil - case "kill_module": - // For testing module reloading & unexpected exists - os.Exit(1) - // unreachable return statement needed for compilation - return nil, errors.New("unreachable error") - case "write_data_file": - // For testing that the module's data directory has been created and that the VIAM_MODULE_DATA env var exists - filename, ok := req["filename"].(string) - if !ok { - return nil, errors.New("missing 'filename' string") - } - contents, ok := req["contents"].(string) - if !ok { - return nil, errors.New("missing 'contents' string") - } - dataFilePath := filepath.Join(os.Getenv("VIAM_MODULE_DATA"), filename) - err := os.WriteFile(dataFilePath, []byte(contents), 0o600) - if err != nil { - return map[string]interface{}{}, err - } - return map[string]interface{}{"fullpath": dataFilePath}, nil - case "get_working_directory": - // For testing that modules are started with the correct working directory - workingDir, err := os.Getwd() - if err != nil { - return map[string]interface{}{}, err - } - return map[string]interface{}{"path": workingDir}, nil - case "log": - level, err := logging.LevelFromString(req["level"].(string)) - if err != nil { - return nil, err - } - - msg := req["msg"].(string) - switch level { - case logging.DEBUG: - h.logger.CDebugw(ctx, msg, "foo", "bar") - case logging.INFO: - h.logger.CInfow(ctx, msg, "foo", "bar") - case logging.WARN: - h.logger.CWarnw(ctx, msg, "foo", "bar") - case logging.ERROR: - h.logger.CErrorw(ctx, msg, "foo", "bar") - } - - return map[string]any{}, nil - case "get_num_reconfigurations": - return map[string]any{"num_reconfigurations": h.numReconfigurations}, nil - default: - return nil, fmt.Errorf("unknown command string %s", cmd) - } -} - -// Reconfigure increments numReconfigurations. -func (h *helper) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - h.numReconfigurations++ - return nil -} - -func newOther( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (resource.Resource, error) { - return &other{ - Named: conf.ResourceName().AsNamed(), - }, nil -} - -type other struct { - resource.Named - resource.TriviallyCloseable - numReconfigurations int -} - -// DoCommand looks up the "real" command from the map it's passed. -func (o *other) DoCommand(ctx context.Context, req map[string]interface{}) (map[string]interface{}, error) { - cmd, ok := req["command"] - if !ok { - return nil, errors.New("missing 'command' string") - } - - switch req["command"] { - case "get_num_reconfigurations": - return map[string]any{"num_reconfigurations": o.numReconfigurations}, nil - default: - return nil, fmt.Errorf("unknown command string %s", cmd) - } -} - -// Reconfigure increments numReconfigurations. -func (o *other) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - o.numReconfigurations++ - return nil -} - -func newTestMotor( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (resource.Resource, error) { - return &testMotor{ - Named: conf.ResourceName().AsNamed(), - }, nil -} - -type testMotor struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable -} - -var _ motor.Motor = &testMotor{} - -// SetPower trivially implements motor.Motor. -func (tm *testMotor) SetPower(_ context.Context, _ float64, _ map[string]interface{}) error { - return nil -} - -// GoFor trivially implements motor.Motor. -func (tm *testMotor) GoFor(_ context.Context, _, _ float64, _ map[string]interface{}) error { - return nil -} - -// GoTo trivially implements motor.Motor. -func (tm *testMotor) GoTo(_ context.Context, _, _ float64, _ map[string]interface{}) error { - return nil -} - -// ResetZeroPosition trivially implements motor.Motor. -func (tm *testMotor) ResetZeroPosition(_ context.Context, _ float64, _ map[string]interface{}) error { - return nil -} - -// Position trivially implements motor.Motor. -func (tm *testMotor) Position(_ context.Context, _ map[string]interface{}) (float64, error) { - return 0.0, nil -} - -// Properties trivially implements motor.Motor. -func (tm *testMotor) Properties(_ context.Context, _ map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, nil -} - -// Stop trivially implements motor.Motor. -func (tm *testMotor) Stop(_ context.Context, _ map[string]interface{}) error { - return nil -} - -// IsPowered trivally implements motor.Motor. -func (tm *testMotor) IsPowered(_ context.Context, _ map[string]interface{}) (bool, float64, error) { - return false, 0.0, nil -} - -// DoCommand trivially implements motor.Motor. -func (tm *testMotor) DoCommand(_ context.Context, _ map[string]interface{}) (map[string]interface{}, error) { - //nolint:nilnil - return nil, nil -} - -// IsMoving trivially implements motor.Motor. -func (tm *testMotor) IsMoving(context.Context) (bool, error) { - return false, nil -} diff --git a/module/testmodule/module.json b/module/testmodule/module.json deleted file mode 100644 index 955376c5e58..00000000000 --- a/module/testmodule/module.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "modules": [ - { - "name": "TestModule", - "executable_path": "module/testmodule/run.sh" - } - ], - "components": [ - { - "namespace": "rdk", - "type": "generic", - "name": "helper1", - "model": "rdk:test:helper" - } - ], - "network": { - "bind_address": ":8080" - } -} diff --git a/module/testmodule/run.sh b/module/testmodule/run.sh deleted file mode 100755 index 481b13928fd..00000000000 --- a/module/testmodule/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build ./ -exec ./testmodule $@ diff --git a/module/testmodule2/.gitignore b/module/testmodule2/.gitignore deleted file mode 100644 index 6b4ec3d1b9c..00000000000 --- a/module/testmodule2/.gitignore +++ /dev/null @@ -1 +0,0 @@ -testmodule diff --git a/module/testmodule2/main.go b/module/testmodule2/main.go deleted file mode 100644 index bd612840338..00000000000 --- a/module/testmodule2/main.go +++ /dev/null @@ -1,228 +0,0 @@ -// Package main is a module for testing, with an inline generic component to return internal data and perform other test functions. -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/pkg/errors" - "go.viam.com/utils" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module" - "go.viam.com/rdk/resource" -) - -// TODO(RSDK-7044): this entire module is a copy of `module/testmodule` but with -// resources in a different namespace. It exists to support tests that require more than -// one module. It would be better to have a facility to create test modules on demand. -var ( - helperModel = resource.NewModel("rdk", "test", "helper2") - testMotorModel = resource.NewModel("rdk", "test", "motor2") - myMod *module.Module -) - -func main() { - utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("TestModule2")) -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error { - logger.Debug("debug mode enabled") - - var err error - myMod, err = module.NewModuleFromArgs(ctx, logger) - if err != nil { - return err - } - - resource.RegisterComponent( - generic.API, - helperModel, - resource.Registration[resource.Resource, resource.NoNativeConfig]{Constructor: newHelper}) - err = myMod.AddModelFromRegistry(ctx, generic.API, helperModel) - if err != nil { - return err - } - - resource.RegisterComponent( - motor.API, - testMotorModel, - resource.Registration[resource.Resource, resource.NoNativeConfig]{Constructor: newTestMotor}) - err = myMod.AddModelFromRegistry(ctx, motor.API, testMotorModel) - if err != nil { - return err - } - - err = myMod.Start(ctx) - defer myMod.Close(ctx) - if err != nil { - return err - } - <-ctx.Done() - return nil -} - -func newHelper( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (resource.Resource, error) { - return &helper{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - }, nil -} - -type helper struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - logger logging.Logger -} - -// DoCommand is the only method of this component. It looks up the "real" command from the map it's passed. -// - -func (h *helper) DoCommand(ctx context.Context, req map[string]interface{}) (map[string]interface{}, error) { - cmd, ok := req["command"] - if !ok { - return nil, errors.New("missing 'command' string") - } - - switch req["command"] { - case "sleep": - time.Sleep(time.Second * 1) - //nolint:nilnil - return nil, nil - case "get_ops": - // For testing the module's operation manager - ops := myMod.OperationManager().All() - var opsOut []string - for _, op := range ops { - opsOut = append(opsOut, op.ID.String()) - } - return map[string]interface{}{"ops": opsOut}, nil - case "echo": - // For testing module liveliness - return req, nil - case "kill_module": - // For testing module reloading & unexpected exists - os.Exit(1) - // unreachable return statement needed for compilation - return nil, errors.New("unreachable error") - case "write_data_file": - // For testing that the module's data directory has been created and that the VIAM_MODULE_DATA env var exists - filename, ok := req["filename"].(string) - if !ok { - return nil, errors.New("missing 'filename' string") - } - contents, ok := req["contents"].(string) - if !ok { - return nil, errors.New("missing 'contents' string") - } - dataFilePath := filepath.Join(os.Getenv("VIAM_MODULE_DATA"), filename) - err := os.WriteFile(dataFilePath, []byte(contents), 0o600) - if err != nil { - return map[string]interface{}{}, err - } - return map[string]interface{}{"fullpath": dataFilePath}, nil - case "get_working_directory": - // For testing that modules are started with the correct working directory - workingDir, err := os.Getwd() - if err != nil { - return map[string]interface{}{}, err - } - return map[string]interface{}{"path": workingDir}, nil - case "log": - level, err := logging.LevelFromString(req["level"].(string)) - if err != nil { - return nil, err - } - - msg := req["msg"].(string) - switch level { - case logging.DEBUG: - h.logger.CDebugw(ctx, msg, "foo", "bar") - case logging.INFO: - h.logger.CInfow(ctx, msg, "foo", "bar") - case logging.WARN: - h.logger.CWarnw(ctx, msg, "foo", "bar") - case logging.ERROR: - h.logger.CErrorw(ctx, msg, "foo", "bar") - } - - return map[string]any{}, nil - default: - return nil, fmt.Errorf("unknown command string %s", cmd) - } -} - -func newTestMotor( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (resource.Resource, error) { - return &testMotor{ - Named: conf.ResourceName().AsNamed(), - }, nil -} - -type testMotor struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable -} - -var _ motor.Motor = &testMotor{} - -// SetPower trivially implements motor.Motor. -func (tm *testMotor) SetPower(_ context.Context, _ float64, _ map[string]interface{}) error { - return nil -} - -// GoFor trivially implements motor.Motor. -func (tm *testMotor) GoFor(_ context.Context, _, _ float64, _ map[string]interface{}) error { - return nil -} - -// GoTo trivially implements motor.Motor. -func (tm *testMotor) GoTo(_ context.Context, _, _ float64, _ map[string]interface{}) error { - return nil -} - -// ResetZeroPosition trivially implements motor.Motor. -func (tm *testMotor) ResetZeroPosition(_ context.Context, _ float64, _ map[string]interface{}) error { - return nil -} - -// Position trivially implements motor.Motor. -func (tm *testMotor) Position(_ context.Context, _ map[string]interface{}) (float64, error) { - return 0.0, nil -} - -// Properties trivially implements motor.Motor. -func (tm *testMotor) Properties(_ context.Context, _ map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, nil -} - -// Stop trivially implements motor.Motor. -func (tm *testMotor) Stop(_ context.Context, _ map[string]interface{}) error { - return nil -} - -// IsPowered trivally implements motor.Motor. -func (tm *testMotor) IsPowered(_ context.Context, _ map[string]interface{}) (bool, float64, error) { - return false, 0.0, nil -} - -// DoCommand trivially implements motor.Motor. -func (tm *testMotor) DoCommand(_ context.Context, _ map[string]interface{}) (map[string]interface{}, error) { - //nolint:nilnil - return nil, nil -} - -// IsMoving trivially implements motor.Motor. -func (tm *testMotor) IsMoving(context.Context) (bool, error) { - return false, nil -} diff --git a/module/testmodule2/run.sh b/module/testmodule2/run.sh deleted file mode 100755 index 481b13928fd..00000000000 --- a/module/testmodule2/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cd `dirname $0` - -go build ./ -exec ./testmodule $@ diff --git a/module/version_reconfigure_test.go b/module/version_reconfigure_test.go deleted file mode 100644 index 5873f2df1ae..00000000000 --- a/module/version_reconfigure_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package module_test - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/encoder" - fakeencoder "go.viam.com/rdk/components/encoder/fake" - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/motor" - fakemotor "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/utils" -) - -func TestValidationFailureDuringReconfiguration(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "AcmeModule", - ExePath: "multiversionmodule/run_version1.sh", - LogLevel: "debug", - }, - }, - Components: []resource.Config{ - { - Name: "generic1", - Model: resource.NewModel("acme", "demo", "multiversionmodule"), - API: generic.API, - Attributes: utils.AttributeMap{}, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "motor1", - Model: resource.DefaultModelFamily.WithModel("fake"), - API: motor.API, - Attributes: utils.AttributeMap{}, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "encoder1", - Model: resource.DefaultModelFamily.WithModel("fake"), - API: encoder.API, - Attributes: utils.AttributeMap{}, - ConvertedAttributes: &fakeencoder.Config{}, - }, - }, - } - - robot, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - defer robot.Close(ctx) - - // Assert that generic1 was added. - _, err = robot.ResourceByName(generic.Named("generic1")) - test.That(t, err, test.ShouldBeNil) - - // Assert that there were no validation or component building errors - test.That(t, logs.FilterMessageSnippet( - "modular resource config validation error").Len(), test.ShouldEqual, 0) - test.That(t, logs.FilterMessageSnippet("error building component").Len(), test.ShouldEqual, 0) - - // Read the config, swap to `run_version2.sh`, and overwrite the config, triggering a - // reconfigure where `generic1` will fail validation. - cfg.Modules[0].ExePath = utils.ResolveFile("module/multiversionmodule/run_version2.sh") - robot.Reconfigure(ctx, cfg) - - // Check that generic1 now has a config validation error. - _, err = robot.ResourceByName(generic.Named("generic1")) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, - `rdk:component:generic/generic1`) - test.That(t, err.Error(), test.ShouldContainSubstring, - `version 2 requires a parameter`) - - // Assert that Validation failure message is present - // - // Race condition safety: Resource removal should occur after modular resource validation (during completeConfig), so if - // ResourceByName is failing, these errors should already be present - test.That(t, logs.FilterMessageSnippet( - "modular resource config validation error").Len(), test.ShouldEqual, 1) - test.That(t, logs.FilterMessageSnippet("error building component").Len(), test.ShouldEqual, 0) -} - -func TestVersionBumpWithNewImplicitDeps(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "AcmeModule", - ExePath: "multiversionmodule/run_version1.sh", - LogLevel: "debug", - }, - }, - Components: []resource.Config{ - { - Name: "generic1", - Model: resource.NewModel("acme", "demo", "multiversionmodule"), - API: generic.API, - Attributes: utils.AttributeMap{}, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "motor1", - Model: resource.DefaultModelFamily.WithModel("fake"), - API: motor.API, - Attributes: utils.AttributeMap{}, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "encoder1", - Model: resource.DefaultModelFamily.WithModel("fake"), - API: encoder.API, - Attributes: utils.AttributeMap{}, - ConvertedAttributes: &fakeencoder.Config{}, - }, - }, - } - - robot, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - defer robot.Close(ctx) - - // Assert that generic1 was added. - _, err = robot.ResourceByName(generic.Named("generic1")) - test.That(t, err, test.ShouldBeNil) - - // Assert that there were no validation or component building errors - test.That(t, logs.FilterMessageSnippet( - "modular resource config validation error").Len(), test.ShouldEqual, 0) - test.That(t, logs.FilterMessageSnippet("error building component").Len(), test.ShouldEqual, 0) - - // Swap in `run_version3.sh`. Version 3 requires `generic1` to have a `motor` in its - // attributes. This config change should result in `generic1` becoming unavailable. - cfg.Modules[0].ExePath = utils.ResolveFile("module/multiversionmodule/run_version3.sh") - robot.Reconfigure(ctx, cfg) - - _, err = robot.ResourceByName(generic.Named("generic1")) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `resource "rdk:component:generic/generic1" not available`) - test.That(t, err.Error(), test.ShouldContainSubstring, `version 3 requires a motor`) - - // Assert that Validation failure message is present - // - // Race condition safety: Resource removal should occur after modular resource validation (during completeConfig), so if - // ResourceByName is failing, these errors should already be present - test.That(t, logs.FilterMessageSnippet( - "modular resource config validation error").Len(), test.ShouldEqual, 1) - test.That(t, logs.FilterMessageSnippet("error building component").Len(), test.ShouldEqual, 0) - - // Update the generic1 configuration to have a `motor` attribute. The following reconfiguration - // round should make the `generic1` component available again. - for i, c := range cfg.Components { - if c.Name == "generic1" { - cfg.Components[i].Attributes = utils.AttributeMap{"motor": "motor1"} - } - } - robot.Reconfigure(ctx, cfg) - _, err = robot.ResourceByName(generic.Named("generic1")) - test.That(t, err, test.ShouldBeNil) -} diff --git a/motionplan/cBiRRT_test.go b/motionplan/cBiRRT_test.go deleted file mode 100644 index 5378471cf47..00000000000 --- a/motionplan/cBiRRT_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package motionplan - -import ( - "context" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan/ik" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" - rutils "go.viam.com/rdk/utils" -) - -var interp = referenceframe.FloatsToInputs([]float64{ - 0.22034293025523666, - 0.023301860367034785, - 0.0035938741832804775, - 0.03706780636626979, - -0.006010542176591475, - 0.013764993693680328, - 0.22994099248696265, -}) - -// This should test a simple linear motion. -// This test will step through the different stages of cbirrt and test each one in turn. -func TestSimpleLinearMotion(t *testing.T) { - nSolutions := 5 - inputSteps := []node{} - ctx := context.Background() - logger := logging.NewTestLogger(t) - m, err := referenceframe.ParseModelJSONFile(rutils.ResolveFile("components/arm/xarm/xarm7_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - - goalPos := spatialmath.NewPose(r3.Vector{X: 206, Y: 100, Z: 120.5}, &spatialmath.OrientationVectorDegrees{OY: -1}) - - opt := newBasicPlannerOptions(m) - opt.SetGoal(goalPos) - mp, err := newCBiRRTMotionPlanner(m, rand.New(rand.NewSource(42)), logger, opt) - test.That(t, err, test.ShouldBeNil) - cbirrt, _ := mp.(*cBiRRTMotionPlanner) - - solutions, err := mp.getSolutions(ctx, home7) - test.That(t, err, test.ShouldBeNil) - - near1 := &basicNode{q: home7} - seedMap := make(map[node]node) - seedMap[near1] = nil - target := interp - - goalMap := make(map[node]node) - - if len(solutions) < nSolutions { - nSolutions = len(solutions) - } - - for _, solution := range solutions[:nSolutions] { - goalMap[solution] = nil - } - nn := &neighborManager{nCPU: nCPU} - - _, err = newCbirrtOptions(opt) - test.That(t, err, test.ShouldBeNil) - - m1chan := make(chan node, 1) - defer close(m1chan) - - // Extend tree seedMap as far towards target as it can get. It may or may not reach it. - utils.PanicCapturingGo(func() { - cbirrt.constrainedExtend(ctx, cbirrt.randseed, seedMap, near1, &basicNode{q: target}, m1chan) - }) - seedReached := <-m1chan - // Find the nearest point in goalMap to the furthest point reached in seedMap - near2 := nn.nearestNeighbor(ctx, opt, seedReached, goalMap) - // extend goalMap towards the point in seedMap - utils.PanicCapturingGo(func() { - cbirrt.constrainedExtend(ctx, cbirrt.randseed, goalMap, near2, seedReached, m1chan) - }) - goalReached := <-m1chan - dist := opt.DistanceFunc(&ik.Segment{StartConfiguration: seedReached.Q(), EndConfiguration: goalReached.Q()}) - test.That(t, dist < cbirrt.planOpts.JointSolveDist, test.ShouldBeTrue) - - seedReached.SetCorner(true) - goalReached.SetCorner(true) - - // extract the path to the seed - for seedReached != nil { - inputSteps = append(inputSteps, seedReached) - seedReached = seedMap[seedReached] - } - // reverse the slice - for i, j := 0, len(inputSteps)-1; i < j; i, j = i+1, j-1 { - inputSteps[i], inputSteps[j] = inputSteps[j], inputSteps[i] - } - // extract the path to the goal - for goalReached != nil { - inputSteps = append(inputSteps, goalReached) - goalReached = goalMap[goalReached] - } - - // Test that smoothing succeeds and does not lengthen the path (it may be the same length) - unsmoothLen := len(inputSteps) - finalSteps := cbirrt.smoothPath(ctx, inputSteps) - test.That(t, len(finalSteps), test.ShouldBeLessThanOrEqualTo, unsmoothLen) - // Test that path has changed after smoothing was applied - test.That(t, finalSteps, test.ShouldNotResemble, inputSteps) -} diff --git a/motionplan/collision_test.go b/motionplan/collision_test.go deleted file mode 100644 index b32fbffbfb4..00000000000 --- a/motionplan/collision_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package motionplan - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - frame "go.viam.com/rdk/referenceframe" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func TestCollisionsEqual(t *testing.T) { - expected := Collision{name1: "a", name2: "b", penetrationDepth: 1} - cases := []struct { - collision Collision - success bool - }{ - {Collision{name1: "a", name2: "b", penetrationDepth: 1}, true}, - {Collision{name1: "b", name2: "a", penetrationDepth: 1}, true}, - {Collision{name1: "a", name2: "c", penetrationDepth: 1}, false}, - {Collision{name1: "b", name2: "a", penetrationDepth: 2}, false}, - } - for _, c := range cases { - test.That(t, c.success == collisionsAlmostEqual(expected, c.collision), test.ShouldBeTrue) - } -} - -func TestCollisionListsEqual(t *testing.T) { - c1a := Collision{name1: "a", name2: "b", penetrationDepth: 1} - c1b := Collision{name1: "a", name2: "b", penetrationDepth: 1} - c2a := Collision{name1: "c", name2: "d", penetrationDepth: 2} - c2b := Collision{name1: "d", name2: "c", penetrationDepth: 2} - c3a := Collision{name1: "e", name2: "f", penetrationDepth: 2} - c3b := Collision{name1: "f", name2: "e", penetrationDepth: 2} - list1 := []Collision{c1a, c2b, c3a} - list2 := []Collision{c3b, c1a, c2a} - list3 := []Collision{c3b, c1a, c1b} - test.That(t, collisionListsAlmostEqual(list1, list2), test.ShouldBeTrue) - test.That(t, collisionListsAlmostEqual(list1, list3), test.ShouldBeFalse) - test.That(t, collisionListsAlmostEqual(list1, []Collision{}), test.ShouldBeFalse) -} - -func TestCheckCollisions(t *testing.T) { - // case 1: small collection of custom geometries, expecting: - // - collisions reported between robot and obstacles - // - no collision between two obstacle geometries or robot geometries - bc1, err := spatial.NewBox(spatial.NewZeroPose(), r3.Vector{2, 2, 2}, "") - test.That(t, err, test.ShouldBeNil) - robot := []spatial.Geometry{} - robot = append(robot, bc1.Transform(spatial.NewZeroPose())) - robot[0].SetLabel("robotCube000") - robot = append(robot, bc1.Transform(spatial.NewPoseFromPoint(r3.Vector{3, 3, 3}))) - robot[1].SetLabel("robotCube333") - robot = append(robot, bc1.Transform(spatial.NewPoseFromPoint(r3.Vector{9, 9, 9}))) - robot[2].SetLabel("robotCube999") - - obstacles := []spatial.Geometry{} - obstacles = append(obstacles, bc1.Transform(spatial.NewZeroPose())) - obstacles[0].SetLabel("obstacleCube000") - obstacles = append(obstacles, bc1.Transform(spatial.NewPoseFromPoint(r3.Vector{4, 4, 4}))) - obstacles[1].SetLabel("obstacleCube444") - obstacles = append(obstacles, bc1.Transform(spatial.NewPoseFromPoint(r3.Vector{6, 6, 6}))) - obstacles[2].SetLabel("obstacleCube666") - cg, err := newCollisionGraph(robot, obstacles, nil, true, defaultCollisionBufferMM) - test.That(t, err, test.ShouldBeNil) - expectedCollisions := []Collision{ - {"robotCube333", "obstacleCube444", -1}, - {"robotCube000", "obstacleCube000", -2}, - } - test.That(t, collisionListsAlmostEqual(cg.collisions(defaultCollisionBufferMM), expectedCollisions), test.ShouldBeTrue) - - // case 2: zero position of xArm6 arm - should have number of collisions = to number of geometries - 1 - // no external geometries considered, self collision only - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - gf, _ := m.Geometries(make([]frame.Input, len(m.DoF()))) - test.That(t, gf, test.ShouldNotBeNil) - cg, err = newCollisionGraph(gf.Geometries(), gf.Geometries(), nil, true, defaultCollisionBufferMM) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(cg.collisions(defaultCollisionBufferMM)), test.ShouldEqual, 4) -} - -func TestUniqueCollisions(t *testing.T) { - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - - // zero position of xarm6 arm - input := make([]frame.Input, len(m.DoF())) - internalGeometries, _ := m.Geometries(input) - test.That(t, internalGeometries, test.ShouldNotBeNil) - zeroPositionCG, err := newCollisionGraph( - internalGeometries.Geometries(), - internalGeometries.Geometries(), - nil, - true, - defaultCollisionBufferMM, - ) - test.That(t, err, test.ShouldBeNil) - - // case 1: no self collision - check no new collisions are returned - input[0] = frame.Input{Value: 1} - internalGeometries, _ = m.Geometries(input) - test.That(t, internalGeometries, test.ShouldNotBeNil) - cg, err := newCollisionGraph( - internalGeometries.Geometries(), - internalGeometries.Geometries(), - zeroPositionCG, - true, - defaultCollisionBufferMM, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(cg.collisions(defaultCollisionBufferMM)), test.ShouldEqual, 0) - - // case 2: self collision - check only new collisions are returned - input[4] = frame.Input{Value: 2} - internalGeometries, _ = m.Geometries(input) - test.That(t, internalGeometries, test.ShouldNotBeNil) - cg, err = newCollisionGraph( - internalGeometries.Geometries(), - internalGeometries.Geometries(), - zeroPositionCG, - true, - defaultCollisionBufferMM, - ) - test.That(t, err, test.ShouldBeNil) - expectedCollisions := []Collision{{"xArm6:base_top", "xArm6:wrist_link", -66.6}, {"xArm6:wrist_link", "xArm6:upper_arm", -48.1}} - test.That(t, collisionListsAlmostEqual(cg.collisions(defaultCollisionBufferMM), expectedCollisions), test.ShouldBeTrue) - - // case 3: add a collision specification that the last element of expectedCollisions should be ignored - zeroPositionCG.addCollisionSpecification(&expectedCollisions[1]) - - cg, err = newCollisionGraph( - internalGeometries.Geometries(), - internalGeometries.Geometries(), - zeroPositionCG, - true, - defaultCollisionBufferMM, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, collisionListsAlmostEqual(cg.collisions(defaultCollisionBufferMM), expectedCollisions[:1]), test.ShouldBeTrue) -} diff --git a/motionplan/constraint_test.go b/motionplan/constraint_test.go deleted file mode 100644 index 23b11fd3046..00000000000 --- a/motionplan/constraint_test.go +++ /dev/null @@ -1,335 +0,0 @@ -package motionplan - -import ( - "context" - "fmt" - "math" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan/ik" - frame "go.viam.com/rdk/referenceframe" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func TestIKTolerances(t *testing.T) { - logger := logging.NewTestLogger(t) - - m, err := frame.ParseModelJSONFile(utils.ResolveFile("referenceframe/testjson/ur5eDH.json"), "") - test.That(t, err, test.ShouldBeNil) - mp, err := newCBiRRTMotionPlanner(m, rand.New(rand.NewSource(1)), logger, newBasicPlannerOptions(m)) - test.That(t, err, test.ShouldBeNil) - - // Test inability to arrive at another position due to orientation - pos := spatial.NewPoseFromProtobuf(&commonpb.Pose{ - X: -46, - Y: 0, - Z: 372, - OX: -1.78, - OY: -3.3, - OZ: -1.11, - }) - _, err = mp.plan(context.Background(), pos, frame.FloatsToInputs([]float64{0, 0})) - test.That(t, err, test.ShouldNotBeNil) - - // Now verify that setting tolerances to zero allows the same arm to reach that position - opt := newBasicPlannerOptions(m) - opt.goalMetricConstructor = ik.NewPositionOnlyMetric - opt.SetMaxSolutions(50) - mp, err = newCBiRRTMotionPlanner(m, rand.New(rand.NewSource(1)), logger, opt) - test.That(t, err, test.ShouldBeNil) - _, err = mp.plan(context.Background(), pos, frame.FloatsToInputs(make([]float64, 6))) - test.That(t, err, test.ShouldBeNil) -} - -func TestConstraintPath(t *testing.T) { - homePos := frame.FloatsToInputs([]float64{0, 0, 0, 0, 0, 0}) - toPos := frame.FloatsToInputs([]float64{0, 0, 0, 0, 0, 1}) - - modelXarm, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - - test.That(t, err, test.ShouldBeNil) - ci := &ik.Segment{StartConfiguration: homePos, EndConfiguration: toPos, Frame: modelXarm} - err = resolveSegmentsToPositions(ci) - test.That(t, err, test.ShouldBeNil) - - handler := &ConstraintHandler{} - - // No constraints, should pass - ok, failCI := handler.CheckSegmentAndStateValidity(ci, 0.5) - test.That(t, failCI, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - - // Test interpolating - constraint, _ := NewProportionalLinearInterpolatingConstraint(ci.StartPosition, ci.EndPosition, 0.01) - handler.AddStateConstraint("interp", constraint) - ok, failCI = handler.CheckSegmentAndStateValidity(ci, 0.5) - test.That(t, failCI, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, len(handler.StateConstraints()), test.ShouldEqual, 1) - - badInterpPos := frame.FloatsToInputs([]float64{6.2, 0, 0, 0, 0, 0}) - ciBad := &ik.Segment{StartConfiguration: homePos, EndConfiguration: badInterpPos, Frame: modelXarm} - err = resolveSegmentsToPositions(ciBad) - test.That(t, err, test.ShouldBeNil) - ok, failCI = handler.CheckSegmentAndStateValidity(ciBad, 0.5) - test.That(t, failCI, test.ShouldNotBeNil) // With linear constraint, should be valid at the first step - test.That(t, ok, test.ShouldBeFalse) -} - -func TestLineFollow(t *testing.T) { - p1 := spatial.NewPoseFromProtobuf(&commonpb.Pose{ - X: 440, - Y: -447, - Z: 500, - OY: -1, - }) - p2 := spatial.NewPoseFromProtobuf(&commonpb.Pose{ - X: 140, - Y: -447, - Z: 550, - OY: -1, - }) - mp1 := frame.JointPositionsFromRadians([]float64{ - 3.75646398939225, - -1.0162453766159272, - 1.2142890600914453, - 1.0521227724322786, - -0.21337105357552288, - -0.006502311329196852, - -4.3822913510408945, - }) - mp2 := frame.JointPositionsFromRadians([]float64{ - 3.896845654143853, - -0.8353398707254642, - 1.1306783805718412, - 0.8347159514038981, - 0.49562136809544177, - -0.2260694386799326, - -4.383397470889424, - }) - mpFail := frame.JointPositionsFromRadians([]float64{ - 3.896845654143853, - -1.8353398707254642, - 1.1306783805718412, - 0.8347159514038981, - 0.49562136809544177, - -0.2260694386799326, - -4.383397470889424, - }) - - query := spatial.NewPoseFromProtobuf(&commonpb.Pose{ - X: 289.94907586421124, - Y: -447, - Z: 525.0086401700755, - OY: -1, - }) - - validFunc, gradFunc := NewLineConstraint(p1.Point(), p2.Point(), 0.001) - - pointGrad := gradFunc(&ik.State{Position: query}) - test.That(t, pointGrad, test.ShouldBeLessThan, 0.001*0.001) - - fs := frame.NewEmptyFrameSystem("test") - - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm7_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - - err = fs.AddFrame(m, fs.World()) - test.That(t, err, test.ShouldBeNil) - - markerFrame, err := frame.NewStaticFrame("marker", spatial.NewPoseFromPoint(r3.Vector{0, 0, 105})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(markerFrame, m) - test.That(t, err, test.ShouldBeNil) - goalFrame := fs.World() - - // Create a frame to solve for, and an IK solver with that frame. - sf, err := newSolverFrame(fs, markerFrame.Name(), goalFrame.Name(), frame.StartPositions(fs)) - test.That(t, err, test.ShouldBeNil) - - opt := newBasicPlannerOptions(sf) - opt.SetPathMetric(gradFunc) - opt.AddStateConstraint("whiteboard", validFunc) - - ok, lastGood := opt.CheckSegmentAndStateValidity( - &ik.Segment{ - StartConfiguration: sf.InputFromProtobuf(mp1), - EndConfiguration: sf.InputFromProtobuf(mp2), - Frame: sf, - }, - 1, - ) - test.That(t, ok, test.ShouldBeFalse) - // lastGood.StartConfiguration and EndConfiguration should pass constraints - lastGood.Frame = sf - stateCheck := &ik.State{Configuration: lastGood.StartConfiguration, Frame: lastGood.Frame} - pass, _ := opt.CheckStateConstraints(stateCheck) - test.That(t, pass, test.ShouldBeTrue) - - stateCheck.Configuration = lastGood.EndConfiguration - stateCheck.Position = nil - pass, _ = opt.CheckStateConstraints(stateCheck) - test.That(t, pass, test.ShouldBeTrue) - - // Check that a deviating configuration will fail - stateCheck.Configuration = sf.InputFromProtobuf(mpFail) - stateCheck.Position = nil - pass, failName := opt.CheckStateConstraints(stateCheck) - test.That(t, pass, test.ShouldBeFalse) - test.That(t, failName, test.ShouldEqual, "whiteboard") -} - -func TestCollisionConstraints(t *testing.T) { - zeroPos := frame.FloatsToInputs([]float64{0, 0, 0, 0, 0, 0}) - cases := []struct { - input []frame.Input - expected bool - failName string - }{ - {zeroPos, true, ""}, - {frame.FloatsToInputs([]float64{math.Pi / 2, 0, 0, 0, 0, 0}), true, ""}, - {frame.FloatsToInputs([]float64{math.Pi, 0, 0, 0, 0, 0}), false, defaultObstacleConstraintDesc}, - {frame.FloatsToInputs([]float64{math.Pi / 2, 0, 0, 0, 2, 0}), false, defaultSelfCollisionConstraintDesc}, - } - - // define external obstacles - bc, err := spatial.NewBox(spatial.NewZeroPose(), r3.Vector{2, 2, 2}, "") - test.That(t, err, test.ShouldBeNil) - obstacles := []spatial.Geometry{} - obstacles = append(obstacles, bc.Transform(spatial.NewZeroPose())) - obstacles = append(obstacles, bc.Transform(spatial.NewPoseFromPoint(r3.Vector{-130, 0, 300}))) - worldState, err := frame.NewWorldState([]*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, obstacles)}, nil) - test.That(t, err, test.ShouldBeNil) - - // setup zero position as reference CollisionGraph and use it in handler - model, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - fs := frame.NewEmptyFrameSystem("test") - err = fs.AddFrame(model, fs.Frame(frame.World)) - test.That(t, err, test.ShouldBeNil) - sf, err := newSolverFrame(fs, model.Name(), frame.World, frame.StartPositions(fs)) - test.That(t, err, test.ShouldBeNil) - seedMap := frame.StartPositions(fs) - frameInputs, err := sf.mapToSlice(seedMap) - test.That(t, err, test.ShouldBeNil) - handler := &ConstraintHandler{} - - // create robot collision entities - movingGeometriesInFrame, err := sf.Geometries(frameInputs) - movingRobotGeometries := movingGeometriesInFrame.Geometries() // solver frame returns geoms in frame World - test.That(t, err, test.ShouldBeNil) - - // find all geometries that are not moving but are in the frame system - staticRobotGeometries := make([]spatial.Geometry, 0) - frameSystemGeometries, err := frame.FrameSystemGeometries(fs, seedMap) - test.That(t, err, test.ShouldBeNil) - for name, geometries := range frameSystemGeometries { - if !sf.movingFrame(name) { - staticRobotGeometries = append(staticRobotGeometries, geometries.Geometries()...) - } - } - - // Note that all obstacles in worldState are assumed to be static so it is ok to transform them into the world frame - // TODO(rb) it is bad practice to assume that the current inputs of the robot correspond to the passed in world state - // the state that observed the worldState should ultimately be included as part of the worldState message - worldGeometries, err := worldState.ObstaclesInWorldFrame(fs, seedMap) - test.That(t, err, test.ShouldBeNil) - - collisionConstraints, err := createAllCollisionConstraints( - movingRobotGeometries, - staticRobotGeometries, - worldGeometries.Geometries(), - nil, - defaultCollisionBufferMM, - ) - test.That(t, err, test.ShouldBeNil) - for name, constraint := range collisionConstraints { - handler.AddStateConstraint(name, constraint) - } - - // loop through cases and check constraint handler processes them correctly - for i, c := range cases { - t.Run(fmt.Sprintf("Test %d", i), func(t *testing.T) { - response, failName := handler.CheckStateConstraints(&ik.State{Configuration: c.input, Frame: model}) - test.That(t, response, test.ShouldEqual, c.expected) - test.That(t, failName, test.ShouldEqual, c.failName) - }) - } -} - -var bt bool - -func BenchmarkCollisionConstraints(b *testing.B) { - // define external obstacles - bc, err := spatial.NewBox(spatial.NewZeroPose(), r3.Vector{2, 2, 2}, "") - test.That(b, err, test.ShouldBeNil) - obstacles := []spatial.Geometry{} - obstacles = append(obstacles, bc.Transform(spatial.NewZeroPose())) - obstacles = append(obstacles, bc.Transform(spatial.NewPoseFromPoint(r3.Vector{-130, 0, 300}))) - worldState, err := frame.NewWorldState([]*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, obstacles)}, nil) - test.That(b, err, test.ShouldBeNil) - - // setup zero position as reference CollisionGraph and use it in handler - model, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(b, err, test.ShouldBeNil) - fs := frame.NewEmptyFrameSystem("test") - err = fs.AddFrame(model, fs.Frame(frame.World)) - test.That(b, err, test.ShouldBeNil) - sf, err := newSolverFrame(fs, model.Name(), frame.World, frame.StartPositions(fs)) - test.That(b, err, test.ShouldBeNil) - seedMap := frame.StartPositions(fs) - frameInputs, err := sf.mapToSlice(seedMap) - test.That(b, err, test.ShouldBeNil) - handler := &ConstraintHandler{} - - // create robot collision entities - movingGeometriesInFrame, err := sf.Geometries(frameInputs) - movingRobotGeometries := movingGeometriesInFrame.Geometries() // solver frame returns geoms in frame World - test.That(b, err, test.ShouldBeNil) - - // find all geometries that are not moving but are in the frame system - staticRobotGeometries := make([]spatial.Geometry, 0) - frameSystemGeometries, err := frame.FrameSystemGeometries(fs, seedMap) - test.That(b, err, test.ShouldBeNil) - for name, geometries := range frameSystemGeometries { - if !sf.movingFrame(name) { - staticRobotGeometries = append(staticRobotGeometries, geometries.Geometries()...) - } - } - - // Note that all obstacles in worldState are assumed to be static so it is ok to transform them into the world frame - // TODO(rb) it is bad practice to assume that the current inputs of the robot correspond to the passed in world state - // the state that observed the worldState should ultimately be included as part of the worldState message - worldGeometries, err := worldState.ObstaclesInWorldFrame(fs, seedMap) - test.That(b, err, test.ShouldBeNil) - - collisionConstraints, err := createAllCollisionConstraints( - movingRobotGeometries, - staticRobotGeometries, - worldGeometries.Geometries(), - nil, - defaultCollisionBufferMM, - ) - test.That(b, err, test.ShouldBeNil) - for name, constraint := range collisionConstraints { - handler.AddStateConstraint(name, constraint) - } - rseed := rand.New(rand.NewSource(1)) - var b1 bool - var n int - - // loop through cases and check constraint handler processes them correctly - for n = 0; n < b.N; n++ { - rfloats := frame.GenerateRandomConfiguration(model, rseed) - b1, _ = handler.CheckStateConstraints(&ik.State{Configuration: frame.FloatsToInputs(rfloats), Frame: model}) - } - bt = b1 -} diff --git a/motionplan/ik/inverseKinematics_test.go b/motionplan/ik/inverseKinematics_test.go deleted file mode 100644 index 15d09c5c7fc..00000000000 --- a/motionplan/ik/inverseKinematics_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package ik - -import ( - "context" - "errors" - "math" - "runtime" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - frame "go.viam.com/rdk/referenceframe" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var ( - home = frame.FloatsToInputs([]float64{0, 0, 0, 0, 0, 0}) - nCPU = int(math.Max(1.0, float64(runtime.NumCPU()/4))) -) - -func TestCombinedIKinematics(t *testing.T) { - logger := logging.NewTestLogger(t) - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - ik, err := CreateCombinedIKSolver(m, logger, nCPU, defaultGoalThreshold) - test.That(t, err, test.ShouldBeNil) - - // Test ability to arrive at another position - pos := spatial.NewPose( - r3.Vector{X: -46, Y: -133, Z: 372}, - &spatial.OrientationVectorDegrees{OX: 1.79, OY: -1.32, OZ: -1.11}, - ) - solution, err := solveTest(context.Background(), ik, pos, home) - test.That(t, err, test.ShouldBeNil) - - // Test moving forward 20 in X direction from previous position - pos = spatial.NewPose( - r3.Vector{X: -66, Y: -133, Z: 372}, - &spatial.OrientationVectorDegrees{OX: 1.78, OY: -3.3, OZ: -1.11}, - ) - _, err = solveTest(context.Background(), ik, pos, solution[0]) - test.That(t, err, test.ShouldBeNil) -} - -func TestUR5NloptIKinematics(t *testing.T) { - logger := logging.NewTestLogger(t) - - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "") - test.That(t, err, test.ShouldBeNil) - ik, err := CreateCombinedIKSolver(m, logger, nCPU, defaultGoalThreshold) - test.That(t, err, test.ShouldBeNil) - - goalJP := frame.JointPositionsFromRadians([]float64{-4.128, 2.71, 2.798, 2.3, 1.291, 0.62}) - goal, err := m.Transform(m.InputFromProtobuf(goalJP)) - test.That(t, err, test.ShouldBeNil) - _, err = solveTest(context.Background(), ik, goal, home) - test.That(t, err, test.ShouldBeNil) -} - -func TestCombinedCPUs(t *testing.T) { - logger := logging.NewTestLogger(t) - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm7_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - ik, err := CreateCombinedIKSolver(m, logger, runtime.NumCPU()/400000, defaultGoalThreshold) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ik.solvers), test.ShouldEqual, 1) -} - -func solveTest(ctx context.Context, solver InverseKinematics, goal spatial.Pose, seed []frame.Input) ([][]frame.Input, error) { - solutionGen := make(chan *Solution) - ikErr := make(chan error) - ctxWithCancel, cancel := context.WithCancel(ctx) - defer cancel() - - // Spawn the IK solver to generate solutions until done - go func() { - defer close(ikErr) - ikErr <- solver.Solve(ctxWithCancel, solutionGen, seed, NewSquaredNormMetric(goal), 1) - }() - - var solutions [][]frame.Input - - // Solve the IK solver. Loop labels are required because `break` etc in a `select` will break only the `select`. -IK: - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - } - - select { - case step := <-solutionGen: - solutions = append(solutions, step.Configuration) - // Skip the return check below until we have nothing left to read from solutionGen - continue IK - default: - } - - select { - case <-ikErr: - // If we have a return from the IK solver, there are no more solutions, so we finish processing above - // until we've drained the channel - break IK - default: - } - } - cancel() - if len(solutions) == 0 { - return nil, errors.New("unable to solve for position") - } - - return solutions, nil -} diff --git a/motionplan/ik/metrics_test.go b/motionplan/ik/metrics_test.go deleted file mode 100644 index d41b470d769..00000000000 --- a/motionplan/ik/metrics_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package ik - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - spatial "go.viam.com/rdk/spatialmath" -) - -func TestSqNormMetric(t *testing.T) { - p1 := spatial.NewPoseFromPoint(r3.Vector{0, 0, 0}) - p2 := spatial.NewPoseFromPoint(r3.Vector{0, 0, 10}) - sqMet := NewSquaredNormMetric(p1) - - d1 := sqMet(&State{Position: p1}) - test.That(t, d1, test.ShouldAlmostEqual, 0) - sqMet = NewSquaredNormMetric(p2) - d2 := sqMet(&State{Position: p1}) - test.That(t, d2, test.ShouldAlmostEqual, 100) -} - -func TestBasicMetric(t *testing.T) { - sqMet := func(from, to spatial.Pose) float64 { - return spatial.PoseDelta(from, to).Point().Norm2() - } - p1 := spatial.NewPoseFromPoint(r3.Vector{0, 0, 0}) - p2 := spatial.NewPoseFromPoint(r3.Vector{0, 0, 10}) - d1 := sqMet(p1, p1) - test.That(t, d1, test.ShouldAlmostEqual, 0) - d2 := sqMet(p1, p2) - test.That(t, d2, test.ShouldAlmostEqual, 100) -} - -var ( - ov = &spatial.OrientationVector{math.Pi / 2, 0, 0, -1} - p1b = &State{Position: spatial.NewPose(r3.Vector{1, 2, 3}, ov)} - p2b = spatial.NewPose(r3.Vector{2, 3, 4}, ov) - result float64 -) - -func BenchmarkDeltaPose1(b *testing.B) { - var r float64 - weightedSqNormDist := NewSquaredNormMetric(p2b) - for n := 0; n < b.N; n++ { - r = weightedSqNormDist(p1b) - } - // Prevent compiler optimizations interfering with benchmark - result = r -} diff --git a/motionplan/ik/nloptInverseKinematics_test.go b/motionplan/ik/nloptInverseKinematics_test.go deleted file mode 100644 index e2f7da5757a..00000000000 --- a/motionplan/ik/nloptInverseKinematics_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package ik - -import ( - "context" - "testing" - - "github.com/golang/geo/r3" - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func TestCreateNloptIKSolver(t *testing.T) { - logger := logging.NewTestLogger(t) - m, err := referenceframe.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - ik, err := CreateNloptIKSolver(m, logger, -1, false, true) - test.That(t, err, test.ShouldBeNil) - ik.id = 1 - - // matches xarm home end effector position - pos := spatialmath.NewPoseFromPoint(r3.Vector{X: 207, Z: 112}) - seed := referenceframe.FloatsToInputs([]float64{1, 1, -1, 1, 1, 0}) - _, err = solveTest(context.Background(), ik, pos, seed) - test.That(t, err, test.ShouldBeNil) - - pos = spatialmath.NewPose( - r3.Vector{X: -46, Y: -23, Z: 372}, - &spatialmath.OrientationVectorDegrees{Theta: 0, OX: 0, OY: 0, OZ: -1}, - ) - - seed = m.InputFromProtobuf(&pb.JointPositions{Values: []float64{49, 28, -101, 0, -73, 0}}) - - _, err = solveTest(context.Background(), ik, pos, seed) - test.That(t, err, test.ShouldBeNil) -} diff --git a/motionplan/kinematic_test.go b/motionplan/kinematic_test.go deleted file mode 100644 index ff4e92fbb59..00000000000 --- a/motionplan/kinematic_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package motionplan - -import ( - "math" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" - "gonum.org/v1/gonum/num/quat" - - frame "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/referenceframe/urdf" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func BenchmarkFK(b *testing.B) { - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm7_kinematics.json"), "") - test.That(b, err, test.ShouldBeNil) - for n := 0; n < b.N; n++ { - _, err := ComputePosition(m, &pb.JointPositions{Values: make([]float64, 7)}) - test.That(b, err, test.ShouldBeNil) - } -} - -// This should test forward kinematics functions. -func TestForwardKinematics(t *testing.T) { - // Test the 5DOF yahboom arm to confirm kinematics works with non-6dof arms - m, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/fake/dofbot.json"), "") - test.That(t, err, test.ShouldBeNil) - - // Confirm end effector starts at 248.55, 0, 115 - expect := spatial.NewPose( - r3.Vector{X: 248.55, Y: 0, Z: 115}, - &spatial.OrientationVectorDegrees{Theta: 0, OX: 0, OY: 0, OZ: 1}, - ) - pos, err := ComputePosition(m, &pb.JointPositions{Values: make([]float64, 5)}) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(expect, pos), test.ShouldBeTrue) - - // Test the 6dof xarm we actually have - m, err = frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - - // Confirm end effector starts at 207, 0, 112 - expect = spatial.NewPose( - r3.Vector{X: 207, Y: 0, Z: 112}, - &spatial.OrientationVectorDegrees{Theta: 0, OX: 0, OY: 0, OZ: -1}, - ) - pos, err = ComputePosition(m, &pb.JointPositions{Values: make([]float64, 6)}) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(expect, pos), test.ShouldBeTrue) - - // Test incorrect joints - _, err = ComputePosition(m, &pb.JointPositions{Values: []float64{}}) - test.That(t, err, test.ShouldNotBeNil) - _, err = ComputePosition(m, &pb.JointPositions{Values: make([]float64, 7)}) - test.That(t, err, test.ShouldNotBeNil) - - newPos := []float64{45, -45, 0, 0, 0, 0} - pos, err = ComputePosition(m, &pb.JointPositions{Values: newPos}) - test.That(t, err, test.ShouldBeNil) - expect = spatial.NewPose( - r3.Vector{X: 181, Y: 181, Z: 303.76}, - &spatial.OrientationVectorDegrees{Theta: 0, OX: 0.5, OY: 0.5, OZ: -0.707}, - ) - test.That(t, spatial.PoseAlmostEqualEps(expect, pos, 0.01), test.ShouldBeTrue) - - newPos = []float64{-45, 0, 0, 0, 0, 45} - pos, err = ComputePosition(m, &pb.JointPositions{Values: newPos}) - test.That(t, err, test.ShouldBeNil) - expect = spatial.NewPose( - r3.Vector{X: 146.37, Y: -146.37, Z: 112}, - &spatial.OrientationVectorDegrees{Theta: 90, OX: 0, OY: 0, OZ: -1}, - ) - test.That(t, spatial.PoseAlmostEqualEps(expect, pos, 0.01), test.ShouldBeTrue) - - // Test out of bounds. Note that ComputePosition will return nil on OOB. - newPos = []float64{-45, 0, 0, 0, 0, 999} - pos, err = ComputePosition(m, &pb.JointPositions{Values: newPos}) - test.That(t, pos, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - // Test out of bounds. Note that ComputeOOBPosition will NOT return nil on OOB. - newPos = []float64{-45, 0, 0, 0, 0, 999} - pos, err = ComputeOOBPosition(m, &pb.JointPositions{Values: newPos}) - expect = spatial.NewPose( - r3.Vector{X: 146.37, Y: -146.37, Z: 112}, - &spatial.R4AA{Theta: math.Pi, RX: 0.31, RY: -0.95, RZ: 0}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqualEps(expect, pos, 0.01), test.ShouldBeTrue) -} - -const derivEqualityEpsilon = 1e-16 - -func derivComponentAlmostEqual(left, right float64) bool { - return math.Abs(left-right) <= derivEqualityEpsilon -} - -func areDerivsEqual(q1, q2 []quat.Number) bool { - if len(q1) != len(q2) { - return false - } - for i, dq1 := range q1 { - dq2 := q2[i] - if !derivComponentAlmostEqual(dq1.Real, dq2.Real) { - return false - } - if !derivComponentAlmostEqual(dq1.Imag, dq2.Imag) { - return false - } - if !derivComponentAlmostEqual(dq1.Jmag, dq2.Jmag) { - return false - } - if !derivComponentAlmostEqual(dq1.Kmag, dq2.Kmag) { - return false - } - } - return true -} - -func TestDeriv(t *testing.T) { - // Test identity quaternion - q := quat.Number{1, 0, 0, 0} - qDeriv := []quat.Number{{0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}} - - match := areDerivsEqual(qDeriv, deriv(q)) - test.That(t, match, test.ShouldBeTrue) - - // Test non-identity single-axis unit quaternion - q = quat.Exp(quat.Number{0, 2, 0, 0}) - - qDeriv = []quat.Number{ - {-0.9092974268256816, -0.4161468365471424, 0, 0}, - {0, 0, 0.4546487134128408, 0}, - {0, 0, 0, 0.4546487134128408}, - } - - match = areDerivsEqual(qDeriv, deriv(q)) - test.That(t, match, test.ShouldBeTrue) - - // Test non-identity multi-axis unit quaternion - q = quat.Exp(quat.Number{0, 2, 1.5, 0.2}) - - qDeriv = []quat.Number{ - {-0.472134934000233, -0.42654977821280804, -0.4969629339096933, -0.06626172452129245}, - {-0.35410120050017474, -0.4969629339096933, -0.13665473343215354, -0.049696293390969336}, - {-0.0472134934000233, -0.06626172452129245, -0.049696293390969336, 0.22944129454798728}, - } - - match = areDerivsEqual(qDeriv, deriv(q)) - test.That(t, match, test.ShouldBeTrue) -} - -// Test dynamic frame systems -// Since kinematics imports reference frame, this needs to be here to avoid circular dependencies. -func TestDynamicFrameSystemXArm(t *testing.T) { - fs := frame.NewEmptyFrameSystem("test") - - model, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(model, fs.World()) - - positions := frame.StartPositions(fs) - - // World point of xArm at 0 position - poseWorld1 := spatial.NewPoseFromPoint(r3.Vector{207, 0, 112}) - // World point of xArm at (90,-90,90,-90,90,-90) joint positions - poseWorld2 := spatial.NewPoseFromPoint(r3.Vector{97, -207, -98}) - - // Note that because the arm is pointing in a different direction, this point is not a direct inverse of pointWorld2 - pointXarm := spatial.NewPoseFromPoint(r3.Vector{207, 98, -97}) - - transformPoint1, err := fs.Transform(positions, frame.NewPoseInFrame("xArm6", spatial.NewZeroPose()), frame.World) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostCoincident(transformPoint1.(*frame.PoseInFrame).Pose(), poseWorld1), test.ShouldBeTrue) - - // Test ability to calculate hypothetical out-of-bounds positions for the arm, but still return an error - positions["xArm6"] = frame.FloatsToInputs( - []float64{math.Pi / 2, -math.Pi / 2, math.Pi / 2, -math.Pi / 2, math.Pi / 2, -math.Pi / 2}) - transformPoint2, err := fs.Transform(positions, frame.NewPoseInFrame("xArm6", spatial.NewZeroPose()), frame.World) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostCoincident(transformPoint2.(*frame.PoseInFrame).Pose(), poseWorld2), test.ShouldBeTrue) - - transformPoint3, err := fs.Transform(positions, frame.NewPoseInFrame(frame.World, spatial.NewZeroPose()), "xArm6") - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostCoincident(transformPoint3.(*frame.PoseInFrame).Pose(), pointXarm), test.ShouldBeTrue) -} - -// Test a complicated dynamic frame system. We model a UR5 at (100,100,200) holding a camera pointing in line with the -// gripper on a 3cm stick. We also model a xArm6 which is placed on an XY gantry, which is zeroed at (-50,-50,-200). -// Ensure that we are able to transform points from the camera frame into world frame, to gantry frame, and to xarm frame. -func TestComplicatedDynamicFrameSystem(t *testing.T) { - fs := frame.NewEmptyFrameSystem("test") - - // robot offsets - urOffset, err := frame.NewStaticFrame("urOffset", spatial.NewPoseFromPoint(r3.Vector{100, 100, 200})) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(urOffset, fs.World()) - gantryOffset, err := frame.NewStaticFrame("gantryXOffset", spatial.NewPoseFromPoint(r3.Vector{-50, -50, -200})) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(gantryOffset, fs.World()) - - // build 2 axis gantry manually - gantryX, err := frame.NewTranslationalFrame("gantryX", r3.Vector{1, 0, 0}, frame.Limit{math.Inf(-1), math.Inf(1)}) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(gantryX, gantryOffset) - gantryY, err := frame.NewTranslationalFrame("gantryY", r3.Vector{0, 1, 0}, frame.Limit{math.Inf(-1), math.Inf(1)}) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(gantryY, gantryX) - - // xarm on gantry - modelXarm, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(modelXarm, gantryY) - - // ur5 - modelUR5e, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "") - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(modelUR5e, urOffset) - - // Note that positive Z is always "forwards". If the position of the arm is such that it is pointing elsewhere, - // the resulting translation will be similarly oriented - urCamera, err := frame.NewStaticFrame("urCamera", spatial.NewPoseFromPoint(r3.Vector{0, 0, 30})) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(urCamera, modelUR5e) - - positions := frame.StartPositions(fs) - - poseUR5e := spatial.NewPoseFromPoint(r3.Vector{-717.2, -132.9, 262.8}) - // Camera translates by 30, gripper is pointed at -Y - poseUR5eCam := spatial.NewPoseFromPoint(r3.Vector{-717.2, -162.9, 262.8}) - - poseXarm := spatial.NewPoseFromPoint(r3.Vector{157., -50, -88}) - poseXarmFromCam := spatial.NewPoseFromPoint(r3.Vector{874.2, -112.9, -350.8}) - - // Check the UR5e and camera default positions - transformPoint1, err := fs.Transform(positions, frame.NewPoseInFrame("UR5e", spatial.NewZeroPose()), frame.World) - test.That(t, err, test.ShouldBeNil) - transformPoint2, err := fs.Transform(positions, frame.NewPoseInFrame("urCamera", spatial.NewZeroPose()), frame.World) - test.That(t, err, test.ShouldBeNil) - transformPoint3, err := fs.Transform(positions, frame.NewPoseInFrame("xArm6", spatial.NewZeroPose()), frame.World) - test.That(t, err, test.ShouldBeNil) - transformPoint4, err := fs.Transform(positions, frame.NewPoseInFrame("urCamera", spatial.NewZeroPose()), "xArm6") - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostCoincident(transformPoint1.(*frame.PoseInFrame).Pose(), poseUR5e), test.ShouldBeTrue) - test.That(t, spatial.PoseAlmostCoincident(transformPoint2.(*frame.PoseInFrame).Pose(), poseUR5eCam), test.ShouldBeTrue) - test.That(t, spatial.PoseAlmostCoincident(transformPoint3.(*frame.PoseInFrame).Pose(), poseXarm), test.ShouldBeTrue) - test.That(t, spatial.PoseAlmostCoincident(transformPoint4.(*frame.PoseInFrame).Pose(), poseXarmFromCam), test.ShouldBeTrue) - - // Move the UR5e so its local Z axis is pointing approximately towards the xArm (at positive X) - positions["UR5e"] = frame.FloatsToInputs([]float64{0, 0, 0, 0, -math.Pi / 2, -math.Pi / 2}) - - // A point that is 813.6, -50, 200 from the camera - // This puts the point in the Z plane of the xArm6 - targetPoint := spatial.NewPoseFromPoint(r3.Vector{350.8, -50, 200}) - // Target point in world - tf, err := fs.Transform(positions, frame.NewPoseInFrame("urCamera", targetPoint), frame.World) - test.That(t, err, test.ShouldBeNil) - worldPointLoc := spatial.NewPoseFromPoint(tf.(*frame.PoseInFrame).Pose().Point()) - - // Move the XY gantry such that the xArm6 is now at the point specified - positions["gantryX"] = frame.FloatsToInputs([]float64{worldPointLoc.Point().X - poseXarm.Point().X}) - positions["gantryY"] = frame.FloatsToInputs([]float64{worldPointLoc.Point().Y - poseXarm.Point().Y}) - - // Confirm the xArm6 is now at the same location as the point - newPointXarm, err := fs.Transform(positions, frame.NewPoseInFrame("xArm6", spatial.NewZeroPose()), frame.World) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostCoincident(newPointXarm.(*frame.PoseInFrame).Pose(), worldPointLoc), test.ShouldBeTrue) - - // If the above passes, then converting one directly to the other should be (0,0,0) - pointCamToXarm, err := fs.Transform(positions, frame.NewPoseInFrame("urCamera", targetPoint), "xArm6") - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostCoincident(pointCamToXarm.(*frame.PoseInFrame).Pose(), spatial.NewZeroPose()), test.ShouldBeTrue) -} - -func TestSVAvsDH(t *testing.T) { - mSVA, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "") - test.That(t, err, test.ShouldBeNil) - mDH, err := frame.ParseModelJSONFile(utils.ResolveFile("referenceframe/testjson/ur5eDH.json"), "") - test.That(t, err, test.ShouldBeNil) - - numTests := 10000 - - seed := rand.New(rand.NewSource(23)) - for i := 0; i < numTests; i++ { - joints := mSVA.ProtobufFromInput(frame.RandomFrameInputs(mSVA, seed)) - - posSVA, err := ComputePosition(mSVA, joints) - test.That(t, err, test.ShouldBeNil) - posDH, err := ComputePosition(mDH, joints) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(posSVA, posDH), test.ShouldBeTrue) - } -} - -// Test loading model kinematics of the same arm via ModelJSON parsing and URDF parsing and comparing results. -func TestKinematicsJSONvsURDF(t *testing.T) { - numTests := 100 - - mJSON, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "") - test.That(t, err, test.ShouldBeNil) - mURDF, err := urdf.ParseModelXMLFile(utils.ResolveFile("referenceframe/urdf/testfiles/ur5e.urdf"), "") - test.That(t, err, test.ShouldBeNil) - - seed := rand.New(rand.NewSource(50)) - for i := 0; i < numTests; i++ { - joints := frame.JointPositionsFromRadians(frame.GenerateRandomConfiguration(mURDF, seed)) - posJSON, err := ComputePosition(mJSON, joints) - test.That(t, err, test.ShouldBeNil) - posURDF, err := ComputePosition(mURDF, joints) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(posJSON, posURDF), test.ShouldBeTrue) - } -} - -func TestComputeOOBPosition(t *testing.T) { - model, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "foo") - test.That(t, err, test.ShouldBeNil) - test.That(t, model.Name(), test.ShouldEqual, "foo") - - jointPositions := &pb.JointPositions{Values: []float64{1.1, 2.2, 3.3, 1.1, 2.2, 3.3}} - - t.Run("succeed", func(t *testing.T) { - pose, err := ComputeOOBPosition(model, jointPositions) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose, test.ShouldNotBeNil) - }) - - t.Run("fail when JointPositions are nil", func(t *testing.T) { - var NilJointPositions *pb.JointPositions - - pose, err := ComputeOOBPosition(model, NilJointPositions) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, pose, test.ShouldBeNil) - test.That(t, err, test.ShouldEqual, frame.ErrNilJointPositions) - }) - - t.Run("fail when model frame is nil", func(t *testing.T) { - var NilModel frame.Model - - pose, err := ComputeOOBPosition(NilModel, jointPositions) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, pose, test.ShouldBeNil) - test.That(t, err, test.ShouldEqual, frame.ErrNilModelFrame) - }) -} diff --git a/motionplan/motionPlanner_test.go b/motionplan/motionPlanner_test.go deleted file mode 100644 index fdadf09a6ea..00000000000 --- a/motionplan/motionPlanner_test.go +++ /dev/null @@ -1,1266 +0,0 @@ -package motionplan - -import ( - "context" - "math" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.uber.org/zap" - commonpb "go.viam.com/api/common/v1" - motionpb "go.viam.com/api/service/motion/v1" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan/ik" - "go.viam.com/rdk/motionplan/tpspace" - frame "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var ( - home7 = frame.FloatsToInputs([]float64{0, 0, 0, 0, 0, 0, 0}) - home6 = frame.FloatsToInputs([]float64{0, 0, 0, 0, 0, 0}) -) - -var logger = logging.FromZapCompatible(zap.Must(zap.Config{ - Level: zap.NewAtomicLevelAt(zap.FatalLevel), - Encoding: "console", - DisableStacktrace: true, -}.Build()).Sugar()) - -type planConfig struct { - Start []frame.Input - Goal spatialmath.Pose - RobotFrame frame.Frame - Options *plannerOptions -} - -type planConfigConstructor func() (*planConfig, error) - -func TestUnconstrainedMotion(t *testing.T) { - t.Parallel() - planners := []plannerConstructor{ - newRRTStarConnectMotionPlanner, - newCBiRRTMotionPlanner, - } - testCases := []struct { - name string - config planConfigConstructor - }{ - {"2D plan test", simple2DMap}, - {"6D plan test", simpleUR5eMotion}, - {"7D plan test", simpleXArmMotion}, - } - for _, testCase := range testCases { - tcCopy := testCase - t.Run(tcCopy.name, func(t *testing.T) { - t.Parallel() - for _, p := range planners { - testPlanner(t, p, tcCopy.config, 1) - } - }) - } -} - -func TestConstrainedMotion(t *testing.T) { - t.Parallel() - planners := []plannerConstructor{ - newCBiRRTMotionPlanner, - } - testCases := []struct { - name string - config planConfigConstructor - }{ - {"linear motion, no-spill", constrainedXArmMotion}, - } - for _, testCase := range testCases { - tcCopy := testCase - t.Run(tcCopy.name, func(t *testing.T) { - t.Parallel() - for _, p := range planners { - testPlanner(t, p, tcCopy.config, 1) - } - }) - } -} - -// TestConstrainedArmMotion tests a simple linear motion on a longer path, with a no-spill constraint. -func constrainedXArmMotion() (*planConfig, error) { - model, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm7_kinematics.json"), "") - if err != nil { - return nil, err - } - - // Test ability to arrive at another position - pos := spatialmath.NewPoseFromProtobuf(&commonpb.Pose{X: -206, Y: 100, Z: 120, OZ: -1}) - - opt := newBasicPlannerOptions(model) - opt.SmoothIter = 2 - orientMetric := ik.NewPoseFlexOVMetricConstructor(0.09) - - oFunc := ik.OrientDistToRegion(pos.Orientation(), 0.1) - oFuncMet := func(from *ik.State) float64 { - err := resolveStatesToPositions(from) - if err != nil { - return math.Inf(1) - } - return oFunc(from.Position.Orientation()) - } - orientConstraint := func(cInput *ik.State) bool { - err := resolveStatesToPositions(cInput) - if err != nil { - return false - } - - return oFunc(cInput.Position.Orientation()) == 0 - } - - opt.goalMetricConstructor = orientMetric - opt.SetPathMetric(oFuncMet) - opt.AddStateConstraint("orientation", orientConstraint) - - return &planConfig{ - Start: home7, - Goal: pos, - RobotFrame: model, - Options: opt, - }, nil -} - -func TestPlanningWithGripper(t *testing.T) { - fs := frame.NewEmptyFrameSystem("") - ur5e, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "ur") - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(ur5e, fs.World()) - test.That(t, err, test.ShouldBeNil) - bc, _ := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{Z: 75}), r3.Vector{200, 200, 200}, "") - gripper, err := frame.NewStaticFrameWithGeometry("gripper", spatialmath.NewPoseFromPoint(r3.Vector{Z: 150}), bc) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(gripper, ur5e) - test.That(t, err, test.ShouldBeNil) - zeroPos := frame.StartPositions(fs) - - newPose := frame.NewPoseInFrame("gripper", spatialmath.NewPoseFromPoint(r3.Vector{100, 100, 0})) - solutionMap, err := PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: newPose, - Frame: gripper, - StartConfiguration: zeroPos, - FrameSystem: fs, - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(solutionMap.Trajectory()), test.ShouldBeGreaterThanOrEqualTo, 2) -} - -// simple2DMapConfig returns a planConfig with the following map -// - start at (-9, 9) and end at (9, 9) -// - bounds are from (-10, -10) to (10, 10) -// - obstacle from (-4, 2) to (4, 10) -// -// ------------------------ -// | + | | + | -// | | | | -// | | | | -// | | | | -// | ------ | -// | * | -// | | -// | | -// | | -// ------------------------. -func simple2DMap() (*planConfig, error) { - // build model - limits := []frame.Limit{{Min: -100, Max: 100}, {Min: -100, Max: 100}, {Min: -2 * math.Pi, Max: 2 * math.Pi}} - physicalGeometry, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{X: 10, Y: 10, Z: 10}, "") - if err != nil { - return nil, err - } - modelName := "mobile-base" - model, err := frame.New2DMobileModelFrame(modelName, limits, physicalGeometry) - if err != nil { - return nil, err - } - - // add it to the frame system - fs := frame.NewEmptyFrameSystem("test") - if err := fs.AddFrame(model, fs.Frame(frame.World)); err != nil { - return nil, err - } - - // obstacles - box, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{0, 50, 0}), r3.Vector{80, 80, 1}, "") - if err != nil { - return nil, err - } - worldState, err := frame.NewWorldState( - []*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, []spatialmath.Geometry{box})}, - nil, - ) - if err != nil { - return nil, err - } - - // setup planner options - opt := newBasicPlannerOptions(model) - startInput := frame.StartPositions(fs) - startInput[modelName] = frame.FloatsToInputs([]float64{-90., 90., 0}) - goal := spatialmath.NewPoseFromPoint(r3.Vector{X: 90, Y: 90, Z: 0}) - opt.SetGoal(goal) - sf, err := newSolverFrame(fs, modelName, frame.World, startInput) - if err != nil { - return nil, err - } - seedMap := frame.StartPositions(fs) - frameInputs, err := sf.mapToSlice(seedMap) - if err != nil { - return nil, err - } - - // create robot collision entities - movingGeometriesInFrame, err := sf.Geometries(frameInputs) - movingRobotGeometries := movingGeometriesInFrame.Geometries() // solver frame returns geoms in frame World - if err != nil { - return nil, err - } - - // find all geometries that are not moving but are in the frame system - staticRobotGeometries := make([]spatialmath.Geometry, 0) - frameSystemGeometries, err := frame.FrameSystemGeometries(fs, seedMap) - if err != nil { - return nil, err - } - for name, geometries := range frameSystemGeometries { - if !sf.movingFrame(name) { - staticRobotGeometries = append(staticRobotGeometries, geometries.Geometries()...) - } - } - - // Note that all obstacles in worldState are assumed to be static so it is ok to transform them into the world frame - // TODO(rb) it is bad practice to assume that the current inputs of the robot correspond to the passed in world state - // the state that observed the worldState should ultimately be included as part of the worldState message - worldGeometries, err := worldState.ObstaclesInWorldFrame(fs, seedMap) - if err != nil { - return nil, err - } - - collisionConstraints, err := createAllCollisionConstraints( - movingRobotGeometries, - staticRobotGeometries, - worldGeometries.Geometries(), - nil, - defaultCollisionBufferMM, - ) - if err != nil { - return nil, err - } - for name, constraint := range collisionConstraints { - opt.AddStateConstraint(name, constraint) - } - - return &planConfig{ - Start: startInput[modelName], - Goal: goal, - RobotFrame: model, - Options: opt, - }, nil -} - -// simpleArmMotion tests moving an xArm7. -func simpleXArmMotion() (*planConfig, error) { - xarm, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm7_kinematics.json"), "") - if err != nil { - return nil, err - } - - // add it to the frame system - fs := frame.NewEmptyFrameSystem("test") - if err := fs.AddFrame(xarm, fs.Frame(frame.World)); err != nil { - return nil, err - } - - goal := spatialmath.NewPoseFromProtobuf(&commonpb.Pose{X: 206, Y: 100, Z: 120, OZ: -1}) - - // setup planner options - opt := newBasicPlannerOptions(xarm) - opt.SmoothIter = 20 - opt.SetGoal(goal) - sf, err := newSolverFrame(fs, xarm.Name(), frame.World, frame.StartPositions(fs)) - if err != nil { - return nil, err - } - seedMap := frame.StartPositions(fs) - frameInputs, err := sf.mapToSlice(seedMap) - if err != nil { - return nil, err - } - - // create robot collision entities - movingGeometriesInFrame, err := sf.Geometries(frameInputs) - movingRobotGeometries := movingGeometriesInFrame.Geometries() // solver frame returns geoms in frame World - if err != nil { - return nil, err - } - - // find all geometries that are not moving but are in the frame system - staticRobotGeometries := make([]spatialmath.Geometry, 0) - frameSystemGeometries, err := frame.FrameSystemGeometries(fs, seedMap) - if err != nil { - return nil, err - } - for name, geometries := range frameSystemGeometries { - if !sf.movingFrame(name) { - staticRobotGeometries = append(staticRobotGeometries, geometries.Geometries()...) - } - } - - collisionConstraints, err := createAllCollisionConstraints( - movingRobotGeometries, - staticRobotGeometries, - nil, - nil, - defaultCollisionBufferMM, - ) - if err != nil { - return nil, err - } - for name, constraint := range collisionConstraints { - opt.AddStateConstraint(name, constraint) - } - - return &planConfig{ - Start: home7, - Goal: goal, - RobotFrame: xarm, - Options: opt, - }, nil -} - -// simpleUR5eMotion tests a simple motion for a UR5e. -func simpleUR5eMotion() (*planConfig, error) { - ur5e, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "") - if err != nil { - return nil, err - } - fs := frame.NewEmptyFrameSystem("test") - if err = fs.AddFrame(ur5e, fs.Frame(frame.World)); err != nil { - return nil, err - } - goal := spatialmath.NewPoseFromProtobuf(&commonpb.Pose{X: -750, Y: -250, Z: 200, OX: -1}) - - // setup planner options - opt := newBasicPlannerOptions(ur5e) - opt.SmoothIter = 20 - opt.SetGoal(goal) - sf, err := newSolverFrame(fs, ur5e.Name(), frame.World, frame.StartPositions(fs)) - if err != nil { - return nil, err - } - seedMap := frame.StartPositions(fs) - frameInputs, err := sf.mapToSlice(seedMap) - if err != nil { - return nil, err - } - - // create robot collision entities - movingGeometriesInFrame, err := sf.Geometries(frameInputs) - movingRobotGeometries := movingGeometriesInFrame.Geometries() // solver frame returns geoms in frame World - if err != nil { - return nil, err - } - - // find all geometries that are not moving but are in the frame system - staticRobotGeometries := make([]spatialmath.Geometry, 0) - frameSystemGeometries, err := frame.FrameSystemGeometries(fs, seedMap) - if err != nil { - return nil, err - } - for name, geometries := range frameSystemGeometries { - if !sf.movingFrame(name) { - staticRobotGeometries = append(staticRobotGeometries, geometries.Geometries()...) - } - } - - collisionConstraints, err := createAllCollisionConstraints( - movingRobotGeometries, - staticRobotGeometries, - nil, - nil, - defaultCollisionBufferMM, - ) - if err != nil { - return nil, err - } - for name, constraint := range collisionConstraints { - opt.AddStateConstraint(name, constraint) - } - - return &planConfig{ - Start: home6, - Goal: goal, - RobotFrame: ur5e, - Options: opt, - }, nil -} - -// testPlanner is a helper function that takes a planner and a planning query specified through a config object and tests that it -// returns a valid set of waypoints. -func testPlanner(t *testing.T, plannerFunc plannerConstructor, config planConfigConstructor, seed int) { - t.Helper() - - // plan - cfg, err := config() - test.That(t, err, test.ShouldBeNil) - mp, err := plannerFunc(cfg.RobotFrame, rand.New(rand.NewSource(int64(seed))), logger, cfg.Options) - test.That(t, err, test.ShouldBeNil) - nodes, err := mp.plan(context.Background(), cfg.Goal, cfg.Start) - test.That(t, err, test.ShouldBeNil) - - // test that path doesn't violate constraints - test.That(t, len(nodes), test.ShouldBeGreaterThanOrEqualTo, 2) - for j := 0; j < len(nodes)-1; j++ { - ok, _ := cfg.Options.ConstraintHandler.CheckSegmentAndStateValidity(&ik.Segment{ - StartConfiguration: nodes[j].Q(), - EndConfiguration: nodes[j+1].Q(), - Frame: cfg.RobotFrame, - }, cfg.Options.Resolution) - test.That(t, ok, test.ShouldBeTrue) - } -} - -func makeTestFS(t *testing.T) frame.FrameSystem { - t.Helper() - fs := frame.NewEmptyFrameSystem("test") - - urOffset, err := frame.NewStaticFrame("urOffset", spatialmath.NewPoseFromPoint(r3.Vector{100, 100, 200})) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(urOffset, fs.World()) - gantryOffset, err := frame.NewStaticFrame("gantryOffset", spatialmath.NewPoseFromPoint(r3.Vector{-50, -50, -200})) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(gantryOffset, fs.World()) - - gantryX, err := frame.NewTranslationalFrame("gantryX", r3.Vector{1, 0, 0}, frame.Limit{math.Inf(-1), math.Inf(1)}) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(gantryX, gantryOffset) - gantryY, err := frame.NewTranslationalFrame("gantryY", r3.Vector{0, 1, 0}, frame.Limit{math.Inf(-1), math.Inf(1)}) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(gantryY, gantryX) - - modelXarm, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(modelXarm, gantryY) - - modelUR5e, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "") - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(modelUR5e, urOffset) - - // Note that positive Z is always "forwards". If the position of the arm is such that it is pointing elsewhere, - // the resulting translation will be similarly oriented - urCamera, err := frame.NewStaticFrame("urCamera", spatialmath.NewPoseFromPoint(r3.Vector{0, 0, 30})) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(urCamera, modelUR5e) - - // Add static frame for the gripper - bc, _ := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{Z: 100}), r3.Vector{200, 200, 200}, "") - xArmVgripper, err := frame.NewStaticFrameWithGeometry("xArmVgripper", spatialmath.NewPoseFromPoint(r3.Vector{Z: 200}), bc) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(xArmVgripper, modelXarm) - - return fs -} - -func TestArmOOBSolve(t *testing.T) { - fs := makeTestFS(t) - positions := frame.StartPositions(fs) - - // Set a goal unreachable by the UR due to sheer distance - goal1 := spatialmath.NewPose(r3.Vector{X: 257, Y: 21000, Z: -300}, &spatialmath.OrientationVectorDegrees{OZ: -1}) - _, err := PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal1), - Frame: fs.Frame("urCamera"), - StartConfiguration: positions, - FrameSystem: fs, - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, errIKSolve.Error()) -} - -func TestArmObstacleSolve(t *testing.T) { - fs := makeTestFS(t) - positions := frame.StartPositions(fs) - - // Set an obstacle such that it is impossible to reach the goal without colliding with it - obstacle, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{X: 257, Y: 210, Z: -300}), r3.Vector{10, 10, 100}, "") - test.That(t, err, test.ShouldBeNil) - worldState, err := frame.NewWorldState( - []*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, []spatialmath.Geometry{obstacle})}, - nil, - ) - test.That(t, err, test.ShouldBeNil) - - // Set a goal unreachable by the UR - goal1 := spatialmath.NewPose(r3.Vector{X: 257, Y: 210, Z: -300}, &spatialmath.OrientationVectorDegrees{OZ: -1}) - _, err = PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal1), - Frame: fs.Frame("urCamera"), - StartConfiguration: positions, - FrameSystem: fs, - WorldState: worldState, - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errIKConstraint) -} - -func TestArmAndGantrySolve(t *testing.T) { - t.Parallel() - fs := makeTestFS(t) - positions := frame.StartPositions(fs) - pointXarmGripper := spatialmath.NewPoseFromPoint(r3.Vector{157., -50, -288}) - transformPoint, err := fs.Transform( - positions, - frame.NewPoseInFrame("xArmVgripper", spatialmath.NewZeroPose()), - frame.World, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincident(transformPoint.(*frame.PoseInFrame).Pose(), pointXarmGripper), test.ShouldBeTrue) - - // Set a goal such that the gantry and arm must both be used to solve - goal1 := spatialmath.NewPose(r3.Vector{X: 257, Y: 2100, Z: -300}, &spatialmath.OrientationVectorDegrees{OZ: -1}) - plan, err := PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal1), - Frame: fs.Frame("xArmVgripper"), - StartConfiguration: positions, - FrameSystem: fs, - Options: map[string]interface{}{"smooth_iter": 5}, - }) - test.That(t, err, test.ShouldBeNil) - solvedPose, err := fs.Transform( - plan.Trajectory()[len(plan.Trajectory())-1], - frame.NewPoseInFrame("xArmVgripper", spatialmath.NewZeroPose()), - frame.World, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincidentEps(solvedPose.(*frame.PoseInFrame).Pose(), goal1, 0.01), test.ShouldBeTrue) -} - -func TestMultiArmSolve(t *testing.T) { - fs := makeTestFS(t) - positions := frame.StartPositions(fs) - // Solve such that the ur5 and xArm are pointing at each other, 40mm from gripper to camera - goal2 := spatialmath.NewPose(r3.Vector{Z: 60}, &spatialmath.OrientationVectorDegrees{OZ: -1}) - plan, err := PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame("urCamera", goal2), - Frame: fs.Frame("xArmVgripper"), - StartConfiguration: positions, - FrameSystem: fs, - Options: map[string]interface{}{"max_ik_solutions": 10, "timeout": 150.0, "smooth_iter": 5}, - }) - test.That(t, err, test.ShouldBeNil) - - // Both frames should wind up at the goal relative to one another - solvedPose, err := fs.Transform( - plan.Trajectory()[len(plan.Trajectory())-1], - frame.NewPoseInFrame("xArmVgripper", spatialmath.NewZeroPose()), - "urCamera", - ) - test.That(t, err, test.ShouldBeNil) - solvedPose2, err := fs.Transform( - plan.Trajectory()[len(plan.Trajectory())-1], - frame.NewPoseInFrame("urCamera", spatialmath.NewZeroPose()), - "xArmVgripper", - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincidentEps(solvedPose.(*frame.PoseInFrame).Pose(), goal2, 0.1), test.ShouldBeTrue) - test.That(t, spatialmath.PoseAlmostCoincidentEps(solvedPose2.(*frame.PoseInFrame).Pose(), goal2, 0.1), test.ShouldBeTrue) -} - -func TestReachOverArm(t *testing.T) { - // setup frame system with an xarm - xarm, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - offset, err := frame.NewStaticFrame("offset", spatialmath.NewPoseFromPoint(r3.Vector{X: -500, Y: 200})) - test.That(t, err, test.ShouldBeNil) - goal := frame.NewPoseInFrame( - "offset", - spatialmath.NewPose(r3.Vector{Y: -500, Z: 100}, &spatialmath.OrientationVector{OZ: -1}), - ) - fs := frame.NewEmptyFrameSystem("test") - fs.AddFrame(offset, fs.World()) - fs.AddFrame(xarm, offset) - - // plan to a location, it should interpolate to get there - opts := map[string]interface{}{"timeout": 150.0} - plan, err := PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: goal, - Frame: xarm, - StartConfiguration: frame.StartPositions(fs), - FrameSystem: fs, - Options: opts, - }) - - test.That(t, err, test.ShouldBeNil) - test.That(t, len(plan.Trajectory()), test.ShouldEqual, 2) - - // now add a UR arm in its way - ur5, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/universalrobots/ur5e.json"), "") - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(ur5, fs.World()) - - // the plan should no longer be able to interpolate, but it should still be able to get there - opts = map[string]interface{}{"timeout": 150.0, "smooth_iter": 5} - plan, err = PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: goal, - Frame: xarm, - StartConfiguration: frame.StartPositions(fs), - FrameSystem: fs, - Options: opts, - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(plan.Trajectory()), test.ShouldBeGreaterThan, 2) -} - -func TestPlanMapMotion(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // build kinematic base model - sphere, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "base") - test.That(t, err, test.ShouldBeNil) - model, err := frame.New2DMobileModelFrame( - "test", - []frame.Limit{{-100, 100}, {-100, 100}, {-2 * math.Pi, 2 * math.Pi}}, - sphere, - ) - test.That(t, err, test.ShouldBeNil) - dst := spatialmath.NewPoseFromPoint(r3.Vector{0, 100, 0}) - box, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{0, 50, 0}), r3.Vector{25, 25, 25}, "impediment") - test.That(t, err, test.ShouldBeNil) - worldState, err := frame.NewWorldState( - []*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, []spatialmath.Geometry{box})}, - nil, - ) - test.That(t, err, test.ShouldBeNil) - - PlanMapMotion := func( - ctx context.Context, - logger logging.Logger, - dst spatialmath.Pose, - f frame.Frame, - seed []frame.Input, - worldState *frame.WorldState, - ) ([][]frame.Input, error) { - // ephemerally create a framesystem containing just the frame for the solve - fs := frame.NewEmptyFrameSystem("") - if err := fs.AddFrame(f, fs.World()); err != nil { - return nil, err - } - destination := frame.NewPoseInFrame(frame.World, dst) - seedMap := map[string][]frame.Input{f.Name(): seed} - plan, err := PlanMotion(ctx, &PlanRequest{ - Logger: logger, - Goal: destination, - Frame: f, - StartConfiguration: seedMap, - FrameSystem: fs, - WorldState: worldState, - }) - if err != nil { - return nil, err - } - return plan.Trajectory().GetFrameInputs(f.Name()) - } - - plan, err := PlanMapMotion(ctx, logger, dst, model, make([]frame.Input, 3), worldState) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(plan), test.ShouldBeGreaterThan, 2) -} - -func TestSliceUniq(t *testing.T) { - fs := makeTestFS(t) - slice := []frame.Frame{} - slice = append(slice, fs.Frame("urCamera")) - slice = append(slice, fs.Frame("gantryOffset")) - slice = append(slice, fs.Frame("xArmVgripper")) - slice = append(slice, fs.Frame("urCamera")) - uniqd := uniqInPlaceSlice(slice) - test.That(t, len(uniqd), test.ShouldEqual, 3) -} - -func TestSolverFrameGeometries(t *testing.T) { - t.Parallel() - fs := makeTestFS(t) - sf, err := newSolverFrame(fs, "xArmVgripper", frame.World, frame.StartPositions(fs)) - test.That(t, err, test.ShouldBeNil) - - sfPlanner, err := newPlanManager(sf, logger, 1) - test.That(t, err, test.ShouldBeNil) - - request := &PlanRequest{ - Logger: logger, - Frame: fs.Frame("xArmVgripper"), - FrameSystem: fs, - StartConfiguration: sf.sliceToMap(make([]frame.Input, len(sf.DoF()))), - Goal: frame.NewPoseInFrame(frame.World, spatialmath.NewPoseFromPoint(r3.Vector{300, 300, 100})), - Options: map[string]interface{}{"smooth_iter": 5}, - } - - err = request.validatePlanRequest() - test.That(t, err, test.ShouldBeNil) - - plan, err := sfPlanner.PlanSingleWaypoint( - context.Background(), - request, - nil, - ) - test.That(t, err, test.ShouldBeNil) - inputs, err := sf.mapToSlice(plan.Trajectory()[len(plan.Trajectory())-1]) - test.That(t, err, test.ShouldBeNil) - gf, err := sf.Geometries(inputs) - test.That(t, err, test.ShouldBeNil) - test.That(t, gf, test.ShouldNotBeNil) - - geoms := gf.Geometries() - for _, geom := range geoms { - if geom.Label() == "xArmVgripper" { - gripperCenter := geom.Pose().Point() - test.That(t, spatialmath.R3VectorAlmostEqual(gripperCenter, r3.Vector{300, 300, 0}, 1e-2), test.ShouldBeTrue) - } - } -} - -func TestArmConstraintSpecificationSolve(t *testing.T) { - fs := frame.NewEmptyFrameSystem("") - x, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - test.That(t, fs.AddFrame(x, fs.World()), test.ShouldBeNil) - bc, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{Z: 100}), r3.Vector{200, 200, 200}, "") - test.That(t, err, test.ShouldBeNil) - xArmVgripper, err := frame.NewStaticFrameWithGeometry("xArmVgripper", spatialmath.NewPoseFromPoint(r3.Vector{Z: 200}), bc) - test.That(t, err, test.ShouldBeNil) - test.That(t, fs.AddFrame(xArmVgripper, x), test.ShouldBeNil) - - checkReachable := func(worldState *frame.WorldState, constraints *motionpb.Constraints) error { - goal := spatialmath.NewPose(r3.Vector{X: 600, Y: 100, Z: 300}, &spatialmath.OrientationVectorDegrees{OX: 1}) - _, err := PlanMotion(context.Background(), &PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal), - Frame: fs.Frame("xArmVgripper"), - FrameSystem: fs, - StartConfiguration: frame.StartPositions(fs), - WorldState: worldState, - ConstraintSpecs: constraints, - }) - return err - } - - // Verify that the goal position is reachable with no obstacles - test.That(t, checkReachable(frame.NewEmptyWorldState(), &motionpb.Constraints{}), test.ShouldBeNil) - - // Add an obstacle to the WorldState - box, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{350, 0, 0}), r3.Vector{10, 8000, 8000}, "theWall") - test.That(t, err, test.ShouldBeNil) - worldState1, err := frame.NewWorldState( - []*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, []spatialmath.Geometry{box})}, - nil, - ) - test.That(t, err, test.ShouldBeNil) - - testCases := []struct { - name string - worldState *frame.WorldState - }{ - {"obstacle specified through WorldState obstacles", worldState1}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Not reachable without a collision specification - constraints := &motionpb.Constraints{} - err = checkReachable(tc.worldState, constraints) - test.That(t, err, test.ShouldNotBeNil) - - // Reachable if xarm6 and gripper ignore collisions with The Wall - constraints = &motionpb.Constraints{ - CollisionSpecification: []*motionpb.CollisionSpecification{ - { - Allows: []*motionpb.CollisionSpecification_AllowedFrameCollisions{ - {Frame1: "xArm6", Frame2: "theWall"}, {Frame1: "xArmVgripper", Frame2: "theWall"}, - }, - }, - }, - } - err = checkReachable(tc.worldState, constraints) - test.That(t, err, test.ShouldBeNil) - - // Reachable if the specific bits of the xarm that collide are specified instead - constraints = &motionpb.Constraints{ - CollisionSpecification: []*motionpb.CollisionSpecification{ - { - Allows: []*motionpb.CollisionSpecification_AllowedFrameCollisions{ - {Frame1: "xArmVgripper", Frame2: "theWall"}, - {Frame1: "xArm6:wrist_link", Frame2: "theWall"}, - {Frame1: "xArm6:lower_forearm", Frame2: "theWall"}, - }, - }, - }, - } - err = checkReachable(tc.worldState, constraints) - test.That(t, err, test.ShouldBeNil) - }) - } -} - -func TestMovementWithGripper(t *testing.T) { - // TODO(rb): move these tests to a separate repo eventually, as they take up too much time for general CI pipeline - t.Skip() - - // setup solverFrame and planning query - fs := makeTestFS(t) - fs.RemoveFrame(fs.Frame("urOffset")) - sf, err := newSolverFrame(fs, "xArmVgripper", frame.World, frame.StartPositions(fs)) - test.That(t, err, test.ShouldBeNil) - goal := spatialmath.NewPose(r3.Vector{500, 0, -300}, &spatialmath.OrientationVector{OZ: -1}) - zeroPosition := sf.sliceToMap(make([]frame.Input, len(sf.DoF()))) - request := &PlanRequest{ - Logger: logger, - StartConfiguration: zeroPosition, - Goal: frame.NewPoseInFrame(frame.World, goal), - } - - // linearly plan with the gripper - motionConfig := make(map[string]interface{}) - motionConfig["motion_profile"] = LinearMotionProfile - sfPlanner, err := newPlanManager(sf, logger, 1) - test.That(t, err, test.ShouldBeNil) - request.Options = motionConfig - solution, err := sfPlanner.PlanSingleWaypoint(context.Background(), request, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, solution, test.ShouldNotBeNil) - - // plan around the obstacle with the gripper - obstacle, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{300, 0, -400}), r3.Vector{50, 500, 500}, "") - test.That(t, err, test.ShouldBeNil) - worldState, err := frame.NewWorldState( - []*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, []spatialmath.Geometry{obstacle})}, - nil, - ) - test.That(t, err, test.ShouldBeNil) - sfPlanner, err = newPlanManager(sf, logger, 1) - test.That(t, err, test.ShouldBeNil) - request.WorldState = worldState - solution, err = sfPlanner.PlanSingleWaypoint(context.Background(), request, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, solution, test.ShouldNotBeNil) - - // plan with end of arm with gripper attached - this will fail - sf, err = newSolverFrame(fs, "xArm6", frame.World, frame.StartPositions(fs)) - test.That(t, err, test.ShouldBeNil) - goal = spatialmath.NewPose(r3.Vector{500, 0, -100}, &spatialmath.OrientationVector{OZ: -1}) - request.Goal = frame.NewPoseInFrame(frame.World, goal) - sfPlanner, err = newPlanManager(sf, logger, 1) - test.That(t, err, test.ShouldBeNil) - _, err = sfPlanner.PlanSingleWaypoint(context.Background(), request, nil) - test.That(t, err, test.ShouldNotBeNil) - - // remove linear constraint and try again - sfPlanner, err = newPlanManager(sf, logger, 1) - test.That(t, err, test.ShouldBeNil) - request.Options = nil - solution, err = sfPlanner.PlanSingleWaypoint(context.Background(), request, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, solution, test.ShouldNotBeNil) - - // remove gripper and try with linear constraint - fs.RemoveFrame(fs.Frame("xArmVgripper")) - sf, err = newSolverFrame(fs, "xArm6", frame.World, frame.StartPositions(fs)) - test.That(t, err, test.ShouldBeNil) - sfPlanner, err = newPlanManager(sf, logger, 1) - test.That(t, err, test.ShouldBeNil) - request.Options = motionConfig - solution, err = sfPlanner.PlanSingleWaypoint(context.Background(), request, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, solution, test.ShouldNotBeNil) -} - -func TestReplanValidations(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - kinematicFrame, err := tpspace.NewPTGFrameFromKinematicOptions( - "itsabase", - logger, - 200./60., - 2, - nil, - false, - true, - ) - test.That(t, err, test.ShouldBeNil) - - goal := spatialmath.NewPoseFromPoint(r3.Vector{X: 1000, Y: 8000, Z: 0}) - - baseFS := frame.NewEmptyFrameSystem("baseFS") - err = baseFS.AddFrame(kinematicFrame, baseFS.World()) - test.That(t, err, test.ShouldBeNil) - type testCase struct { - extra map[string]interface{} - msg string - err error - } - - testCases := []testCase{ - { - msg: "fails validations when collision_buffer_mm is not a float", - extra: map[string]interface{}{"collision_buffer_mm": "not a float"}, - err: errors.New("could not interpret collision_buffer_mm field as float64"), - }, - { - msg: "fails validations when collision_buffer_mm is negative", - extra: map[string]interface{}{"collision_buffer_mm": -1.}, - err: errors.New("collision_buffer_mm can't be negative"), - }, - { - msg: "passes validations when collision_buffer_mm is a small positive float", - extra: map[string]interface{}{"collision_buffer_mm": 1e-5}, - }, - { - msg: "passes validations when collision_buffer_mm is a positive float", - extra: map[string]interface{}{"collision_buffer_mm": 200.}, - }, - { - msg: "passes validations when extra is empty", - extra: map[string]interface{}{}, - }, - { - msg: "passes validations when extra is nil", - extra: map[string]interface{}{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.msg, func(t *testing.T) { - _, err := Replan(ctx, &PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal), - StartPose: spatialmath.NewZeroPose(), - Frame: kinematicFrame, - FrameSystem: baseFS, - StartConfiguration: frame.StartPositions(baseFS), - Options: tc.extra, - }, nil, 0) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - }) - } -} - -func TestReplan(t *testing.T) { - // TODO(RSDK-5634): this should be unskipped when this bug is fixed - t.Skip() - ctx := context.Background() - logger := logging.NewTestLogger(t) - - sphere, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "base") - test.That(t, err, test.ShouldBeNil) - - kinematicFrame, err := tpspace.NewPTGFrameFromKinematicOptions( - "itsabase", - logger, - 200./60., - 2, - []spatialmath.Geometry{sphere}, - false, - true, - ) - test.That(t, err, test.ShouldBeNil) - - goal := spatialmath.NewPoseFromPoint(r3.Vector{1000, 8000, 0}) - - baseFS := frame.NewEmptyFrameSystem("baseFS") - err = baseFS.AddFrame(kinematicFrame, baseFS.World()) - test.That(t, err, test.ShouldBeNil) - - planRequest := &PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal), - Frame: kinematicFrame, - FrameSystem: baseFS, - StartConfiguration: frame.StartPositions(baseFS), - WorldState: nil, - Options: nil, - } - - firstplan, err := PlanMotion(ctx, planRequest) - test.That(t, err, test.ShouldBeNil) - - // Let's pretend we've moved towards the goal, so the goal is now closer - goal = spatialmath.NewPoseFromPoint(r3.Vector{1000, 5000, 0}) - planRequest.Goal = frame.NewPoseInFrame(frame.World, goal) - - // This should easily pass - newPlan1, err := Replan(ctx, planRequest, firstplan, 1.0) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(newPlan1.Trajectory()), test.ShouldBeGreaterThan, 2) - - // But if we drop the replan factor to a very low number, it should now fail - newPlan2, err := Replan(ctx, planRequest, firstplan, 0.1) - test.That(t, newPlan2, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errHighReplanCost) // Replan factor too low! -} - -func TestPtgPosOnlyBidirectional(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - sphere, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "base") - test.That(t, err, test.ShouldBeNil) - - kinematicFrame, err := tpspace.NewPTGFrameFromKinematicOptions( - "itsabase", - logger, - 200./60., - 2, - []spatialmath.Geometry{sphere}, - false, - true, - ) - test.That(t, err, test.ShouldBeNil) - - goal := spatialmath.NewPoseFromPoint(r3.Vector{1000, -8000, 0}) - - extra := map[string]interface{}{"motion_profile": "position_only", "position_seeds": 2, "smooth_iter": 5} - - baseFS := frame.NewEmptyFrameSystem("baseFS") - err = baseFS.AddFrame(kinematicFrame, baseFS.World()) - test.That(t, err, test.ShouldBeNil) - - planRequest := &PlanRequest{ - Logger: logger, - StartPose: spatialmath.NewZeroPose(), - Goal: frame.NewPoseInFrame(frame.World, goal), - Frame: kinematicFrame, - FrameSystem: baseFS, - StartConfiguration: frame.StartPositions(baseFS), - WorldState: nil, - Options: extra, - } - - bidirectionalPlanRaw, err := PlanMotion(ctx, planRequest) - test.That(t, err, test.ShouldBeNil) - - // If bidirectional planning worked properly, this plan should wind up at the goal with an orientation of Theta = 180 degrees - bidirectionalPlan, err := planToTpspaceRec(bidirectionalPlanRaw, kinematicFrame) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincidentEps(goal, bidirectionalPlan[len(bidirectionalPlan)-1].Pose(), 5), test.ShouldBeTrue) -} - -func TestValidatePlanRequest(t *testing.T) { - t.Parallel() - type testCase struct { - name string - request PlanRequest - expectedErr error - } - - logger := logging.NewTestLogger(t) - fs := frame.NewEmptyFrameSystem("test") - frame1 := frame.NewZeroStaticFrame("frame1") - frame2, err := frame.NewTranslationalFrame("frame2", r3.Vector{1, 0, 0}, frame.Limit{1, 1}) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame1, fs.World()) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame2, fs.World()) - test.That(t, err, test.ShouldBeNil) - - validGoal := frame.NewPoseInFrame("frame1", spatialmath.NewZeroPose()) - badGoal := frame.NewPoseInFrame("non-existent", spatialmath.NewZeroPose()) - - testCases := []testCase{ - { - name: "empty request - fail", - request: PlanRequest{}, - expectedErr: errors.New("PlanRequest cannot have nil logger"), - }, - { - name: "nil frame - fail", - request: PlanRequest{ - Logger: logger, - }, - expectedErr: errors.New("PlanRequest cannot have nil frame"), - }, - { - name: "nil framesystem - fail", - request: PlanRequest{ - Logger: logger, - Frame: frame1, - }, - expectedErr: errors.New("PlanRequest cannot have nil framesystem"), - }, - { - name: "framesystem does not contain frame - fail", - request: PlanRequest{ - Logger: logger, - Frame: frame1, - FrameSystem: frame.NewEmptyFrameSystem("test"), - }, - expectedErr: errors.Errorf("frame with name %q not in frame system", frame1.Name()), - }, - { - name: "nil goal - fail", - request: PlanRequest{ - Logger: logger, - Frame: frame1, - FrameSystem: fs, - }, - expectedErr: errors.New("PlanRequest cannot have nil goal"), - }, - { - name: "goal's parent not in frame system - fail", - request: PlanRequest{ - Logger: logger, - Frame: frame1, - FrameSystem: fs, - Goal: badGoal, - }, - expectedErr: errors.New("part with name references non-existent parent non-existent"), - }, - { - name: "incorrect length StartConfiguration - fail", - request: PlanRequest{ - Logger: logger, - Frame: frame1, - FrameSystem: fs, - Goal: validGoal, - StartConfiguration: map[string][]frame.Input{ - "frame1": frame.FloatsToInputs([]float64{0}), - }, - }, - expectedErr: errors.New("number of inputs does not match frame DoF, expected 0 but got 1"), - }, - { - name: "incorrect length StartConfiguration - fail", - request: PlanRequest{ - Logger: logger, - Frame: frame2, - FrameSystem: fs, - Goal: validGoal, - }, - expectedErr: errors.New("frame2 does not have a start configuration"), - }, - { - name: "incorrect length StartConfiguration - fail", - request: PlanRequest{ - Logger: logger, - Frame: frame2, - FrameSystem: fs, - Goal: validGoal, - StartConfiguration: map[string][]frame.Input{ - "frame2": frame.FloatsToInputs([]float64{0, 0, 0, 0, 0}), - }, - }, - expectedErr: errors.New("number of inputs does not match frame DoF, expected 1 but got 5"), - }, - { - name: "well formed PlanRequest", - request: PlanRequest{ - Logger: logger, - Frame: frame1, - FrameSystem: fs, - Goal: validGoal, - StartConfiguration: map[string][]frame.Input{ - "frame1": {}, - }, - }, - expectedErr: nil, - }, - } - - testFn := func(t *testing.T, tc testCase) { - err := tc.request.validatePlanRequest() - if tc.expectedErr != nil { - test.That(t, err.Error(), test.ShouldEqual, tc.expectedErr.Error()) - } else { - test.That(t, err, test.ShouldBeNil) - } - } - - for _, tc := range testCases { - c := tc // needed to workaround loop variable not being captured by func literals - t.Run(c.name, func(t *testing.T) { - t.Parallel() - testFn(t, c) - }) - } - - // ensure nil PlanRequests are caught - _, err = PlanMotion(context.Background(), nil) - test.That(t, err.Error(), test.ShouldEqual, "PlanRequest cannot be nil") -} - -func TestArmGantryCheckPlan(t *testing.T) { - logger := logging.NewTestLogger(t) - fs := frame.NewEmptyFrameSystem("test") - - gantryOffset, err := frame.NewStaticFrame("gantryOffset", spatialmath.NewPoseFromPoint(r3.Vector{0, 0, 0})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(gantryOffset, fs.World()) - test.That(t, err, test.ShouldBeNil) - - lim := frame.Limit{Min: math.Inf(-1), Max: math.Inf(1)} - gantryX, err := frame.NewTranslationalFrame("gantryX", r3.Vector{1, 0, 0}, lim) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(gantryX, gantryOffset) - test.That(t, err, test.ShouldBeNil) - - modelXarm, err := frame.ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(modelXarm, gantryX) - test.That(t, err, test.ShouldBeNil) - - goal := spatialmath.NewPoseFromPoint(r3.Vector{X: 407, Y: 0, Z: 112}) - - f := fs.Frame("xArm6") - planReq := PlanRequest{ - Logger: logger, - Goal: frame.NewPoseInFrame(frame.World, goal), - Frame: f, - FrameSystem: fs, - StartConfiguration: frame.StartPositions(fs), - } - - plan, err := PlanMotion(context.Background(), &planReq) - test.That(t, err, test.ShouldBeNil) - - startPose := plan.Path()[0][f.Name()].Pose() - errorState := spatialmath.NewZeroPose() - - t.Run("check plan with no obstacles", func(t *testing.T) { - err := CheckPlan(f, plan, 0, nil, fs, startPose, plan.Trajectory()[0], errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldBeNil) - }) - t.Run("check plan with obstacle", func(t *testing.T) { - obstacle, err := spatialmath.NewBox(goal, r3.Vector{10, 10, 1}, "obstacle") - test.That(t, err, test.ShouldBeNil) - - geoms := []spatialmath.Geometry{obstacle} - gifs := []*frame.GeometriesInFrame{frame.NewGeometriesInFrame(frame.World, geoms)} - - worldState, err := frame.NewWorldState(gifs, nil) - test.That(t, err, test.ShouldBeNil) - - err = CheckPlan(f, plan, 0, worldState, fs, startPose, plan.Trajectory()[0], errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldNotBeNil) - }) -} diff --git a/motionplan/nearestNeighbor_test.go b/motionplan/nearestNeighbor_test.go deleted file mode 100644 index c3a5996ff6a..00000000000 --- a/motionplan/nearestNeighbor_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package motionplan - -import ( - "context" - "math" - "runtime" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/referenceframe" -) - -var nCPU = int(math.Max(1.0, float64(runtime.NumCPU()/4))) - -func TestNearestNeighbor(t *testing.T) { - nm := &neighborManager{nCPU: 2, parallelNeighbors: 1000} - rrtMap := map[node]node{} - - j := &basicNode{q: []referenceframe.Input{{0.0}}} - // We add ~110 nodes to the set of candidates. This is smaller than the configured - // `parallelNeighbors` or 1000 meaning the `nearestNeighbor` call will be evaluated in series. - for i := 1.0; i < 110.0; i++ { - iSol := &basicNode{q: []referenceframe.Input{{i}}} - rrtMap[iSol] = j - j = iSol - } - ctx := context.Background() - - seed := []referenceframe.Input{{23.1}} - opt := newBasicPlannerOptions(referenceframe.NewZeroStaticFrame("test-frame")) - nn := nm.nearestNeighbor(ctx, opt, &basicNode{q: seed}, rrtMap) - test.That(t, nn.Q()[0].Value, test.ShouldAlmostEqual, 23.0) - - // We add more nodes to trip the 1000 threshold. The `nearestNeighbor` call will use `nCPU` (2) - // goroutines for evaluation. - for i := 120.0; i < 1100.0; i++ { - iSol := &basicNode{q: []referenceframe.Input{{i}}} - rrtMap[iSol] = j - j = iSol - } - seed = []referenceframe.Input{{723.6}} - nn = nm.nearestNeighbor(ctx, opt, &basicNode{q: seed}, rrtMap) - test.That(t, nn.Q()[0].Value, test.ShouldAlmostEqual, 724.0) -} diff --git a/motionplan/plan_test.go b/motionplan/plan_test.go deleted file mode 100644 index a46280244b5..00000000000 --- a/motionplan/plan_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package motionplan - -import ( - "context" - "math" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - pb "go.viam.com/api/service/motion/v1" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan/ik" - "go.viam.com/rdk/motionplan/tpspace" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func TestEvaluateTrajectory(t *testing.T) { - plan := Trajectory{ - map[string][]referenceframe.Input{"": {{1.}, {2.}, {3.}}}, - map[string][]referenceframe.Input{"": {{1.}, {2.}, {3.}}}, - } - // Test no change - score := plan.EvaluateCost(ik.L2InputMetric) - test.That(t, score, test.ShouldAlmostEqual, 0) - - // Test L2 for "", and nothing for plan with only one entry - plan = append(plan, map[string][]referenceframe.Input{"": {{4.}, {5.}, {6.}}, "test": {{2.}, {3.}, {4.}}}) - score = plan.EvaluateCost(ik.L2InputMetric) - test.That(t, score, test.ShouldAlmostEqual, math.Sqrt(27)) - - // Test cumulative L2 after returning to original inputs - plan = append(plan, map[string][]referenceframe.Input{"": {{1.}, {2.}, {3.}}}) - score = plan.EvaluateCost(ik.L2InputMetric) - test.That(t, score, test.ShouldAlmostEqual, math.Sqrt(27)*2) - - // Test that the "test" inputs are properly evaluated after skipping a step - plan = append(plan, map[string][]referenceframe.Input{"test": {{3.}, {5.}, {6.}}}) - score = plan.EvaluateCost(ik.L2InputMetric) - test.That(t, score, test.ShouldAlmostEqual, math.Sqrt(27)*2+3) - - // Evaluated with the tp-space metric, should be the sum of the distance values (third input) ignoring the first input set for each - // named input set - score = plan.EvaluateCost(tpspace.PTGSegmentMetric) - test.That(t, score, test.ShouldAlmostEqual, 18) -} - -func TestPlanStep(t *testing.T) { - baseNameA := "my-base1" - baseNameB := "my-base2" - poseA := spatialmath.NewZeroPose() - poseB := spatialmath.NewPose(r3.Vector{X: 100}, spatialmath.NewOrientationVector()) - - protoAB := &pb.PlanStep{ - Step: map[string]*pb.ComponentState{ - baseNameA: {Pose: spatialmath.PoseToProtobuf(poseA)}, - baseNameB: {Pose: spatialmath.PoseToProtobuf(poseB)}, - }, - } - stepAB := PathStep{ - baseNameA: referenceframe.NewPoseInFrame(referenceframe.World, poseA), - baseNameB: referenceframe.NewPoseInFrame(referenceframe.World, poseB), - } - - t.Run("pathStepFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.PlanStep - result PathStep - err error - } - - testCases := []testCase{ - { - description: "nil pointer returns an error", - input: nil, - result: PathStep{}, - err: errors.New("received nil *pb.PlanStep"), - }, - { - description: "an empty *pb.PlanStep returns an empty PathStep{}", - input: &pb.PlanStep{}, - result: PathStep{}, - }, - { - description: "a full *pb.PlanStep returns an full PathStep{}", - input: protoAB, - result: stepAB, - }, - } - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := PathStepFromProto(tc.input) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("ToProto()", func(t *testing.T) { - type testCase struct { - description string - input PathStep - result *pb.PlanStep - } - - testCases := []testCase{ - { - description: "an nil PathStep returns an empty *pb.PlanStep", - input: nil, - result: &pb.PlanStep{}, - }, - { - description: "an empty PathStep returns an empty *pb.PlanStep", - input: PathStep{}, - result: &pb.PlanStep{}, - }, - { - description: "a full PathStep{} returns an full *pb.PlanStep", - input: stepAB, - result: protoAB, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res := tc.input.ToProto() - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func TestNewGeoPlan(t *testing.T) { - sphere, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "base") - test.That(t, err, test.ShouldBeNil) - baseName := "myBase" - geoms := []spatialmath.Geometry{sphere} - kinematicFrame, err := tpspace.NewPTGFrameFromKinematicOptions(baseName, logger, 200./60., 2, geoms, false, true) - test.That(t, err, test.ShouldBeNil) - baseFS := referenceframe.NewEmptyFrameSystem("baseFS") - err = baseFS.AddFrame(kinematicFrame, baseFS.World()) - test.That(t, err, test.ShouldBeNil) - - goal := spatialmath.NewPoseFromPoint(r3.Vector{X: 1000, Y: 8000, Z: 0}) - plan, err := Replan(context.Background(), &PlanRequest{ - Logger: logging.NewTestLogger(t), - StartPose: spatialmath.NewZeroPose(), - Goal: referenceframe.NewPoseInFrame(referenceframe.World, goal), - Frame: kinematicFrame, - FrameSystem: baseFS, - StartConfiguration: referenceframe.StartPositions(baseFS), - }, nil, math.NaN()) - test.That(t, err, test.ShouldBeNil) - - // test Path gets constructed correctly - test.That(t, len(plan.Path()), test.ShouldBeGreaterThan, 1) - test.That(t, spatialmath.PoseAlmostEqual(plan.Path()[0][baseName].Pose(), spatialmath.NewZeroPose()), test.ShouldBeTrue) - test.That(t, spatialmath.PoseAlmostCoincidentEps(plan.Path()[len(plan.Path())-1][baseName].Pose(), goal, 10), test.ShouldBeTrue) - - type testCase struct { - name string - origin *geo.Point - expectedGPs []spatialmath.GeoPose - } - - tcs := []testCase{ - { - name: "null island origin", - origin: geo.NewPoint(0, 0), - expectedGPs: []spatialmath.GeoPose{ - *spatialmath.NewGeoPose(geo.NewPoint(0, 0), 0), - *spatialmath.NewGeoPose(geo.NewPoint(7.059656988760095e-05, 1.498635280806064e-05), 8.101305308745282), - }, - }, - { - name: "NE USA origin", - origin: geo.NewPoint(40, -74), - expectedGPs: []spatialmath.GeoPose{ - *spatialmath.NewGeoPose(geo.NewPoint(40, -74), 0), - *spatialmath.NewGeoPose(geo.NewPoint(40+7.059656988760095e-05, -74+1.498635280806064e-05), 278.1013053087453), - }, - }, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - // test Path gets converted to a GeoPlan correctly - gps := NewGeoPlan(plan, tc.origin) - test.That(t, err, test.ShouldBeNil) - pose := gps.Path()[0][baseName].Pose() - pt := pose.Point() - heading := utils.RadToDeg(pose.Orientation().EulerAngles().Yaw) - heading = math.Mod(math.Abs(heading-360), 360) - test.That(t, pt.X, test.ShouldAlmostEqual, tc.expectedGPs[0].Location().Lng(), 1e-6) - test.That(t, pt.Y, test.ShouldAlmostEqual, tc.expectedGPs[0].Location().Lat(), 1e-6) - test.That(t, heading, test.ShouldAlmostEqual, tc.expectedGPs[0].Heading(), 1e-3) - - pose = gps.Path()[len(gps.Path())-1][baseName].Pose() - pt = pose.Point() - test.That(t, pt.X, test.ShouldAlmostEqual, tc.expectedGPs[1].Location().Lng(), 1e-3) - test.That(t, pt.Y, test.ShouldAlmostEqual, tc.expectedGPs[1].Location().Lat(), 1e-3) - }) - } -} diff --git a/motionplan/tpSpaceRRT_test.go b/motionplan/tpSpaceRRT_test.go deleted file mode 100644 index 91c509a20f7..00000000000 --- a/motionplan/tpSpaceRRT_test.go +++ /dev/null @@ -1,413 +0,0 @@ -//go:build !windows - -package motionplan - -import ( - "context" - "math" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan/ik" - "go.viam.com/rdk/motionplan/tpspace" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" -) - -const testTurnRad = 0.3 - -func TestPtgRrtBidirectional(t *testing.T) { - t.Parallel() - logger := logging.NewTestLogger(t) - roverGeom, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{10, 10, 10}, "") - test.That(t, err, test.ShouldBeNil) - geometries := []spatialmath.Geometry{roverGeom} - - ctx := context.Background() - - ackermanFrame, err := tpspace.NewPTGFrameFromKinematicOptions( - "ackframe", - logger, - testTurnRad, - 0, - geometries, - false, - false, - ) - test.That(t, err, test.ShouldBeNil) - - goalPos := spatialmath.NewPose(r3.Vector{X: 200, Y: 7000, Z: 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 90}) - - opt := newBasicPlannerOptions(ackermanFrame) - opt.DistanceFunc = ik.NewSquaredNormSegmentMetric(30.) - opt.StartPose = spatialmath.NewZeroPose() - mp, err := newTPSpaceMotionPlanner(ackermanFrame, rand.New(rand.NewSource(42)), logger, opt) - test.That(t, err, test.ShouldBeNil) - tp, ok := mp.(*tpSpaceRRTMotionPlanner) - if pathdebug { - tp.logger.Debug("$type,X,Y") - tp.logger.Debugf("$SG,%f,%f", 0., 0.) - tp.logger.Debugf("$SG,%f,%f", goalPos.Point().X, goalPos.Point().Y) - } - test.That(t, ok, test.ShouldBeTrue) - plan, err := tp.plan(ctx, goalPos, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(plan), test.ShouldBeGreaterThanOrEqualTo, 2) -} - -func TestPtgWithObstacle(t *testing.T) { - t.Parallel() - logger := logging.NewTestLogger(t) - roverGeom, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{10, 10, 10}, "") - test.That(t, err, test.ShouldBeNil) - geometries := []spatialmath.Geometry{roverGeom} - ackermanFrame, err := tpspace.NewPTGFrameFromKinematicOptions( - "ackframe", - logger, - testTurnRad, - 0, - geometries, - false, - false, - ) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - - goalPos := spatialmath.NewPoseFromPoint(r3.Vector{X: 6500, Y: 0, Z: 0}) - - fs := referenceframe.NewEmptyFrameSystem("test") - fs.AddFrame(ackermanFrame, fs.World()) - - opt := newBasicPlannerOptions(ackermanFrame) - opt.DistanceFunc = ik.NewSquaredNormSegmentMetric(30.) - opt.StartPose = spatialmath.NewPoseFromPoint(r3.Vector{0, -1000, 0}) - opt.GoalThreshold = 5 - // obstacles - obstacle1, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{3300, -500, 0}), r3.Vector{180, 1800, 1}, "") - test.That(t, err, test.ShouldBeNil) - obstacle2, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{3300, 1800, 0}), r3.Vector{180, 1800, 1}, "") - test.That(t, err, test.ShouldBeNil) - obstacle3, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{2500, -1400, 0}), r3.Vector{50000, 30, 1}, "") - test.That(t, err, test.ShouldBeNil) - obstacle4, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{2500, 2400, 0}), r3.Vector{50000, 30, 1}, "") - test.That(t, err, test.ShouldBeNil) - obstacle5, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{-2500, 0, 0}), r3.Vector{50, 5000, 1}, "") - test.That(t, err, test.ShouldBeNil) - obstacle6, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{8500, 0, 0}), r3.Vector{50, 5000, 1}, "") - test.That(t, err, test.ShouldBeNil) - - geoms := []spatialmath.Geometry{obstacle1, obstacle2, obstacle3, obstacle4, obstacle5, obstacle6} - - worldState, err := referenceframe.NewWorldState( - []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(referenceframe.World, geoms)}, - nil, - ) - test.That(t, err, test.ShouldBeNil) - sf, err := newSolverFrame(fs, ackermanFrame.Name(), referenceframe.World, nil) - test.That(t, err, test.ShouldBeNil) - - seedMap := referenceframe.StartPositions(fs) - frameInputs, err := sf.mapToSlice(seedMap) - test.That(t, err, test.ShouldBeNil) - - // create robot collision entities - movingGeometriesInFrame, err := sf.Geometries(frameInputs) - movingRobotGeometries := movingGeometriesInFrame.Geometries() // solver frame returns geoms in frame World - test.That(t, err, test.ShouldBeNil) - - // find all geometries that are not moving but are in the frame system - staticRobotGeometries := make([]spatialmath.Geometry, 0) - frameSystemGeometries, err := referenceframe.FrameSystemGeometries(fs, seedMap) - test.That(t, err, test.ShouldBeNil) - for name, geometries := range frameSystemGeometries { - if !sf.movingFrame(name) { - staticRobotGeometries = append(staticRobotGeometries, geometries.Geometries()...) - } - } - - // Note that all obstacles in worldState are assumed to be static so it is ok to transform them into the world frame - // TODO(rb) it is bad practice to assume that the current inputs of the robot correspond to the passed in world state - // the state that observed the worldState should ultimately be included as part of the worldState message - worldGeometries, err := worldState.ObstaclesInWorldFrame(fs, seedMap) - test.That(t, err, test.ShouldBeNil) - - collisionConstraints, err := createAllCollisionConstraints( - movingRobotGeometries, - staticRobotGeometries, - worldGeometries.Geometries(), - nil, - defaultCollisionBufferMM, - ) - - test.That(t, err, test.ShouldBeNil) - - for name, constraint := range collisionConstraints { - opt.AddStateConstraint(name, constraint) - } - - mp, err := newTPSpaceMotionPlanner(ackermanFrame, rand.New(rand.NewSource(42)), logger, opt) - test.That(t, err, test.ShouldBeNil) - tp, _ := mp.(*tpSpaceRRTMotionPlanner) - if pathdebug { - tp.logger.Debug("$type,X,Y") - for _, geom := range geoms { - pts := geom.ToPoints(1.) - for _, pt := range pts { - if math.Abs(pt.Z) < 0.1 { - tp.logger.Debugf("$OBS,%f,%f", pt.X, pt.Y) - } - } - } - tp.logger.Debugf("$SG,%f,%f", opt.StartPose.Point().X, opt.StartPose.Point().Y) - tp.logger.Debugf("$SG,%f,%f", goalPos.Point().X, goalPos.Point().Y) - } - plan, err := tp.plan(ctx, goalPos, nil) - - test.That(t, err, test.ShouldBeNil) - test.That(t, len(plan), test.ShouldBeGreaterThan, 2) -} - -func TestTPsmoothing(t *testing.T) { - t.Parallel() - logger := logging.NewTestLogger(t) - roverGeom, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{10, 10, 10}, "") - test.That(t, err, test.ShouldBeNil) - geometries := []spatialmath.Geometry{roverGeom} - - ctx := context.Background() - - ackermanFrame, err := tpspace.NewPTGFrameFromKinematicOptions( - "ackframe", - logger, - testTurnRad, - 0, - geometries, - false, - false, - ) - test.That(t, err, test.ShouldBeNil) - - opt := newBasicPlannerOptions(ackermanFrame) - opt.DistanceFunc = ik.NewSquaredNormSegmentMetric(30.) - mp, err := newTPSpaceMotionPlanner(ackermanFrame, rand.New(rand.NewSource(42)), logger, opt) - test.That(t, err, test.ShouldBeNil) - tp, _ := mp.(*tpSpaceRRTMotionPlanner) - - // plan which is known to be able to use some smoothing - planInputs := [][]referenceframe.Input{ - {{0}, {0}, {0}, {0}}, - {{3}, {-0.20713797715976653}, {0}, {848.2300164692441}}, - {{4}, {0.0314906475636095}, {0}, {848.2300108402619}}, - {{4}, {0.0016660735709435135}, {0}, {848.2300146893297}}, - {{0}, {0.00021343061342569985}, {0}, {408}}, - {{4}, {1.9088870836327245}, {0}, {737.7547597081078}}, - {{2}, {-1.3118738553451883}, {0}, {848.2300164692441}}, - {{0}, {-3.1070696573964987}, {0}, {848.2300164692441}}, - {{0}, {-2.5547017183037877}, {0}, {306}}, - {{4}, {-2.31209484211255}, {0}, {408}}, - {{0}, {1.1943809502464207}, {0}, {571.4368241014894}}, - {{0}, {0.724950779684863}, {0}, {848.2300164692441}}, - {{0}, {-1.2295409308605127}, {0}, {848.2294213788913}}, - {{4}, {2.677652944060827}, {0}, {848.230013198154}}, - {{0}, {2.7618396954635545}, {0}, {848.2300164692441}}, - {{0}, {0}, {0}, {0}}, - } - plan := []node{} - for _, inp := range planInputs { - thisNode := &basicNode{ - q: inp, - cost: inp[3].Value - inp[2].Value, - } - plan = append(plan, thisNode) - } - plan, err = rectifyTPspacePath(plan, tp.frame, spatialmath.NewZeroPose()) - test.That(t, err, test.ShouldBeNil) - - tp.planOpts.SmoothIter = 20 - - newplan := tp.smoothPath(ctx, plan) - test.That(t, newplan, test.ShouldNotBeNil) - oldcost := 0. - smoothcost := 0. - for _, planNode := range plan { - oldcost += planNode.Cost() - } - for _, planNode := range newplan { - smoothcost += planNode.Cost() - } - test.That(t, smoothcost, test.ShouldBeLessThan, oldcost) -} - -func TestPtgCheckPlan(t *testing.T) { - logger := logging.NewTestLogger(t) - roverGeom, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{10, 10, 10}, "") - test.That(t, err, test.ShouldBeNil) - geometries := []spatialmath.Geometry{roverGeom} - ackermanFrame, err := tpspace.NewPTGFrameFromKinematicOptions( - "ackframe", - logger, - testTurnRad, - 0, - geometries, - false, - false, - ) - test.That(t, err, test.ShouldBeNil) - - goalPos := spatialmath.NewPoseFromPoint(r3.Vector{X: 5000, Y: 0, Z: 0}) - - fs := referenceframe.NewEmptyFrameSystem("test") - fs.AddFrame(ackermanFrame, fs.World()) - - opt := newBasicPlannerOptions(ackermanFrame) - opt.StartPose = spatialmath.NewZeroPose() - opt.DistanceFunc = ik.NewSquaredNormSegmentMetric(30.) - opt.GoalThreshold = 30. - - test.That(t, err, test.ShouldBeNil) - sf, err := newSolverFrame(fs, ackermanFrame.Name(), referenceframe.World, nil) - test.That(t, err, test.ShouldBeNil) - - mp, err := newTPSpaceMotionPlanner(ackermanFrame, rand.New(rand.NewSource(42)), logger, opt) - test.That(t, err, test.ShouldBeNil) - tp, _ := mp.(*tpSpaceRRTMotionPlanner) - - nodes, err := tp.plan(context.Background(), goalPos, nil) - test.That(t, err, test.ShouldBeNil) - plan, err := newRRTPlan(nodes, sf, true) - test.That(t, err, test.ShouldBeNil) - - startPose := spatialmath.NewPoseFromPoint(r3.Vector{0, 0, 0}) - errorState := startPose - inputs := plan.Trajectory()[0] - - t.Run("base case - validate plan without obstacles", func(t *testing.T) { - err := CheckPlan(ackermanFrame, plan, 0, nil, fs, startPose, inputs, errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("obstacles blocking path", func(t *testing.T) { - obstacle, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{2000, 0, 0}), r3.Vector{20, 2000, 1}, "") - test.That(t, err, test.ShouldBeNil) - - geoms := []spatialmath.Geometry{obstacle} - gifs := []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(referenceframe.World, geoms)} - - worldState, err := referenceframe.NewWorldState(gifs, nil) - test.That(t, err, test.ShouldBeNil) - - err = CheckPlan(ackermanFrame, plan, 0, worldState, fs, startPose, inputs, errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldNotBeNil) - }) - - // create camera_origin frame - cameraOriginFrame, err := referenceframe.NewStaticFrame("camera-origin", spatialmath.NewPoseFromPoint(r3.Vector{0, -20, 0})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(cameraOriginFrame, ackermanFrame) - test.That(t, err, test.ShouldBeNil) - - // create camera geometry - cameraGeom, err := spatialmath.NewBox( - spatialmath.NewZeroPose(), - r3.Vector{1, 1, 1}, "camera", - ) - test.That(t, err, test.ShouldBeNil) - - // create cameraFrame and add to framesystem - cameraFrame, err := referenceframe.NewStaticFrameWithGeometry( - "camera-frame", spatialmath.NewPoseFromPoint(r3.Vector{0, 0, 0}), cameraGeom, - ) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(cameraFrame, cameraOriginFrame) - test.That(t, err, test.ShouldBeNil) - - t.Run("obstacles NOT in world frame - no collision - integration test", func(t *testing.T) { - obstacle, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{25000, -40, 0}), - r3.Vector{10, 10, 1}, "obstacle", - ) - test.That(t, err, test.ShouldBeNil) - geoms := []spatialmath.Geometry{obstacle} - gifs := []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(cameraFrame.Name(), geoms)} - - worldState, err := referenceframe.NewWorldState(gifs, nil) - test.That(t, err, test.ShouldBeNil) - - err = CheckPlan(ackermanFrame, plan, 1, worldState, fs, startPose, inputs, errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldBeNil) - }) - t.Run("obstacles NOT in world frame cause collision - integration test", func(t *testing.T) { - obstacle, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{2500, 20, 0}), - r3.Vector{10, 2000, 1}, "obstacle", - ) - test.That(t, err, test.ShouldBeNil) - geoms := []spatialmath.Geometry{obstacle} - gifs := []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(cameraFrame.Name(), geoms)} - - worldState, err := referenceframe.NewWorldState(gifs, nil) - test.That(t, err, test.ShouldBeNil) - - err = CheckPlan(ackermanFrame, plan, 1, worldState, fs, startPose, inputs, errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldNotBeNil) - }) - t.Run("checking from partial-plan, ensure success with obstacles - integration test", func(t *testing.T) { - // create obstacle behind where we are - obstacle, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{0, 20, 0}), - r3.Vector{10, 200, 1}, "obstacle", - ) - test.That(t, err, test.ShouldBeNil) - geoms := []spatialmath.Geometry{obstacle} - gifs := []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(referenceframe.World, geoms)} - - worldState, err := referenceframe.NewWorldState(gifs, nil) - test.That(t, err, test.ShouldBeNil) - - ov := spatialmath.NewOrientationVector().Degrees() - ov.OZ = 1.0000000000000004 - ov.Theta = -101.42430306111874 - vector := r3.Vector{669.0803080526971, 234.2834571597409, 0} - - startPose := spatialmath.NewPose(vector, ov) - - err = CheckPlan(ackermanFrame, plan, 2, worldState, fs, startPose, inputs, errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldBeNil) - }) - t.Run("verify partial plan with non-nil errorState and obstacle", func(t *testing.T) { - // create obstacle which is behind where the robot already is, but is on the path it has already traveled - box, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{10, 10, 1}, "obstacle") - test.That(t, err, test.ShouldBeNil) - gifs := []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(referenceframe.World, []spatialmath.Geometry{box})} - - worldState, err := referenceframe.NewWorldState(gifs, nil) - test.That(t, err, test.ShouldBeNil) - - remainingPlan, err := RemainingPlan(plan, 2) - test.That(t, err, test.ShouldBeNil) - - pathPose := remainingPlan.Path()[0][ackermanFrame.Name()].Pose() - startPose := spatialmath.NewPose(r3.Vector{0, 1000, 0}, pathPose.Orientation()) - errorState := spatialmath.PoseDelta(pathPose, startPose) - - err = CheckPlan(ackermanFrame, plan, 2, worldState, fs, startPose, inputs, errorState, math.Inf(1), logger) - test.That(t, err, test.ShouldBeNil) - }) -} - -func planToTpspaceRec(plan Plan, f referenceframe.Frame) ([]node, error) { - nodes := []node{} - for _, inp := range plan.Trajectory() { - thisNode := &basicNode{ - q: inp[f.Name()], - cost: inp[f.Name()][3].Value, - } - nodes = append(nodes, thisNode) - } - return rectifyTPspacePath(nodes, f, spatialmath.NewZeroPose()) -} diff --git a/motionplan/tpspace/ptgGridSim_test.go b/motionplan/tpspace/ptgGridSim_test.go deleted file mode 100644 index 9f129eee684..00000000000 --- a/motionplan/tpspace/ptgGridSim_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package tpspace - -import ( - "testing" - - "go.viam.com/test" -) - -const ( - turnRadMeters = 0.3 - resolution = 20 -) - -func TestSim(t *testing.T) { - simDist := 2500. - alphaCnt := uint(121) - for _, ptg := range defaultPTGs { - ptgGen := ptg(turnRadMeters) - test.That(t, ptgGen, test.ShouldNotBeNil) - grid, err := NewPTGGridSim(ptgGen, alphaCnt, simDist, false) - test.That(t, err, test.ShouldBeNil) - - for i := uint(0); i < alphaCnt; i++ { - traj, err := grid.Trajectory(index2alpha(i, alphaCnt), 0, simDist, resolution) - test.That(t, err, test.ShouldBeNil) - test.That(t, traj, test.ShouldNotBeNil) - } - } -} diff --git a/motionplan/tpspace/ptg_test.go b/motionplan/tpspace/ptg_test.go deleted file mode 100644 index 3017088e510..00000000000 --- a/motionplan/tpspace/ptg_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package tpspace - -import ( - "math" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" -) - -func TestAlphaIdx(t *testing.T) { - for i := uint(0); i < defaultAlphaCnt; i++ { - alpha := index2alpha(i, defaultAlphaCnt) - i2 := alpha2index(alpha, defaultAlphaCnt) - test.That(t, i, test.ShouldEqual, i2) - } -} - -func alpha2index(alpha float64, numPaths uint) uint { - alpha = wrapTo2Pi(alpha+math.Pi) - math.Pi - idx := uint(math.Round(0.5 * (float64(numPaths)*(1.0+alpha/math.Pi) - 1.0))) - return idx -} - -func TestPtgDiffDrive(t *testing.T) { - p := NewDiffDrivePTG(0) - pose, err := p.Transform([]referenceframe.Input{{-3.0}, {10}}) - test.That(t, err, test.ShouldBeNil) - goalPose := spatialmath.NewPoseFromOrientation(&spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -10}) - test.That(t, spatialmath.PoseAlmostEqual(pose, goalPose), test.ShouldBeTrue) -} - -func TestPtgTransform(t *testing.T) { - pFrame, err := NewPTGFrameFromKinematicOptions( - "", - logging.NewTestLogger(t), - 1., - 2, - nil, - true, - true, - ) - test.That(t, err, test.ShouldBeNil) - p, ok := pFrame.(*ptgGroupFrame) - test.That(t, ok, test.ShouldBeTrue) - pose, err := p.Transform([]referenceframe.Input{{0}, {math.Pi / 2}, {0}, {200}}) - test.That(t, err, test.ShouldBeNil) - traj, err := p.PTGSolvers()[0].Trajectory(math.Pi/2, 0, 200, defaultResolution) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostEqual(pose, traj[len(traj)-1].Pose), test.ShouldBeTrue) - - poseInv, err := p.Transform([]referenceframe.Input{{0}, {math.Pi / 2}, {0}, {-200}}) - test.That(t, err, test.ShouldBeNil) - trajInv, err := p.PTGSolvers()[0].Trajectory(math.Pi/2, 0, -200, defaultResolution) - test.That(t, err, test.ShouldBeNil) - - test.That(t, spatialmath.PoseAlmostEqual(poseInv, trajInv[len(trajInv)-1].Pose), test.ShouldBeTrue) -} diff --git a/motionplan/utils_test.go b/motionplan/utils_test.go deleted file mode 100644 index 54bf9211002..00000000000 --- a/motionplan/utils_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package motionplan - -import ( - "testing" - - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - - "go.viam.com/rdk/spatialmath" -) - -func TestFixOvIncrement(t *testing.T) { - pos1 := commonpb.Pose{ - X: -66, - Y: -133, - Z: 372, - Theta: 15, - OX: 0, - OY: 1, - OZ: 0, - } - pos2 := commonpb.Pose{ - X: pos1.X, - Y: pos1.Y, - Z: pos1.Z, - Theta: pos1.Theta, - OX: pos1.OX, - OY: pos1.OY, - OZ: pos1.OZ, - } - - // Increment, but we're not pointing at Z axis, so should do nothing - pos2.OX = -0.1 - outpos := fixOvIncrement(spatialmath.NewPoseFromProtobuf(&pos2), spatialmath.NewPoseFromProtobuf(&pos1)) - test.That(t, outpos, test.ShouldResemble, spatialmath.NewPoseFromProtobuf(&pos2)) - - // point at positive Z axis, decrement OX, should subtract 180 - pos1.OZ = 1 - pos2.OZ = 1 - pos1.OY = 0 - pos2.OY = 0 - outpos = fixOvIncrement(spatialmath.NewPoseFromProtobuf(&pos2), spatialmath.NewPoseFromProtobuf(&pos1)) - test.That(t, outpos.Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, -165) - - // Spatial translation is incremented, should do nothing - pos2.X -= 0.1 - outpos = fixOvIncrement(spatialmath.NewPoseFromProtobuf(&pos2), spatialmath.NewPoseFromProtobuf(&pos1)) - test.That(t, outpos, test.ShouldResemble, spatialmath.NewPoseFromProtobuf(&pos2)) - - // Point at -Z, increment OY - pos2.X += 0.1 - pos2.OX += 0.1 - pos1.OZ = -1 - pos2.OZ = -1 - pos2.OY = 0.1 - outpos = fixOvIncrement(spatialmath.NewPoseFromProtobuf(&pos2), spatialmath.NewPoseFromProtobuf(&pos1)) - test.That(t, outpos.Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 105) - - // OX and OY are both incremented, should do nothing - pos2.OX += 0.1 - outpos = fixOvIncrement(spatialmath.NewPoseFromProtobuf(&pos2), spatialmath.NewPoseFromProtobuf(&pos1)) - test.That(t, outpos, test.ShouldResemble, spatialmath.NewPoseFromProtobuf(&pos2)) -} diff --git a/motionplan/verify_main_test.go b/motionplan/verify_main_test.go deleted file mode 100644 index 3a23e2dd203..00000000000 --- a/motionplan/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package motionplan - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/operation/manager_test.go b/operation/manager_test.go deleted file mode 100644 index bad61e6c292..00000000000 --- a/operation/manager_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package operation - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "testing" - "time" - - "go.viam.com/test" -) - -func TestNestedOperatioDoesNotCancelParent(t *testing.T) { - som := NewSingleOperationManager() - ctx := context.Background() - test.That(t, som.NewTimedWaitOp(ctx, time.Millisecond), test.ShouldBeTrue) - - ctx1, close1 := som.New(ctx) - defer close1() - _, close2 := som.New(ctx1) - defer close2() - test.That(t, ctx1.Err(), test.ShouldBeNil) -} - -func TestCallOnDifferentContext(t *testing.T) { - som := NewSingleOperationManager() - ctx := context.Background() - test.That(t, som.NewTimedWaitOp(ctx, time.Millisecond), test.ShouldBeTrue) - - res := int32(0) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - x := som.NewTimedWaitOp(context.Background(), 10*time.Second) - if x { - atomic.StoreInt32(&res, 1) - } - }() - - for !som.OpRunning() { - time.Sleep(time.Millisecond) - } - - test.That(t, som.NewTimedWaitOp(ctx, time.Millisecond), test.ShouldBeTrue) - - wg.Wait() - test.That(t, res, test.ShouldEqual, 0) -} - -func TestWaitForSuccess(t *testing.T) { - som := NewSingleOperationManager() - ctx := context.Background() - count := int64(0) - - err := som.WaitForSuccess( - ctx, - time.Millisecond, - func(ctx context.Context) (bool, error) { - if atomic.AddInt64(&count, 1) == 5 { - return true, nil - } - return false, nil - }, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, count, test.ShouldEqual, int64(5)) -} - -func TestWaitForError(t *testing.T) { - som := NewSingleOperationManager() - count := int64(0) - - err := som.WaitForSuccess( - context.Background(), - time.Millisecond, - func(ctx context.Context) (bool, error) { - if atomic.AddInt64(&count, 1) == 5 { - return false, errors.New("blah") - } - return false, nil - }, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, count, test.ShouldEqual, int64(5)) -} - -func TestDontCancel(t *testing.T) { - som := NewSingleOperationManager() - ctx, done := som.New(context.Background()) - defer done() - - som.CancelRunning(ctx) - test.That(t, ctx.Err(), test.ShouldBeNil) -} - -func TestCancelRace(t *testing.T) { - // First set up the "worker" context and register an operation on - // the `SingleOperationManager` instance. - som := NewSingleOperationManager() - workerError := make(chan error, 1) - workerCtx, somCleanupFunc := som.New(context.Background()) - - // Spin up a separate go-routine for the worker to listen for cancelation. Canceling an - // operation blocks until the operation completes. This goroutine is responsible for running - // `somCleanupFunc` to signal that canceling has completed. - workerFunc := func() { - defer somCleanupFunc() - - select { - case <-workerCtx.Done(): - workerError <- nil - case <-time.After(5 * time.Second): - workerError <- errors.New("Failed to be signaled via a cancel") - } - - close(workerError) - } - go workerFunc() - - // Set up a "test" context to cancel the worker. - testCtx, testCleanupFunc := context.WithTimeout(context.Background(), time.Second) - defer testCleanupFunc() - som.CancelRunning(testCtx) - // If `workerCtx.Done` was observed to be closed, the worker thread will pass a `nil` error back. - test.That(t, <-workerError, test.ShouldBeNil) - // When `SingleOperationManager` cancels an operation, the operation's context should be in a - // "context canceled" error state. - test.That(t, workerCtx.Err(), test.ShouldNotBeNil) - test.That(t, workerCtx.Err(), test.ShouldEqual, context.Canceled) -} - -func TestStopCalled(t *testing.T) { - som := NewSingleOperationManager() - ctx, done := som.New(context.Background()) - defer done() - mock := &mock{stopCount: 0} - ctx, cancel := context.WithCancel(ctx) - defer cancel() - var wg sync.WaitGroup - - wg.Add(1) - go func() { - som.WaitTillNotPowered(ctx, time.Second, mock, mock.stop) - wg.Done() - }() - - cancel() - wg.Wait() - test.That(t, ctx.Err(), test.ShouldNotBeNil) - test.That(t, mock.stopCount, test.ShouldEqual, 1) -} - -func TestErrorContainsStopAndCancel(t *testing.T) { - som := NewSingleOperationManager() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - mock := &mock{stopCount: 0} - var wg sync.WaitGroup - - wg.Add(1) - var errRet error - go func(errRet *error) { - *errRet = som.WaitTillNotPowered(ctx, time.Second, mock, mock.stopFail) - wg.Done() - }(&errRet) - - cancel() - wg.Wait() - test.That(t, errRet.Error(), test.ShouldEqual, "context canceled; Stop failed") -} - -type mock struct { - stopCount int -} - -func (m *mock) stop(ctx context.Context, extra map[string]interface{}) error { - m.stopCount++ - return nil -} - -func (m *mock) stopFail(ctx context.Context, extra map[string]interface{}) error { - return errors.New("Stop failed") -} - -func (m *mock) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return true, 1, nil -} diff --git a/operation/opid_test.go b/operation/opid_test.go deleted file mode 100644 index 3866a20d371..00000000000 --- a/operation/opid_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package operation - -import ( - "context" - "testing" - - "github.com/google/uuid" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/session" -) - -func TestBasic(t *testing.T) { - ctx := context.Background() - - logger := logging.NewTestLogger(t) - h := NewManager(logger) - o := Get(ctx) - test.That(t, o, test.ShouldBeNil) - - test.That(t, len(h.All()), test.ShouldEqual, 0) - - func() { - ctx2, cleanup := h.Create(ctx, "1", nil) - defer cleanup() - - test.That(t, func() { h.Create(ctx2, "b", nil) }, test.ShouldPanic) - - o := Get(ctx2) - test.That(t, o, test.ShouldNotBeNil) - test.That(t, len(o.myManager.ops), test.ShouldNotEqual, 0) - test.That(t, o.ID.String(), test.ShouldNotEqual, "") - test.That(t, len(h.All()), test.ShouldEqual, 1) - test.That(t, h.All()[0].ID, test.ShouldEqual, o.ID) - test.That(t, h.Find(o.ID).ID, test.ShouldEqual, o.ID) - test.That(t, h.FindString(o.ID.String()).ID, test.ShouldEqual, o.ID) - }() - - test.That(t, len(h.All()), test.ShouldEqual, 0) - - func() { - ctx2, cleanup2 := h.Create(ctx, "a", nil) - defer cleanup2() - - ctx3, cleanup3 := h.Create(ctx, "b", nil) - defer cleanup3() - - CancelOtherWithLabel(ctx2, "foo") - CancelOtherWithLabel(ctx3, "foo") - CancelOtherWithLabel(ctx, "foo") - - test.That(t, ctx3.Err(), test.ShouldBeNil) - test.That(t, ctx2.Err(), test.ShouldNotBeNil) - }() - - func() { - ctx4, cleanup4 := h.Create(ctx, "/proto.rpc.webrtc.v1.SignalingService/Answer", nil) - defer cleanup4() - ctx5, cleanup5 := h.Create(ctx, "/viam.robot.v1.RobotService/StreamStatus", nil) - defer cleanup5() - - test.That(t, ctx4.Value(opidKey), test.ShouldBeNil) - test.That(t, ctx5.Value(opidKey), test.ShouldBeNil) - - ctx6, cleanup6 := h.Create(ctx, "/viam.robot.v1.RobotService/", nil) - defer cleanup6() - o6 := Get(ctx6) - test.That(t, len(o6.myManager.ops), test.ShouldEqual, 1) - }() -} - -func TestCreateWithSession(t *testing.T) { - ctx := context.Background() - - logger := logging.NewTestLogger(t) - manager := NewManager(logger) - - op1Ctx, cleanup := manager.Create(ctx, "foo", nil) - op1 := Get(op1Ctx) - test.That(t, op1.SessionID, test.ShouldEqual, uuid.Nil) - cleanup() - - sess1 := session.New(ctx, "someone", 0, nil) - sess1Ctx := session.ToContext(ctx, sess1) - - op2Ctx, cleanup := manager.Create(sess1Ctx, "foo", nil) - op2 := Get(op2Ctx) - test.That(t, op2.SessionID, test.ShouldEqual, sess1.ID()) - cleanup() -} - -// test that on contexts with the same UUID, the operation is not overwritten. -func TestCreateDuplicate(t *testing.T) { - logger := logging.NewTestLogger(t) - manager := NewManager(logger) - - uuid := uuid.New() - op1Ctx, cleanup := manager.createWithID(context.Background(), uuid, "foo", nil) - op1 := Get(op1Ctx) - test.That(t, op1.ID, test.ShouldEqual, uuid) - - op2Ctx, _ := manager.createWithID(context.Background(), uuid, "bar", nil) - op2 := Get(op2Ctx) - test.That(t, op1, test.ShouldEqual, op2) - - // convoluted but mimicks what happens when a modular resource calls a dependency - // (leaves the local server and then comes back, so the context is related to the parent). - op3Ctx := context.WithValue(op1Ctx, opidKey, nil) - op3Ctx, _ = manager.createWithID(op3Ctx, uuid, "world", nil) - op3 := Get(op3Ctx) - test.That(t, op1, test.ShouldEqual, op3) - - op1.cancel() - cleanup() - test.That(t, op3Ctx.Err(), test.ShouldBeError, context.Canceled) -} diff --git a/operation/web_test.go b/operation/web_test.go deleted file mode 100644 index 4eb6a55736e..00000000000 --- a/operation/web_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package operation - -import ( - "context" - "testing" - - "github.com/google/uuid" - "go.viam.com/test" - "google.golang.org/grpc/metadata" - - "go.viam.com/rdk/logging" -) - -func TestCreateFromIncomingContextWithoutOpid(t *testing.T) { - logger := logging.NewTestLogger(t) - m := NewManager(logger) - - _, done := m.CreateFromIncomingContext(context.Background(), "fake") - defer done() - - ops := m.All() - test.That(t, ops, test.ShouldHaveLength, 1) -} - -func TestCreateFromIncomingContextWithOpid(t *testing.T) { - logger := logging.NewTestLogger(t) - m := NewManager(logger) - - opid := uuid.New() - meta := metadata.New(map[string]string{opidMetadataKey: opid.String()}) - ctx := metadata.NewIncomingContext(context.Background(), meta) - _, done := m.CreateFromIncomingContext(ctx, "fake") - defer done() - - ops := m.All() - - test.That(t, ops, test.ShouldHaveLength, 1) - test.That(t, ops[0].ID.String(), test.ShouldEqual, opid.String()) -} diff --git a/pointcloud/basic_octree_test.go b/pointcloud/basic_octree_test.go deleted file mode 100644 index cf8a48e68b8..00000000000 --- a/pointcloud/basic_octree_test.go +++ /dev/null @@ -1,837 +0,0 @@ -package pointcloud - -import ( - "math" - "os" - "path/filepath" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/spatialmath" -) - -// Helper function for generating a new empty octree. -func createNewOctree(center r3.Vector, side float64) (*BasicOctree, error) { - basicOct, err := NewBasicOctree(center, side) - if err != nil { - return nil, err - } - - return basicOct, err -} - -// Helper function that adds a list of points to a given basic octree. -func addPoints(basicOct *BasicOctree, pointsAndData []PointAndData) error { - for _, pd := range pointsAndData { - if err := basicOct.Set(pd.P, pd.D); err != nil { - return err - } - } - return nil -} - -// Helper function that checks that all valid points from the given list have been added to the basic octree. -func checkPoints(t *testing.T, basicOct *BasicOctree, pointsAndData []PointAndData) { - t.Helper() - - for _, point := range pointsAndData { - d, ok := basicOct.At(point.P.X, point.P.Y, point.P.Z) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, d, test.ShouldResemble, point.D) - } -} - -// Helper function that makes and returns a PointCloud of a given type from an artifact path. -func makeFullPointCloudFromArtifact(t *testing.T, artifactPath string, pcType PCType) (PointCloud, error) { - t.Helper() - - path := filepath.Clean(artifact.MustPath(artifactPath)) - pcdFile, err := os.Open(path) - defer utils.UncheckedErrorFunc(pcdFile.Close) - if err != nil { - return nil, err - } - - var PC PointCloud - switch pcType { - case BasicType: - PC, err = ReadPCD(pcdFile) - case BasicOctreeType: - PC, err = ReadPCDToBasicOctree(pcdFile) - } - - return PC, err -} - -// Test the creation of new basic octrees. -func TestBasicOctreeNew(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - sideInvalid := 0.0 - _, err := createNewOctree(center, sideInvalid) - test.That(t, err, test.ShouldBeError, errors.Errorf("invalid side length (%.2f) for octree", sideInvalid)) - - sideInvalid = -2.0 - _, err = createNewOctree(center, sideInvalid) - test.That(t, err, test.ShouldBeError, errors.Errorf("invalid side length (%.2f) for octree", sideInvalid)) - - sideValid := 1.0 - basicOct, err := createNewOctree(center, sideValid) - test.That(t, err, test.ShouldBeNil) - - t.Run("New Octree as basic octree", func(t *testing.T) { - test.That(t, basicOct.node, test.ShouldResemble, newLeafNodeEmpty()) - test.That(t, basicOct.center, test.ShouldResemble, r3.Vector{X: 0, Y: 0, Z: 0}) - test.That(t, basicOct.sideLength, test.ShouldAlmostEqual, sideValid) - test.That(t, basicOct.meta, test.ShouldResemble, NewMetaData()) - test.That(t, basicOct.MetaData(), test.ShouldResemble, NewMetaData()) - }) - - validateBasicOctree(t, basicOct, center, sideValid) -} - -// Test the Set()function which adds points and associated data to an octree. -func TestBasicOctreeSet(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 2.0 - - t.Run("Set point into empty leaf node into basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - test.That(t, basicOct.Size(), test.ShouldEqual, 0) - - point1 := r3.Vector{X: 0.1, Y: 0, Z: 0} - data1 := NewValueData(1) - err = basicOct.Set(point1, data1) - test.That(t, err, test.ShouldBeNil) - node := newLeafNodeFilled(point1, data1) - test.That(t, basicOct.node, test.ShouldResemble, node) - test.That(t, basicOct.Size(), test.ShouldEqual, 1) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Set point into filled leaf node into basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - d1 := 1 - err = basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(d1)) - test.That(t, err, test.ShouldBeNil) - mp := basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, d1) - - d2 := 2 - err = basicOct.Set(r3.Vector{X: -.5, Y: 0, Z: 0}, NewValueData(d2)) - test.That(t, err, test.ShouldBeNil) - test.That(t, basicOct.node.nodeType, test.ShouldResemble, internalNode) - test.That(t, basicOct.Size(), test.ShouldEqual, 2) - mp = basicOct.node.children[0].MaxVal() - test.That(t, mp, test.ShouldEqual, int(math.Max(float64(d1), float64(d2)))) - mp = basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, int(math.Max(float64(d1), float64(d2)))) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Set point into internal node node into basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - d3 := 3 - err = basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(d3)) - test.That(t, err, test.ShouldBeNil) - mp := basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, d3) - - d2 := 2 - err = basicOct.Set(r3.Vector{X: -.5, Y: 0, Z: 0}, NewValueData(d2)) - test.That(t, err, test.ShouldBeNil) - mp = basicOct.node.children[0].MaxVal() - test.That(t, mp, test.ShouldEqual, int(math.Max(float64(d2), float64(d3)))) - - d4 := 4 - err = basicOct.Set(r3.Vector{X: -.4, Y: 0, Z: 0}, NewValueData(d4)) - test.That(t, err, test.ShouldBeNil) - test.That(t, basicOct.node.nodeType, test.ShouldResemble, internalNode) - test.That(t, basicOct.Size(), test.ShouldEqual, 3) - mp = basicOct.node.children[0].MaxVal() - greatest := int(math.Max(math.Max(float64(d2), float64(d3)), float64(d4))) - test.That(t, mp, test.ShouldEqual, greatest) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Set point that lies outside the basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - err = basicOct.Set(r3.Vector{X: 2, Y: 0, Z: 0}, NewValueData(1)) - test.That(t, err, test.ShouldBeError, errors.New("error point is outside the bounds of this octree")) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Set point at intersection of multiple basic octree nodes", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - d1 := 1 - err = basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(d1)) - test.That(t, err, test.ShouldBeNil) - mp := basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, d1) - - d2 := 2 - err = basicOct.Set(r3.Vector{X: -.5, Y: 0, Z: 0}, NewValueData(d2)) - test.That(t, err, test.ShouldBeNil) - test.That(t, basicOct.size, test.ShouldEqual, 2) - mp = basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, int(math.Max(float64(d1), float64(d2)))) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Set same point with new data in basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - d1 := 1 - err = basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(d1)) - test.That(t, err, test.ShouldBeNil) - test.That(t, basicOct.node.point.D.Value(), test.ShouldEqual, d1) - mp := basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, d1) - - d2 := 2 - err = basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(d2)) - test.That(t, err, test.ShouldBeNil) - test.That(t, basicOct.node.point.D.Value(), test.ShouldEqual, d2) - test.That(t, basicOct.Size(), test.ShouldEqual, 1) - mp = basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, int(math.Max(float64(d1), float64(d2)))) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Set point into invalid internal node", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - basicOct.node = newInternalNode([]*BasicOctree{}) - err = basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(1)) - test.That(t, err, test.ShouldBeError, errors.New("error invalid internal node detected, please check your tree")) - }) - - t.Run("Set point into invalid internal node", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - basicOct.node = newInternalNode([]*BasicOctree{}) - err = basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(1)) - test.That(t, err, test.ShouldBeError, errors.New("error invalid internal node detected, please check your tree")) - }) - - t.Run("Set point, hit max recursion depth", func(t *testing.T) { - side = 2. - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - basicOct = createLopsidedOctree(basicOct, 0, maxRecursionDepth-1) - - d1 := 1 - err = basicOct.Set(r3.Vector{X: -1, Y: -1, Z: -1}, NewValueData(d1)) - test.That(t, err, test.ShouldBeNil) - mp := basicOct.MaxVal() - test.That(t, mp, test.ShouldEqual, d1) - - basicOct = createLopsidedOctree(basicOct, 0, maxRecursionDepth) - err = basicOct.Set(r3.Vector{X: -1, Y: -1, Z: -1}, NewBasicData()) - test.That(t, err, test.ShouldBeError, errors.New("error max allowable recursion depth reached")) - }) - - t.Run("Set empty data point", func(t *testing.T) { - side = 1. - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointAndData := PointAndData{} - - err = basicOct.Set(pointAndData.P, pointAndData.D) - test.That(t, err, test.ShouldBeNil) - }) -} - -// Test the At() function for basic octrees which returns the data at a specific location should it exist. -func TestBasicOctreeAt(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 2.0 - - t.Run("At check of single node basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(1)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - checkPoints(t, basicOct, pointsAndData) - - d, ok := basicOct.At(0.0001, 0, 0) - test.That(t, ok, test.ShouldBeFalse) - test.That(t, d, test.ShouldBeNil) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("At check of multi level basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(1)}, - {P: r3.Vector{X: -.5, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: -0.4, Y: 0, Z: 0}, D: NewValueData(3)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - checkPoints(t, basicOct, pointsAndData) - - d, ok := basicOct.At(-.6, 0, 0) - test.That(t, ok, test.ShouldBeFalse) - test.That(t, d, test.ShouldBeNil) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("At check of empty basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - d, ok := basicOct.At(0, 0, 0) - test.That(t, ok, test.ShouldBeFalse) - test.That(t, d, test.ShouldBeNil) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("At check of point outside octree bounds", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - d, ok := basicOct.At(3, 0, 0) - test.That(t, ok, test.ShouldBeFalse) - test.That(t, d, test.ShouldBeNil) - - validateBasicOctree(t, basicOct, center, side) - }) -} - -// Test the Iterate() function, which will apply a specified function to every point in a basic octree until -// the function returns a false value. -func TestBasicOctreeIterate(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 2.0 - - t.Run("Iterate zero batch check of an empty basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - total := 0 - basicOct.Iterate(0, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, 0) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Iterate zero batch check of a filled basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(2)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - // Full iteration - applies function to all points - total := 0 - basicOct.Iterate(0, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()) - - // Partial iteration - applies function to no points - total = 0 - basicOct.Iterate(0, 0, func(p r3.Vector, d Data) bool { - if d.Value() == 1 { - total += d.Value() - } - return true - }) - test.That(t, total, test.ShouldNotEqual, pointsAndData[0].D.Value()) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Iterate zero batch check of an multi-level basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(1)}, - {P: r3.Vector{X: .5, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: .6, Y: 0, Z: 0}, D: NewValueData(1)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - // Full iteration - applies function to all points - total := 0 - basicOct.Iterate(0, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()+ - pointsAndData[1].D.Value()+ - pointsAndData[2].D.Value()) - - // Partial iteration - applies function to only first point - total = 0 - basicOct.Iterate(0, 0, func(p r3.Vector, d Data) bool { - if d.Value() == 1 { - total += d.Value() - return true - } - return false - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Iterate non-zero batch check of an filled basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(2)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - total := 0 - basicOct.Iterate(1, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()) - - // Invalid batching - total = 0 - basicOct.Iterate(1, 2, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, 0) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Iterate non-zero batch check of an multi-level basic octree", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(1)}, - {P: r3.Vector{X: .5, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: .6, Y: 0, Z: 0}, D: NewValueData(3)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - // Batched process (numBatches = octree size, currentBatch = 0) - total := 0 - basicOct.Iterate(3, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()) - - // Batched process (numBatches = octree size, currentBatch = 1) - total = 0 - basicOct.Iterate(3, 1, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[1].D.Value()) - - // Batched process (numBatches = octree size, currentBatch = 2) - total = 0 - basicOct.Iterate(3, 2, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[2].D.Value()) - - // Batched process (numBatches = octree size, currentBatch = 3) - total = 0 - basicOct.Iterate(3, 3, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, 0) - - // Batched process (numBatches > octree size, currentBatch = 0) - total = 0 - basicOct.Iterate(4, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()) - - // Batched process (numBatches > octree size, currentBatch = 1) - total = 0 - basicOct.Iterate(4, 1, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[1].D.Value()) - - // Batched process (numBatches > octree size, currentBatch = 2) - total = 0 - basicOct.Iterate(4, 2, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[2].D.Value()) - - // Batched process (numBatches > octree size, currentBatch = 3) - total = 0 - basicOct.Iterate(4, 3, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, 0) - - // Batched process (numBatches < octree size, currentBatch = 0) - total = 0 - basicOct.Iterate(2, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()+ - pointsAndData[1].D.Value()) - - // Batched process (numBatches < octree size, currentBatch = 1) - total = 0 - basicOct.Iterate(2, 1, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[2].D.Value()) - - // Batched process (apply function to all data) - total = 0 - basicOct.Iterate(1, 0, func(p r3.Vector, d Data) bool { - total += d.Value() - return true - }) - test.That(t, total, test.ShouldEqual, pointsAndData[0].D.Value()+ - pointsAndData[1].D.Value()+ - pointsAndData[2].D.Value()) - - validateBasicOctree(t, basicOct, center, side) - }) -} - -// Test the functionalities involved with converting a pointcloud into a basic octree. -func TestBasicOctreePointcloudIngestion(t *testing.T) { - startPC, err := makeFullPointCloudFromArtifact(t, "pointcloud/test_short.pcd", BasicType) - test.That(t, err, test.ShouldBeNil) - - center := getCenterFromPcMetaData(startPC.MetaData()) - maxSideLength := getMaxSideLengthFromPcMetaData(startPC.MetaData()) - - basicOct, err := NewBasicOctree(center, maxSideLength) - test.That(t, err, test.ShouldBeNil) - - startPC.Iterate(0, 0, func(p r3.Vector, d Data) bool { - if err = basicOct.Set(p, d); err != nil { - return false - } - return true - }) - - test.That(t, startPC.Size(), test.ShouldEqual, basicOct.Size()) - test.That(t, startPC.MetaData(), test.ShouldResemble, basicOct.meta) - - // Check all points from the pointcloud have been properly added to the new basic octree - startPC.Iterate(0, 0, func(p r3.Vector, d Data) bool { - dOct, ok := basicOct.At(p.X, p.Y, p.Z) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, d, test.ShouldResemble, dOct) - return true - }) - - validateBasicOctree(t, basicOct, center, maxSideLength) -} - -// Test the functionalities involved with converting a pcd into a basic octree. -func TestReadBasicOctreeFromPCD(t *testing.T) { - t.Run("reading from binary PCD to octree", func(t *testing.T) { - binaryArtifactPath := "slam/rplidar_data/rplidar_data_0.pcd" - testPCDToBasicOctree(t, binaryArtifactPath) - }) - - t.Run("reading from ascii PCD to octree", func(t *testing.T) { - asciiArtifactPath := "slam/mock_lidar/0.pcd" - testPCDToBasicOctree(t, asciiArtifactPath) - }) -} - -// Helper function for testing basic octree creation from a given artifact. -func testPCDToBasicOctree(t *testing.T, artifactPath string) { - basicPC, err := makeFullPointCloudFromArtifact(t, artifactPath, BasicType) - test.That(t, err, test.ShouldBeNil) - basic, ok := basicPC.(*basicPointCloud) - test.That(t, ok, test.ShouldBeTrue) - - basicOctPC, err := makeFullPointCloudFromArtifact(t, artifactPath, BasicOctreeType) - test.That(t, err, test.ShouldBeNil) - basicOct, ok := basicOctPC.(*BasicOctree) - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, basic.Size(), test.ShouldEqual, basicOct.Size()) - test.That(t, basic.MetaData(), test.ShouldResemble, basicOct.MetaData()) - - // Check all points from the pcd have been properly added to the new basic octree - basic.Iterate(0, 0, func(p r3.Vector, d Data) bool { - dOct, ok := basicOct.At(p.X, p.Y, p.Z) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, d, test.ShouldResemble, dOct) - return true - }) - - validateBasicOctree(t, basicOct, basicOct.center, basicOct.sideLength) -} - -func TestCachedMaxProbability(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 2.0 - - t.Run("get the max val from an octree", func(t *testing.T) { - octree, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: .5, Y: 0, Z: 0}, D: NewValueData(3)}, - {P: r3.Vector{X: .5, Y: 0, Z: .5}, D: NewValueData(10)}, - {P: r3.Vector{X: .5, Y: .5, Z: 0}, D: NewValueData(1)}, - {P: r3.Vector{X: .55, Y: .55, Z: 0}, D: NewValueData(4)}, - {P: r3.Vector{X: -.55, Y: -.55, Z: 0}, D: NewValueData(5)}, - {P: r3.Vector{X: .755, Y: .755, Z: 0}, D: NewValueData(6)}, - } - - err = addPoints(octree, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - validateBasicOctree(t, octree, octree.center, octree.sideLength) - - mp := octree.MaxVal() - test.That(t, mp, test.ShouldEqual, 10) - - mp = octree.node.children[0].MaxVal() - test.That(t, mp, test.ShouldEqual, 5) - }) - - t.Run("cannot set arbitrary values into the octree", func(t *testing.T) { - d := &basicData{value: 0, hasValue: false} - node := newLeafNodeFilled(r3.Vector{1, 2, 3}, d) - filledNode := basicOctreeNode{ - children: nil, - nodeType: leafNodeFilled, - point: &PointAndData{P: r3.Vector{1, 2, 3}, D: d}, - maxVal: emptyProb, - } - test.That(t, node, test.ShouldResemble, filledNode) - }) - - t.Run("setting negative values", func(t *testing.T) { - octree, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(-2)}, - {P: r3.Vector{X: .5, Y: 0, Z: 0}, D: NewValueData(-3)}, - {P: r3.Vector{X: .5, Y: 0, Z: .5}, D: NewValueData(-10)}, - {P: r3.Vector{X: .5, Y: .5, Z: 0}, D: NewValueData(-1)}, - {P: r3.Vector{X: .55, Y: .55, Z: 0}, D: NewValueData(-4)}, - {P: r3.Vector{X: -.55, Y: -.55, Z: 0}, D: NewValueData(-5)}, - {P: r3.Vector{X: .755, Y: .755, Z: 0}, D: NewValueData(-6)}, - } - - err = addPoints(octree, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - validateBasicOctree(t, octree, octree.center, octree.sideLength) - - mp := octree.MaxVal() - test.That(t, mp, test.ShouldEqual, -1) - - mp = octree.node.children[0].MaxVal() - test.That(t, mp, test.ShouldEqual, -2) - }) -} - -// Test the various geometry-specific interface methods. -func TestBasicOctreeGeometryFunctions(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 2.0 - - octree, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: 1, Y: 0, Z: 0}, D: NewValueData(3)}, - {P: r3.Vector{X: 1, Y: 1, Z: 1}, D: NewValueData(5)}, - } - err = addPoints(octree, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - checkExpectedPoints := func(geom spatialmath.Geometry, pts []PointAndData) { - geomPts := geom.ToPoints(0) - octree, ok := geom.(*BasicOctree) - test.That(t, ok, test.ShouldBeTrue) - - for _, geomPt := range geomPts { - d, ok := octree.At(geomPt.X, geomPt.Y, geomPt.Z) - test.That(t, ok, test.ShouldBeTrue) - anyEqual := false - for _, pd := range pts { - if pointsAlmostEqualEpsilon(geomPt, pd.P, floatEpsilon) { - anyEqual = true - test.That(t, d, test.ShouldResemble, pd.D) - } - } - test.That(t, anyEqual, test.ShouldBeTrue) - } - - dupOctree, err := createNewOctree(pts[0].P, side) - test.That(t, err, test.ShouldBeNil) - err = addPoints(dupOctree, pts) - test.That(t, err, test.ShouldBeNil) - equal := dupOctree.AlmostEqual(geom) - test.That(t, equal, test.ShouldBeTrue) - } - - t.Run("identity transform", func(t *testing.T) { - expected := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: 1, Y: 0, Z: 0}, D: NewValueData(3)}, - {P: r3.Vector{X: 1, Y: 1, Z: 1}, D: NewValueData(5)}, - } - movedOctree := octree.Transform(spatialmath.NewZeroPose()) - checkExpectedPoints(movedOctree, expected) - }) - - t.Run("translate XY", func(t *testing.T) { - expected := []PointAndData{ - {P: r3.Vector{X: -3, Y: 5, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: -2, Y: 5, Z: 0}, D: NewValueData(3)}, - {P: r3.Vector{X: -2, Y: 6, Z: 1}, D: NewValueData(5)}, - } - movedOctree := octree.Transform(spatialmath.NewPoseFromPoint(r3.Vector{-3, 5, 0})) - checkExpectedPoints(movedOctree, expected) - }) - - t.Run("rotate", func(t *testing.T) { - expected := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: -1, Y: 0, Z: 0}, D: NewValueData(3)}, - {P: r3.Vector{X: -1, Y: 1, Z: -1}, D: NewValueData(5)}, - } - movedOctree := octree.Transform(spatialmath.NewPoseFromOrientation(&spatialmath.OrientationVector{OZ: -1})) - checkExpectedPoints(movedOctree, expected) - }) - - t.Run("rotate and translate", func(t *testing.T) { - expected := []PointAndData{ - {P: r3.Vector{X: -10, Y: 5, Z: 10}, D: NewValueData(2)}, - {P: r3.Vector{X: -11, Y: 5, Z: 10}, D: NewValueData(3)}, - {P: r3.Vector{X: -11, Y: 6, Z: 9}, D: NewValueData(5)}, - } - movedOctree := octree.Transform(spatialmath.NewPose( - r3.Vector{-10, 5, 10}, - &spatialmath.OrientationVector{OZ: -1}, - )) - checkExpectedPoints(movedOctree, expected) - }) - - t.Run("rotate and translate twice", func(t *testing.T) { - expected := []PointAndData{ - {P: r3.Vector{X: -35, Y: 60, Z: 110}, D: NewValueData(2)}, - {P: r3.Vector{X: -35, Y: 60, Z: 111}, D: NewValueData(3)}, - {P: r3.Vector{X: -36, Y: 59, Z: 111}, D: NewValueData(5)}, - } - movedOctree1 := octree.Transform(spatialmath.NewPose( - r3.Vector{-10, 5, 10}, - &spatialmath.OrientationVector{OZ: -1}, - )) - movedOctree2 := movedOctree1.Transform(spatialmath.NewPose( - r3.Vector{-30, 50, 100}, - &spatialmath.OrientationVector{OY: 1}, - )) - checkExpectedPoints(movedOctree2, expected) - }) -} - -func TestBasicOctreeAlmostEqual(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 2.0 - - octree, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(2)}, - {P: r3.Vector{X: 1, Y: 0, Z: 0}, D: NewValueData(3)}, - {P: r3.Vector{X: 1, Y: 1, Z: 1}, D: NewValueData(5)}, - } - err = addPoints(octree, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - equal := octree.AlmostEqual(octree) - test.That(t, equal, test.ShouldBeTrue) - - movedOctree := octree.Transform(spatialmath.NewZeroPose()) - // Confirm that an identity transform adjusts the side length but still yields equality - movedOctreeReal, ok := movedOctree.(*BasicOctree) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, movedOctreeReal.sideLength, test.ShouldNotEqual, octree.sideLength) - equal = octree.AlmostEqual(movedOctree) - test.That(t, equal, test.ShouldBeTrue) - - octree.Set(r3.Vector{-1, -1, -1}, NewValueData(999)) - equal = octree.AlmostEqual(movedOctree) - test.That(t, equal, test.ShouldBeFalse) - - movedOctree = octree.Transform(spatialmath.NewPoseFromPoint(r3.Vector{-3, 5, 0})) - equal = octree.AlmostEqual(movedOctree) - test.That(t, equal, test.ShouldBeFalse) -} diff --git a/pointcloud/basic_octree_utils_test.go b/pointcloud/basic_octree_utils_test.go deleted file mode 100644 index 56030d8bade..00000000000 --- a/pointcloud/basic_octree_utils_test.go +++ /dev/null @@ -1,373 +0,0 @@ -package pointcloud - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/spatialmath" -) - -// Test creation of empty leaf node, filled leaf node and internal node. -func TestNodeCreation(t *testing.T) { - t.Run("Create empty leaf node", func(t *testing.T) { - basicOct := newLeafNodeEmpty() - - test.That(t, basicOct.nodeType, test.ShouldResemble, leafNodeEmpty) - test.That(t, basicOct.point, test.ShouldBeNil) - test.That(t, basicOct.children, test.ShouldBeNil) - }) - - t.Run("Create filled leaf node", func(t *testing.T) { - p := r3.Vector{X: 0, Y: 0, Z: 0} - d := NewValueData(1.0) - basicOct := newLeafNodeFilled(p, d) - - test.That(t, basicOct.nodeType, test.ShouldResemble, leafNodeFilled) - test.That(t, basicOct.point.P, test.ShouldResemble, p) - test.That(t, basicOct.point.D, test.ShouldResemble, d) - test.That(t, basicOct.children, test.ShouldBeNil) - }) - - t.Run("Create internal node", func(t *testing.T) { - var children []*BasicOctree - basicOct := newInternalNode(children) - - test.That(t, basicOct.nodeType, test.ShouldResemble, internalNode) - test.That(t, basicOct.point, test.ShouldBeNil) - test.That(t, basicOct.children, test.ShouldResemble, children) - }) -} - -// Tests that splitting a filled octree node results in seven empty leaf nodes and -// one filled one as well as the splitting of an empty octree node will result in -// eight empty children nodes. -func TestSplitIntoOctants(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 1.0 - - t.Run("Splitting empty octree node into octants", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - err = basicOct.splitIntoOctants() - test.That(t, err, test.ShouldBeError, errors.New("error attempted to split empty leaf node")) - }) - - t.Run("Splitting filled basic octree node into octants", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(1)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - err = basicOct.splitIntoOctants() - test.That(t, err, test.ShouldBeNil) - test.That(t, len(basicOct.node.children), test.ShouldEqual, 8) - test.That(t, basicOct.node.nodeType, test.ShouldResemble, internalNode) - test.That(t, basicOct.node.point, test.ShouldBeNil) - - filledLeaves := []*BasicOctree{} - emptyLeaves := []*BasicOctree{} - internalLeaves := []*BasicOctree{} - - for _, child := range basicOct.node.children { - switch child.node.nodeType { - case leafNodeFilled: - filledLeaves = append(filledLeaves, child) - case leafNodeEmpty: - emptyLeaves = append(emptyLeaves, child) - case internalNode: - internalLeaves = append(internalLeaves, child) - } - } - test.That(t, len(filledLeaves), test.ShouldEqual, 1) - test.That(t, len(emptyLeaves), test.ShouldEqual, 7) - test.That(t, len(internalLeaves), test.ShouldEqual, 0) - test.That(t, filledLeaves[0].checkPointPlacement(pointsAndData[0].P), test.ShouldBeTrue) - - checkPoints(t, basicOct, pointsAndData) - - validateBasicOctree(t, basicOct, center, side) - }) - - t.Run("Splitting internal basic octree node with point into octants", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - pointsAndData := []PointAndData{ - {P: r3.Vector{X: 0, Y: 0, Z: 0}, D: NewValueData(1)}, - {P: r3.Vector{X: .5, Y: 0, Z: 0}, D: NewValueData(2)}, - } - - err = addPoints(basicOct, pointsAndData) - test.That(t, err, test.ShouldBeNil) - - checkPoints(t, basicOct, pointsAndData) - - d, ok := basicOct.At(0, 1, .5) - test.That(t, ok, test.ShouldBeFalse) - test.That(t, d, test.ShouldBeNil) - - err = basicOct.splitIntoOctants() - test.That(t, err, test.ShouldBeError, errors.New("error attempted to split internal node")) - }) - - t.Run("Splitting invalid basic octree node", func(t *testing.T) { - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - basicOct.node = newLeafNodeFilled(r3.Vector{X: 0, Y: 0, Z: 10}, NewValueData(1.0)) - err = basicOct.splitIntoOctants() - test.That(t, err, test.ShouldBeError, errors.New("error point is outside the bounds of this octree")) - - basicOct.node = newLeafNodeFilled(r3.Vector{X: 0, Y: 0, Z: 10}, NewValueData(1.0)) - err1 := basicOct.Set(r3.Vector{X: 0, Y: 0, Z: 0}, NewValueData(1.0)) - test.That(t, err1, test.ShouldBeError, errors.Errorf("error in splitting octree into new octants: %v", err)) - }) -} - -// Test the function responsible for checking if the specified point will fit in the octree given its center and side. -func TestCheckPointPlacement(t *testing.T) { - center := r3.Vector{X: 0, Y: 0, Z: 0} - side := 2.0 - - basicOct, err := createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: 0, Y: 0, Z: 0}), test.ShouldBeTrue) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: .25, Y: .25, Z: .25}), test.ShouldBeTrue) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: .5, Y: .5, Z: .5}), test.ShouldBeTrue) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: 1.01, Y: 0, Z: 0}), test.ShouldBeFalse) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: 1.00, Y: 1.01, Z: 0}), test.ShouldBeFalse) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: 0.99, Y: 0, Z: 1.01}), test.ShouldBeFalse) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: 2, Y: 0, Z: 0}), test.ShouldBeFalse) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: -1000, Y: 0, Z: 0}), test.ShouldBeFalse) - - center = r3.Vector{X: 1000, Y: -1000, Z: 10} - side = 24.0 - - basicOct, err = createNewOctree(center, side) - test.That(t, err, test.ShouldBeNil) - - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: 1000, Y: -1000, Z: 5}), test.ShouldBeTrue) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: 1000, Y: -994, Z: .5}), test.ShouldBeTrue) - test.That(t, basicOct.checkPointPlacement(r3.Vector{X: -1000, Y: 0, Z: 0}), test.ShouldBeFalse) - - validateBasicOctree(t, basicOct, center, side) -} - -// Helper function that recursively checks a basic octree's structure and metadata. -func validateBasicOctree(t *testing.T, bOct *BasicOctree, center r3.Vector, sideLength float64) (int, int) { - t.Helper() - - test.That(t, sideLength, test.ShouldEqual, bOct.sideLength) - test.That(t, center, test.ShouldResemble, bOct.center) - - validateMetadata(t, bOct) - - var size int - maxVal := emptyProb - switch bOct.node.nodeType { - case internalNode: - test.That(t, len(bOct.node.children), test.ShouldEqual, 8) - test.That(t, bOct.node.point, test.ShouldBeNil) - - numLeafNodeFilledNodes := 0 - numLeafNodeEmptyNodes := 0 - numInternalNodes := 0 - for c, child := range bOct.node.children { - var i, j, k float64 - if c%8 < 4 { - i = -1 - } else { - i = 1 - } - if c%4 < 2 { - j = -1 - } else { - j = 1 - } - if c%2 < 1 { - k = -1 - } else { - k = 1 - } - - switch child.node.nodeType { - case internalNode: - numInternalNodes++ - case leafNodeFilled: - numLeafNodeFilledNodes++ - case leafNodeEmpty: - numLeafNodeEmptyNodes++ - } - - childSize, childMaxProb := validateBasicOctree(t, child, r3.Vector{ - X: center.X + i*sideLength/4., - Y: center.Y + j*sideLength/4., - Z: center.Z + k*sideLength/4., - }, sideLength/2.) - size += childSize - maxVal = int(math.Max(float64(maxVal), float64(childMaxProb))) - } - test.That(t, size, test.ShouldEqual, bOct.size) - test.That(t, bOct.node.maxVal, test.ShouldEqual, maxVal) - test.That(t, bOct.node.maxVal, test.ShouldEqual, bOct.MaxVal()) - test.That(t, numInternalNodes+numLeafNodeEmptyNodes+numLeafNodeFilledNodes, test.ShouldEqual, 8) - case leafNodeFilled: - test.That(t, len(bOct.node.children), test.ShouldEqual, 0) - test.That(t, bOct.node.point, test.ShouldNotBeNil) - test.That(t, bOct.checkPointPlacement(bOct.node.point.P), test.ShouldBeTrue) - test.That(t, bOct.size, test.ShouldEqual, 1) - size = bOct.size - maxVal = bOct.node.maxVal - case leafNodeEmpty: - test.That(t, len(bOct.node.children), test.ShouldEqual, 0) - test.That(t, bOct.node.point, test.ShouldBeNil) - test.That(t, bOct.size, test.ShouldEqual, 0) - size = bOct.size - } - return size, maxVal -} - -// Helper function for checking basic octree metadata. -func validateMetadata(t *testing.T, bOct *BasicOctree) { - t.Helper() - - metadata := NewMetaData() - bOct.Iterate(0, 0, func(p r3.Vector, d Data) bool { - metadata.Merge(p, d) - return true - }) - - test.That(t, bOct.meta.HasColor, test.ShouldEqual, metadata.HasColor) - test.That(t, bOct.meta.HasValue, test.ShouldEqual, metadata.HasValue) - test.That(t, bOct.meta.MaxX, test.ShouldEqual, metadata.MaxX) - test.That(t, bOct.meta.MinX, test.ShouldEqual, metadata.MinX) - test.That(t, bOct.meta.MaxY, test.ShouldEqual, metadata.MaxY) - test.That(t, bOct.meta.MinY, test.ShouldEqual, metadata.MinY) - test.That(t, bOct.meta.MaxZ, test.ShouldEqual, metadata.MaxZ) - test.That(t, bOct.meta.MinZ, test.ShouldEqual, metadata.MinZ) - - // tolerance value to handle uncertainties in float point calculations - tolerance := 0.0001 - test.That(t, bOct.meta.TotalX(), test.ShouldBeBetween, metadata.TotalX()-tolerance, metadata.TotalX()+tolerance) - test.That(t, bOct.meta.TotalY(), test.ShouldBeBetween, metadata.TotalY()-tolerance, metadata.TotalY()+tolerance) - test.That(t, bOct.meta.TotalZ(), test.ShouldBeBetween, metadata.TotalZ()-tolerance, metadata.TotalZ()+tolerance) -} - -// Helper function to create lopsided octree for testing of recursion depth limit. -func createLopsidedOctree(oct *BasicOctree, i, max int) *BasicOctree { - if i >= max { - return oct - } - - children := []*BasicOctree{} - newSideLength := oct.sideLength / 2 - for _, i := range []float64{-1.0, 1.0} { - for _, j := range []float64{-1.0, 1.0} { - for _, k := range []float64{-1.0, 1.0} { - centerOffset := r3.Vector{ - X: i * newSideLength / 2., - Y: j * newSideLength / 2., - Z: k * newSideLength / 2., - } - newCenter := oct.center.Add(centerOffset) - - // Create a new basic octree child - child := &BasicOctree{ - center: newCenter, - sideLength: newSideLength, - size: 0, - node: newLeafNodeEmpty(), - meta: NewMetaData(), - } - children = append(children, child) - } - } - } - oct.node = newInternalNode(children) - oct.node.children[0] = createLopsidedOctree(oct.node.children[0], i+1, max) - return oct -} - -// Test the functionalities involved with converting a pointcloud into a basic octree. -func TestBasicOctreeCollision(t *testing.T) { - startPC, err := makeFullPointCloudFromArtifact( - t, - "pointcloud/collision_pointcloud_0.pcd", - BasicType, - ) - test.That(t, err, test.ShouldBeNil) - - center := getCenterFromPcMetaData(startPC.MetaData()) - maxSideLength := getMaxSideLengthFromPcMetaData(startPC.MetaData()) - - basicOct, err := NewBasicOctree(center, maxSideLength) - test.That(t, err, test.ShouldBeNil) - - startPC.Iterate(0, 0, func(p r3.Vector, d Data) bool { - // Blue channel is used to determine probability in pcds produced by cartographer - _, _, blueProb := d.RGB255() - d.SetValue(int(blueProb)) - if err = basicOct.Set(p, d); err != nil { - return false - } - return true - }) - - test.That(t, startPC.Size(), test.ShouldEqual, basicOct.Size()) - - t.Run("no collision with box far from octree points", func(t *testing.T) { - // create a non-colliding obstacle far away from any octree point - far, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{1, 2, 3}, "far") - test.That(t, err, test.ShouldBeNil) - collides, err := basicOct.CollidesWithGeometry(far, 80, 1.0, 1e-8) - test.That(t, err, test.ShouldBeNil) - test.That(t, collides, test.ShouldBeFalse) - }) - - t.Run("no collision with box near octree points", func(t *testing.T) { - // create a non-colliding obstacle near an octree point - near, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{-2443, 0, 3855}), r3.Vector{1, 2, 3}, "near") - test.That(t, err, test.ShouldBeNil) - collides, err := basicOct.CollidesWithGeometry(near, 80, 1.0, 1e-8) - test.That(t, err, test.ShouldBeNil) - test.That(t, collides, test.ShouldBeFalse) - }) - - t.Run("collision with box near octree points when a large buffer is used", func(t *testing.T) { - // create a non-colliding obstacle near an octree point - near, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{-2443, 0, 3855}), r3.Vector{1, 2, 3}, "near") - test.That(t, err, test.ShouldBeNil) - collides, err := basicOct.CollidesWithGeometry(near, 80, 10.0, 1e-8) - test.That(t, err, test.ShouldBeNil) - test.That(t, collides, test.ShouldBeTrue) - }) - - t.Run("no collision with box overlapping low-probability octree points", func(t *testing.T) { - // create a colliding obstacle overlapping an octree point that has sub-threshold probability - lowprob, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{-2471, 0, 3790}), r3.Vector{3, 2, 3}, "lowprob") - test.That(t, err, test.ShouldBeNil) - collides, err := basicOct.CollidesWithGeometry(lowprob, 80, 1.0, 1e-8) - test.That(t, err, test.ShouldBeNil) - test.That(t, collides, test.ShouldBeFalse) - }) - - t.Run("collision with box overlapping octree points", func(t *testing.T) { - // create a colliding obstacle overlapping an octree point - hit, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{-2443, 0, 3855}), r3.Vector{12, 2, 30}, "hit") - test.That(t, err, test.ShouldBeNil) - collides, err := basicOct.CollidesWithGeometry(hit, 80, 1.0, 1e-8) - test.That(t, err, test.ShouldBeNil) - test.That(t, collides, test.ShouldBeTrue) - }) -} diff --git a/pointcloud/icp_test.go b/pointcloud/icp_test.go deleted file mode 100644 index b77748fda53..00000000000 --- a/pointcloud/icp_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package pointcloud - -import ( - "os" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/spatialmath" -) - -func pointCloudFromArtifact(t *testing.T, artifactPath string) (PointCloud, error) { - t.Helper() - pcdFile, err := os.Open(artifact.MustPath(artifactPath)) - if err != nil { - return nil, err - } - pc, err := ReadPCD(pcdFile) - if err != nil { - return nil, err - } - - return pc, nil -} - -func TestICPRegistration(t *testing.T) { - if os.Getenv("VIAM_DEBUG") == "" { - t.Skip("Test is too large for now.") - } - - targetCloud, err := pointCloudFromArtifact(t, "pointcloud/bun000.pcd") - targetKD := ToKDTree(targetCloud) - test.That(t, err, test.ShouldBeNil) - - sourceCloud, err := pointCloudFromArtifact(t, "pointcloud/bun045.pcd") - test.That(t, err, test.ShouldBeNil) - - guess := spatialmath.NewPose( - r3.Vector{-60, 0, -10}, - &spatialmath.EulerAngles{ - Roll: 0, - Pitch: 0.6, - Yaw: 0, - }) - registered, info, err := RegisterPointCloudICP(sourceCloud, targetKD, guess, true, numThreadsPointCloud) - test.That(t, err, test.ShouldBeNil) - test.That(t, registered, test.ShouldNotBeNil) - test.That(t, info, test.ShouldNotBeNil) - - test.That(t, info.OptResult.F, test.ShouldBeLessThan, 20.) -} diff --git a/pointcloud/kdtree_test.go b/pointcloud/kdtree_test.go deleted file mode 100644 index 46985ccf334..00000000000 --- a/pointcloud/kdtree_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package pointcloud - -import ( - "errors" - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func makePointCloud(t *testing.T) PointCloud { - t.Helper() - cloud := New() - p0 := r3.Vector{0, 0, 0} - test.That(t, cloud.Set(p0, nil), test.ShouldBeNil) - p1 := r3.Vector{1, 1, 1} - test.That(t, cloud.Set(p1, nil), test.ShouldBeNil) - p2 := r3.Vector{2, 2, 2} - test.That(t, cloud.Set(p2, nil), test.ShouldBeNil) - p3 := r3.Vector{3, 3, 3} - test.That(t, cloud.Set(p3, nil), test.ShouldBeNil) - n1 := r3.Vector{-1.1, -1.1, -1.1} - test.That(t, cloud.Set(n1, nil), test.ShouldBeNil) - n2 := r3.Vector{-2.2, -2.2, -2.2} - test.That(t, cloud.Set(n2, nil), test.ShouldBeNil) - n3 := r3.Vector{-3.2, -3.2, -3.2} - test.That(t, cloud.Set(n3, nil), test.ShouldBeNil) - // outlier points - o2 := r3.Vector{2000, 2000, 2000} - test.That(t, cloud.Set(o2, nil), test.ShouldBeNil) - return cloud -} - -func TestNearestNeighor(t *testing.T) { - cloud := makePointCloud(t) - kd := ToKDTree(cloud) - - testPt := r3.Vector{3, 3, 3} - _, got := cloud.At(3, 3, 3) - test.That(t, got, test.ShouldBeTrue) - - nn, _, dist, _ := kd.NearestNeighbor(testPt) - test.That(t, nn, test.ShouldResemble, r3.Vector{3, 3, 3}) - test.That(t, dist, test.ShouldEqual, 0) - - testPt = r3.Vector{0.5, 0, 0} - nn, _, dist, _ = kd.NearestNeighbor(testPt) - test.That(t, nn, test.ShouldResemble, r3.Vector{0, 0, 0}) - test.That(t, dist, test.ShouldEqual, 0.5) -} - -func TestKNearestNeighor(t *testing.T) { - cloud := makePointCloud(t) - kd := ToKDTree(cloud) - - testPt := r3.Vector{0, 0, 0} - nns := kd.KNearestNeighbors(testPt, 3, true) - test.That(t, nns, test.ShouldHaveLength, 3) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{0, 0, 0}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - test.That(t, nns[2].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - nns = kd.KNearestNeighbors(testPt, 3, false) - test.That(t, nns, test.ShouldHaveLength, 3) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - test.That(t, nns[2].P, test.ShouldResemble, r3.Vector{2, 2, 2}) - - nns = kd.KNearestNeighbors(testPt, 100, true) - test.That(t, nns, test.ShouldHaveLength, 8) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{0, 0, 0}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - test.That(t, nns[2].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - test.That(t, nns[3].P, test.ShouldResemble, r3.Vector{2, 2, 2}) - test.That(t, nns[4].P, test.ShouldResemble, r3.Vector{-2.2, -2.2, -2.2}) - test.That(t, nns[5].P, test.ShouldResemble, r3.Vector{3, 3, 3}) - test.That(t, nns[6].P, test.ShouldResemble, r3.Vector{-3.2, -3.2, -3.2}) - test.That(t, nns[7].P, test.ShouldResemble, r3.Vector{2000, 2000, 2000}) - nns = kd.KNearestNeighbors(testPt, 100, false) - test.That(t, nns, test.ShouldHaveLength, 7) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - test.That(t, nns[2].P, test.ShouldResemble, r3.Vector{2, 2, 2}) - test.That(t, nns[3].P, test.ShouldResemble, r3.Vector{-2.2, -2.2, -2.2}) - test.That(t, nns[4].P, test.ShouldResemble, r3.Vector{3, 3, 3}) - test.That(t, nns[5].P, test.ShouldResemble, r3.Vector{-3.2, -3.2, -3.2}) - test.That(t, nns[6].P, test.ShouldResemble, r3.Vector{2000, 2000, 2000}) - - testPt = r3.Vector{4, 4, 4} - nns = kd.KNearestNeighbors(testPt, 2, true) - test.That(t, nns, test.ShouldHaveLength, 2) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{3, 3, 3}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{2, 2, 2}) - nns = kd.KNearestNeighbors(testPt, 2, false) - test.That(t, nns, test.ShouldHaveLength, 2) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{3, 3, 3}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{2, 2, 2}) -} - -func TestRadiusNearestNeighor(t *testing.T) { - cloud := makePointCloud(t) - kd := ToKDTree(cloud) - - testPt := r3.Vector{0, 0, 0} - nns := kd.RadiusNearestNeighbors(testPt, math.Sqrt(3), true) - test.That(t, nns, test.ShouldHaveLength, 2) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{0, 0, 0}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - nns = kd.RadiusNearestNeighbors(testPt, math.Sqrt(3), false) - test.That(t, nns, test.ShouldHaveLength, 1) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - - testPt = r3.Vector{0, 0, 0} - nns = kd.RadiusNearestNeighbors(testPt, 1.2*math.Sqrt(3), true) - test.That(t, nns, test.ShouldHaveLength, 3) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{0, 0, 0}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - test.That(t, nns[2].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - nns = kd.RadiusNearestNeighbors(testPt, 1.2*math.Sqrt(3), false) - test.That(t, nns, test.ShouldHaveLength, 2) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{1, 1, 1}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - - testPt = r3.Vector{-2.2, -2.2, -2.2} - nns = kd.RadiusNearestNeighbors(testPt, 1.3*math.Sqrt(3), true) - test.That(t, nns, test.ShouldHaveLength, 3) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{-2.2, -2.2, -2.2}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{-3.2, -3.2, -3.2}) - test.That(t, nns[2].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - nns = kd.RadiusNearestNeighbors(testPt, 1.3*math.Sqrt(3), false) - test.That(t, nns, test.ShouldHaveLength, 2) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{-3.2, -3.2, -3.2}) - test.That(t, nns[1].P, test.ShouldResemble, r3.Vector{-1.1, -1.1, -1.1}) - - testPt = r3.Vector{4, 4, 4} - nns = kd.RadiusNearestNeighbors(testPt, math.Sqrt(3), true) - test.That(t, nns, test.ShouldHaveLength, 1) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{3, 3, 3}) - nns = kd.RadiusNearestNeighbors(testPt, math.Sqrt(3), false) - test.That(t, nns, test.ShouldHaveLength, 1) - test.That(t, nns[0].P, test.ShouldResemble, r3.Vector{3, 3, 3}) - - testPt = r3.Vector{5, 5, 5} - nns = kd.RadiusNearestNeighbors(testPt, math.Sqrt(3), true) - test.That(t, nns, test.ShouldHaveLength, 0) - nns = kd.RadiusNearestNeighbors(testPt, math.Sqrt(3), false) - test.That(t, nns, test.ShouldHaveLength, 0) -} - -func TestNewEmptyKDtree(t *testing.T) { - pt0 := r3.Vector{0, 0, 0} - pt1 := r3.Vector{0, 0, 1} - // empty tree - pc := New() - kdt := ToKDTree(pc) - _, _, d, got := kdt.NearestNeighbor(pt0) - test.That(t, got, test.ShouldBeFalse) - test.That(t, d, test.ShouldEqual, 0.) - ps := kdt.KNearestNeighbors(pt0, 5, false) - test.That(t, ps, test.ShouldResemble, []*PointAndData{}) - ps = kdt.RadiusNearestNeighbors(pt0, 3.2, false) - test.That(t, ps, test.ShouldResemble, []*PointAndData{}) - // add one point - err := kdt.Set(pt1, nil) - test.That(t, err, test.ShouldBeNil) - p, _, d, _ := kdt.NearestNeighbor(pt0) - test.That(t, p, test.ShouldResemble, pt1) - test.That(t, d, test.ShouldEqual, 1.) - ps = kdt.KNearestNeighbors(pt0, 5, false) - test.That(t, ps, test.ShouldHaveLength, 1) - test.That(t, ps[0].P, test.ShouldResemble, pt1) - ps = kdt.RadiusNearestNeighbors(pt0, 3.2, false) - test.That(t, ps, test.ShouldHaveLength, 1) - test.That(t, ps[0].P, test.ShouldResemble, pt1) -} - -func TestStatisticalOutlierFilter(t *testing.T) { - _, err := StatisticalOutlierFilter(-1, 2.0) - test.That(t, err, test.ShouldBeError, errors.New("argument meanK must be a positive int, got -1")) - _, err = StatisticalOutlierFilter(4, 0.0) - test.That(t, err, test.ShouldBeError, errors.New("argument stdDevThresh must be a positive float, got 0.00")) - - filter, err := StatisticalOutlierFilter(3, 1.5) - test.That(t, err, test.ShouldBeNil) - cloud := makePointCloud(t) - kd := ToKDTree(cloud) - - filtered, err := filter(kd) - test.That(t, err, test.ShouldBeNil) - test.That(t, CloudContains(filtered, 0, 0, 0), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 1, 1, 1), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, -1.1, -1.1, -1.1), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 2, 2, 2), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, -2.2, -2.2, -2.2), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 3, 3, 3), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, -3.2, -3.2, -3.2), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 2000, 2000, 2000), test.ShouldBeFalse) - - filtered, err = filter(cloud) - test.That(t, err, test.ShouldBeNil) - test.That(t, CloudContains(filtered, 0, 0, 0), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 1, 1, 1), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, -1.1, -1.1, -1.1), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 2, 2, 2), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, -2.2, -2.2, -2.2), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 3, 3, 3), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, -3.2, -3.2, -3.2), test.ShouldBeTrue) - test.That(t, CloudContains(filtered, 2000, 2000, 2000), test.ShouldBeFalse) -} diff --git a/pointcloud/matrix_storage_test.go b/pointcloud/matrix_storage_test.go deleted file mode 100644 index e9b62b5258f..00000000000 --- a/pointcloud/matrix_storage_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package pointcloud - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestMatrixStorage(t *testing.T) { - ms := matrixStorage{points: make([]PointAndData, 0), indexMap: make(map[r3.Vector]uint)} - test.That(t, ms.IsOrdered(), test.ShouldEqual, true) - testPointCloudStorage(t, &ms) -} - -func BenchmarkMatrixStorage(b *testing.B) { - ms := matrixStorage{points: make([]PointAndData, 0), indexMap: make(map[r3.Vector]uint)} - benchPointCloudStorage(b, &ms) -} diff --git a/pointcloud/merging_test.go b/pointcloud/merging_test.go deleted file mode 100644 index d84a68068e0..00000000000 --- a/pointcloud/merging_test.go +++ /dev/null @@ -1,176 +0,0 @@ -package pointcloud - -import ( - "context" - "image/color" - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/spatialmath" -) - -func makeThreeCloudsWithOffsets(t *testing.T) []CloudAndOffsetFunc { - t.Helper() - pc1 := NewWithPrealloc(1) - err := pc1.Set(NewVector(1, 0, 0), NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - pc2 := NewWithPrealloc(1) - err = pc2.Set(NewVector(0, 1, 0), NewColoredData(color.NRGBA{0, 255, 0, 255})) - test.That(t, err, test.ShouldBeNil) - pc3 := NewWithPrealloc(1) - err = pc3.Set(NewVector(0, 0, 1), NewColoredData(color.NRGBA{0, 0, 255, 255})) - test.That(t, err, test.ShouldBeNil) - pose1 := spatialmath.NewPoseFromPoint(r3.Vector{100, 0, 0}) - pose2 := spatialmath.NewPoseFromPoint(r3.Vector{100, 0, 100}) - pose3 := spatialmath.NewPoseFromPoint(r3.Vector{100, 100, 100}) - func1 := func(context context.Context) (PointCloud, spatialmath.Pose, error) { - return pc1, pose1, nil - } - func2 := func(context context.Context) (PointCloud, spatialmath.Pose, error) { - return pc2, pose2, nil - } - func3 := func(context context.Context) (PointCloud, spatialmath.Pose, error) { - return pc3, pose3, nil - } - return []CloudAndOffsetFunc{func1, func2, func3} -} - -func TestApplyOffset(t *testing.T) { - // TODO(RSDK-1200): remove skip when complete - t.Skip("remove skip once RSDK-1200 improvement is complete") - logger := logging.NewTestLogger(t) - pc1 := NewWithPrealloc(3) - err := pc1.Set(NewVector(1, 0, 0), NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = pc1.Set(NewVector(1, 1, 0), NewColoredData(color.NRGBA{0, 255, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = pc1.Set(NewVector(1, 1, 1), NewColoredData(color.NRGBA{0, 0, 255, 255})) - test.That(t, err, test.ShouldBeNil) - // apply a simple translation - transPose := spatialmath.NewPoseFromPoint(r3.Vector{0, 99, 0}) - transPc, err := ApplyOffset(context.Background(), pc1, transPose, logger) - test.That(t, err, test.ShouldBeNil) - correctCount := 0 - transPc.Iterate(0, 0, func(p r3.Vector, d Data) bool { // check if all points transformed as expected - r, g, b := d.RGB255() - if r == 255 { - correctPoint := spatialmath.NewPoint(r3.Vector{1, 99, 0}, "") - test.That(t, correctPoint.Pose().Point().X, test.ShouldAlmostEqual, p.X) - test.That(t, correctPoint.Pose().Point().Y, test.ShouldAlmostEqual, p.Y) - test.That(t, correctPoint.Pose().Point().Z, test.ShouldAlmostEqual, p.Z) - correctCount++ - } - if g == 255 { - correctPoint := spatialmath.NewPoint(r3.Vector{1, 100, 0}, "") - test.That(t, correctPoint.Pose().Point().X, test.ShouldAlmostEqual, p.X) - test.That(t, correctPoint.Pose().Point().Y, test.ShouldAlmostEqual, p.Y) - test.That(t, correctPoint.Pose().Point().Z, test.ShouldAlmostEqual, p.Z) - correctCount++ - } - if b == 255 { - correctPoint := spatialmath.NewPoint(r3.Vector{1, 100, 1}, "") - test.That(t, correctPoint.Pose().Point().X, test.ShouldAlmostEqual, p.X) - test.That(t, correctPoint.Pose().Point().Y, test.ShouldAlmostEqual, p.Y) - test.That(t, correctPoint.Pose().Point().Z, test.ShouldAlmostEqual, p.Z) - correctCount++ - } - return true - }) - test.That(t, correctCount, test.ShouldEqual, 3) - // apply a translation and rotation - transrotPose := spatialmath.NewPose(r3.Vector{0, 99, 0}, &spatialmath.R4AA{math.Pi / 2., 0., 0., 1.}) - transrotPc, err := ApplyOffset(context.Background(), pc1, transrotPose, logger) - test.That(t, err, test.ShouldBeNil) - correctCount = 0 - transrotPc.Iterate(0, 0, func(p r3.Vector, d Data) bool { // check if all points transformed as expected - r, g, b := d.RGB255() - if r == 255 { - correctPoint := spatialmath.NewPoint(r3.Vector{0, 100, 0}, "") - test.That(t, correctPoint.Pose().Point().X, test.ShouldAlmostEqual, p.X) - test.That(t, correctPoint.Pose().Point().Y, test.ShouldAlmostEqual, p.Y) - test.That(t, correctPoint.Pose().Point().Z, test.ShouldAlmostEqual, p.Z) - correctCount++ - } - if g == 255 { - correctPoint := spatialmath.NewPoint(r3.Vector{-1, 100, 0}, "") - test.That(t, correctPoint.Pose().Point().X, test.ShouldAlmostEqual, p.X) - test.That(t, correctPoint.Pose().Point().Y, test.ShouldAlmostEqual, p.Y) - test.That(t, correctPoint.Pose().Point().Z, test.ShouldAlmostEqual, p.Z) - correctCount++ - } - if b == 255 { - correctPoint := spatialmath.NewPoint(r3.Vector{-1, 100, 1}, "") - test.That(t, correctPoint.Pose().Point().X, test.ShouldAlmostEqual, p.X) - test.That(t, correctPoint.Pose().Point().Y, test.ShouldAlmostEqual, p.Y) - test.That(t, correctPoint.Pose().Point().Z, test.ShouldAlmostEqual, p.Z) - correctCount++ - } - return true - }) - test.That(t, correctCount, test.ShouldEqual, 3) -} - -func TestMergePoints1(t *testing.T) { - // TODO(RSDK-1200): remove skip when complete - t.Skip("remove skip once RSDK-1200 improvement is complete") - logger := logging.NewTestLogger(t) - clouds := makeClouds(t) - cloudsWithOffset := make([]CloudAndOffsetFunc, 0, len(clouds)) - for _, cloud := range clouds { - cloudCopy := cloud - cloudFunc := func(ctx context.Context) (PointCloud, spatialmath.Pose, error) { - return cloudCopy, nil, nil - } - cloudsWithOffset = append(cloudsWithOffset, cloudFunc) - } - mergedCloud, err := MergePointClouds(context.Background(), cloudsWithOffset, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, mergedCloud, test.ShouldNotBeNil) - test.That(t, mergedCloud.Size(), test.ShouldEqual, 9) -} - -func TestMergePoints2(t *testing.T) { - // TODO(RSDK-1200): remove skip when complete - t.Skip("remove skip once RSDK-1200 improvement is complete") - logger := logging.NewTestLogger(t) - clouds := makeThreeCloudsWithOffsets(t) - pc, err := MergePointClouds(context.Background(), clouds, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldNotBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 3) - - data, got := pc.At(101, 0, 0) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{255, 0, 0, 255}) - - data, got = pc.At(100, 1, 100) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{0, 255, 0, 255}) - - data, got = pc.At(100, 100, 101) - test.That(t, got, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, &color.NRGBA{0, 0, 255, 255}) -} - -func TestMergePointsWithColor(t *testing.T) { - clouds := makeClouds(t) - mergedCloud, err := MergePointCloudsWithColor(clouds) - test.That(t, err, test.ShouldBeNil) - test.That(t, mergedCloud.Size(), test.ShouldResemble, 9) - - a, got := mergedCloud.At(0, 0, 0) - test.That(t, got, test.ShouldBeTrue) - - b, got := mergedCloud.At(0, 0, 1) - test.That(t, got, test.ShouldBeTrue) - - c, got := mergedCloud.At(30, 0, 0) - test.That(t, got, test.ShouldBeTrue) - - test.That(t, a.Color(), test.ShouldResemble, b.Color()) - test.That(t, a.Color(), test.ShouldNotResemble, c.Color()) -} diff --git a/pointcloud/plane_test.go b/pointcloud/plane_test.go deleted file mode 100644 index e048f279aaf..00000000000 --- a/pointcloud/plane_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package pointcloud - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestEmptyPlane(t *testing.T) { - plane := NewEmptyPlane() - test.That(t, plane.Equation(), test.ShouldResemble, [4]float64{}) - test.That(t, plane.Normal(), test.ShouldResemble, r3.Vector{}) - test.That(t, plane.Center(), test.ShouldResemble, r3.Vector{}) - test.That(t, plane.Offset(), test.ShouldEqual, 0.0) - cloud, err := plane.PointCloud() - test.That(t, err, test.ShouldBeNil) - test.That(t, cloud, test.ShouldNotBeNil) - test.That(t, cloud.Size(), test.ShouldEqual, 0) - pt := r3.Vector{1, 2, 3} - test.That(t, plane.Distance(pt), test.ShouldEqual, 0) -} - -func TestNewPlane(t *testing.T) { - // make the point cloud, a diamond of slope 1 in x and y - pc := New() - p0 := NewVector(0., 0., 0.) - test.That(t, pc.Set(p0, nil), test.ShouldBeNil) - p1 := NewVector(0., 2., 2.) - test.That(t, pc.Set(p1, nil), test.ShouldBeNil) - p2 := NewVector(2., 0., 2.) - test.That(t, pc.Set(p2, nil), test.ShouldBeNil) - p3 := NewVector(2., 2., 4.) - test.That(t, pc.Set(p3, nil), test.ShouldBeNil) - eq := [4]float64{1, 1, -1, 0} - // run the tests - plane := NewPlane(pc, eq) - test.That(t, plane.Equation(), test.ShouldResemble, eq) - test.That(t, plane.Normal(), test.ShouldResemble, r3.Vector{1, 1, -1}) - test.That(t, plane.Center(), test.ShouldResemble, r3.Vector{1, 1, 2}) - test.That(t, plane.Offset(), test.ShouldEqual, 0.0) - cloud, err := plane.PointCloud() - test.That(t, err, test.ShouldBeNil) - test.That(t, cloud, test.ShouldNotBeNil) - test.That(t, cloud.Size(), test.ShouldEqual, 4) - pt := r3.Vector{-1, -1, 1} - test.That(t, math.Abs(plane.Distance(pt)), test.ShouldAlmostEqual, math.Sqrt(3)) -} - -func TestIntersect(t *testing.T) { - // plane at z = 0 - plane := NewPlane(nil, [4]float64{0, 0, 1, 0}) - // perpendicular line at x= 4, y= 9, should intersect at (4,9,0) - p0, p1 := r3.Vector{4, 9, 22}, r3.Vector{4, 9, 12.3} - result := plane.Intersect(p0, p1) - test.That(t, result, test.ShouldNotBeNil) - test.That(t, result.X, test.ShouldAlmostEqual, 4.0) - test.That(t, result.Y, test.ShouldAlmostEqual, 9.0) - test.That(t, result.Z, test.ShouldAlmostEqual, 0.0) - // parallel line at z=4 should return nil - p0, p1 = r3.Vector{4, 9, 4}, r3.Vector{22, -3, 4} - result = plane.Intersect(p0, p1) - test.That(t, result, test.ShouldBeNil) - // tilted line with slope of 1 should intersect at (2, 9, 0) - p0, p1 = r3.Vector{4, 9, 2}, r3.Vector{3, 9, 1} - result = plane.Intersect(p0, p1) - test.That(t, result, test.ShouldNotBeNil) - test.That(t, result.X, test.ShouldAlmostEqual, 2.0) - test.That(t, result.Y, test.ShouldAlmostEqual, 9.0) - test.That(t, result.Z, test.ShouldAlmostEqual, 0.0) - // if p1 is before p0, should still give the same result - result = plane.Intersect(p1, p0) - test.That(t, result, test.ShouldNotBeNil) - test.That(t, result.X, test.ShouldAlmostEqual, 2.0) - test.That(t, result.Y, test.ShouldAlmostEqual, 9.0) - test.That(t, result.Z, test.ShouldAlmostEqual, 0.0) -} diff --git a/pointcloud/pointcloud_file_test.go b/pointcloud/pointcloud_file_test.go deleted file mode 100644 index 4901682789e..00000000000 --- a/pointcloud/pointcloud_file_test.go +++ /dev/null @@ -1,431 +0,0 @@ -package pointcloud - -import ( - "bytes" - "encoding/binary" - "image/color" - "math" - "os" - "strings" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" -) - -func BenchmarkNewFromFile(b *testing.B) { - logger := logging.NewTestLogger(b) - for i := 0; i < b.N; i++ { - _, err := NewFromFile(artifact.MustPath("pointcloud/test.las"), logger) - test.That(b, err, test.ShouldBeNil) - } -} - -func TestNewFromFile(t *testing.T) { - logger := logging.NewTestLogger(t) - cloud, err := NewFromFile(artifact.MustPath("pointcloud/test.las"), logger) - test.That(t, err, test.ShouldBeNil) - numPoints := cloud.Size() - test.That(t, numPoints, test.ShouldEqual, 8413) - - temp, err := os.CreateTemp(t.TempDir(), "*.las") - test.That(t, err, test.ShouldBeNil) - defer os.Remove(temp.Name()) - - err = WriteToLASFile(cloud, temp.Name()) - test.That(t, err, test.ShouldBeNil) - - nextCloud, err := NewFromFile(temp.Name(), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, nextCloud, test.ShouldResemble, cloud) - - cloud, err = NewFromFile(artifact.MustPath("pointcloud/test.pcd"), logger) - test.That(t, err, test.ShouldBeNil) - numPoints = cloud.Size() - test.That(t, numPoints, test.ShouldEqual, 293363) - - tempPCD, err := os.CreateTemp(t.TempDir(), "*.pcd") - test.That(t, err, test.ShouldBeNil) - defer os.Remove(tempPCD.Name()) - - err = ToPCD(cloud, tempPCD, PCDAscii) - test.That(t, err, test.ShouldBeNil) - - nextCloud, err = NewFromFile(tempPCD.Name(), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, nextCloud, test.ShouldResemble, cloud) -} - -func TestPCD(t *testing.T) { - cloud := New() - test.That(t, cloud.Set(NewVector(-1, -2, 5), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(5)), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(582, 12, 0), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(-1)), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(7, 6, 1), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(1)), test.ShouldBeNil) - test.That(t, cloud.Size(), test.ShouldEqual, 3) - /* - The expected string is below, cannot do direct comparison because maps print out in random order. - "VERSION .7\n" + - "FIELDS x y z rgb\n" + - "SIZE 4 4 4 4\n" + - "TYPE F F F I\n" + - "COUNT 1 1 1 1\n" + - "WIDTH 3\n" + - "HEIGHT 1\n" + - "VIEWPOINT 0 0 0 1 0 0 0\n" + - "POINTS 3\n" + - "DATA ascii\n" + - "-0.001000 0.002000 0.005000 16711938\n" + - "0.582000 0.012000 0.000000 16711938\n" + - "0.007000 0.006000 0.001000 16711938\n" - */ - testPCDHeaders(t) - testASCIIRoundTrip(t, cloud) - testBinaryRoundTrip(t, cloud) -} - -func testPCDHeaders(t *testing.T) { - t.Helper() - - fakeHeader := pcdHeader{} - var err error - // VERSION - err = parsePCDHeaderLine("VERSION .7", 0, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("VERSION 0.7", 0, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("VERSION .8", 0, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "unsupported pcd version") - // FIELDS - err = parsePCDHeaderLine("FIELDS x y z rgb", 1, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakeHeader.fields, test.ShouldEqual, pcdPointColor) - err = parsePCDHeaderLine("FIELDS x y z", 1, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakeHeader.fields, test.ShouldEqual, pcdPointOnly) - err = parsePCDHeaderLine("FIELDS a b c", 1, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "unsupported pcd fields") - // SIZE - _ = parsePCDHeaderLine("FIELDS x y z rgb", 1, &fakeHeader) - err = parsePCDHeaderLine("SIZE 4 4 4 4", 2, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - _ = parsePCDHeaderLine("FIELDS x y z rgb", 1, &fakeHeader) - err = parsePCDHeaderLine("SIZE 4 4 4", 2, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "unexpected number of fields") - // TYPE - _ = parsePCDHeaderLine("FIELDS x y z rgb", 1, &fakeHeader) - err = parsePCDHeaderLine("TYPE F F F I", 3, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("TYPE F F F", 3, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "unexpected number of fields") - // COUNT - _ = parsePCDHeaderLine("FIELDS x y z rgb", 1, &fakeHeader) - err = parsePCDHeaderLine("COUNT 1 1 1 1", 4, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("COUNT 1 1 1", 4, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "unexpected number of fields") - // WIDTH - err = parsePCDHeaderLine("WIDTH 3", 5, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("WIDTH NOTANUM", 5, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid WIDTH field") - // HEIGHT - err = parsePCDHeaderLine("HEIGHT 1", 6, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("HEIGHT NOTANUM", 6, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid HEIGHT field") - // VIEWPOINT - err = parsePCDHeaderLine("VIEWPOINT 0 0 0 1 0 0 0", 7, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("VIEWPOINT 0 0 0 1 0 0", 7, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "unexpected number of fields in VIEWPOINT line.") - // POINTS - _ = parsePCDHeaderLine("WIDTH 3", 5, &fakeHeader) - _ = parsePCDHeaderLine("HEIGHT 1", 6, &fakeHeader) - err = parsePCDHeaderLine("POINTS 3", 8, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - err = parsePCDHeaderLine("POINTS NOTANUM", 8, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid POINTS field") - err = parsePCDHeaderLine("POINTS 2", 8, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "POINTS field 2 does not match WIDTH*HEIGHT") - // DATA - err = parsePCDHeaderLine("DATA ascii", 9, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakeHeader.data, test.ShouldEqual, PCDAscii) - err = parsePCDHeaderLine("DATA binary", 9, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakeHeader.data, test.ShouldEqual, PCDBinary) - err = parsePCDHeaderLine("DATA binary_compressed", 9, &fakeHeader) - test.That(t, err, test.ShouldBeNil) - test.That(t, fakeHeader.data, test.ShouldEqual, PCDCompressed) - err = parsePCDHeaderLine("DATA garbage", 9, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "unsupported data type") - // WRONG LINE - err = parsePCDHeaderLine("VERSION 0.7", 1, &fakeHeader) - test.That(t, err.Error(), test.ShouldContainSubstring, "line is supposed to start with") -} - -func TestPCDNoColor(t *testing.T) { - cloud := New() - test.That(t, cloud.Set(NewVector(-1, -2, 5), NewBasicData()), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(582, 12, 0), NewBasicData()), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(7, 6, 1), NewBasicData()), test.ShouldBeNil) - test.That(t, cloud.Size(), test.ShouldEqual, 3) - - testNoColorASCIIRoundTrip(t, cloud) - testNoColorBinaryRoundTrip(t, cloud) - testLargeBinaryNoError(t) -} - -func testNoColorASCIIRoundTrip(t *testing.T, cloud PointCloud) { - t.Helper() - // write to .pcd - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDAscii) - test.That(t, err, test.ShouldBeNil) - gotPCD := buf.String() - test.That(t, gotPCD, test.ShouldContainSubstring, "WIDTH 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "HEIGHT 1\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "POINTS 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "DATA ascii\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "FIELDS x y z\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "-0.001000 -0.002000 0.005000\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "0.582000 0.012000 0.000000\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "0.007000 0.006000 0.001000\n") - - cloud2, err := ReadPCD(strings.NewReader(gotPCD)) - test.That(t, err, test.ShouldBeNil) - testPCDOutput(t, cloud2) -} - -func testNoColorBinaryRoundTrip(t *testing.T, cloud PointCloud) { - t.Helper() - // write to .pcd - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDBinary) - test.That(t, err, test.ShouldBeNil) - gotPCD := buf.String() - test.That(t, gotPCD, test.ShouldContainSubstring, "WIDTH 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "HEIGHT 1\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "POINTS 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "FIELDS x y z\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "DATA binary\n") - - cloud2, err := ReadPCD(strings.NewReader(gotPCD)) - test.That(t, err, test.ShouldBeNil) - testPCDOutput(t, cloud2) - data, dataFlag := cloud2.At(-1, -2, 5) - test.That(t, dataFlag, test.ShouldBeTrue) - test.That(t, data.HasColor(), test.ShouldBeFalse) -} - -func testPCDOutput(t *testing.T, cloud2 PointCloud) { - t.Helper() - test.That(t, cloud2.Size(), test.ShouldEqual, 3) - test.That(t, CloudContains(cloud2, 0, 0, 0), test.ShouldBeFalse) - test.That(t, CloudContains(cloud2, -1, -2, 5), test.ShouldBeTrue) -} - -func testASCIIRoundTrip(t *testing.T, cloud PointCloud) { - t.Helper() - // write to .pcd - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDAscii) - test.That(t, err, test.ShouldBeNil) - gotPCD := buf.String() - test.That(t, gotPCD, test.ShouldContainSubstring, "WIDTH 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "HEIGHT 1\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "POINTS 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "DATA ascii\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "-0.001000 -0.002000 0.005000 16711938\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "0.582000 0.012000 0.000000 16711938\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "0.007000 0.006000 0.001000 16711938\n") - - cloud2, err := ReadPCD(strings.NewReader(gotPCD)) - test.That(t, err, test.ShouldBeNil) - testPCDOutput(t, cloud2) -} - -func testBinaryRoundTrip(t *testing.T, cloud PointCloud) { - t.Helper() - // write to .pcd - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDBinary) - test.That(t, err, test.ShouldBeNil) - gotPCD := buf.String() - test.That(t, gotPCD, test.ShouldContainSubstring, "WIDTH 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "HEIGHT 1\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "POINTS 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "DATA binary\n") - - cloud2, err := ReadPCD(strings.NewReader(gotPCD)) - test.That(t, err, test.ShouldBeNil) - testPCDOutput(t, cloud2) - data, dataFlag := cloud2.At(-1, -2, 5) - test.That(t, dataFlag, test.ShouldBeTrue) - test.That(t, data.HasColor(), test.ShouldBeTrue) - r, g, b := data.RGB255() - test.That(t, r, test.ShouldEqual, 255) - test.That(t, g, test.ShouldEqual, 1) - test.That(t, b, test.ShouldEqual, 2) -} - -func testLargeBinaryNoError(t *testing.T) { - // This tests whether large pointclouds that exceed the usual buffered page size for a file error on reads - t.Helper() - var buf bytes.Buffer - largeCloud := newBigPC() - err := ToPCD(largeCloud, &buf, PCDBinary) - test.That(t, err, test.ShouldBeNil) - - readPointCloud, err := ReadPCD(strings.NewReader(buf.String())) - test.That(t, err, test.ShouldBeNil) - test.That(t, readPointCloud.Size(), test.ShouldEqual, largeCloud.Size()) -} - -func TestRoundTripFileWithColorFloat(t *testing.T) { - logger := logging.NewTestLogger(t) - cloud := New() - test.That(t, cloud.Set(NewVector(-1, -2, 5), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(5)), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(582, 12, 0), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(-1)), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(7, 6, 1), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(1)), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(1, 2, 9), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(0)), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(1, 2, 9), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(0)), test.ShouldBeNil) - - floatBytes := make([]byte, 8) - v := 1.4 - bits := math.Float64bits(v) - binary.LittleEndian.PutUint64(floatBytes, bits) - outBits := binary.LittleEndian.Uint64(floatBytes) - outV := math.Float64frombits(outBits) - test.That(t, outV, test.ShouldEqual, v) - - // write to .las - temp, err := os.CreateTemp(t.TempDir(), "*.las") - test.That(t, err, test.ShouldBeNil) - defer os.Remove(temp.Name()) - - err = WriteToLASFile(cloud, temp.Name()) - test.That(t, err, test.ShouldBeNil) - - nextCloud, err := NewFromFile(temp.Name(), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, nextCloud, test.ShouldResemble, cloud) -} - -func createNewPCD(t *testing.T) string { - t.Helper() - - cloud := NewKDTree() - test.That(t, cloud.Set(NewVector(-1, -2, 5), NewBasicData()), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(582, 12, 0), NewBasicData()), test.ShouldBeNil) - test.That(t, cloud.Set(NewVector(7, 6, 1), NewBasicData()), test.ShouldBeNil) - test.That(t, cloud.Size(), test.ShouldEqual, 3) - - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDBinary) - test.That(t, err, test.ShouldBeNil) - gotPCD := buf.String() - test.That(t, gotPCD, test.ShouldContainSubstring, "WIDTH 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "HEIGHT 1\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "POINTS 3\n") - test.That(t, gotPCD, test.ShouldContainSubstring, "DATA binary\n") - - return gotPCD -} - -func TestPCDKDTree(t *testing.T) { - gotPCD := createNewPCD(t) - - cloud2, err := ReadPCDToKDTree(strings.NewReader(gotPCD)) - test.That(t, err, test.ShouldBeNil) - test.That(t, cloud2.Size(), test.ShouldEqual, 3) - gotPt, found := cloud2.At(-1, -2, 5) - test.That(t, found, test.ShouldBeTrue) - test.That(t, gotPt, test.ShouldNotBeNil) -} - -func TestPCDOctree(t *testing.T) { - gotPCD := createNewPCD(t) - - basicOct, err := ReadPCDToBasicOctree(strings.NewReader(gotPCD)) - test.That(t, err, test.ShouldBeNil) - test.That(t, basicOct.Size(), test.ShouldEqual, 3) - gotPt, found := basicOct.At(-1, -2, 5) - test.That(t, found, test.ShouldBeTrue) - test.That(t, gotPt, test.ShouldNotBeNil) -} - -func TestPCDColor(t *testing.T) { - c := color.NRGBA{5, 31, 123, 255} - p := NewColoredData(c) - x := _colorToPCDInt(p) - c2 := _pcdIntToColor(x) - test.That(t, c, test.ShouldResemble, c2) -} - -func newBigPC() PointCloud { - cloud := New() - for x := 10.0; x <= 50; x++ { - for y := 10.0; y <= 50; y++ { - for z := 10.0; z <= 50; z++ { - if err := cloud.Set(NewVector(x, y, z), NewColoredData(color.NRGBA{255, 1, 2, 255}).SetValue(5)); err != nil { - panic(err) - } - } - } - } - return cloud -} - -func BenchmarkPCDASCIIWrite(b *testing.B) { - cloud := newBigPC() - b.ResetTimer() - for i := 0; i < b.N; i++ { - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDAscii) - test.That(b, err, test.ShouldBeNil) - } -} - -func BenchmarkPCDASCIIRead(b *testing.B) { - cloud := newBigPC() - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDAscii) - test.That(b, err, test.ShouldBeNil) - - gotPCD := buf.String() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := ReadPCD(strings.NewReader(gotPCD)) - test.That(b, err, test.ShouldBeNil) - } -} - -func BenchmarkPCDBinaryWrite(b *testing.B) { - cloud := newBigPC() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDBinary) - test.That(b, err, test.ShouldBeNil) - } -} - -func BenchmarkPCDBinaryRead(b *testing.B) { - cloud := newBigPC() - var buf bytes.Buffer - err := ToPCD(cloud, &buf, PCDBinary) - test.That(b, err, test.ShouldBeNil) - - gotPCD := buf.String() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := ReadPCD(strings.NewReader(gotPCD)) - test.That(b, err, test.ShouldBeNil) - } -} diff --git a/pointcloud/pointcloud_storage_test.go b/pointcloud/pointcloud_storage_test.go deleted file mode 100644 index 463d374c992..00000000000 --- a/pointcloud/pointcloud_storage_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package pointcloud - -import ( - "image/color" - "math/rand" - "sync" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils" -) - -func testPointCloudStorage(t *testing.T, ms storage) { - t.Helper() - - var point r3.Vector - var data, gotData Data - var found bool - // Empty - test.That(t, ms.Size(), test.ShouldEqual, 0) - // Iterate on Empty - testPointCloudIterate(t, ms, 0, r3.Vector{}) - testPointCloudIterate(t, ms, 4, r3.Vector{}) - - // Insertion - point = r3.Vector{1, 2, 3} - data = NewColoredData(color.NRGBA{255, 124, 43, 255}) - test.That(t, ms.Set(point, data), test.ShouldEqual, nil) - test.That(t, ms.Size(), test.ShouldEqual, 1) - gotData, found = ms.At(1, 2, 3) - test.That(t, found, test.ShouldEqual, true) - test.That(t, gotData, test.ShouldEqual, data) - - // Second Insertion - point = r3.Vector{4, 2, 3} - data = NewColoredData(color.NRGBA{232, 111, 75, 255}) - test.That(t, ms.Set(point, data), test.ShouldEqual, nil) - test.That(t, ms.Size(), test.ShouldEqual, 2) - - // Insertion of duplicate point - data = NewColoredData(color.NRGBA{22, 1, 78, 255}) - test.That(t, ms.Set(point, data), test.ShouldEqual, nil) - test.That(t, ms.Size(), test.ShouldEqual, 2) - gotData, found = ms.At(4, 2, 3) - test.That(t, found, test.ShouldEqual, true) - test.That(t, gotData, test.ShouldEqual, data) - - // Retrieval of non-existent point - gotData, found = ms.At(3, 1, 7) - test.That(t, found, test.ShouldEqual, false) - test.That(t, gotData, test.ShouldBeNil) - - // Iteration - ms.Set(r3.Vector{3, 1, 7}, NewColoredData(color.NRGBA{22, 1, 78, 255})) - expectedCentroid := r3.Vector{8 / 3.0, 5 / 3.0, 13 / 3.0} - - // Zero batches - testPointCloudIterate(t, ms, 0, expectedCentroid) - - // One batch - testPointCloudIterate(t, ms, 1, expectedCentroid) - - // Batches equal to the number of points - testPointCloudIterate(t, ms, ms.Size(), expectedCentroid) - - // Batches greater than the number of points - testPointCloudIterate(t, ms, ms.Size()*2, expectedCentroid) - - // Batches that don't divide evenly - ms.Set(r3.Vector{1, 1, 7}, NewColoredData(color.NRGBA{22, 1, 78, 255})) - ms.Set(r3.Vector{5, 1, 7}, NewColoredData(color.NRGBA{22, 1, 78, 255})) - ms.Set(r3.Vector{9, 1, 7}, NewColoredData(color.NRGBA{22, 1, 78, 255})) - expectedCentroid = r3.Vector{23 / 6.0, 8 / 6.0, 34 / 6.0} - testPointCloudIterate(t, ms, 4, expectedCentroid) -} - -func testPointCloudIterate(t *testing.T, ms storage, numBatches int, expectedCentroid r3.Vector) { - t.Helper() - - if numBatches == 0 { - var totalX, totalY, totalZ float64 - count := 0 - ms.Iterate(0, 0, func(p r3.Vector, d Data) bool { - totalX += p.X - totalY += p.Y - totalZ += p.Z - count++ - return true - }) - test.That(t, count, test.ShouldEqual, ms.Size()) - if count == 0 { - test.That(t, totalX, test.ShouldEqual, 0) - test.That(t, totalY, test.ShouldEqual, 0) - test.That(t, totalZ, test.ShouldEqual, 0) - } else { - test.That(t, totalX/float64(count), test.ShouldAlmostEqual, expectedCentroid.X) - test.That(t, totalY/float64(count), test.ShouldAlmostEqual, expectedCentroid.Y) - test.That(t, totalZ/float64(count), test.ShouldAlmostEqual, expectedCentroid.Z) - } - } else { - var totalX, totalY, totalZ float64 - var count int - var wg sync.WaitGroup - wg.Add(numBatches) - totalXChan := make(chan float64, numBatches) - totalYChan := make(chan float64, numBatches) - totalZChan := make(chan float64, numBatches) - countChan := make(chan int, numBatches) - for loop := 0; loop < numBatches; loop++ { - f := func(myBatch int) { - defer wg.Done() - var totalXBuf, totalYBuf, totalZBuf float64 - var countBuf int - ms.Iterate(numBatches, myBatch, func(p r3.Vector, d Data) bool { - totalXBuf += p.X - totalYBuf += p.Y - totalZBuf += p.Z - countBuf++ - return true - }) - totalXChan <- totalXBuf - totalYChan <- totalYBuf - totalZChan <- totalZBuf - countChan <- countBuf - } - loopCopy := loop - utils.PanicCapturingGo(func() { f(loopCopy) }) - } - wg.Wait() - for loop := 0; loop < numBatches; loop++ { - totalX += <-totalXChan - totalY += <-totalYChan - totalZ += <-totalZChan - count += <-countChan - } - test.That(t, count, test.ShouldEqual, ms.Size()) - if count == 0 { - test.That(t, totalX, test.ShouldEqual, 0) - test.That(t, totalY, test.ShouldEqual, 0) - test.That(t, totalZ, test.ShouldEqual, 0) - } else { - test.That(t, totalX/float64(count), test.ShouldAlmostEqual, expectedCentroid.X) - test.That(t, totalY/float64(count), test.ShouldAlmostEqual, expectedCentroid.Y) - test.That(t, totalZ/float64(count), test.ShouldAlmostEqual, expectedCentroid.Z) - } - } -} - -func benchPointCloudStorage(b *testing.B, ms storage) { - b.Helper() - - pcMax := 10_000. - for i := 0; i < b.N; i++ { - r := rand.New(rand.NewSource(0)) - pointList := make([]PointAndData, 0, 10_000) - for j := 0; j < cap(pointList); j++ { - pointList = append(pointList, PointAndData{ - r3.Vector{r.Float64() * pcMax, r.Float64() * pcMax, r.Float64() * pcMax}, - NewColoredData(color.NRGBA{uint8(rand.Intn(256)), uint8(rand.Intn(256)), uint8(rand.Intn(256)), 255}), - }) - } - // Set all points - for _, p := range pointList { - ms.Set(p.P, p.D) - } - // Retrieve all points - for _, p := range pointList { - _, found := ms.At(p.P.X, p.P.Y, p.P.Z) - if !found { - b.Errorf("Point %v not found", p.P) - } - } - // Overwrite all points - for _, p := range pointList { - ms.Set(p.P, p.D) - } - } -} diff --git a/pointcloud/pointcloud_test.go b/pointcloud/pointcloud_test.go deleted file mode 100644 index bb8b183eef6..00000000000 --- a/pointcloud/pointcloud_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package pointcloud - -import ( - "image/color" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "gonum.org/v1/gonum/mat" -) - -func TestPointCloudBasic(t *testing.T) { - pc := New() - - p0 := NewVector(0, 0, 0) - d0 := NewValueData(5) - - test.That(t, pc.Set(p0, d0), test.ShouldBeNil) - d, got := pc.At(0, 0, 0) - test.That(t, got, test.ShouldBeTrue) - test.That(t, d, test.ShouldResemble, d0) - - _, got = pc.At(1, 0, 1) - test.That(t, got, test.ShouldBeFalse) - - p1 := NewVector(1, 0, 1) - d1 := NewValueData(17) - test.That(t, pc.Set(p1, d1), test.ShouldBeNil) - - d, got = pc.At(1, 0, 1) - test.That(t, got, test.ShouldBeTrue) - test.That(t, d, test.ShouldResemble, d1) - test.That(t, d, test.ShouldNotResemble, d0) - - p2 := NewVector(-1, -2, 1) - d2 := NewValueData(81) - test.That(t, pc.Set(p2, d2), test.ShouldBeNil) - d, got = pc.At(-1, -2, 1) - test.That(t, got, test.ShouldBeTrue) - test.That(t, d, test.ShouldResemble, d2) - - count := 0 - pc.Iterate(0, 0, func(p r3.Vector, d Data) bool { - switch p.X { - case 0: - test.That(t, p, test.ShouldResemble, p0) - case 1: - test.That(t, p, test.ShouldResemble, p1) - case -1: - test.That(t, p, test.ShouldResemble, p2) - } - count++ - return true - }) - test.That(t, count, test.ShouldEqual, 3) - - test.That(t, CloudContains(pc, 1, 1, 1), test.ShouldBeFalse) - - pMax := NewVector(minPreciseFloat64, maxPreciseFloat64, minPreciseFloat64) - test.That(t, pc.Set(pMax, nil), test.ShouldBeNil) - - pBad := NewVector(minPreciseFloat64-1, maxPreciseFloat64, minPreciseFloat64) - err := pc.Set(pBad, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "x component") - - pBad = NewVector(minPreciseFloat64, maxPreciseFloat64+1, minPreciseFloat64) - err = pc.Set(pBad, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "y component") - - pBad = NewVector(minPreciseFloat64, maxPreciseFloat64, minPreciseFloat64-1) - err = pc.Set(pBad, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "z component") - - meta := pc.MetaData() - test.That(t, meta.totalX, test.ShouldEqual, meta.TotalX()) - test.That(t, meta.totalY, test.ShouldEqual, meta.TotalY()) - test.That(t, meta.totalZ, test.ShouldEqual, meta.TotalZ()) -} - -func TestPointCloudCentroid(t *testing.T) { - var point r3.Vector - var data Data - pc := New() - - test.That(t, pc.Size(), test.ShouldResemble, 0) - test.That(t, CloudCentroid(pc), test.ShouldResemble, r3.Vector{0, 0, 0}) - - point = NewVector(10, 100, 1000) - data = NewValueData(1) - test.That(t, pc.Set(point, data), test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldResemble, 1) - test.That(t, CloudCentroid(pc), test.ShouldResemble, point) - - point = NewVector(20, 200, 2000) - data = NewValueData(2) - test.That(t, pc.Set(point, data), test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldResemble, 2) - test.That(t, CloudCentroid(pc), test.ShouldResemble, r3.Vector{15, 150, 1500}) - - point = NewVector(30, 300, 3000) - data = NewValueData(3) - test.That(t, pc.Set(point, data), test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldResemble, 3) - test.That(t, CloudCentroid(pc), test.ShouldResemble, r3.Vector{20, 200, 2000}) - - point = NewVector(30, 300, 3000) - data = NewValueData(3) - test.That(t, pc.Set(point, data), test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldResemble, 3) - test.That(t, CloudCentroid(pc), test.ShouldResemble, r3.Vector{20, 200, 2000}) -} - -func TestPointCloudMatrix(t *testing.T) { - pc := New() - - // Empty Cloud - m, h := CloudMatrix(pc) - test.That(t, h, test.ShouldBeNil) - test.That(t, m, test.ShouldBeNil) - - // Bare Points - p := NewVector(1, 2, 3) - test.That(t, pc.Set(p, nil), test.ShouldBeNil) - m, h = CloudMatrix(pc) - test.That(t, h, test.ShouldResemble, []CloudMatrixCol{CloudMatrixColX, CloudMatrixColY, CloudMatrixColZ}) - test.That(t, m, test.ShouldResemble, mat.NewDense(1, 3, []float64{1, 2, 3})) - - // Points with Value (Multiple Points) - pc = New() - p = NewVector(1, 2, 3) - d := NewValueData(4) - test.That(t, pc.Set(p, d), test.ShouldBeNil) - p = NewVector(0, 0, 0) - d = NewValueData(5) - - test.That(t, pc.Set(p, d), test.ShouldBeNil) - - refMatrix := mat.NewDense(2, 4, []float64{0, 0, 0, 5, 1, 2, 3, 4}) - refMatrix2 := mat.NewDense(2, 4, []float64{1, 2, 3, 4, 0, 0, 0, 5}) - m, h = CloudMatrix(pc) - test.That(t, h, test.ShouldResemble, []CloudMatrixCol{CloudMatrixColX, CloudMatrixColY, CloudMatrixColZ, CloudMatrixColV}) - test.That(t, m, test.ShouldBeIn, refMatrix, refMatrix2) // This is not a great test format, but it works. - - // Test with Color - pc = New() - p = NewVector(1, 2, 3) - d = NewColoredData(color.NRGBA{123, 45, 67, 255}) - test.That(t, pc.Set(p, d), test.ShouldBeNil) - - mc, hc := CloudMatrix(pc) - test.That(t, hc, test.ShouldResemble, []CloudMatrixCol{ - CloudMatrixColX, CloudMatrixColY, - CloudMatrixColZ, CloudMatrixColR, CloudMatrixColG, CloudMatrixColB, - }) - test.That(t, mc, test.ShouldResemble, mat.NewDense(1, 6, []float64{1, 2, 3, 123, 45, 67})) - - // Test with Color and Value - pc = New() - p = NewVector(1, 2, 3) - d = NewColoredData(color.NRGBA{123, 45, 67, 255}) - d.SetValue(5) - test.That(t, pc.Set(p, d), test.ShouldBeNil) - - mcv, hcv := CloudMatrix(pc) - test.That(t, hcv, test.ShouldResemble, []CloudMatrixCol{ - CloudMatrixColX, CloudMatrixColY, - CloudMatrixColZ, CloudMatrixColR, CloudMatrixColG, CloudMatrixColB, CloudMatrixColV, - }) - test.That(t, mcv, test.ShouldResemble, mat.NewDense(1, 7, []float64{1, 2, 3, 123, 45, 67, 5})) -} - -func TestVectorsToPointCloud(t *testing.T) { - vecLst := []r3.Vector{{0, 0.75, 0}, {0, -0.75, 0}, {1, 0.75, 1}, {-1, 0.75, -1}} - color := &color.NRGBA{0, 0, 255, 255} - pc, err := VectorsToPointCloud(vecLst, *color) - test.That(t, err, test.ShouldBeNil) - // make sure all points were added with the specified color - for _, v := range vecLst { - data, boolVal := pc.At(v.X, v.Y, v.Z) - test.That(t, boolVal, test.ShouldBeTrue) - test.That(t, data.Color(), test.ShouldResemble, color) - } -} diff --git a/pointcloud/pointcloud_utils_test.go b/pointcloud/pointcloud_utils_test.go deleted file mode 100644 index dfd24eda980..00000000000 --- a/pointcloud/pointcloud_utils_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package pointcloud - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/spatialmath" -) - -func makeClouds(t *testing.T) []PointCloud { - t.Helper() - // create cloud 0 - cloud0 := New() - p00 := NewVector(0, 0, 0) - test.That(t, cloud0.Set(p00, nil), test.ShouldBeNil) - p01 := NewVector(0, 0, 1) - test.That(t, cloud0.Set(p01, nil), test.ShouldBeNil) - p02 := NewVector(0, 1, 0) - test.That(t, cloud0.Set(p02, nil), test.ShouldBeNil) - p03 := NewVector(0, 1, 1) - test.That(t, cloud0.Set(p03, nil), test.ShouldBeNil) - // create cloud 1 - cloud1 := New() - p10 := NewVector(30, 0, 0) - test.That(t, cloud1.Set(p10, nil), test.ShouldBeNil) - p11 := NewVector(30, 0, 1) - test.That(t, cloud1.Set(p11, nil), test.ShouldBeNil) - p12 := NewVector(30, 1, 0) - test.That(t, cloud1.Set(p12, nil), test.ShouldBeNil) - p13 := NewVector(30, 1, 1) - test.That(t, cloud1.Set(p13, nil), test.ShouldBeNil) - p14 := NewVector(28, 0.5, 0.5) - test.That(t, cloud1.Set(p14, nil), test.ShouldBeNil) - - return []PointCloud{cloud0, cloud1} -} - -func TestBoundingBoxFromPointCloud(t *testing.T) { - clouds := makeClouds(t) - cases := []struct { - pc PointCloud - expectedCenter r3.Vector - expectedDims r3.Vector - }{ - {clouds[0], r3.Vector{0, 0.5, 0.5}, r3.Vector{0, 1, 1}}, - {clouds[1], r3.Vector{29.6, 0.5, 0.5}, r3.Vector{2, 1, 1}}, - } - - for _, c := range cases { - expectedBox, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(c.expectedCenter), c.expectedDims, "") - test.That(t, err, test.ShouldBeNil) - box, err := BoundingBoxFromPointCloudWithLabel(c.pc, "box") - test.That(t, err, test.ShouldBeNil) - test.That(t, box, test.ShouldNotBeNil) - test.That(t, spatialmath.GeometriesAlmostEqual(box, expectedBox), test.ShouldBeTrue) - test.That(t, box.Label(), test.ShouldEqual, "box") - } -} - -func TestPrune(t *testing.T) { - clouds := makeClouds(t) - // before prune - test.That(t, len(clouds), test.ShouldEqual, 2) - test.That(t, clouds[0].Size(), test.ShouldEqual, 4) - test.That(t, clouds[1].Size(), test.ShouldEqual, 5) - // prune - clouds = PrunePointClouds(clouds, 5) - test.That(t, len(clouds), test.ShouldEqual, 1) - test.That(t, clouds[0].Size(), test.ShouldEqual, 5) -} diff --git a/pointcloud/verify_main_test.go b/pointcloud/verify_main_test.go deleted file mode 100644 index 120897f6245..00000000000 --- a/pointcloud/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package pointcloud - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/pointcloud/voxel_segmentation_test.go b/pointcloud/voxel_segmentation_test.go deleted file mode 100644 index 3289899ee07..00000000000 --- a/pointcloud/voxel_segmentation_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package pointcloud - -import ( - "math" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -// RandomCubeSide choose a random integer between 0 and 5 that correspond to one facet of a cube. -func RandomCubeSide() int { - min := 0 - max := 6 - return rand.Intn(max-min) + min -} - -// GeneratePointsOnPlaneZ0 generates points on the z=0 plane. -func GeneratePointsOnPlaneZ0(nPoints int, normal r3.Vector, offset float64) PointCloud { - pc := New() - for i := 0; i < nPoints; i++ { - // Point in the R3 unit cube - p := r3.Vector{rand.Float64(), rand.Float64(), 0} - - pt := NewVector(p.X, p.Y, p.Z) - err := pc.Set(pt, nil) - if err != nil { - panic(err) - } - } - return pc -} - -// GenerateCubeTestData generate 3d points on the R^3 unit cube. -func GenerateCubeTestData(nPoints int) PointCloud { - pc := New() - for i := 0; i < nPoints; i++ { - // get cube side number - s := RandomCubeSide() - // get normal vector axis - // if c in {0,3}, generated point will be on a plane with normal vector (1,0,0) - // if c in {1,4}, generated point will be on a plane with normal vector (0,1,0) - // if c in {2,5}, generated point will be on a plane with normal vector (0,0,1) - c := int(math.Mod(float64(s), 3)) - pt := make([]float64, 3) - pt[c] = 0 - // if side number is >=3, get side of cube at side=1 - if s > 2 { - pt[c] = 1.0 - } - // get other 2 point coordinates in [0,1] - idx2 := int(math.Mod(float64(c+1), 3)) - pt[idx2] = rand.Float64() - idx3 := int(math.Mod(float64(c+2), 3)) - pt[idx3] = rand.Float64() - // add point to slice - p := NewVector(pt[0], pt[1], pt[2]) - err := pc.Set(p, nil) - if err != nil { - panic(err) - } - } - return pc -} - -func TestVoxelPlaneSegmentationOnePlane(t *testing.T) { - nPoints := 100000 - pc := GeneratePointsOnPlaneZ0(nPoints, r3.Vector{0, 0, 1}, 0.01) - vg := NewVoxelGridFromPointCloud(pc, 0.1, 1.0) - test.That(t, len(vg.Voxels), test.ShouldAlmostEqual, 100) - vg.SegmentPlanesRegionGrowing(0.5, 25, 0.1, 0.05) - pcOut, err := vg.ConvertToPointCloudWithValue() - test.That(t, err, test.ShouldBeNil) - test.That(t, pcOut.Size(), test.ShouldBeGreaterThan, 0) - // Labeling should find one plane - test.That(t, vg.maxLabel, test.ShouldEqual, 1) -} - -func TestVoxelPlaneSegmentationCube(t *testing.T) { - nPoints := 10000 - pc := GenerateCubeTestData(nPoints) - vg := NewVoxelGridFromPointCloud(pc, 0.5, 0.01) - vg.SegmentPlanesRegionGrowing(0.7, 25, 0.1, 1.0) - pcOut, err := vg.ConvertToPointCloudWithValue() - test.That(t, err, test.ShouldBeNil) - test.That(t, pcOut.Size(), test.ShouldBeGreaterThan, 0) - // Labeling should find 6 planes - test.That(t, vg.maxLabel, test.ShouldEqual, 6) -} diff --git a/pointcloud/voxel_test.go b/pointcloud/voxel_test.go deleted file mode 100644 index c91ce72d9f8..00000000000 --- a/pointcloud/voxel_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package pointcloud - -import ( - "math/rand" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestVoxelCoords(t *testing.T) { - // Test creation - c1 := VoxelCoords{} - test.ShouldEqual(c1.I, 0) - test.ShouldEqual(c1.J, 0) - test.ShouldEqual(c1.K, 0) - // Test IsEqual function - c2 := VoxelCoords{2, 1, 3} - c3 := VoxelCoords{2, 1, 3} - test.ShouldBeTrue(c2.IsEqual(c3)) -} - -func TestVoxelCreation(t *testing.T) { - pt := r3.Vector{ - X: 1.2, - Y: 0.5, - Z: 2.8, - } - ptMin := r3.Vector{ - X: 0, - Y: 0, - Z: 0, - } - voxelSize := 1.0 - vox := NewVoxelFromPoint(pt, ptMin, voxelSize) - test.ShouldEqual(vox.Key.I, 1) - test.ShouldEqual(vox.Key.J, 0) - test.ShouldEqual(vox.Key.I, 2) - test.ShouldEqual(vox.Label, 0) - test.ShouldEqual(vox.PointLabels, nil) - - vox.SetLabel(10) - test.ShouldEqual(vox.Label, 10) -} - -func TestVoxelGridCreation(t *testing.T) { - nPoints := 10000 - pc := GenerateCubeTestData(nPoints) - vg := NewVoxelGridFromPointCloud(pc, 0.5, 0.01) - test.ShouldEqual(len(vg.Voxels), 571) - test.ShouldEqual(vg.maxLabel, 0) -} - -func TestVoxelGridCubeSegmentation(t *testing.T) { - nPoints := 10000 - pc := GenerateCubeTestData(nPoints) - vg := NewVoxelGridFromPointCloud(pc, 0.5, 0.01) - vg.SegmentPlanesRegionGrowing(0.7, 25, 0.1, 1.0) - test.ShouldEqual(vg.maxLabel, 6) - _, err := vg.ConvertToPointCloudWithValue() - test.That(t, err, test.ShouldBeNil) - planes, nonPlaneCloud, err := vg.GetPlanesFromLabels() - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planes), test.ShouldEqual, 6) - test.That(t, nonPlaneCloud.Size(), test.ShouldEqual, 0) -} - -func TestEstimatePlaneNormalFromPoints(t *testing.T) { - nPoints := 1000 - points := make([]r3.Vector, 0, nPoints) - for i := 0; i < nPoints; i++ { - // Point in the R3 unit cube, on plane z=0 - p := r3.Vector{rand.Float64(), rand.Float64(), 0} - points = append(points, p) - } - normalPlane := estimatePlaneNormalFromPoints(points) - test.ShouldAlmostEqual(normalPlane.X, 0.) - test.ShouldAlmostEqual(normalPlane.Y, 0.) - test.ShouldAlmostEqual(normalPlane.Z, 1.) -} - -func TestGetVoxelCenterWeightResidual(t *testing.T) { - nPoints := 10000 - points := make([]r3.Vector, 0, nPoints) - for i := 0; i < nPoints; i++ { - // Point in the R3 unit cube, on plane z=0 - p := r3.Vector{rand.Float64(), rand.Float64(), 0} - points = append(points, p) - } - center := GetVoxelCenter(points) - test.ShouldAlmostEqual(center.X, 0.5) - test.ShouldAlmostEqual(center.Y, 0.5) - test.ShouldAlmostEqual(center.Z, 0.) - - w := GetWeight(points, 1., 0.) - test.ShouldAlmostEqual(w, 1.0) - plane := &voxelPlane{ - normal: r3.Vector{0, 0, 1}, - center: r3.Vector{}, - offset: 0, - points: nil, - voxelKeys: nil, - } - res := GetResidual(points, plane) - test.ShouldAlmostEqual(res, 0.0) -} - -func TestGetVoxelCoordinates(t *testing.T) { - // Get point in [0,1]x[0,1]x0 - p := r3.Vector{rand.Float64(), rand.Float64(), 0} - ptMin := r3.Vector{} - // if voxel of size 1, voxel coordinates should be (0,0,0) - coords := GetVoxelCoordinates(p, ptMin, 1.0) - test.ShouldAlmostEqual(coords.I, 0.) - test.ShouldAlmostEqual(coords.J, 0.) - test.ShouldAlmostEqual(coords.K, 0.) -} - -func TestNNearestVoxel(t *testing.T) { - // make the voxel grid - voxelSize := 1.0 - pc := New() - vox0 := NewVector(0., 0., 0.) - test.That(t, pc.Set(vox0, nil), test.ShouldBeNil) - vox1 := NewVector(1.1, 1.2, 1.3) - test.That(t, pc.Set(vox1, nil), test.ShouldBeNil) - vox2 := NewVector(0.5, 0.5, 1.8) - test.That(t, pc.Set(vox2, nil), test.ShouldBeNil) - vox3 := NewVector(0.3, 1.9, 0.1) - test.That(t, pc.Set(vox3, nil), test.ShouldBeNil) - vox4 := NewVector(1.3, 0.8, 0.5) - test.That(t, pc.Set(vox4, nil), test.ShouldBeNil) - vox5 := NewVector(4.5, 4.2, 3.9) - test.That(t, pc.Set(vox5, nil), test.ShouldBeNil) - vg := NewVoxelGridFromPointCloud(pc, voxelSize, 0.01) - // expect 4 voxels nearest - neighbors := vg.GetNNearestVoxels(vg.GetVoxelFromKey(VoxelCoords{0, 0, 0}), 1) - test.That(t, len(neighbors), test.ShouldEqual, 4) - // expect 5 voxels nearest - neighbors = vg.GetNNearestVoxels(vg.GetVoxelFromKey(VoxelCoords{0, 0, 0}), 5) - test.That(t, len(neighbors), test.ShouldEqual, 5) - // expect 5 voxels nearest from the other side - neighbors = vg.GetNNearestVoxels(vg.GetVoxelFromKey(VoxelCoords{4, 4, 3}), 5) - test.That(t, len(neighbors), test.ShouldEqual, 5) - // no nearest voxels - neighbors = vg.GetNNearestVoxels(vg.GetVoxelFromKey(VoxelCoords{4, 4, 3}), 1) - test.That(t, len(neighbors), test.ShouldEqual, 0) -} diff --git a/protoutils/messages_test.go b/protoutils/messages_test.go deleted file mode 100644 index f0e628fae40..00000000000 --- a/protoutils/messages_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package protoutils - -import ( - "math" - "strconv" - "testing" - - "go.viam.com/test" - "google.golang.org/protobuf/types/known/wrapperspb" -) - -func TestStringToAnyPB(t *testing.T) { - anyVal, err := ConvertStringToAnyPB("12") - test.That(t, err, test.ShouldBeNil) - wrappedVal := wrapperspb.Int64(int64(12)) - test.That(t, anyVal.MessageIs(wrappedVal), test.ShouldBeTrue) - - anyVal, err = ConvertStringToAnyPB(strconv.Itoa(math.MaxInt)) - test.That(t, err, test.ShouldBeNil) - wrappedVal = wrapperspb.Int64(math.MaxInt64) - test.That(t, anyVal.MessageIs(wrappedVal), test.ShouldBeTrue) - - anyVal, err = ConvertStringToAnyPB("123.456") - test.That(t, err, test.ShouldBeNil) - wrappedVal1 := wrapperspb.Double(float64(123.456)) - test.That(t, anyVal.MessageIs(wrappedVal1), test.ShouldBeTrue) - - anyVal, err = ConvertStringToAnyPB(strconv.FormatUint(math.MaxUint64, 10)) - test.That(t, err, test.ShouldBeNil) - wrappedVal2 := wrapperspb.UInt64(uint64(math.MaxUint64)) - test.That(t, anyVal.MessageIs(wrappedVal2), test.ShouldBeTrue) - - anyVal, err = ConvertStringToAnyPB("true") - test.That(t, err, test.ShouldBeNil) - wrappedVal3 := wrapperspb.Bool(true) - test.That(t, anyVal.MessageIs(wrappedVal3), test.ShouldBeTrue) - - anyVal, err = ConvertStringToAnyPB("abcd") - test.That(t, err, test.ShouldBeNil) - wrappedVal4 := wrapperspb.String("abcd") - test.That(t, anyVal.MessageIs(wrappedVal4), test.ShouldBeTrue) -} diff --git a/protoutils/sensor_test.go b/protoutils/sensor_test.go deleted file mode 100644 index d39129430ae..00000000000 --- a/protoutils/sensor_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package protoutils - -import ( - "testing" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - "go.viam.com/rdk/spatialmath" -) - -func TestRoundtrip(t *testing.T) { - m1 := map[string]interface{}{ - "d": 5.4, - "av": spatialmath.AngularVelocity{1, 2, 3}, - "vv": r3.Vector{1, 2, 3}, - "ea": &spatialmath.EulerAngles{Roll: 3, Pitch: 5, Yaw: 4}, - "q": &spatialmath.Quaternion{1, 2, 3, 4}, - "ov": &spatialmath.OrientationVector{Theta: 1, OX: 2, OY: 3, OZ: 4}, - "ovd": &spatialmath.OrientationVectorDegrees{Theta: 1, OX: 2, OY: 3, OZ: 4}, - "aa": &spatialmath.R4AA{Theta: 1, RX: 2, RY: 3, RZ: 4}, - "gp": geo.NewPoint(12, 13), - } - - p, err := ReadingGoToProto(m1) - test.That(t, err, test.ShouldBeNil) - - m2, err := ReadingProtoToGo(p) - test.That(t, err, test.ShouldBeNil) - - test.That(t, m2, test.ShouldResemble, m1) -} - -func TestInvalidValue(t *testing.T) { - data := map[string]interface{}{ - "now": time.Now(), - } - - _, err := ReadingGoToProto(data) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, "proto: invalid type: time.Time") -} diff --git a/referenceframe/dynamic_frame_system_test.go b/referenceframe/dynamic_frame_system_test.go deleted file mode 100644 index 13d98529ee5..00000000000 --- a/referenceframe/dynamic_frame_system_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package referenceframe - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - spatial "go.viam.com/rdk/spatialmath" -) - -func TestSimpleRotationalFrame(t *testing.T) { - fs := NewEmptyFrameSystem("test") - - // Revolute joint around X axis - joint, err := NewRotationalFrame("joint", spatial.R4AA{RX: 1, RY: 0, RZ: 0}, Limit{Min: -math.Pi * 2, Max: math.Pi * 2}) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(joint, fs.World()) - - // define the point coordinates - pose := NewPoseInFrame("joint", spatial.NewPoseFromPoint(r3.Vector{2., 2., 10.})) - expected1 := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{2., 2., 10.})) - expected2 := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{2., -10, 2})) - expected3 := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{2., 10, -2})) - - positions := StartPositions(fs) // zero position - testTransformPoint(t, fs, positions, pose, expected1) - positions["joint"] = []Input{{math.Pi / 2}} // Rotate 90 degrees one way - testTransformPoint(t, fs, positions, pose, expected2) - positions["joint"] = []Input{{-math.Pi / 2}} // Rotate 90 degrees the other way - testTransformPoint(t, fs, positions, pose, expected3) -} - -func TestSimpleTranslationalFrame(t *testing.T) { - fs := NewEmptyFrameSystem("test") - - // 1D gantry that slides in X - gantry, err := NewTranslationalFrame("gantry", r3.Vector{1, 0, 0}, Limit{Min: math.Inf(-1), Max: math.Inf(1)}) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(gantry, fs.World()) - - // define the point coordinates - poseStart := NewPoseInFrame("gantry", spatial.NewPoseFromPoint(r3.Vector{})) // the point from PoV of gantry - poseEnd1 := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{0, 0, 0})) // gantry starts at origin of world - poseEnd2 := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{45, 0, 0})) // after gantry moves 45 - - // test transformations - positions := StartPositions(fs) - testTransformPoint(t, fs, positions, poseStart, poseEnd1) - positions["gantry"] = []Input{{45.}} - testTransformPoint(t, fs, positions, poseStart, poseEnd2) -} diff --git a/referenceframe/frame_system_test.go b/referenceframe/frame_system_test.go deleted file mode 100644 index 1d07e3d554f..00000000000 --- a/referenceframe/frame_system_test.go +++ /dev/null @@ -1,402 +0,0 @@ -package referenceframe - -import ( - "encoding/json" - "math" - "os" - "testing" - - "github.com/golang/geo/r3" - commonpb "go.viam.com/api/common/v1" - robotpb "go.viam.com/api/robot/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - spatial "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -func TestFrameModelPart(t *testing.T) { - jsonData, err := os.ReadFile(rdkutils.ResolveFile("config/data/model_frame.json")) - test.That(t, err, test.ShouldBeNil) - var modelJSONMap map[string]interface{} - json.Unmarshal(jsonData, &modelJSONMap) - model, err := UnmarshalModelJSON(jsonData, "") - test.That(t, err, test.ShouldBeNil) - - // minimally specified part - part := &FrameSystemPart{ - FrameConfig: &LinkInFrame{PoseInFrame: &PoseInFrame{name: "test"}}, - ModelFrame: nil, - } - _, err = part.ToProtobuf() - test.That(t, err, test.ShouldBeNil) - - // slightly specified part - part = &FrameSystemPart{ - FrameConfig: &LinkInFrame{PoseInFrame: &PoseInFrame{name: "test", parent: "world"}}, - ModelFrame: nil, - } - result, err := part.ToProtobuf() - test.That(t, err, test.ShouldBeNil) - pose := &commonpb.Pose{} // zero pose - exp := &robotpb.FrameSystemConfig{ - Frame: &commonpb.Transform{ - ReferenceFrame: "test", - PoseInObserverFrame: &commonpb.PoseInFrame{ - ReferenceFrame: "world", - Pose: pose, - }, - }, - } - test.That(t, result.Frame.ReferenceFrame, test.ShouldEqual, exp.Frame.ReferenceFrame) - test.That(t, result.Frame.PoseInObserverFrame, test.ShouldResemble, exp.Frame.PoseInObserverFrame) - // exp.Kinematics is nil, but the struct in the struct PB - expKin, err := protoutils.StructToStructPb(exp.Kinematics) - test.That(t, err, test.ShouldBeNil) - test.That(t, result.Kinematics, test.ShouldResemble, expKin) - // return to FrameSystemPart - partAgain, err := ProtobufToFrameSystemPart(result) - test.That(t, err, test.ShouldBeNil) - test.That(t, partAgain.FrameConfig.name, test.ShouldEqual, part.FrameConfig.name) - test.That(t, partAgain.FrameConfig.parent, test.ShouldEqual, part.FrameConfig.parent) - test.That(t, partAgain.FrameConfig.pose, test.ShouldResemble, spatial.NewZeroPose()) - // nil orientations become specified as zero orientations - test.That(t, partAgain.FrameConfig.pose.Orientation(), test.ShouldResemble, spatial.NewZeroOrientation()) - test.That(t, partAgain.ModelFrame, test.ShouldResemble, part.ModelFrame) - - orientConf, err := spatial.NewOrientationConfig(spatial.NewZeroOrientation()) - test.That(t, err, test.ShouldBeNil) - - lc := &LinkConfig{ - ID: "test", - Parent: "world", - Translation: r3.Vector{1, 2, 3}, - Orientation: orientConf, - } - lif, err := lc.ParseConfig() - test.That(t, err, test.ShouldBeNil) - // fully specified part - part = &FrameSystemPart{ - FrameConfig: lif, - ModelFrame: model, - } - result, err = part.ToProtobuf() - test.That(t, err, test.ShouldBeNil) - pose = &commonpb.Pose{X: 1, Y: 2, Z: 3, OZ: 1, Theta: 0} - exp = &robotpb.FrameSystemConfig{ - Frame: &commonpb.Transform{ - ReferenceFrame: "test", - PoseInObserverFrame: &commonpb.PoseInFrame{ - ReferenceFrame: "world", - Pose: pose, - }, - }, - } - test.That(t, result.Frame.ReferenceFrame, test.ShouldEqual, exp.Frame.ReferenceFrame) - test.That(t, result.Frame.PoseInObserverFrame, test.ShouldResemble, exp.Frame.PoseInObserverFrame) - test.That(t, result.Kinematics, test.ShouldNotBeNil) - // return to FrameSystemPart - partAgain, err = ProtobufToFrameSystemPart(result) - test.That(t, err, test.ShouldBeNil) - test.That(t, partAgain.FrameConfig.name, test.ShouldEqual, part.FrameConfig.name) - test.That(t, partAgain.FrameConfig.parent, test.ShouldEqual, part.FrameConfig.parent) - test.That(t, partAgain.FrameConfig.pose, test.ShouldResemble, part.FrameConfig.pose) - test.That(t, partAgain.ModelFrame.Name, test.ShouldEqual, part.ModelFrame.Name) - test.That(t, - len(partAgain.ModelFrame.(*SimpleModel).OrdTransforms), - test.ShouldEqual, - len(part.ModelFrame.(*SimpleModel).OrdTransforms), - ) -} - -func TestFramesFromPart(t *testing.T) { - jsonData, err := os.ReadFile(rdkutils.ResolveFile("config/data/model_frame_geoms.json")) - test.That(t, err, test.ShouldBeNil) - model, err := UnmarshalModelJSON(jsonData, "") - test.That(t, err, test.ShouldBeNil) - // minimally specified part - part := &FrameSystemPart{ - FrameConfig: &LinkInFrame{PoseInFrame: &PoseInFrame{name: "test"}}, - ModelFrame: nil, - } - _, _, err = createFramesFromPart(part) - test.That(t, err, test.ShouldBeNil) - - // slightly specified part - part = &FrameSystemPart{ - FrameConfig: &LinkInFrame{PoseInFrame: &PoseInFrame{name: "test", parent: "world"}}, - ModelFrame: nil, - } - modelFrame, originFrame, err := createFramesFromPart(part) - test.That(t, err, test.ShouldBeNil) - test.That(t, modelFrame, test.ShouldResemble, NewZeroStaticFrame(part.FrameConfig.name)) - originTailFrame, ok := NewZeroStaticFrame(part.FrameConfig.name + "_origin").(*staticFrame) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, originFrame, test.ShouldResemble, &tailGeometryStaticFrame{originTailFrame}) - orientConf, err := spatial.NewOrientationConfig(spatial.NewZeroOrientation()) - test.That(t, err, test.ShouldBeNil) - - lc := &LinkConfig{ - ID: "test", - Parent: "world", - Translation: r3.Vector{1, 2, 3}, - Orientation: orientConf, - } - lif, err := lc.ParseConfig() - test.That(t, err, test.ShouldBeNil) - - // fully specified part - part = &FrameSystemPart{ - FrameConfig: lif, - ModelFrame: model, - } - modelFrame, originFrame, err = createFramesFromPart(part) - test.That(t, err, test.ShouldBeNil) - test.That(t, modelFrame.Name(), test.ShouldEqual, part.FrameConfig.name) - test.That(t, modelFrame.DoF(), test.ShouldResemble, part.ModelFrame.DoF()) - test.That(t, originFrame.Name(), test.ShouldEqual, part.FrameConfig.name+"_origin") - test.That(t, originFrame.DoF(), test.ShouldHaveLength, 0) - - // Test geometries are not overwritten for non-zero DOF frames - lc = &LinkConfig{ - ID: "test", - Parent: "world", - Translation: r3.Vector{1, 2, 3}, - Orientation: orientConf, - Geometry: &spatial.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, - } - lif, err = lc.ParseConfig() - test.That(t, err, test.ShouldBeNil) - part = &FrameSystemPart{ - FrameConfig: lif, - ModelFrame: model, - } - modelFrame, originFrame, err = createFramesFromPart(part) - test.That(t, err, test.ShouldBeNil) - modelGeoms, err := modelFrame.Geometries(make([]Input, len(modelFrame.DoF()))) - test.That(t, err, test.ShouldBeNil) - originGeoms, err := originFrame.Geometries(make([]Input, len(originFrame.DoF()))) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(modelGeoms.Geometries()), test.ShouldBeGreaterThan, 1) - test.That(t, len(originGeoms.Geometries()), test.ShouldEqual, 1) - - // Test that zero-DOF geometries ARE overwritten - jsonData, err = os.ReadFile(rdkutils.ResolveFile("config/data/gripper_model.json")) - test.That(t, err, test.ShouldBeNil) - model, err = UnmarshalModelJSON(jsonData, "") - test.That(t, err, test.ShouldBeNil) - lc = &LinkConfig{ - ID: "test", - Parent: "world", - Translation: r3.Vector{1, 2, 3}, - Orientation: orientConf, - Geometry: &spatial.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, - } - lif, err = lc.ParseConfig() - test.That(t, err, test.ShouldBeNil) - part = &FrameSystemPart{ - FrameConfig: lif, - ModelFrame: model, - } - modelFrame, originFrame, err = createFramesFromPart(part) - test.That(t, err, test.ShouldBeNil) - modelFrameGeoms, err := modelFrame.Geometries(make([]Input, len(modelFrame.DoF()))) - test.That(t, err, test.ShouldBeNil) - modelGeoms, err = model.Geometries(make([]Input, len(model.DoF()))) - test.That(t, err, test.ShouldBeNil) - originGeoms, err = originFrame.Geometries(make([]Input, len(originFrame.DoF()))) - test.That(t, err, test.ShouldBeNil) - - // Orig model should have 1 geometry, but new model should be wrapped with zero - test.That(t, len(modelFrameGeoms.Geometries()), test.ShouldEqual, 0) - test.That(t, len(modelGeoms.Geometries()), test.ShouldEqual, 1) - test.That(t, len(originGeoms.Geometries()), test.ShouldEqual, 1) -} - -func TestConvertTransformProtobufToFrameSystemPart(t *testing.T) { - t.Run("fails on missing reference frame name", func(t *testing.T) { - transform := &LinkInFrame{PoseInFrame: NewPoseInFrame("parent", spatial.NewZeroPose())} - part, err := LinkInFrameToFrameSystemPart(transform) - test.That(t, err, test.ShouldBeError, ErrEmptyStringFrameName) - test.That(t, part, test.ShouldBeNil) - }) - t.Run("converts to frame system part", func(t *testing.T) { - testPose := spatial.NewPose(r3.Vector{X: 1., Y: 2., Z: 3.}, &spatial.R4AA{Theta: math.Pi / 2, RX: 0, RY: 1, RZ: 0}) - transform := NewLinkInFrame("parent", testPose, "child", nil) - part, err := LinkInFrameToFrameSystemPart(transform) - test.That(t, err, test.ShouldBeNil) - test.That(t, part.FrameConfig.name, test.ShouldEqual, transform.Name()) - test.That(t, part.FrameConfig.parent, test.ShouldEqual, transform.Parent()) - test.That(t, spatial.R3VectorAlmostEqual(part.FrameConfig.pose.Point(), testPose.Point(), 1e-8), test.ShouldBeTrue) - test.That(t, spatial.OrientationAlmostEqual(part.FrameConfig.pose.Orientation(), testPose.Orientation()), test.ShouldBeTrue) - }) -} - -func TestFrameSystemGeometries(t *testing.T) { - fs := NewEmptyFrameSystem("test") - dims := r3.Vector{1, 1, 1} - - // add a static frame with a box - name0 := "frame0" - pose0 := spatial.NewPoseFromPoint(r3.Vector{-4, -4, -4}) - box0, err := spatial.NewBox(pose0, dims, name0) - test.That(t, err, test.ShouldBeNil) - frame0, err := NewStaticFrameWithGeometry(name0, pose0, box0) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(frame0, fs.World()) - - // add a static frame with a box as a child of the first - name1 := "frame1" - pose1 := spatial.NewPoseFromPoint(r3.Vector{2, 2, 2}) - box1, err := spatial.NewBox(pose1, dims, name1) - test.That(t, err, test.ShouldBeNil) - frame1, err := NewStaticFrameWithGeometry(name1, pose1, box1) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(frame1, frame0) - - // function to check that boxes are returned and where they are supposed to be - staticGeometriesOK := func(t *testing.T, geometries map[string]*GeometriesInFrame) { - t.Helper() - g0, ok := geometries[name0] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, g0.Parent(), test.ShouldResemble, World) - test.That(t, spatial.GeometriesAlmostEqual(g0.Geometries()[0], box0), test.ShouldBeTrue) - g1, ok := geometries[name1] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, g1.Parent(), test.ShouldResemble, World) - test.That(t, spatial.PoseAlmostCoincident(g1.Geometries()[0].Pose(), spatial.Compose(pose0, pose1)), test.ShouldBeTrue) - } - - type testCase struct { - name string - inputs map[string][]Input - success bool - } - - // test that boxes are where they should be regardless of input, since neither depend on input to be located - for _, tc := range []testCase{ - {name: "non-nil inputs, zero DOF", inputs: StartPositions(fs)}, - {name: "nil inputs, zero DOF", inputs: nil}, - } { - t.Run(tc.name, func(t *testing.T) { - geometries, err := FrameSystemGeometries(fs, tc.inputs) - test.That(t, err, test.ShouldBeNil) - staticGeometriesOK(t, geometries) - }) - } - - // add an arm model to the fs - jsonData, err := os.ReadFile(rdkutils.ResolveFile("config/data/model_frame_geoms.json")) - test.That(t, err, test.ShouldBeNil) - model, err := UnmarshalModelJSON(jsonData, "") - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(model, fs.World()) - eePose, err := model.Transform(make([]Input, len(model.DoF()))) - test.That(t, err, test.ShouldBeNil) - - // add a static frame as a child of the model - name2 := "block" - pose2 := spatial.NewPoseFromPoint(r3.Vector{2, 2, 2}) - box2, err := spatial.NewBox(pose2, dims, name2) - test.That(t, err, test.ShouldBeNil) - blockFrame, err := NewStaticFrameWithGeometry(name2, pose2, box2) - test.That(t, err, test.ShouldBeNil) - fs.AddFrame(blockFrame, model) - - // function to check that boxes relying on inputs are returned and where they are supposed to be - dynamicGeometriesOK := func(t *testing.T, geometries map[string]*GeometriesInFrame) { - t.Helper() - g0, ok := geometries[model.Name()] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, g0.Parent(), test.ShouldResemble, World) - test.That(t, len(g0.Geometries()), test.ShouldBeGreaterThan, 0) - g1, ok := geometries[name2] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, g1.Parent(), test.ShouldResemble, World) - test.That(t, spatial.PoseAlmostCoincident(g1.Geometries()[0].Pose(), spatial.Compose(eePose, pose1)), test.ShouldBeTrue) - } - - // test that boxes are where they should be regardless of input, since neither depend on input to be located - for _, tc := range []testCase{ - {name: "non-nil inputs, non-zero DOF", inputs: StartPositions(fs), success: true}, - {name: "nil inputs, non-zero DOF", inputs: nil, success: false}, - } { - t.Run(tc.name, func(t *testing.T) { - geometries, err := FrameSystemGeometries(fs, tc.inputs) - if !tc.success { - test.That(t, err, test.ShouldNotBeNil) - } else { - test.That(t, err, test.ShouldBeNil) - dynamicGeometriesOK(t, geometries) - } - staticGeometriesOK(t, geometries) - }) - } -} - -func TestReplaceFrame(t *testing.T) { - fs := NewEmptyFrameSystem("test") - // fill framesystem - pose := spatial.NewZeroPose() - box, err := spatial.NewBox(pose, r3.Vector{1, 1, 1}, "box") - test.That(t, err, test.ShouldBeNil) - replaceMe, err := NewStaticFrameWithGeometry("replaceMe", pose, box) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(replaceMe, fs.World()) - test.That(t, err, test.ShouldBeNil) - - frame1 := NewZeroStaticFrame("frame1") - err = fs.AddFrame(frame1, replaceMe) - test.That(t, err, test.ShouldBeNil) - - frame2 := NewZeroStaticFrame("frame2") - err = fs.AddFrame(frame2, frame1) - test.That(t, err, test.ShouldBeNil) - - leafNode, err := NewStaticFrameWithGeometry("leafNode", pose, box) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(leafNode, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // ------ fail with replacing world - err = fs.ReplaceFrame(fs.World()) - test.That(t, err, test.ShouldNotBeNil) - - // ------ fail replacing a frame not found in the framesystem - ghostFrame := NewZeroStaticFrame("ghost") - err = fs.ReplaceFrame(ghostFrame) - test.That(t, err, test.ShouldNotBeNil) - - // ------ replace a non-leaf node - replaceWith := NewZeroStaticFrame("replaceMe") - err = fs.ReplaceFrame(replaceWith) - test.That(t, err, test.ShouldBeNil) - - // ------ replace a leaf node - newLeafNode := NewZeroStaticFrame("leafNode") - err = fs.ReplaceFrame(newLeafNode) - test.That(t, err, test.ShouldBeNil) - - // make sure replaceMe and leafNode are gone - test.That(t, fs.Frame(replaceWith.Name()), test.ShouldNotResemble, replaceMe) - test.That(t, fs.Frame(newLeafNode.Name()), test.ShouldNotResemble, leafNode) - - // make sure parentage is transferred successfully - f, err := fs.Parent(replaceWith) - test.That(t, err, test.ShouldBeNil) - test.That(t, f, test.ShouldResemble, fs.World()) - - // make sure parentage is preserved - f, err = fs.Parent(frame1) - test.That(t, err, test.ShouldBeNil) - test.That(t, f, test.ShouldResemble, replaceWith) - - f, err = fs.Parent(frame2) - test.That(t, err, test.ShouldBeNil) - test.That(t, f, test.ShouldResemble, frame1) - - f, err = fs.Parent(newLeafNode) - test.That(t, err, test.ShouldBeNil) - test.That(t, f, test.ShouldResemble, fs.World()) -} diff --git a/referenceframe/frame_test.go b/referenceframe/frame_test.go deleted file mode 100644 index 934ac4afa9d..00000000000 --- a/referenceframe/frame_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package referenceframe - -import ( - "encoding/json" - "io" - "math" - "math/rand" - "os" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" - "go.viam.com/utils" - - spatial "go.viam.com/rdk/spatialmath" -) - -func TestStaticFrame(t *testing.T) { - // define a static transform - expPose := spatial.NewPose(r3.Vector{1, 2, 3}, &spatial.R4AA{math.Pi / 2, 0., 0., 1.}) - frame, err := NewStaticFrame("test", expPose) - test.That(t, err, test.ShouldBeNil) - // get expected transform back - emptyInput := FloatsToInputs([]float64{}) - pose, err := frame.Transform(emptyInput) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose, test.ShouldResemble, expPose) - // if you feed in non-empty input, should get err back - nonEmptyInput := FloatsToInputs([]float64{0, 0, 0}) - _, err = frame.Transform(nonEmptyInput) - test.That(t, err, test.ShouldNotBeNil) - // check that there are no limits on the static frame - limits := frame.DoF() - test.That(t, limits, test.ShouldResemble, []Limit{}) - - errExpect := errors.New("pose is not allowed to be nil") - f, err := NewStaticFrame("test2", nil) - test.That(t, err.Error(), test.ShouldEqual, errExpect.Error()) - test.That(t, f, test.ShouldBeNil) -} - -func TestPrismaticFrame(t *testing.T) { - // define a prismatic transform - limit := Limit{Min: -30, Max: 30} - frame, err := NewTranslationalFrame("test", r3.Vector{3, 4, 0}, limit) - test.That(t, err, test.ShouldBeNil) - - // get expected transform back - expPose := spatial.NewPoseFromPoint(r3.Vector{3, 4, 0}) - input := FloatsToInputs([]float64{5}) - pose, err := frame.Transform(input) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(pose, expPose), test.ShouldBeTrue) - - // if you feed in too many inputs, should get an error back - input = FloatsToInputs([]float64{0, 20, 0}) - _, err = frame.Transform(input) - test.That(t, err, test.ShouldNotBeNil) - - // if you feed in empty input, should get an error - input = FloatsToInputs([]float64{}) - _, err = frame.Transform(input) - test.That(t, err, test.ShouldNotBeNil) - - // if you try to move beyond set limits, should get an error - overLimit := 50.0 - input = FloatsToInputs([]float64{overLimit}) - _, err = frame.Transform(input) - s := "joint 0 input out of bounds, input 50.00000 needs to be within range [30.00000 -30.00000]" - test.That(t, err.Error(), test.ShouldEqual, s) - - // gets the correct limits back - frameLimits := frame.DoF() - test.That(t, frameLimits[0], test.ShouldResemble, limit) - - randomInputs := RandomFrameInputs(frame, nil) - test.That(t, len(randomInputs), test.ShouldEqual, len(frame.DoF())) - - for i := 0; i < 10; i++ { - restrictRandomInputs, err := RestrictedRandomFrameInputs(frame, nil, 0.001, FloatsToInputs([]float64{-10})) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(restrictRandomInputs), test.ShouldEqual, len(frame.DoF())) - test.That(t, restrictRandomInputs[0].Value, test.ShouldBeLessThan, -9.07) - test.That(t, restrictRandomInputs[0].Value, test.ShouldBeGreaterThan, -10.03) - } -} - -func TestRevoluteFrame(t *testing.T) { - axis := r3.Vector{1, 0, 0} // axis of rotation is x axis - frame := &rotationalFrame{&baseFrame{"test", []Limit{{-math.Pi / 2, math.Pi / 2}}}, axis} // limits between -90 and 90 degrees - // expected output - expPose := spatial.NewPoseFromOrientation(&spatial.R4AA{math.Pi / 4, 1, 0, 0}) // 45 degrees - // get expected transform back - input := frame.InputFromProtobuf(&pb.JointPositions{Values: []float64{45}}) - pose, err := frame.Transform(input) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose, test.ShouldResemble, expPose) - // if you feed in too many inputs, should get error back - input = frame.InputFromProtobuf(&pb.JointPositions{Values: []float64{45, 55}}) - _, err = frame.Transform(input) - test.That(t, err, test.ShouldNotBeNil) - // if you feed in empty input, should get errr back - input = frame.InputFromProtobuf(&pb.JointPositions{Values: []float64{}}) - _, err = frame.Transform(input) - test.That(t, err, test.ShouldNotBeNil) - // if you try to move beyond set limits, should get an error - overLimit := 100.0 // degrees - input = frame.InputFromProtobuf(&pb.JointPositions{Values: []float64{overLimit}}) - _, err = frame.Transform(input) - s := "joint 0 input out of bounds, input 1.74533 needs to be within range [1.57080 -1.57080]" - test.That(t, err.Error(), test.ShouldEqual, s) - // gets the correct limits back - limit := frame.DoF() - expLimit := []Limit{{Min: -math.Pi / 2, Max: math.Pi / 2}} - test.That(t, limit, test.ShouldHaveLength, 1) - test.That(t, limit[0], test.ShouldResemble, expLimit[0]) -} - -func TestGeometries(t *testing.T) { - bc, err := spatial.NewBox(spatial.NewZeroPose(), r3.Vector{1, 1, 1}, "") - test.That(t, err, test.ShouldBeNil) - pose := spatial.NewPoseFromPoint(r3.Vector{0, 10, 0}) - expectedBox := bc.Transform(pose) - - // test creating a new translational frame with a geometry - tf, err := NewTranslationalFrameWithGeometry("", r3.Vector{0, 1, 0}, Limit{Min: -30, Max: 30}, bc) - test.That(t, err, test.ShouldBeNil) - geometries, err := tf.Geometries(FloatsToInputs([]float64{10})) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.GeometriesAlmostEqual(expectedBox, geometries.Geometries()[0]), test.ShouldBeTrue) - - // test erroring correctly from trying to create a geometry for a rotational frame - rf, err := NewRotationalFrame("", spatial.R4AA{3.7, 2.1, 3.1, 4.1}, Limit{5, 6}) - test.That(t, err, test.ShouldBeNil) - geometries, err = rf.Geometries([]Input{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(geometries.Geometries()), test.ShouldEqual, 0) - - // test creating a new static frame with a geometry - expectedBox = bc.Transform(spatial.NewZeroPose()) - sf, err := NewStaticFrameWithGeometry("", pose, bc) - test.That(t, err, test.ShouldBeNil) - geometries, err = sf.Geometries([]Input{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.GeometriesAlmostEqual(expectedBox, geometries.Geometries()[0]), test.ShouldBeTrue) -} - -func TestSerializationStatic(t *testing.T) { - f, err := NewStaticFrame("foo", spatial.NewPose(r3.Vector{1, 2, 3}, &spatial.R4AA{math.Pi / 2, 4, 5, 6})) - test.That(t, err, test.ShouldBeNil) - - data, err := f.MarshalJSON() - test.That(t, err, test.ShouldBeNil) - - f2Cfg := &LinkConfig{} - err = json.Unmarshal(data, f2Cfg) - test.That(t, err, test.ShouldBeNil) - - f2if, err := f2Cfg.ParseConfig() - test.That(t, err, test.ShouldBeNil) - - f2, err := f2if.ToStaticFrame("") - test.That(t, err, test.ShouldBeNil) - - test.That(t, f2.Name(), test.ShouldResemble, f.Name()) - p1, err := f.Transform(nil) - test.That(t, err, test.ShouldBeNil) - p2, err := f2.Transform(nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(p1, p2), test.ShouldBeTrue) -} - -func TestSerializationTranslation(t *testing.T) { - f, err := NewTranslationalFrame("foo", r3.Vector{1, 0, 0}, Limit{1, 2}) - test.That(t, err, test.ShouldBeNil) - - data, err := f.MarshalJSON() - test.That(t, err, test.ShouldBeNil) - - f2Cfg := &JointConfig{} - err = json.Unmarshal(data, f2Cfg) - test.That(t, err, test.ShouldBeNil) - - f2, err := f2Cfg.ToFrame() - test.That(t, err, test.ShouldBeNil) - - test.That(t, f2, test.ShouldResemble, f) -} - -func TestSerializationRotations(t *testing.T) { - f, err := NewRotationalFrame("foo", spatial.R4AA{3.7, 2.1, 3.1, 4.1}, Limit{5, 6}) - test.That(t, err, test.ShouldBeNil) - - data, err := f.MarshalJSON() - test.That(t, err, test.ShouldBeNil) - - f2Cfg := &JointConfig{} - err = json.Unmarshal(data, f2Cfg) - test.That(t, err, test.ShouldBeNil) - - f2, err := f2Cfg.ToFrame() - test.That(t, err, test.ShouldBeNil) - - test.That(t, f2, test.ShouldResemble, f) -} - -func TestRandomFrameInputs(t *testing.T) { - frame, _ := NewTranslationalFrame("", r3.Vector{X: 1}, Limit{-10, 10}) - seed := rand.New(rand.NewSource(23)) - for i := 0; i < 100; i++ { - _, err := frame.Transform(RandomFrameInputs(frame, seed)) - test.That(t, err, test.ShouldBeNil) - } - - limitedFrame, _ := NewTranslationalFrame("", r3.Vector{X: 1}, Limit{-2, 2}) - for i := 0; i < 100; i++ { - r, err := RestrictedRandomFrameInputs(frame, seed, .2, FloatsToInputs([]float64{0})) - test.That(t, err, test.ShouldBeNil) - _, err = limitedFrame.Transform(r) - test.That(t, err, test.ShouldBeNil) - } -} - -func TestFrame(t *testing.T) { - file, err := os.Open("../config/data/frame.json") - test.That(t, err, test.ShouldBeNil) - defer utils.UncheckedErrorFunc(file.Close) - - data, err := io.ReadAll(file) - test.That(t, err, test.ShouldBeNil) - // Parse into map of tests - var testMap map[string]json.RawMessage - err = json.Unmarshal(data, &testMap) - test.That(t, err, test.ShouldBeNil) - - frame := LinkConfig{} - err = json.Unmarshal(testMap["test"], &frame) - test.That(t, err, test.ShouldBeNil) - bc, err := spatial.NewBox(spatial.NewPoseFromPoint(r3.Vector{4, 5, 6}), r3.Vector{1, 2, 3}, "") - test.That(t, err, test.ShouldBeNil) - pose := spatial.NewPose(r3.Vector{1, 2, 3}, &spatial.OrientationVectorDegrees{Theta: 85, OZ: 1}) - expFrame, err := NewStaticFrameWithGeometry("", pose, bc) - test.That(t, err, test.ShouldBeNil) - sFrameif, err := frame.ParseConfig() - test.That(t, err, test.ShouldBeNil) - sFrame, err := sFrameif.ToStaticFrame("") - test.That(t, err, test.ShouldBeNil) - - test.That(t, sFrame, test.ShouldResemble, expFrame) - - // test going back to json and validating. - rd, err := json.Marshal(&frame) - test.That(t, err, test.ShouldBeNil) - frame2 := LinkConfig{} - err = json.Unmarshal(rd, &frame2) - test.That(t, err, test.ShouldBeNil) - - sFrame2if, err := frame2.ParseConfig() - test.That(t, err, test.ShouldBeNil) - sFrame2, err := sFrame2if.ToStaticFrame("") - test.That(t, err, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, sFrame2, test.ShouldResemble, expFrame) - - pose, err = frame.Pose() - test.That(t, err, test.ShouldBeNil) - expPose, err := expFrame.Transform([]Input{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose, test.ShouldResemble, expPose) - - bc.SetLabel("test") - sFrameif, err = frame.ParseConfig() - test.That(t, err, test.ShouldBeNil) - sFrame, err = sFrameif.ToStaticFrame("test") - test.That(t, err, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - expStaticFrame, err := NewStaticFrameWithGeometry("test", expPose, bc) - test.That(t, err, test.ShouldBeNil) - test.That(t, sFrame, test.ShouldResemble, expStaticFrame) -} diff --git a/referenceframe/model_json_test.go b/referenceframe/model_json_test.go deleted file mode 100644 index 355b49b9079..00000000000 --- a/referenceframe/model_json_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package referenceframe - -import ( - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/utils" -) - -// Tests that yml files are properly parsed and correctly loaded into the model -// Should not need to actually test the contained rotation/translation values -// since that will be caught by tests to the actual kinematics -// So we'll just check that we read in the right number of joints. -func TestParseJSONFile(t *testing.T) { - goodFiles := []string{ - "components/arm/eva/eva_kinematics.json", - "components/arm/xarm/xarm6_kinematics.json", - "components/arm/xarm/xarm7_kinematics.json", - "referenceframe/testjson/ur5eDH.json", - "components/arm/universalrobots/ur5e.json", - "components/arm/fake/dofbot.json", - } - - badFiles := []string{ - "referenceframe/testjson/kinematicsloop.json", - "referenceframe/testjson/worldjoint.json", - "referenceframe/testjson/worldlink.json", - "referenceframe/testjson/worldDH.json", - "referenceframe/testjson/missinglink.json", - } - - badFilesErrors := []error{ - ErrCircularReference, - NewReservedWordError("link", "world"), - NewReservedWordError("joint", "world"), - ErrAtLeastOneEndEffector, - NewFrameNotInListOfTransformsError("base"), - } - - for _, f := range goodFiles { - t.Run(f, func(tt *testing.T) { - model, err := ParseModelJSONFile(utils.ResolveFile(f), "") - test.That(t, err, test.ShouldBeNil) - - data, err := model.MarshalJSON() - test.That(t, err, test.ShouldBeNil) - - model2, err := UnmarshalModelJSON(data, "") - test.That(t, err, test.ShouldBeNil) - - data2, err := model2.MarshalJSON() - test.That(t, err, test.ShouldBeNil) - - test.That(t, data, test.ShouldResemble, data2) - }) - } - - for i, f := range badFiles { - t.Run(f, func(tt *testing.T) { - _, err := ParseModelJSONFile(utils.ResolveFile(f), "") - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, badFilesErrors[i].Error()) - }) - } -} diff --git a/referenceframe/model_test.go b/referenceframe/model_test.go deleted file mode 100644 index 686967c846d..00000000000 --- a/referenceframe/model_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package referenceframe - -import ( - "math" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func TestModelLoading(t *testing.T) { - m, err := ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - test.That(t, m.Name(), test.ShouldEqual, "xArm6") - simpleM, ok := m.(*SimpleModel) - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, len(m.DoF()), test.ShouldEqual, 6) - - err = simpleM.validInputs(FloatsToInputs([]float64{0.1, 0.1, 0.1, 0.1, 0.1, 0.1})) - test.That(t, err, test.ShouldBeNil) - err = simpleM.validInputs(FloatsToInputs([]float64{0.1, 0.1, 0.1, 0.1, 0.1, 99.1})) - test.That(t, err, test.ShouldNotBeNil) - - orig := []float64{0.1, 0.1, 0.1, 0.1, 0.1, 0.1} - orig[5] += math.Pi * 2 - orig[4] -= math.Pi * 4 - - randpos := GenerateRandomConfiguration(m, rand.New(rand.NewSource(1))) - test.That(t, simpleM.validInputs(FloatsToInputs(randpos)), test.ShouldBeNil) - - m, err = ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "foo") - test.That(t, err, test.ShouldBeNil) - test.That(t, m.Name(), test.ShouldEqual, "foo") -} - -func TestTransform(t *testing.T) { - m, err := ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - simpleM, ok := m.(*SimpleModel) - test.That(t, ok, test.ShouldBeTrue) - - joints := []Frame{} - for _, tform := range simpleM.OrdTransforms { - if len(tform.DoF()) > 0 { - joints = append(joints, tform) - } - } - test.That(t, len(joints), test.ShouldEqual, 6) - pose, err := joints[0].Transform([]Input{{0}}) - test.That(t, err, test.ShouldBeNil) - firstJov := pose.Orientation().OrientationVectorRadians() - firstJovExpect := &spatial.OrientationVector{Theta: 0, OX: 0, OY: 0, OZ: 1} - test.That(t, firstJov, test.ShouldResemble, firstJovExpect) - - pose, err = joints[0].Transform([]Input{{1.5708}}) - test.That(t, err, test.ShouldBeNil) - firstJov = pose.Orientation().OrientationVectorRadians() - firstJovExpect = &spatial.OrientationVector{Theta: 1.5708, OX: 0, OY: 0, OZ: 1} - test.That(t, firstJov.Theta, test.ShouldAlmostEqual, firstJovExpect.Theta) - test.That(t, firstJov.OX, test.ShouldAlmostEqual, firstJovExpect.OX) - test.That(t, firstJov.OY, test.ShouldAlmostEqual, firstJovExpect.OY) - test.That(t, firstJov.OZ, test.ShouldAlmostEqual, firstJovExpect.OZ) -} - -func TestIncorrectInputs(t *testing.T) { - m, err := ParseModelJSONFile(utils.ResolveFile("components/arm/xarm/xarm6_kinematics.json"), "") - test.That(t, err, test.ShouldBeNil) - dof := len(m.DoF()) - - // test incorrect number of inputs - pose, err := m.Transform(make([]Input, dof+1)) - test.That(t, pose, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, NewIncorrectInputLengthError(dof+1, dof).Error()) - - // test incorrect number of inputs to Geometries - gf, err := m.Geometries(make([]Input, dof-1)) - test.That(t, gf, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, NewIncorrectInputLengthError(dof-1, dof).Error()) -} - -func TestModelGeometries(t *testing.T) { - // build a test model - offset := spatial.NewPoseFromPoint(r3.Vector{0, 0, 10}) - bc, err := spatial.NewBox(offset, r3.Vector{1, 1, 1}, "") - test.That(t, err, test.ShouldBeNil) - frame1, err := NewStaticFrameWithGeometry("link1", offset, bc) - test.That(t, err, test.ShouldBeNil) - frame2, err := NewRotationalFrame("", spatial.R4AA{RY: 1}, Limit{Min: -360, Max: 360}) - test.That(t, err, test.ShouldBeNil) - frame3, err := NewStaticFrameWithGeometry("link2", offset, bc) - test.That(t, err, test.ShouldBeNil) - m := &SimpleModel{baseFrame: &baseFrame{name: "test"}, OrdTransforms: []Frame{frame1, frame2, frame3}} - - // test zero pose of model - inputs := make([]Input, len(m.DoF())) - geometries, err := m.Geometries(inputs) - test.That(t, err, test.ShouldBeNil) - link1 := geometries.GeometryByName("test:link1").Pose().Point() - test.That(t, spatial.R3VectorAlmostEqual(link1, r3.Vector{0, 0, 10}, 1e-8), test.ShouldBeTrue) - link2 := geometries.GeometryByName("test:link2").Pose().Point() - test.That(t, spatial.R3VectorAlmostEqual(link2, r3.Vector{0, 0, 20}, 1e-8), test.ShouldBeTrue) - - // transform the model 90 degrees at the joint - inputs[0] = Input{math.Pi / 2} - geometries, _ = m.Geometries(inputs) - test.That(t, geometries, test.ShouldNotBeNil) - link1 = geometries.GeometryByName("test:link1").Pose().Point() - test.That(t, spatial.R3VectorAlmostEqual(link1, r3.Vector{0, 0, 10}, 1e-8), test.ShouldBeTrue) - link2 = geometries.GeometryByName("test:link2").Pose().Point() - test.That(t, spatial.R3VectorAlmostEqual(link2, r3.Vector{10, 0, 10}, 1e-8), test.ShouldBeTrue) -} - -func Test2DMobileModelFrame(t *testing.T) { - expLimit := []Limit{{-10, 10}, {-10, 10}, {-2 * math.Pi, 2 * math.Pi}} - sphere, err := spatial.NewSphere(spatial.NewZeroPose(), 10, "") - test.That(t, err, test.ShouldBeNil) - frame, err := New2DMobileModelFrame("test", expLimit, sphere) - test.That(t, err, test.ShouldBeNil) - // expected output - expPose := spatial.NewPose(r3.Vector{3, 5, 0}, &spatial.OrientationVector{OZ: 1, Theta: math.Pi / 2}) - // get expected transform back - pose, err := frame.Transform(FloatsToInputs([]float64{3, 5, math.Pi / 2})) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose, test.ShouldResemble, expPose) - // if you feed in too many inputs, should get error back - _, err = frame.Transform(FloatsToInputs([]float64{3, 5, 0, 10})) - test.That(t, err, test.ShouldNotBeNil) - // if you feed in too few inputs, should get errr back - _, err = frame.Transform(FloatsToInputs([]float64{3})) - test.That(t, err, test.ShouldNotBeNil) - // if you try to move beyond set limits, should get an error - _, err = frame.Transform(FloatsToInputs([]float64{3, 100})) - test.That(t, err, test.ShouldNotBeNil) - // gets the correct limits back - limit := frame.DoF() - test.That(t, limit[0], test.ShouldResemble, expLimit[0]) -} diff --git a/referenceframe/primitives_test.go b/referenceframe/primitives_test.go deleted file mode 100644 index 0d9e55ba2e4..00000000000 --- a/referenceframe/primitives_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package referenceframe - -import ( - "math" - "testing" - - pb "go.viam.com/api/component/arm/v1" - "go.viam.com/test" -) - -func TestJointPositions(t *testing.T) { - in := []float64{0, math.Pi} - j := JointPositionsFromRadians(in) - test.That(t, j.Values[0], test.ShouldEqual, 0.0) - test.That(t, j.Values[1], test.ShouldEqual, 180.0) - test.That(t, JointPositionsToRadians(j), test.ShouldResemble, in) -} - -func TestBasicConversions(t *testing.T) { - jp := &pb.JointPositions{Values: []float64{45, 55}} - radians := JointPositionsToRadians(jp) - test.That(t, jp, test.ShouldResemble, JointPositionsFromRadians(radians)) - - floats := []float64{45, 55, 27} - inputs := FloatsToInputs(floats) - test.That(t, floats, test.ShouldResemble, InputsToFloats(inputs)) -} - -func TestInterpolateValues(t *testing.T) { - jp1 := FloatsToInputs([]float64{0, 4}) - jp2 := FloatsToInputs([]float64{8, -8}) - jpHalf := FloatsToInputs([]float64{4, -2}) - jpQuarter := FloatsToInputs([]float64{2, 1}) - - interp1 := interpolateInputs(jp1, jp2, 0.5) - interp2 := interpolateInputs(jp1, jp2, 0.25) - test.That(t, interp1, test.ShouldResemble, jpHalf) - test.That(t, interp2, test.ShouldResemble, jpQuarter) -} diff --git a/referenceframe/static_frame_system_test.go b/referenceframe/static_frame_system_test.go deleted file mode 100644 index b98fa4fd69d..00000000000 --- a/referenceframe/static_frame_system_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package referenceframe - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - spatial "go.viam.com/rdk/spatialmath" -) - -func frameNames(frames []Frame) []string { - names := make([]string, len(frames)) - for i, f := range frames { - names[i] = f.Name() - } - return names -} - -func TestSimpleFrameSystemFunctions(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - frame3Pt := r3.Vector{0., 4., 0.} // location of frame3 with respect to world frame - f3, err := NewStaticFrame("frame3", spatial.NewPoseFromPoint(frame3Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f3, fs.World()) - test.That(t, err, test.ShouldBeNil) - frame1Pt := r3.Vector{0., 3., 0.} // location of frame1 with respect to frame3 - f1, err := NewStaticFrame("frame1", spatial.NewPoseFromPoint(frame1Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f1, fs.Frame("frame3")) - test.That(t, err, test.ShouldBeNil) - frame2Pt := r3.Vector{5., 1., 0.} // location of frame2 with respect to world frame - f2, err := NewStaticFrame("frame2", spatial.NewPoseFromPoint(frame2Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f2, fs.World()) - test.That(t, err, test.ShouldBeNil) - - frames := fs.FrameNames() - test.That(t, len(frames), test.ShouldEqual, 3) - - f1Parents, err := fs.TracebackFrame(f1) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(f1Parents), test.ShouldEqual, 3) - test.That(t, frameNames(f1Parents), test.ShouldResemble, []string{"frame1", "frame3", "world"}) - - parent, err := fs.Parent(fs.Frame("frame1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, parent, test.ShouldResemble, fs.Frame("frame3")) - - parent, err = fs.Parent(fs.Frame("frame3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, parent, test.ShouldResemble, fs.World()) - // Pruning frame3 should also remove frame1 - fs.RemoveFrame(fs.Frame("frame3")) - frames = fs.FrameNames() - test.That(t, len(frames), test.ShouldEqual, 1) - - err = fs.AddFrame(f2, nil) - test.That(t, err, test.ShouldBeError, NewParentFrameNilError(f2.Name())) - - err = fs.AddFrame(NewZeroStaticFrame("bar"), NewZeroStaticFrame("foo")) - test.That(t, err, test.ShouldBeError, NewFrameMissingError("foo")) - - err = fs.AddFrame(NewZeroStaticFrame("bar"), nil) - test.That(t, err, test.ShouldBeError, NewParentFrameNilError("bar")) - - err = fs.AddFrame(NewZeroStaticFrame("frame2"), fs.World()) - test.That(t, err, test.ShouldBeError, NewFrameAlreadyExistsError("frame2")) - - _, err = fs.TracebackFrame(f1) - test.That(t, err, test.ShouldBeError, NewFrameMissingError("frame1")) -} - -// A simple Frame translation from the world frame to a frame right above it at (0, 3, 0) -// And then back to the world frame -// transforming a point at (1, 3, 0). -func TestSimpleFrameTranslation(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - frame, err := NewStaticFrame("frame", spatial.NewPoseFromPoint(r3.Vector{0., 3., 0.})) // location of frame with respect to world frame - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // define the point coordinates and transform between them both ways - poseWorld := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{1, 3, 0})) // the point from PoV of world - poseFrame := NewPoseInFrame("frame", spatial.NewPoseFromPoint(r3.Vector{1, 0, 0})) // the point from PoV of frame - testTransformPoint(t, fs, map[string][]Input{}, poseWorld, poseFrame) - testTransformPoint(t, fs, map[string][]Input{}, poseFrame, poseWorld) -} - -// A simple Frame translation from the world frame to a frame right above it at (0, 3, 0) rotated 180 around Z -// And then back to the world frame -// transforming a point at (1, 3, 0). -func TestSimpleFrameTranslationWithRotation(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - framePose := spatial.NewPose(r3.Vector{0., 3., 0.}, &spatial.R4AA{math.Pi, 0., 0., 1.}) - f1, err := NewStaticFrame("frame", framePose) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f1, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // define the point coordinates and transform between them both ways - poseWorld := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{1, 3, 0})) - poseFrame := NewPoseInFrame("frame", spatial.NewPose(r3.Vector{-1., 0, 0}, &spatial.R4AA{math.Pi, 0., 0., 1.})) - testTransformPoint(t, fs, map[string][]Input{}, poseWorld, poseFrame) - testTransformPoint(t, fs, map[string][]Input{}, poseFrame, poseWorld) -} - -/* -Transforms the pose of *object from *frame1 into *frame2. The Orientation of *frame1 and *frame2 -are the same, so the final composed transformation is only made up of one translation. - -| | -|*frame1 |*object -| | -| -|*frame3 -| -| -| *frame2 -|________________ -world - -transform the point that is in the world frame is at (5, 7, 0) from frame1 to frame2. -frame1 has its origin at (0, 7, 0) in the world referenceframe. and frame2 has its origin at (5, 1, 0). -frame3 is an intermediate frame at (0, 4, 0) in the world referenceframe. -All 4 frames have the same orientation. -*/ -func TestFrameTranslation(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - frame3Pt := r3.Vector{0., 4., 0.} // location of frame3 with respect to world frame - f3, err := NewStaticFrame("frame3", spatial.NewPoseFromPoint(frame3Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f3, fs.World()) - test.That(t, err, test.ShouldBeNil) - frame1Pt := r3.Vector{0., 3., 0.} // location of frame1 with respect to frame3 - f1, err := NewStaticFrame("frame1", spatial.NewPoseFromPoint(frame1Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f1, fs.Frame("frame3")) - test.That(t, err, test.ShouldBeNil) - frame2Pt := r3.Vector{5., 1., 0.} // location of frame2 with respect to world frame - f2, err := NewStaticFrame("frame2", spatial.NewPoseFromPoint(frame2Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f2, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // do the transformation - poseStart := NewPoseInFrame("frame1", spatial.NewPoseFromPoint(r3.Vector{5, 0, 0})) // the point from PoV of frame 1 - poseEnd := NewPoseInFrame("frame2", spatial.NewPoseFromPoint(r3.Vector{0, 6, 0})) // the point from PoV of frame 2 - testTransformPoint(t, fs, map[string][]Input{}, poseStart, poseEnd) - testTransformPoint(t, fs, map[string][]Input{}, poseEnd, poseStart) -} - -/* -Very similar to test above, but this time *frame2 is oriented 90 degrees counterclockwise around the z-axis -(+z is pointing out of the page), which means the orientation of the object will also change. - -| | -|*frame1 |*object -| | -| -|*frame3 -| | -| | -| ____|*frame2 -|________________ -world -*/ - -// transform the point that is in the world frame is at (5, 7, 0) from frame1 to frame2. -// frame1 has its origin at (0, 7, 0) in the world referenceframe. and frame2 has its origin -// at (5, 1, 0), and orientation 90 degrees around z. -// frame3 is an intermediate frame at (0, 4, 0) in the world referenceframe. -func TestFrameTransform(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - // location of frame3 with respect to world frame - frame3Pt := r3.Vector{0., 4., 0.} // location of frame3 with respect to world frame - f3, err := NewStaticFrame("frame3", spatial.NewPoseFromPoint(frame3Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f3, fs.World()) - test.That(t, err, test.ShouldBeNil) - frame1Pt := r3.Vector{0., 3., 0.} // location of frame1 with respect to frame3 - f1, err := NewStaticFrame("frame1", spatial.NewPoseFromPoint(frame1Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f1, fs.Frame("frame3")) - test.That(t, err, test.ShouldBeNil) - frame2Pose := spatial.NewPose(r3.Vector{5., 1., 0.}, &spatial.R4AA{math.Pi / 2, 0., 0., 1.}) - f2, err := NewStaticFrame("frame2", frame2Pose) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f2, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // do the transformation - poseStart := NewPoseInFrame("frame1", spatial.NewPoseFromPoint(r3.Vector{5, 0, 0})) - poseEnd := NewPoseInFrame("frame2", spatial.NewPose(r3.Vector{6, 0, 0.}, &spatial.R4AA{math.Pi / 2, 0., 0., 1.})) - testTransformPoint(t, fs, map[string][]Input{}, poseStart, poseEnd) -} - -/* -This test uses the same setup as the above test, but this time is concede with representing a geometry in a difference reference frame - -| | -|*frame1 |*object -| | -| -|*frame3 -| | -| | -| ____|*frame2 -|________________ -world -*/ - -// transform the object geometry that is in the world frame is at (5, 7, 0) from frame1 to frame2. -// frame1 has its origin at (0, 7, 0) in the world referenceframe. and frame2 has its origin -// at (5, 1, 0), and orientation 90 degrees around z. -// frame3 is an intermediate frame at (0, 4, 0) in the world referenceframe. -func TestGeomtriesTransform(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - // location of frame3 with respect to world frame - frame3Pt := r3.Vector{0., 4., 0.} // location of frame3 with respect to world frame - f3, err := NewStaticFrame("frame3", spatial.NewPoseFromPoint(frame3Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f3, fs.World()) - test.That(t, err, test.ShouldBeNil) - frame1Pt := r3.Vector{0., 3., 0.} // location of frame1 with respect to frame3 - f1, err := NewStaticFrame("frame1", spatial.NewPoseFromPoint(frame1Pt)) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f1, fs.Frame("frame3")) - test.That(t, err, test.ShouldBeNil) - frame2Pose := spatial.NewPose(r3.Vector{5., 1., 0.}, &spatial.R4AA{math.Pi / 2, 0., 0., 1.}) - f2, err := NewStaticFrame("frame2", frame2Pose) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(f2, fs.World()) - test.That(t, err, test.ShouldBeNil) - objectFromFrame1 := spatial.NewPoseFromPoint(r3.Vector{5, 0, 0}) - gc, err := spatial.NewBox(objectFromFrame1, r3.Vector{2, 2, 2}, "") - test.That(t, err, test.ShouldBeNil) - // it shouldn't matter where the transformation of the frame associated with the object is if we are just looking at its geometry - object, err := NewStaticFrameWithGeometry("object", spatial.NewPoseFromPoint(r3.Vector{1000, 1000, 1000}), gc) - - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(object, f1) - test.That(t, err, test.ShouldBeNil) - - objectFromFrame2 := spatial.NewPoseFromPoint(r3.Vector{6., 0., 0.}) // the point from PoV of frame 2 - geometries, err := object.Geometries([]Input{}) - test.That(t, err, test.ShouldBeNil) - tf, err := fs.Transform(map[string][]Input{}, geometries, "frame2") - test.That(t, err, test.ShouldBeNil) - framedGeometries, _ := tf.(*GeometriesInFrame) - test.That(t, framedGeometries.Parent(), test.ShouldResemble, "frame2") - test.That(t, spatial.PoseAlmostCoincident(framedGeometries.GeometryByName("object").Pose(), objectFromFrame2), test.ShouldBeTrue) - - gf := NewGeometriesInFrame(World, geometries.Geometries()) - tf, err = fs.Transform(map[string][]Input{}, gf, "frame3") - test.That(t, err, test.ShouldBeNil) - framedGeometries, _ = tf.(*GeometriesInFrame) - test.That(t, framedGeometries.Parent(), test.ShouldResemble, "frame3") - objectFromFrame3 := spatial.NewPoseFromPoint(r3.Vector{5, -4, 0.}) // the point from PoV of frame 2 - test.That(t, spatial.PoseAlmostCoincident(framedGeometries.GeometryByName("object").Pose(), objectFromFrame3), test.ShouldBeTrue) -} - -func TestComplicatedFrameTransform(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - - // frame 1 rotate by 45 degrees around z axis and translate - frame1, err := NewStaticFrame("frame1", spatial.NewPose(r3.Vector{-1., 2., 0.}, &spatial.R4AA{math.Pi / 4, 0., 0., 1.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame1, fs.World()) - test.That(t, err, test.ShouldBeNil) - // frame 2 rotate by 45 degree (relative to frame 1) around z axis and translate - frame2, err := NewStaticFrame("frame2", - spatial.NewPose(r3.Vector{2. * math.Sqrt(2), 0., 0.}, &spatial.R4AA{math.Pi / 4, 0., 0., 1.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame2, fs.Frame("frame1")) - test.That(t, err, test.ShouldBeNil) - - // test out a transform from world to frame - poseStart := NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{1, 7, 0})) // the point from PoV of world - poseEnd := NewPoseInFrame("frame2", spatial.NewPoseFromPoint(r3.Vector{3, 0, 0})) // the point from PoV of frame 2 - testTransformPoint(t, fs, map[string][]Input{}, poseStart, poseEnd) - - // test out transform between frames - // frame3 - pure rotation around y 90 degrees - frame3, err := NewStaticFrame("frame3", spatial.NewPoseFromOrientation(&spatial.R4AA{math.Pi / 2, 0., 1., 0.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame3, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // frame4 - pure translation - frame4, err := NewStaticFrame("frame4", spatial.NewPoseFromPoint(r3.Vector{-2., 7., 1.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame4, fs.Frame("frame3")) - test.That(t, err, test.ShouldBeNil) - - poseStart = NewPoseInFrame("frame2", spatial.NewPoseFromPoint(r3.Vector{3, 0, 0})) // the point from PoV of frame 2 - poseEnd = NewPoseInFrame("frame4", spatial.NewPoseFromPoint(r3.Vector{2, 0, 0})) // the point from PoV of frame 4 - testTransformPoint(t, fs, map[string][]Input{}, poseStart, poseEnd) - - // back to world frame - poseStart = NewPoseInFrame("frame4", spatial.NewPoseFromPoint(r3.Vector{2, 0, 0})) // the point from PoV of frame 4 - poseEnd = NewPoseInFrame(World, spatial.NewPoseFromPoint(r3.Vector{1, 7, 0})) // the point from PoV of world - testTransformPoint(t, fs, map[string][]Input{}, poseStart, poseEnd) -} - -func TestSystemSplitAndRejoin(t *testing.T) { - // build the system - fs := NewEmptyFrameSystem("test") - - // frame 1 rotate by 45 degrees around z axis and translate - frame1, err := NewStaticFrame("frame1", spatial.NewPose(r3.Vector{-1., 2., 0.}, &spatial.R4AA{math.Pi / 4, 0., 0., 1.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame1, fs.World()) - test.That(t, err, test.ShouldBeNil) - // frame 2 rotate by 45 degree (relative to frame 1) around z axis and translate - frame2, err := NewStaticFrame("frame2", - spatial.NewPose(r3.Vector{2. * math.Sqrt(2), 0., 0.}, &spatial.R4AA{math.Pi / 4, 0., 0., 1.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame2, fs.Frame("frame1")) - test.That(t, err, test.ShouldBeNil) - - // frame3 - pure rotation around y 90 degrees - frame3, err := NewStaticFrame("frame3", spatial.NewPoseFromOrientation(&spatial.R4AA{math.Pi / 2, 0., 1., 0.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame3, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // frame4 - pure translation - frame4, err := NewStaticFrame("frame4", spatial.NewPoseFromPoint(r3.Vector{-2., 7., 1.})) - test.That(t, err, test.ShouldBeNil) - err = fs.AddFrame(frame4, fs.Frame("frame3")) - test.That(t, err, test.ShouldBeNil) - - // complete fs - t.Logf("frames in fs: %v", fs.FrameNames()) - - // This should remove frames 3 and 4 from fs - fs2, err := fs.DivideFrameSystem(frame3) - test.That(t, err, test.ShouldBeNil) - t.Logf("frames in fs after divide: %v", fs.FrameNames()) - t.Logf("frames in fs2 after divide: %v", fs2.FrameNames()) - - f4 := fs.Frame("frame4") - test.That(t, f4, test.ShouldBeNil) - f1 := fs.Frame("frame1") - test.That(t, f1, test.ShouldNotBeNil) - - f4 = fs2.Frame("frame4") - test.That(t, f4, test.ShouldNotBeNil) - f1 = fs2.Frame("frame1") - test.That(t, f1, test.ShouldBeNil) - - _, err = fs.Transform(map[string][]Input{}, NewPoseInFrame("frame4", spatial.NewPoseFromPoint(r3.Vector{2, 0, 0})), "frame2") - test.That(t, err, test.ShouldNotBeNil) - - // Put frame3 back where it was - err = fs.MergeFrameSystem(fs2, fs.World()) - test.That(t, err, test.ShouldBeNil) - - // Comfirm that fs2 is empty now - t.Logf("frames in fs after merge: %v", fs.FrameNames()) - t.Logf("frames in fs2 after merge: %v", fs2.FrameNames()) - - // Confirm new combined frame system now works as it did before - poseStart := NewPoseInFrame("frame2", spatial.NewPoseFromPoint(r3.Vector{3, 0, 0})) // the point from PoV of frame 2 - poseEnd := NewPoseInFrame("frame4", spatial.NewPoseFromPoint(r3.Vector{2, 0, 0})) // the point from PoV of frame 4 - testTransformPoint(t, fs, map[string][]Input{}, poseStart, poseEnd) -} - -func testTransformPoint(t *testing.T, fs FrameSystem, positions map[string][]Input, start, end *PoseInFrame) { - t.Helper() - tf, err := fs.Transform(positions, start, end.Parent()) - test.That(t, err, test.ShouldBeNil) - pf, ok := tf.(*PoseInFrame) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, pf.Parent(), test.ShouldResemble, end.Parent()) - test.That(t, spatial.PoseAlmostCoincident(pf.Pose(), end.Pose()), test.ShouldBeTrue) -} diff --git a/referenceframe/transformable_test.go b/referenceframe/transformable_test.go deleted file mode 100644 index e03e57aa317..00000000000 --- a/referenceframe/transformable_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package referenceframe - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - spatial "go.viam.com/rdk/spatialmath" -) - -func TestPoseInFrame(t *testing.T) { - pose := spatial.NewPose(r3.Vector{1, 2, 3}, &spatial.OrientationVector{math.Pi / 2, 0, 0, -1}) - pF := NewPoseInFrame("frame", pose) - test.That(t, pF.Parent(), test.ShouldEqual, "frame") - test.That(t, spatial.PoseAlmostEqual(pF.Pose(), pose), test.ShouldBeTrue) - convertedPF := ProtobufToPoseInFrame(PoseInFrameToProtobuf(pF)) - test.That(t, pF.Parent(), test.ShouldEqual, convertedPF.Parent()) - test.That(t, spatial.PoseAlmostEqual(pF.Pose(), convertedPF.Pose()), test.ShouldBeTrue) -} - -func TestGeometriesInFrame(t *testing.T) { - pose := spatial.NewPose(r3.Vector{1, 2, 3}, &spatial.OrientationVector{math.Pi / 2, 0, 0, -1}) - zero, err := spatial.NewBox(pose, r3.Vector{1, 2, 3}, "zero") - test.That(t, err, test.ShouldBeNil) - one, err := spatial.NewBox(pose, r3.Vector{2, 3, 4}, "one") - test.That(t, err, test.ShouldBeNil) - two, err := spatial.NewBox(pose, r3.Vector{3, 4, 5}, "two") - test.That(t, err, test.ShouldBeNil) - three, err := spatial.NewBox(pose, r3.Vector{4, 5, 6}, "three") - test.That(t, err, test.ShouldBeNil) - geometryList := []spatial.Geometry{zero, one, two, three} - - test.That(t, err, test.ShouldBeNil) - gF := NewGeometriesInFrame("frame", geometryList) - test.That(t, gF.Parent(), test.ShouldEqual, "frame") - test.That(t, spatial.GeometriesAlmostEqual(one, gF.GeometryByName("one")), test.ShouldBeTrue) - convertedGF, err := ProtobufToGeometriesInFrame(GeometriesInFrameToProtobuf(gF)) - test.That(t, err, test.ShouldBeNil) - test.That(t, gF.Parent(), test.ShouldEqual, convertedGF.Parent()) - test.That(t, spatial.GeometriesAlmostEqual(one, convertedGF.GeometryByName("one")), test.ShouldBeTrue) -} diff --git a/referenceframe/urdf/geometry_test.go b/referenceframe/urdf/geometry_test.go deleted file mode 100644 index b74f16e0489..00000000000 --- a/referenceframe/urdf/geometry_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package urdf - -import ( - "encoding/xml" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/spatialmath" -) - -func TestGeometrySerialization(t *testing.T) { - box, err := spatialmath.NewBox( - spatialmath.NewPose(r3.Vector{X: 10, Y: 2, Z: 3}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 30}), - r3.Vector{X: 4, Y: 5, Z: 6}, - "", - ) - test.That(t, err, test.ShouldBeNil) - sphere, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 3.3, "") - test.That(t, err, test.ShouldBeNil) - capsule, err := spatialmath.NewCapsule(spatialmath.NewZeroPose(), 1, 10, "") - test.That(t, err, test.ShouldBeNil) - - testCases := []struct { - name string - g spatialmath.Geometry - success bool - }{ - {"box", box, true}, - {"sphere", sphere, true}, - {"capsule", capsule, false}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - urdf, err := newCollision(tc.g) - if !tc.success { - test.That(t, err.Error(), test.ShouldContainSubstring, errGeometryTypeUnsupported.Error()) - return - } - test.That(t, err, test.ShouldBeNil) - bytes, err := xml.MarshalIndent(urdf, "", " ") - test.That(t, err, test.ShouldBeNil) - var urdf2 collision - xml.Unmarshal(bytes, &urdf2) - g2, err := urdf2.toGeometry() - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.GeometriesAlmostEqual(tc.g, g2), test.ShouldBeTrue) - }) - } -} diff --git a/referenceframe/urdf/model_test.go b/referenceframe/urdf/model_test.go deleted file mode 100644 index b1906f0e4ca..00000000000 --- a/referenceframe/urdf/model_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package urdf - -import ( - "encoding/xml" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func TestParseURDFFile(t *testing.T) { - // Test a URDF which has prismatic joints - u, err := ParseModelXMLFile(utils.ResolveFile("referenceframe/urdf/testfiles/example_gantry.xml"), "") - test.That(t, err, test.ShouldBeNil) - test.That(t, len(u.DoF()), test.ShouldEqual, 2) - - // Test a URDF will has collision geometries we can evaluate and a DoF of 6 - u, err = ParseModelXMLFile(utils.ResolveFile("referenceframe/urdf/testfiles/ur5e.urdf"), "") - test.That(t, err, test.ShouldBeNil) - model, ok := u.(*referenceframe.SimpleModel) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, u.Name(), test.ShouldEqual, "ur5") - test.That(t, len(u.DoF()), test.ShouldEqual, 6) - modelGeo, err := model.Geometries(make([]referenceframe.Input, len(model.DoF()))) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(modelGeo.Geometries()), test.ShouldEqual, 5) // notably we only have 5 geometries for this model - - // Test naming of a URDF to something other than the robot's name element - u, err = ParseModelXMLFile(utils.ResolveFile("referenceframe/urdf/testfiles/ur5e.urdf"), "foo") - test.That(t, err, test.ShouldBeNil) - test.That(t, u.Name(), test.ShouldEqual, "foo") -} - -func TestURDFTransforms(t *testing.T) { - u, err := ParseModelXMLFile(utils.ResolveFile("referenceframe/urdf/testfiles/ur5e.urdf"), "") - test.That(t, err, test.ShouldBeNil) - simple, ok := u.(*referenceframe.SimpleModel) - test.That(t, ok, test.ShouldBeTrue) - - joints := []referenceframe.Frame{} - for _, tform := range simple.OrdTransforms { - if len(tform.DoF()) > 0 { - joints = append(joints, tform) - } - } - test.That(t, len(joints), test.ShouldEqual, 6) - pose, err := joints[0].Transform([]referenceframe.Input{{0}}) - test.That(t, err, test.ShouldBeNil) - firstJov := pose.Orientation().OrientationVectorRadians() - firstJovExpect := &spatialmath.OrientationVector{Theta: 0, OX: 0, OY: 0, OZ: 1} - test.That(t, firstJov, test.ShouldResemble, firstJovExpect) - - pose, err = joints[0].Transform([]referenceframe.Input{{1.5708}}) - test.That(t, err, test.ShouldBeNil) - firstJov = pose.Orientation().OrientationVectorRadians() - firstJovExpect = &spatialmath.OrientationVector{Theta: 1.5708, OX: 0, OY: 0, OZ: 1} - test.That(t, firstJov.Theta, test.ShouldAlmostEqual, firstJovExpect.Theta) - test.That(t, firstJov.OX, test.ShouldAlmostEqual, firstJovExpect.OX) - test.That(t, firstJov.OY, test.ShouldAlmostEqual, firstJovExpect.OY) - test.That(t, firstJov.OZ, test.ShouldAlmostEqual, firstJovExpect.OZ) -} - -func TestWorldStateConversion(t *testing.T) { - foo, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "foo") - test.That(t, err, test.ShouldBeNil) - bar, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{X: 1, Y: 2, Z: 3}, "bar") - test.That(t, err, test.ShouldBeNil) - ws, err := referenceframe.NewWorldState( - []*referenceframe.GeometriesInFrame{referenceframe.NewGeometriesInFrame(referenceframe.World, []spatialmath.Geometry{foo, bar})}, - nil, - ) - test.That(t, err, test.ShouldBeNil) - - cfg, err := NewModelFromWorldState(ws, "test") - test.That(t, err, test.ShouldBeNil) - bytes, err := xml.MarshalIndent(cfg, "", " ") - test.That(t, err, test.ShouldBeNil) - _ = bytes -} diff --git a/referenceframe/worldstate_test.go b/referenceframe/worldstate_test.go deleted file mode 100644 index 4ba787671d0..00000000000 --- a/referenceframe/worldstate_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package referenceframe - -import ( - "fmt" - "testing" - - "github.com/jedib0t/go-pretty/v6/table" - "go.viam.com/test" - - "go.viam.com/rdk/spatialmath" -) - -func TestWorldStateConstruction(t *testing.T) { - foo, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "foo") - test.That(t, err, test.ShouldBeNil) - bar, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "bar") - test.That(t, err, test.ShouldBeNil) - noname, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "") - test.That(t, err, test.ShouldBeNil) - unnamed, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "") - test.That(t, err, test.ShouldBeNil) - expectedErr := NewDuplicateGeometryNameError(foo.Label()).Error() - - // test that you can add two geometries of different names - _, err = NewWorldState([]*GeometriesInFrame{NewGeometriesInFrame("world", []spatialmath.Geometry{foo, bar})}, nil) - test.That(t, err, test.ShouldBeNil) - - // test that you can't add two "foos" to the same frame - _, err = NewWorldState([]*GeometriesInFrame{NewGeometriesInFrame("", []spatialmath.Geometry{foo, foo})}, nil) - test.That(t, err.Error(), test.ShouldResemble, expectedErr) - - // test that you can't add two "foos" to different frames - _, err = NewWorldState( - []*GeometriesInFrame{ - NewGeometriesInFrame("", []spatialmath.Geometry{foo, bar}), - NewGeometriesInFrame("", []spatialmath.Geometry{foo}), - }, - nil, - ) - test.That(t, err.Error(), test.ShouldResemble, expectedErr) - - // test that you can add multiple geometries with no name - _, err = NewWorldState([]*GeometriesInFrame{NewGeometriesInFrame("", []spatialmath.Geometry{noname, unnamed})}, nil) - test.That(t, err, test.ShouldBeNil) -} - -func TestString(t *testing.T) { - foo, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 10, "foo") - test.That(t, err, test.ShouldBeNil) - bar, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 5, "bar") - test.That(t, err, test.ShouldBeNil) - testgeo, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 7, "testgeo") - test.That(t, err, test.ShouldBeNil) - - ws, err := NewWorldState([]*GeometriesInFrame{ - NewGeometriesInFrame("world", []spatialmath.Geometry{foo, bar}), - NewGeometriesInFrame("camera", []spatialmath.Geometry{testgeo}), - }, nil) - test.That(t, err, test.ShouldBeNil) - - testTable := table.NewWriter() - testTable.AppendHeader(table.Row{"Name", "Geometry Type", "Parent"}) - testTable.AppendRow([]interface{}{ - "foo", - foo.(fmt.Stringer).String(), - "world", - }) - testTable.AppendRow([]interface{}{ - "bar", - bar.(fmt.Stringer).String(), - "world", - }) - testTable.AppendRow([]interface{}{ - "testgeo", - testgeo.(fmt.Stringer).String(), - "camera", - }) - - test.That(t, ws.String(), test.ShouldEqual, testTable.Render()) -} diff --git a/resource/api_model_test.go b/resource/api_model_test.go deleted file mode 100644 index cac85dac09f..00000000000 --- a/resource/api_model_test.go +++ /dev/null @@ -1,549 +0,0 @@ -//nolint:dupl -package resource - -import ( - "testing" - - "go.viam.com/test" -) - -func TestLegacyAPIParsingWithoutNamespace(t *testing.T) { - // A legacy API configuration on a component/service does not include the `api` field. But - // rather a `type` field and optionally a `namespace` field. - var resConfig Config - err := resConfig.UnmarshalJSON([]byte(` -{ - "name": "legacy", - "type": "board", - "model": "fake" -}`)) - test.That(t, err, test.ShouldBeNil) - test.That(t, resConfig.Name, test.ShouldEqual, "legacy") - test.That(t, resConfig.API.Type.Namespace, test.ShouldEqual, "") - test.That(t, resConfig.API.Type.Name, test.ShouldEqual, "") - test.That(t, resConfig.API.SubtypeName, test.ShouldEqual, "board") - // Unqualified `model`s (single word rather than colon delimited triplet) automatically acquire - // the `rdk:builtin` model family at parse time. - test.That(t, resConfig.Model.Family.Namespace, test.ShouldEqual, "rdk") - test.That(t, resConfig.Model.Family.Name, test.ShouldEqual, "builtin") - test.That(t, resConfig.Model.Name, test.ShouldEqual, "fake") - - // After parsing a `resource.Config` as json, fill the API Type in with `rdk:component`. - resConfig.AdjustPartialNames("component") - test.That(t, resConfig.API.Type.Namespace, test.ShouldEqual, "rdk") - test.That(t, resConfig.API.Type.Name, test.ShouldEqual, "component") -} - -func TestLegacyAPIParsing(t *testing.T) { - // A legacy API configuration on a component/service does not include the `api` field. But - // rather a `type` field and optionally a `namespace` field. - var resConfig Config - err := resConfig.UnmarshalJSON([]byte(` -{ - "name": "legacy", - "namespace": "not_rdk", - "type": "board", - "model": "fake" -}`)) - test.That(t, err, test.ShouldBeNil) - test.That(t, resConfig.Name, test.ShouldEqual, "legacy") - test.That(t, resConfig.API.Type.Namespace, test.ShouldEqual, "not_rdk") - test.That(t, resConfig.API.Type.Name, test.ShouldEqual, "") - test.That(t, resConfig.API.SubtypeName, test.ShouldEqual, "board") - // Unqualified `model`s (single word rather than colon delimited triplet) automatically acquire - // the `rdk:builtin` model family at parse time. - test.That(t, resConfig.Model.Family.Namespace, test.ShouldEqual, "rdk") - test.That(t, resConfig.Model.Family.Name, test.ShouldEqual, "builtin") - test.That(t, resConfig.Model.Name, test.ShouldEqual, "fake") - - // After parsing a `resource.Config` as json, fill the API Type in with `rdk:component`. - resConfig.AdjustPartialNames("component") - test.That(t, resConfig.API.Type.Namespace, test.ShouldEqual, "not_rdk") - test.That(t, resConfig.API.Type.Name, test.ShouldEqual, "component") -} - -func TestServiceAdjustPartialNames(t *testing.T) { - resConfig := Config{ - API: API{ - SubtypeName: "nav", - }, - } - - // Running `AdjustPartialNames` on a service should fill in all of the remaining `API` and - // `Model` fields. - resConfig.AdjustPartialNames("service") - test.That(t, resConfig.Name, test.ShouldEqual, "builtin") - test.That(t, resConfig.API.Type.Namespace, test.ShouldEqual, "rdk") - test.That(t, resConfig.API.Type.Name, test.ShouldEqual, "service") - test.That(t, resConfig.API.SubtypeName, test.ShouldEqual, "nav") - test.That(t, resConfig.Model.Family.Namespace, test.ShouldEqual, "rdk") - test.That(t, resConfig.Model.Family.Name, test.ShouldEqual, "builtin") - test.That(t, resConfig.Model.Name, test.ShouldEqual, "builtin") -} - -func TestAPIStringParsing(t *testing.T) { - // Test that a colon-delimited triplet string for the `API` is parsed into its - // namespace/type/subtype tuple. - var resConfig Config - err := resConfig.UnmarshalJSON([]byte(` -{ - "name": "legacy", - "type": "board", - "model": "fake", - "api": "namespace:component_type:subtype" -}`)) - test.That(t, err, test.ShouldBeNil) - - test.That(t, resConfig.API.Type.Namespace, test.ShouldEqual, "namespace") - test.That(t, resConfig.API.Type.Name, test.ShouldEqual, "component_type") - test.That(t, resConfig.API.SubtypeName, test.ShouldEqual, "subtype") - - // Test that a malformed string gives a string specific error message. - err = resConfig.UnmarshalJSON([]byte(` -{ - "name": "legacy", - "type": "board", - "model": "fake", - "api": "double::colons::are_bad" -}`)) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not a valid API config string.") -} - -func TestAPIObjectParsing(t *testing.T) { - // Test that an API object with all three types parses correctly. - var resConfig Config - err := resConfig.UnmarshalJSON([]byte(` -{ - "name": "legacy", - "type": "board", - "model": "fake", - "api": { - "namespace": "namespace", - "type": "component", - "subtype": "subtype" - } -}`)) - test.That(t, err, test.ShouldBeNil) - - test.That(t, resConfig.API.Type.Namespace, test.ShouldEqual, "namespace") - test.That(t, resConfig.API.Type.Name, test.ShouldEqual, "component") - test.That(t, resConfig.API.SubtypeName, test.ShouldEqual, "subtype") - - // Test that a malformed API object does not parse. Without enumerating all of the ways an API - // object can be malformed. - err = resConfig.UnmarshalJSON([]byte(` -{ - "name": "legacy", - "type": "board", - "model": "fake", - "api": {} -}`)) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "namespace field for resource missing or invalid") -} - -func TestModel(t *testing.T) { - for _, tc := range []struct { - TestName string - Namespace ModelNamespace - Family string - Model string - Expected Model - Err string - }{ - { - "missing namespace", - "", - "test", - "modelA", - Model{ - Family: ModelFamily{Namespace: "", Name: "test"}, - Name: "modelA", - }, - `Error validating, missing required field. Field: "namespace"`, - }, - { - "missing family", - "acme", - "", - "modelA", - Model{ - Family: ModelFamily{Namespace: "acme", Name: ""}, - Name: "modelA", - }, - `Error validating, missing required field. Field: "model_family"`, - }, - { - "missing name", - "acme", - "test", - "", - Model{ - Family: ModelFamily{Namespace: "acme", Name: "test"}, - Name: "", - }, - `Error validating, missing required field. Field: "name"`, - }, - { - "reserved character in model namespace", - "ac:me", - "test", - "modelA", - Model{ - Family: ModelFamily{Namespace: "ac:me", Name: "test"}, - Name: "modelA", - }, - "reserved character : used", - }, - { - "reserved character in model family", - "acme", - "te:st", - "modelA", - Model{ - Family: ModelFamily{Namespace: "acme", Name: "te:st"}, - Name: "modelA", - }, - "reserved character : used", - }, - { - "reserved character in model name", - "acme", - "test", - "model:A", - Model{ - Family: ModelFamily{Namespace: "acme", Name: "test"}, - Name: "model:A", - }, - "reserved character : used", - }, - { - "valid model", - "acme", - "test", - "modelA", - Model{ - Family: ModelFamily{Namespace: "acme", Name: "test"}, - Name: "modelA", - }, - "", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed := tc.Namespace.WithFamily(tc.Family).WithModel(tc.Model) - test.That(t, observed, test.ShouldResemble, tc.Expected) - err := observed.Validate() - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestModelFromString(t *testing.T) { - for _, tc := range []struct { - TestName string - StrModel string - Expected Model - Err string - }{ - { - "valid", - "acme:test:modelA", - Model{ - Family: ModelFamily{Namespace: "acme", Name: "test"}, - Name: "modelA", - }, - "", - }, - { - "valid with special characters and numbers", - "acme_corp1:test-collection99:model_a2", - Model{ - Family: ModelFamily{Namespace: "acme_corp1", Name: "test-collection99"}, - Name: "model_a2", - }, - "", - }, - { - "invalid with slash", - "acme/corp:test:modelA", - Model{}, - "not a valid model name", - }, - { - "invalid with caret", - "acme:test:model^A", - Model{}, - "not a valid model name", - }, - { - "missing field", - "acme:test", - Model{}, - "not a valid model name", - }, - { - "empty namespace", - ":test:modelA", - Model{}, - "not a valid model name", - }, - { - "empty family", - "acme::modelA", - Model{}, - "not a valid model name", - }, - { - "empty name", - "acme:test::", - Model{}, - "not a valid model name", - }, - { - "extra field", - "acme:test:modelA:fail", - Model{}, - "not a valid model name", - }, - { - "mistaken resource name", - "acme:test:modelA/fail", - Model{}, - "not a valid model name", - }, - { - "short form", - "modelB", - Model{ - Family: DefaultModelFamily, - Name: "modelB", - }, - "", - }, - { - "invalid short form", - "model^B", - Model{}, - "not a valid model name", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed, err := NewModelFromString(tc.StrModel) - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, observed.Validate(), test.ShouldBeNil) - test.That(t, observed, test.ShouldResemble, tc.Expected) - test.That(t, observed.String(), test.ShouldResemble, tc.Expected.String()) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestModelFromJSONObject(t *testing.T) { - for _, tc := range []struct { - TestName string - StrModel string - Expected Model - ErrJSON string - }{ - { - "valid nested json", - `{"namespace": "acme", "model_family": "test", "name": "modelB"}`, - Model{ - Family: ModelFamily{Namespace: "acme", Name: "test"}, - Name: "modelB", - }, - "", - }, - { - "invalid nested json family", - `{"namespace": "acme", "model_family": "te^st", "name": "modelB"}`, - Model{}, - "not a valid model family", - }, - { - "invalid nested json namespace", - `{"namespace": "$acme", "model_family": "test", "name": "modelB"}`, - Model{}, - "not a valid model namespace", - }, - { - "invalid nested json name", - `{"namespace": "acme", "model_family": "test", "name": "model#B"}`, - Model{}, - "not a valid model name", - }, - { - "missing nested json field", - `{"namespace": "acme", "name": "model#B"}`, - Model{}, - `Error validating, missing required field. Field: "model_family"`, - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - fromJSON := &Model{} - errJSON := fromJSON.UnmarshalJSON([]byte(tc.StrModel)) - if tc.ErrJSON == "" { - test.That(t, errJSON, test.ShouldBeNil) - test.That(t, fromJSON.Validate(), test.ShouldBeNil) - test.That(t, fromJSON, test.ShouldResemble, &tc.Expected) - test.That(t, fromJSON.String(), test.ShouldResemble, tc.Expected.String()) - } else { - test.That(t, errJSON, test.ShouldNotBeNil) - test.That(t, errJSON.Error(), test.ShouldContainSubstring, tc.ErrJSON) - } - }) - } -} - -func TestAPIFromString(t *testing.T) { - for _, tc := range []struct { - TestName string - StrAPI string - Expected API - Err string - }{ - { - "valid", - "rdk:component:arm", - APINamespaceRDK.WithComponentType("arm"), - "", - }, - { - "valid with special characters and numbers", - "acme_corp1:test-collection99:api_a2", - API{ - Type: APIType{Namespace: "acme_corp1", Name: "test-collection99"}, - SubtypeName: "api_a2", - }, - "", - }, - { - "invalid with slash", - "acme/corp:test:subtypeA", - API{}, - "not a valid api name", - }, - { - "invalid with caret", - "acme:test:subtype^A", - API{}, - "not a valid api name", - }, - { - "missing field", - "acme:test", - API{}, - "not a valid api name", - }, - { - "empty namespace", - ":test:subtypeA", - API{}, - "not a valid api name", - }, - { - "empty family", - "acme::subtypeA", - API{}, - "not a valid api name", - }, - { - "empty name", - "acme:test::", - API{}, - "not a valid api name", - }, - { - "extra field", - "acme:test:subtypeA:fail", - API{}, - "not a valid api name", - }, - { - "mistaken resource name", - "acme:test:subtypeA/fail", - API{}, - "not a valid api name", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed, err := NewAPIFromString(tc.StrAPI) - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, observed.Validate(), test.ShouldBeNil) - test.That(t, observed, test.ShouldResemble, tc.Expected) - test.That(t, observed.String(), test.ShouldResemble, tc.Expected.String()) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestAPIFromJSONObject(t *testing.T) { - for _, tc := range []struct { - TestName string - StrAPI string - Expected API - ErrJSON string - }{ - { - "valid nested json", - `{"namespace": "acme", "type": "test", "subtype": "subtypeB"}`, - API{ - Type: APIType{Namespace: "acme", Name: "test"}, - SubtypeName: "subtypeB", - }, - "", - }, - { - "invalid nested json type", - `{"namespace": "acme", "type": "te^st", "subtype": "subtypeB"}`, - API{}, - "not a valid type name", - }, - { - "invalid nested json namespace", - `{"namespace": "$acme", "type": "test", "subtype": "subtypeB"}`, - API{}, - "not a valid type namespace", - }, - { - "invalid nested json subtype", - `{"namespace": "acme", "type": "test", "subtype": "subtype#B"}`, - API{}, - "not a valid subtype name", - }, - { - "missing nested json field", - `{"namespace": "acme", "name": "subtype#B"}`, - API{}, - "field for resource missing", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - fromJSON := &API{} - errJSON := fromJSON.UnmarshalJSON([]byte(tc.StrAPI)) - if tc.ErrJSON == "" { - test.That(t, errJSON, test.ShouldBeNil) - test.That(t, fromJSON.Validate(), test.ShouldBeNil) - test.That(t, fromJSON, test.ShouldResemble, &tc.Expected) - test.That(t, fromJSON.String(), test.ShouldResemble, tc.Expected.String()) - } else { - test.That(t, errJSON, test.ShouldNotBeNil) - test.That(t, errJSON.Error(), test.ShouldContainSubstring, tc.ErrJSON) - } - }) - } -} diff --git a/resource/api_resource_collection_test.go b/resource/api_resource_collection_test.go deleted file mode 100644 index f4aefe7bf76..00000000000 --- a/resource/api_resource_collection_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package resource_test - -import ( - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" -) - -func TestAPIResourceCollection(t *testing.T) { - res1 := testutils.NewUnimplementedResource(generic.Named("name1")) - res2 := testutils.NewUnimplementedResource(generic.Named("name2")) - resources := map[resource.Name]resource.Resource{ - res1.Name(): res1, - } - svc, err := resource.NewAPIResourceCollection(generic.API, resources) - test.That(t, err, test.ShouldBeNil) - res, err := svc.Resource(res1.Name().ShortName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res1) - _, err = svc.Resource(res2.Name().ShortName()) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(res2.Name())) - - resources[res2.Name()] = res2 - err = svc.ReplaceAll(resources) - test.That(t, err, test.ShouldBeNil) - res, err = svc.Resource(res1.Name().ShortName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res1) - res, err = svc.Resource(res2.Name().ShortName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res2) - - err = svc.ReplaceAll(map[resource.Name]resource.Resource{}) - test.That(t, err, test.ShouldBeNil) - _, err = svc.Resource(res1.Name().ShortName()) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(res1.Name())) - _, err = svc.Resource(res2.Name().ShortName()) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(res2.Name())) - // Test should error if resource name is empty - resources = map[resource.Name]resource.Resource{ - resource.NewName( - resource.APINamespaceRDK.WithComponentType("foo"), - "", - ): testutils.NewUnimplementedResource(generic.Named("")), - } - err = svc.ReplaceAll(resources) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "empty name used for resource:") -} - -func TestAPIRemoteNames(t *testing.T) { - name0 := "name0" - name1 := "remote1:name1" - name2 := "remote2:name2" - name3 := "remote2:remote1:name1" - name4 := "remote2:remote1:name4" - name5 := "remote2:remote1:name5name6" - name7 := "remote2:remote4:name1" - name8 := "remote1:name0" - name9 := "remote1:nameX" - name10 := "remote2:nameX" - - res0 := testutils.NewUnimplementedResource(generic.Named(name0)) - res1 := testutils.NewUnimplementedResource(generic.Named(name1)) - res2 := testutils.NewUnimplementedResource(generic.Named(name2)) - res3 := testutils.NewUnimplementedResource(generic.Named(name3)) - res4 := testutils.NewUnimplementedResource(generic.Named(name4)) - res5 := testutils.NewUnimplementedResource(generic.Named(name5)) - res7 := testutils.NewUnimplementedResource(generic.Named(name7)) - res8 := testutils.NewUnimplementedResource(generic.Named(name8)) - res9 := testutils.NewUnimplementedResource(generic.Named(name9)) - res10 := testutils.NewUnimplementedResource(generic.Named(name10)) - - resources := map[resource.Name]resource.Resource{ - generic.Named(name0): res0, - generic.Named(name1): res1, - generic.Named(name2): res2, - generic.Named(name3): res3, - generic.Named(name4): res4, - generic.Named(name5): res5, - generic.Named(name7): res7, - generic.Named(name8): res8, - generic.Named(name9): res9, - generic.Named(name10): res10, - } - svc, err := resource.NewAPIResourceCollection(generic.API, resources) - test.That(t, err, test.ShouldBeNil) - res, err := svc.Resource(name0) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res0) - res, err = svc.Resource(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res1) - res, err = svc.Resource(name2) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res2) - res, err = svc.Resource(name3) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res3) - res, err = svc.Resource(name4) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res4) - res, err = svc.Resource(name5) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res5) - res, err = svc.Resource(name7) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res7) - res, err = svc.Resource(name8) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res8) - res, err = svc.Resource(name9) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res9) - res, err = svc.Resource(name10) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res10) - - res, err = svc.Resource("name2") - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res2) - _, err = svc.Resource("remote1:name2") - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("remote1:name2"))) - res, err = svc.Resource("remote2:name2") - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res2) - _, err = svc.Resource("name1") - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("name1"))) - res, err = svc.Resource("remote1:name1") - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res1) - res, err = svc.Resource("name4") - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res4) - _, err = svc.Resource("remote1:name3") - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("remote1:name3"))) - _, err = svc.Resource("remote2:name3") - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("remote2:name3"))) - _, err = svc.Resource("name5") - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("name5"))) - _, err = svc.Resource("name6") - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("name6"))) - res, err = svc.Resource("name5name6") - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res5) - - res, err = svc.Resource("name0") - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, res0) - _, err = svc.Resource("nameX") - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("nameX"))) -} - -func TestAPIAddRemoveReplaceOne(t *testing.T) { - ns := resource.APINamespace("acme") - ct := "test" - st := "string" - api := ns.WithType(ct).WithSubtype(st) - - name1 := "name1" - name2 := "name2" - name3 := "name3" - name4 := "remote1:name4" - name4d := "remote2:name4" - name4s := "name4" - - str1 := "string1" - str2 := "string2" - str3 := "string3" - str4 := "string4" - strR := "stringReplaced" - - key1 := resource.NewName(api, name1) - key2 := resource.NewName(api, name2) - key3 := resource.NewName(api, name3) - key4 := resource.NewName(api, name4) - key4d := resource.NewName(api, name4d) - - svc, err := resource.NewAPIResourceCollection(generic.API, map[resource.Name]resource.Resource{ - key1: testutils.NewUnimplementedResource(generic.Named(str1)), - key4: testutils.NewUnimplementedResource(generic.Named(str4)), - }) - test.That(t, err, test.ShouldBeNil) - res, err := svc.Resource(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(str1))) - _, err = svc.Resource(name2) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named(name2))) - _, err = svc.Resource(name3) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named(name3))) - res, err = svc.Resource(name4) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(str4))) - res, err = svc.Resource(name4s) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(str4))) - - test.That(t, svc.Add(key2, testutils.NewUnimplementedResource(generic.Named(str2))), test.ShouldBeNil) - res, err = svc.Resource(name2) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(str2))) - - test.That(t, svc.Add(key3, testutils.NewUnimplementedResource(generic.Named(str3))), test.ShouldBeNil) - res, err = svc.Resource(name3) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(str3))) - - test.That(t, svc.ReplaceOne(key2, testutils.NewUnimplementedResource(generic.Named(strR))), test.ShouldBeNil) - res, err = svc.Resource(name2) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(strR))) - - test.That(t, svc.ReplaceOne(key4, testutils.NewUnimplementedResource(generic.Named(strR))), test.ShouldBeNil) - res, err = svc.Resource(name4) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(strR))) - res, err = svc.Resource(name4s) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(strR))) - - test.That(t, svc.Remove(key3), test.ShouldBeNil) - _, err = svc.Resource(name3) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named(name3))) - - test.That(t, svc.Add(key4d, testutils.NewUnimplementedResource(generic.Named(str4))), test.ShouldBeNil) - res, err = svc.Resource(name4d) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(str4))) - _, err = svc.Resource(name4s) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named(name4s))) - - test.That(t, svc.Remove(key4d), test.ShouldBeNil) - _, err = svc.Resource(name4d) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named(name4d))) - res, err = svc.Resource(name4) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(strR))) - res, err = svc.Resource(name4s) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldResemble, testutils.NewUnimplementedResource(generic.Named(strR))) -} diff --git a/resource/config_test.go b/resource/config_test.go deleted file mode 100644 index 1e4ee6d1c8a..00000000000 --- a/resource/config_test.go +++ /dev/null @@ -1,652 +0,0 @@ -package resource_test - -import ( - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/testutils" -) - -var ( - acmeAPINamespace = resource.APINamespace("acme") - fakeModel = resource.DefaultModelFamily.WithModel("fake") - extModel = resource.ModelNamespace("acme").WithFamily("test").WithModel("model") - extAPI = acmeAPINamespace.WithComponentType("gizmo") - extServiceAPI = acmeAPINamespace.WithServiceType("gadget") - invalidNameString = "must start with a letter or number and must only contain letters, numbers, dashes, and underscores" -) - -func TestComponentValidate(t *testing.T) { - t.Run("config invalid", func(t *testing.T) { - var emptyConf resource.Config - deps, err := emptyConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "name") - }) - - t.Run("config invalid name", func(t *testing.T) { - validConf := resource.Config{ - Name: "foo arm", - - Model: fakeModel, - } - deps, err := validConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - validConf.Name = "foo.arm" - deps, err = validConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - validConf.Name = "9" - deps, err = validConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - }) - t.Run("config valid", func(t *testing.T) { - validConf := resource.Config{ - Name: "foo", - API: arm.API, - Model: fakeModel, - } - deps, err := validConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - validConf.Name = "A" - deps, err = validConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("ConvertedAttributes", func(t *testing.T) { - t.Run("config invalid", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "foo", - API: base.API, - Model: fakeModel, - ConvertedAttributes: &testutils.FakeConvertedAttributes{Thing: ""}, - } - deps, err := invalidConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, "Thing") - }) - - t.Run("config valid", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "foo", - API: arm.API, - Model: fakeModel, - ConvertedAttributes: &testutils.FakeConvertedAttributes{ - Thing: "i am a thing!", - }, - } - deps, err := invalidConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - }) - }) - - t.Run("no namespace", func(t *testing.T) { - validConf := resource.Config{ - Name: "foo", - API: resource.APINamespace("").WithComponentType("foo"), - Model: fakeModel, - } - deps, err := validConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, validConf.API, test.ShouldResemble, resource.APINamespaceRDK.WithComponentType("foo")) - }) - - t.Run("with namespace", func(t *testing.T) { - validConf := resource.Config{ - Name: "foo", - API: resource.APINamespace("acme").WithComponentType("foo"), - Model: fakeModel, - } - deps, err := validConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, validConf.API, test.ShouldResemble, resource.APINamespace("acme").WithComponentType("foo")) - }) - - t.Run("reserved character in name", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "fo:o", - Model: fakeModel, - } - _, err := invalidConf.Validate("path", resource.APITypeComponentName) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - }) - - t.Run("reserved character in namespace", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "foo", - API: resource.APINamespace("ac:me").WithComponentType("foo"), - Model: fakeModel, - } - _, err := invalidConf.Validate("path", resource.APITypeComponentName) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "reserved character : used") - }) - - t.Run("model variations", func(t *testing.T) { - t.Run("config valid short model", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: base.API, - Model: resource.Model{Name: "fake"}, - } - deps, err := shortConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.Model.Family, test.ShouldResemble, resource.DefaultModelFamily) - test.That(t, shortConf.Model.Name, test.ShouldEqual, "fake") - }) - - t.Run("config valid full model", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: base.API, - Model: fakeModel, - } - deps, err := shortConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.Model.Family, test.ShouldResemble, resource.DefaultModelFamily) - test.That(t, shortConf.Model.Name, test.ShouldEqual, "fake") - }) - - t.Run("config valid external model", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: base.API, - Model: extModel, - } - deps, err := shortConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.Model.Family, test.ShouldResemble, resource.NewModelFamily("acme", "test")) - test.That(t, shortConf.Model.Name, test.ShouldEqual, "model") - }) - }) - - t.Run("api subtype namespace variations", func(t *testing.T) { - t.Run("empty API and builtin type", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: resource.APINamespace("").WithType("").WithSubtype("base"), - Model: fakeModel, - } - deps, err := shortConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.API, test.ShouldResemble, base.API) - }) - - t.Run("filled API with builtin type", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - Model: fakeModel, - API: base.API, - } - deps, err := shortConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.API, test.ShouldResemble, base.API) - }) - - t.Run("empty API with external type", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: resource.APINamespace("acme").WithType("").WithSubtype("gizmo"), - Model: fakeModel, - } - deps, err := shortConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.API, test.ShouldResemble, extAPI) - }) - - t.Run("filled API with external type", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - Model: fakeModel, - API: extAPI, - } - deps, err := shortConf.Validate("path", resource.APITypeComponentName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.API, test.ShouldResemble, extAPI) - }) - }) -} - -func TestComponentResourceName(t *testing.T) { - for _, tc := range []struct { - Name string - Conf resource.Config - ExpectedAPI resource.API - ExpectedName resource.Name - ExpectedError string - }{ - { - "all fields included", - resource.Config{ - Name: "foo", - API: arm.API, - Model: fakeModel, - }, - arm.API, - arm.Named("foo"), - "", - }, - { - "missing subtype", - resource.Config{ - API: resource.APINamespaceRDK.WithType("").WithSubtype(""), - Name: "foo", - }, - resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: "", - }, - resource.Name{ - API: resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: "", - }, - Name: "foo", - }, - "name", - }, - { - "sensor with no subtype", - resource.Config{ - API: resource.APINamespaceRDK.WithComponentType("sensor"), - Name: "foo", - }, - resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: sensor.SubtypeName, - }, - resource.Name{ - API: resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: sensor.SubtypeName, - }, - Name: "foo", - }, - "name", - }, - { - "sensor with subtype", - resource.Config{ - API: resource.APINamespaceRDK.WithComponentType("movement_sensor"), - Name: "foo", - }, - resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: movementsensor.SubtypeName, - }, - resource.Name{ - API: resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: movementsensor.SubtypeName, - }, - Name: "foo", - }, - "name", - }, - { - "sensor missing name", - resource.Config{ - API: resource.APINamespaceRDK.WithComponentType("sensor"), - Name: "", - }, - resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: movementsensor.SubtypeName, - }, - resource.Name{ - API: resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: movementsensor.SubtypeName, - }, - Name: "", - }, - "name", - }, - { - "all fields included with external type", - resource.Config{ - Name: "foo", - API: extAPI, - Model: extModel, - }, - extAPI, - resource.NewName(extAPI, "foo"), - "", - }, - } { - t.Run(tc.Name, func(t *testing.T) { - _, err := tc.Conf.Validate("", resource.APITypeComponentName) - if tc.ExpectedError == "" { - test.That(t, err, test.ShouldBeNil) - rName := tc.Conf.ResourceName() - test.That(t, rName.API, test.ShouldResemble, tc.ExpectedAPI) - test.That(t, rName, test.ShouldResemble, tc.ExpectedName) - return - } - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resource.GetFieldFromFieldRequiredError(err), test.ShouldEqual, tc.ExpectedError) - }) - } -} - -func TestServiceValidate(t *testing.T) { - t.Run("config invalid", func(t *testing.T) { - var emptyConf resource.Config - deps, err := emptyConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `subtype field`) - }) - - t.Run("config valid", func(t *testing.T) { - validConf := resource.Config{ - Name: "frame1", - API: resource.APINamespaceRDK.WithServiceType("frame_system"), - } - deps, err := validConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - validConf.Name = "A" - deps, err = validConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - }) - t.Run("config invalid name", func(t *testing.T) { - validConf := resource.Config{ - Name: "frame 1", - API: resource.APINamespaceRDK.WithServiceType("frame_system"), - } - deps, err := validConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - validConf.Name = "frame.1" - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - validConf.Name = "3" - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - }) - - t.Run("ConvertedAttributes", func(t *testing.T) { - t.Run("config invalid", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "frame1", - API: resource.APINamespaceRDK.WithServiceType("frame_system"), - ConvertedAttributes: &testutils.FakeConvertedAttributes{Thing: ""}, - } - deps, err := invalidConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, `Field: "Thing"`) - }) - - t.Run("config valid", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "frame1", - API: resource.APINamespaceRDK.WithServiceType("frame_system"), - ConvertedAttributes: &testutils.FakeConvertedAttributes{ - Thing: "i am a thing!", - }, - } - deps, err := invalidConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - }) - }) - - t.Run("no namespace", func(t *testing.T) { - validConf := resource.Config{ - Name: "foo", - API: resource.APINamespace("").WithServiceType("frame_system"), - } - deps, err := validConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("no name", func(t *testing.T) { - testConfig := resource.Config{ - Name: "", - API: resource.APINamespace("").WithServiceType("frame_system"), - } - deps, err := testConfig.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, testConfig.Name, test.ShouldEqual, resource.DefaultServiceName) - }) - - t.Run("with namespace", func(t *testing.T) { - validConf := resource.Config{ - Name: "foo", - API: acmeAPINamespace.WithServiceType("thingy"), - } - deps, err := validConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("reserved character in name", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "fo:o", - API: acmeAPINamespace.WithServiceType("thingy"), - } - _, err := invalidConf.Validate("path", resource.APITypeServiceName) - test.That(t, err, test.ShouldNotBeNil) - test.That( - t, - err.Error(), - test.ShouldContainSubstring, - invalidNameString, - ) - }) - - t.Run("reserved character in namespace", func(t *testing.T) { - invalidConf := resource.Config{ - Name: "foo", - API: resource.APINamespace("ac:me").WithServiceType("thingy"), - } - _, err := invalidConf.Validate("path", resource.APITypeServiceName) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "reserved character : used") - }) - - t.Run("default model to default", func(t *testing.T) { - validConf := resource.Config{ - Name: "foo", - API: acmeAPINamespace.WithServiceType("thingy"), - } - deps, err := validConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, validConf.Model.String(), test.ShouldEqual, "rdk:builtin:builtin") - }) - - t.Run("model variations", func(t *testing.T) { - t.Run("config valid short model", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: resource.APINamespaceRDK.WithComponentType("bar"), - Model: resource.Model{Name: "fake"}, - } - deps, err := shortConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.Model.Family, test.ShouldResemble, resource.DefaultModelFamily) - test.That(t, shortConf.Model.Name, test.ShouldEqual, "fake") - }) - - t.Run("config valid full model", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: resource.APINamespaceRDK.WithComponentType("bar"), - Model: fakeModel, - } - deps, err := shortConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.Model.Family, test.ShouldResemble, resource.DefaultModelFamily) - test.That(t, shortConf.Model.Name, test.ShouldEqual, "fake") - }) - - t.Run("config valid external model", func(t *testing.T) { - shortConf := resource.Config{ - Name: "foo", - API: resource.APINamespaceRDK.WithComponentType("bar"), - Model: extModel, - } - deps, err := shortConf.Validate("path", resource.APITypeServiceName) - test.That(t, deps, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, shortConf.Model.Family, test.ShouldResemble, resource.NewModelFamily("acme", "test")) - test.That(t, shortConf.Model.Name, test.ShouldEqual, "model") - }) - }) -} - -func TestServiceResourceName(t *testing.T) { - for _, tc := range []struct { - Name string - Conf resource.Config - ExpectedAPI resource.API - ExpectedName resource.Name - }{ - { - "all fields included", - resource.Config{ - Name: "motion1", - API: motion.API, - }, - motion.API, - resource.NewName(motion.API, "motion1"), - }, - { - "all fields included with external type", - resource.Config{ - Name: "foo", - API: extServiceAPI, - Model: extModel, - }, - extServiceAPI, - resource.NewName(extServiceAPI, "foo"), - }, - } { - t.Run(tc.Name, func(t *testing.T) { - _, err := tc.Conf.Validate("", resource.APITypeServiceName) - test.That(t, err, test.ShouldBeNil) - rName := tc.Conf.ResourceName() - test.That(t, rName.API, test.ShouldResemble, tc.ExpectedAPI) - test.That(t, rName, test.ShouldResemble, tc.ExpectedName) - }) - } -} - -func TestEqual(t *testing.T) { - t.Run("test associated config equality", func(t *testing.T) { - assocConfA := &mockAssociatedConfig{Field1: "foo", capName: arm.Named("foo")} - assocConfB := &mockAssociatedConfig{Field1: "bar", capName: arm.Named("bar")} - t.Run("success when equal", func(t *testing.T) { - confA := resource.Config{AssociatedAttributes: map[resource.Name]resource.AssociatedConfig{ - arm.Named("foo"): assocConfA, - arm.Named("bar"): assocConfB, - }} - //nolint: gocritic - test.That(t, confA.Equals(confA), test.ShouldBeTrue) - }) - t.Run("fail when different lengths", func(t *testing.T) { - confA := resource.Config{AssociatedAttributes: map[resource.Name]resource.AssociatedConfig{ - arm.Named("foo"): assocConfA, - arm.Named("bar"): assocConfB, - }} - confB := resource.Config{AssociatedAttributes: map[resource.Name]resource.AssociatedConfig{ - arm.Named("foo"): assocConfA, - }} - test.That(t, confA.Equals(confB), test.ShouldBeFalse) - }) - t.Run("fail when different data", func(t *testing.T) { - confA := resource.Config{AssociatedAttributes: map[resource.Name]resource.AssociatedConfig{ - arm.Named("foo"): assocConfA, - arm.Named("bar"): assocConfB, - }} - confB := resource.Config{AssociatedAttributes: map[resource.Name]resource.AssociatedConfig{ - arm.Named("foo"): assocConfA, - arm.Named("bar"): assocConfA, - }} - test.That(t, confA.Equals(confB), test.ShouldBeFalse) - }) - }) - t.Run("test equality of other fields", func(t *testing.T) { - confA := resource.Config{API: arm.API} - t.Run("success when equal", func(t *testing.T) { - //nolint: gocritic - test.That(t, confA.Equals(confA), test.ShouldBeTrue) - }) - t.Run("success when unequal", func(t *testing.T) { - test.That(t, confA.Equals(resource.Config{API: base.API}), test.ShouldBeFalse) - }) - }) -} diff --git a/resource/errors_test.go b/resource/errors_test.go deleted file mode 100644 index a9aaac0ad14..00000000000 --- a/resource/errors_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package resource - -import ( - "testing" - - "go.viam.com/test" -) - -func TestDependencyTypeError(t *testing.T) { - name := NewName(APINamespace("foo").WithType("bar").WithSubtype("baz"), "bark") - test.That(t, - DependencyTypeError[someRes1](name, someRes2{}).Error(), - test.ShouldContainSubstring, - `dependency "foo:bar:baz/bark" should be an implementation of resource.someRes1 but it was a resource.someRes2`, - ) - - test.That(t, - DependencyTypeError[someIfc](name, someRes2{}).Error(), - test.ShouldContainSubstring, - `dependency "foo:bar:baz/bark" should be an implementation of resource.someIfc but it was a resource.someRes2`, - ) -} - -func TestTypeError(t *testing.T) { - test.That(t, - TypeError[someRes1](someRes2{}).Error(), - test.ShouldContainSubstring, - "expected implementation of resource.someRes1 but it was a resource.someRes2", - ) - - test.That(t, - TypeError[someIfc](someRes2{}).Error(), - test.ShouldContainSubstring, - "expected implementation of resource.someIfc but it was a resource.someRes2", - ) -} - -type someIfc Resource - -type someRes1 struct { - Named - TriviallyReconfigurable - TriviallyCloseable -} - -type someRes2 struct { - Named - TriviallyReconfigurable - TriviallyCloseable -} diff --git a/resource/graph_node_test.go b/resource/graph_node_test.go deleted file mode 100644 index ef616beedea..00000000000 --- a/resource/graph_node_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package resource_test - -import ( - "context" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/utils" -) - -func TestUninitializedLifecycle(t *testing.T) { - // empty - node := resource.NewUninitializedNode() - test.That(t, node.IsUninitialized(), test.ShouldBeTrue) - test.That(t, node.UpdatedAt(), test.ShouldEqual, 0) - _, err := node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not initialized") - _, err = node.UnsafeResource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not initialized") - test.That(t, node.ResourceModel(), test.ShouldResemble, resource.Model{}) - test.That(t, node.HasResource(), test.ShouldBeFalse) - test.That(t, node.Config(), test.ShouldResemble, resource.Config{}) - test.That(t, node.NeedsReconfigure(), test.ShouldBeFalse) - - lifecycleTest(t, node, []string(nil)) -} - -func TestUnconfiguredLifecycle(t *testing.T) { - someConf := resource.Config{Attributes: utils.AttributeMap{"3": 4}} - initialDeps := []string{"dep1", "dep2"} - node := resource.NewUnconfiguredGraphNode(someConf, initialDeps) - - test.That(t, node.IsUninitialized(), test.ShouldBeTrue) - test.That(t, node.UpdatedAt(), test.ShouldEqual, 0) - _, err := node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not initialized") - _, err = node.UnsafeResource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not initialized") - test.That(t, node.ResourceModel(), test.ShouldResemble, resource.Model{}) - test.That(t, node.HasResource(), test.ShouldBeFalse) - test.That(t, node.Config(), test.ShouldResemble, someConf) - test.That(t, node.NeedsReconfigure(), test.ShouldBeTrue) - test.That(t, node.UnresolvedDependencies(), test.ShouldResemble, initialDeps) - - lifecycleTest(t, node, initialDeps) -} - -func TestConfiguredLifecycle(t *testing.T) { - someConf := resource.Config{Attributes: utils.AttributeMap{"3": 4}} - - ourRes := &someResource{Resource: testutils.NewUnimplementedResource(generic.Named("some"))} - node := resource.NewConfiguredGraphNode(someConf, ourRes, resource.DefaultModelFamily.WithModel("bar")) - - test.That(t, node.IsUninitialized(), test.ShouldBeFalse) - test.That(t, node.UpdatedAt(), test.ShouldEqual, 0) - res, err := node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, res) - res, err = node.UnsafeResource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, res) - test.That(t, node.ResourceModel(), test.ShouldResemble, resource.DefaultModelFamily.WithModel("bar")) - test.That(t, node.HasResource(), test.ShouldBeTrue) - test.That(t, node.Config(), test.ShouldResemble, someConf) - test.That(t, node.NeedsReconfigure(), test.ShouldBeFalse) - test.That(t, node.UnresolvedDependencies(), test.ShouldBeEmpty) - - lifecycleTest(t, node, []string(nil)) -} - -func lifecycleTest(t *testing.T, node *resource.GraphNode, initialDeps []string) { - // mark it for removal - test.That(t, node.MarkedForRemoval(), test.ShouldBeFalse) - node.MarkForRemoval() - test.That(t, node.MarkedForRemoval(), test.ShouldBeTrue) - - _, err := node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "pending removal") - - ourErr := errors.New("whoops") - node.LogAndSetLastError(ourErr) - _, err = node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "pending removal") - - test.That(t, node.UnresolvedDependencies(), test.ShouldResemble, initialDeps) - - // but we end up configuring it - ourRes := &someResource{Resource: testutils.NewUnimplementedResource(generic.Named("foo"))} - node.SwapResource(ourRes, resource.DefaultModelFamily.WithModel("bar")) - test.That(t, node.ResourceModel(), test.ShouldResemble, resource.DefaultModelFamily.WithModel("bar")) - test.That(t, node.MarkedForRemoval(), test.ShouldBeFalse) - test.That(t, node.IsUninitialized(), test.ShouldBeFalse) - - res, err := node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, ourRes) - - // now it needs update - node.SetNeedsUpdate() - res, err = node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, ourRes) - test.That(t, node.MarkedForRemoval(), test.ShouldBeFalse) - - // but an error happened - node.LogAndSetLastError(ourErr) - _, err = node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, ourErr.Error()) - res, err = node.UnsafeResource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, ourRes) - test.That(t, node.IsUninitialized(), test.ShouldBeFalse) - - // it reconfigured - ourRes2 := &someResource{Resource: testutils.NewUnimplementedResource(generic.Named("foo"))} - node.SwapResource(ourRes2, resource.DefaultModelFamily.WithModel("baz")) - test.That(t, node.ResourceModel(), test.ShouldResemble, resource.DefaultModelFamily.WithModel("baz")) - res, err = node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldNotEqual, ourRes) - test.That(t, res, test.ShouldEqual, ourRes2) - test.That(t, node.MarkedForRemoval(), test.ShouldBeFalse) - - // it needs a new config - ourConf := resource.Config{Attributes: utils.AttributeMap{"1": 2}} - node.SetNewConfig(ourConf, []string{"3", "4", "5"}) - res, err = node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, ourRes2) - test.That(t, node.NeedsReconfigure(), test.ShouldBeTrue) - test.That(t, node.Config(), test.ShouldResemble, resource.Config{Attributes: utils.AttributeMap{"1": 2}}) - test.That(t, node.UnresolvedDependencies(), test.ShouldResemble, []string{"3", "4", "5"}) - node.SetNeedsUpdate() // noop - res, err = node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, ourRes2) - test.That(t, node.NeedsReconfigure(), test.ShouldBeTrue) - test.That(t, node.Config(), test.ShouldResemble, resource.Config{Attributes: utils.AttributeMap{"1": 2}}) - test.That(t, node.UnresolvedDependencies(), test.ShouldResemble, []string{"3", "4", "5"}) - - // but an error happened - node.LogAndSetLastError(ourErr) - _, err = node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, ourErr.Error()) - res, err = node.UnsafeResource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, ourRes2) - - // it reconfigured - ourRes3 := &someResource{Resource: testutils.NewUnimplementedResource(generic.Named("fooa"))} - node.SwapResource(ourRes3, resource.DefaultModelFamily.WithModel("bazz")) - test.That(t, node.ResourceModel(), test.ShouldResemble, resource.DefaultModelFamily.WithModel("bazz")) - res, err = node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldNotEqual, ourRes2) - test.That(t, res, test.ShouldEqual, ourRes3) - test.That(t, node.MarkedForRemoval(), test.ShouldBeFalse) - test.That(t, node.IsUninitialized(), test.ShouldBeFalse) - test.That(t, node.Config(), test.ShouldResemble, resource.Config{Attributes: utils.AttributeMap{"1": 2}}) - test.That(t, node.UnresolvedDependencies(), test.ShouldBeEmpty) - - //nolint - test.That(t, node.Close(context.WithValue(context.Background(), "foo", "hi")), test.ShouldBeNil) - test.That(t, ourRes.closeCap, test.ShouldBeEmpty) - test.That(t, ourRes2.closeCap, test.ShouldBeEmpty) - test.That(t, ourRes3.closeCap, test.ShouldHaveLength, 1) - test.That(t, ourRes3.closeCap, test.ShouldResemble, []interface{}{"hi"}) - - test.That(t, node.IsUninitialized(), test.ShouldBeTrue) - _, err = node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not initialized") - - ourRes4 := &someResource{Resource: testutils.NewUnimplementedResource(generic.Named("foob")), shoudlErr: true} - node.SwapResource(ourRes4, resource.DefaultModelFamily.WithModel("bazzz")) - test.That(t, node.ResourceModel(), test.ShouldResemble, resource.DefaultModelFamily.WithModel("bazzz")) - res, err = node.Resource() - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldNotEqual, ourRes3) - test.That(t, res, test.ShouldEqual, ourRes4) - test.That(t, node.MarkedForRemoval(), test.ShouldBeFalse) - test.That(t, node.IsUninitialized(), test.ShouldBeFalse) - - //nolint - err = node.Close(context.WithValue(context.Background(), "foo", "bye")) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "bad close") - test.That(t, ourRes.closeCap, test.ShouldBeEmpty) - test.That(t, ourRes2.closeCap, test.ShouldBeEmpty) - test.That(t, ourRes3.closeCap, test.ShouldHaveLength, 1) - test.That(t, ourRes4.closeCap, test.ShouldHaveLength, 1) - test.That(t, ourRes4.closeCap, test.ShouldResemble, []interface{}{"bye"}) - - test.That(t, node.IsUninitialized(), test.ShouldBeTrue) - _, err = node.Resource() - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not initialized") -} - -type someResource struct { - resource.Resource - closeCap []interface{} - shoudlErr bool -} - -func (s *someResource) Close(ctx context.Context) error { - s.closeCap = append(s.closeCap, ctx.Value("foo")) - if s.shoudlErr { - return errors.New("bad close") - } - return nil -} diff --git a/resource/matcher_test.go b/resource/matcher_test.go deleted file mode 100644 index ccb4eb53db6..00000000000 --- a/resource/matcher_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package resource_test - -import ( - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/testutils" -) - -func TestMatchers(t *testing.T) { - armComponent := testutils.NewUnimplementedResource(arm.Named("arm")) - sensorService := testutils.NewUnimplementedResource(sensor.Named("sensor")) - motionService := testutils.NewUnimplementedResource(motion.Named("motion")) - t.Run("type matcher", func(t *testing.T) { - matcher := resource.TypeMatcher{Type: resource.APITypeComponentName} - test.That(t, matcher.IsMatch(armComponent), test.ShouldBeTrue) - test.That(t, matcher.IsMatch(motionService), test.ShouldBeFalse) - }) - - t.Run("subtype matcher", func(t *testing.T) { - matcher := resource.SubtypeMatcher{Subtype: sensor.SubtypeName} - test.That(t, matcher.IsMatch(sensorService), test.ShouldBeTrue) - test.That(t, matcher.IsMatch(motionService), test.ShouldBeFalse) - }) - - t.Run("interface matcher", func(t *testing.T) { - // define a resource that trivially satisfies the Actuator interface - type unimplActuator struct { - resource.Resource - resource.Actuator - } - var testActuator unimplActuator - - matcher := resource.InterfaceMatcher{Interface: new(resource.Actuator)} - test.That(t, matcher.IsMatch(testActuator), test.ShouldBeTrue) - test.That(t, matcher.IsMatch(armComponent), test.ShouldBeFalse) - }) -} diff --git a/resource/resource_graph_test.go b/resource/resource_graph_test.go deleted file mode 100644 index 6499ae3576a..00000000000 --- a/resource/resource_graph_test.go +++ /dev/null @@ -1,1083 +0,0 @@ -package resource - -import ( - "fmt" - "testing" - "time" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" -) - -type fakeComponent struct { - Name Name - DependsOn []Name -} - -var apiA = APINamespace("namespace").WithType("atype").WithSubtype("aapi") - -var commonCfg = []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "D"), - DependsOn: []Name{ - NewName(apiA, "B"), - NewName(apiA, "E"), - }, - }, - { - Name: NewName(apiA, "E"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "F"), - DependsOn: []Name{ - NewName(apiA, "A"), - NewName(apiA, "C"), - NewName(apiA, "E"), - }, - }, - { - Name: NewName(apiA, "G"), - DependsOn: []Name{}, - }, -} - -func TestResourceGraphConstruct(t *testing.T) { - for idx, c := range []struct { - conf []fakeComponent - err string - }{ - { - []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "D"), - DependsOn: []Name{NewName(apiA, "C")}, - }, - { - Name: NewName(apiA, "E"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "F"), - DependsOn: []Name{ - NewName(apiA, "A"), - NewName(apiA, "E"), - NewName(apiA, "B"), - }, - }, - }, - "", - }, - { - []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - }, - "circular dependency - \"A\" already depends on \"B\"", - }, - { - []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - }, - "\"B\" cannot depend on itself", - }, - } { - t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) { - g := NewGraph() - test.That(t, g, test.ShouldNotBeNil) - for i, component := range c.conf { - test.That(t, g.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - err := g.AddChild(component.Name, dep) - if i > 0 && c.err != "" { - test.That(t, err.Error(), test.ShouldContainSubstring, c.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - } - } - }) - } -} - -func TestResourceGraphGetParentsAndChildren(t *testing.T) { - g := NewGraph() - test.That(t, g, test.ShouldNotBeNil) - for _, component := range commonCfg { - test.That(t, g.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, g.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - out := g.GetAllChildrenOf(NewName(apiA, "A")) - test.That(t, len(out), test.ShouldEqual, 2) - test.That(t, out, test.ShouldContain, - NewName(apiA, "F"), - ) - test.That(t, out, test.ShouldContain, - NewName(apiA, "B"), - ) - out = g.GetAllParentsOf(NewName(apiA, "F")) - test.That(t, len(out), test.ShouldEqual, 3) - test.That(t, out, test.ShouldContain, - NewName(apiA, "C"), - ) - test.That(t, out, test.ShouldContain, - NewName(apiA, "A"), - ) - out = g.GetAllChildrenOf(NewName(apiA, "C")) - test.That(t, len(out), test.ShouldEqual, 1) - test.That(t, out, test.ShouldContain, - NewName(apiA, "F"), - ) - g.RemoveChild(NewName(apiA, "F"), - NewName(apiA, "C")) - out = g.GetAllChildrenOf(NewName(apiA, "C")) - test.That(t, len(out), test.ShouldEqual, 0) - - test.That(t, g.GetAllParentsOf(NewName(apiA, "Z")), - test.ShouldBeEmpty) - - test.That(t, g.IsNodeDependingOn(NewName(apiA, "A"), - NewName(apiA, "F")), test.ShouldBeTrue) - test.That(t, g.IsNodeDependingOn(NewName(apiA, "F"), - NewName(apiA, "A")), test.ShouldBeFalse) - test.That(t, g.IsNodeDependingOn(NewName(apiA, "Z"), - NewName(apiA, "F")), test.ShouldBeFalse) - test.That(t, g.IsNodeDependingOn(NewName(apiA, "A"), - NewName(apiA, "Z")), test.ShouldBeFalse) - - for _, p := range g.GetAllParentsOf(NewName(apiA, "F")) { - g.removeChild(NewName(apiA, "F"), p) - } - g.remove(NewName(apiA, "F")) - out = g.TopologicalSort() - test.That(t, newResourceNameSet(out[0:3]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "G"), - NewName(apiA, "C"), - NewName(apiA, "D"), - }...)) - test.That(t, newResourceNameSet(out[3]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "E"), - }...)) - test.That(t, newResourceNameSet(out[4]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "B"), - }...)) - test.That(t, newResourceNameSet(out[5]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "A"), - }...)) -} - -func TestResourceGraphSubGraph(t *testing.T) { - cfg := []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "D"), - DependsOn: []Name{ - NewName(apiA, "B"), - NewName(apiA, "C"), - }, - }, - { - Name: NewName(apiA, "E"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "F"), - DependsOn: []Name{ - NewName(apiA, "A"), - NewName(apiA, "C"), - }, - }, - } - g := NewGraph() - test.That(t, g, test.ShouldNotBeNil) - for _, component := range cfg { - test.That(t, g.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, g.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - sg, err := g.SubGraphFrom(NewName(apiA, "W")) - test.That(t, sg, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldResemble, - "cannot create sub-graph from non existing node \"W\" ") - sg, err = g.SubGraphFrom(NewName(apiA, "C")) - test.That(t, sg, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - out := sg.TopologicalSort() - test.That(t, newResourceNameSet(out...), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "D"), - NewName(apiA, "F"), - NewName(apiA, "C"), - }...)) -} - -func TestResourceGraphDepTree(t *testing.T) { - cfg := []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "D"), - DependsOn: []Name{ - NewName(apiA, "B"), - NewName(apiA, "E"), - }, - }, - { - Name: NewName(apiA, "E"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "F"), - DependsOn: []Name{NewName(apiA, "E")}, - }, - } - g := NewGraph() - test.That(t, g, test.ShouldNotBeNil) - for _, component := range cfg { - test.That(t, g.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, g.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - err := g.AddChild(NewName(apiA, "A"), - NewName(apiA, "F")) - test.That(t, err.Error(), test.ShouldEqual, "circular dependency - \"F\" already depends on \"A\"") - test.That(t, g.AddChild(NewName(apiA, "D"), - NewName(apiA, "F")), test.ShouldBeNil) -} - -func TestResourceGraphTopologicalSort(t *testing.T) { - cfg := []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "D"), - DependsOn: []Name{NewName(apiA, "C")}, - }, - { - Name: NewName(apiA, "E"), - DependsOn: []Name{NewName(apiA, "D")}, - }, - { - Name: NewName(apiA, "F"), - DependsOn: []Name{ - NewName(apiA, "A"), - NewName(apiA, "E"), - NewName(apiA, "B"), - }, - }, - } - g := NewGraph() - test.That(t, g, test.ShouldNotBeNil) - for _, component := range cfg { - test.That(t, g.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, g.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - out := g.TopologicalSort() - test.That(t, out, test.ShouldResemble, []Name{ - NewName(apiA, "F"), - NewName(apiA, "E"), - NewName(apiA, "D"), - NewName(apiA, "C"), - NewName(apiA, "B"), - NewName(apiA, "A"), - }) - - outLevels := g.TopologicalSortInLevels() - test.That(t, outLevels, test.ShouldHaveLength, 6) - test.That(t, outLevels, test.ShouldResemble, [][]Name{ - { - NewName(apiA, "F"), - }, - { - NewName(apiA, "E"), - }, - { - NewName(apiA, "D"), - }, - { - NewName(apiA, "C"), - }, - { - NewName(apiA, "B"), - }, - { - NewName(apiA, "A"), - }, - }) - - gNode, ok := g.Node(NewName(apiA, "F")) - test.That(t, ok, test.ShouldBeTrue) - gNode.MarkForRemoval() - test.That(t, g.RemoveMarked(), test.ShouldHaveLength, 1) - out = g.TopologicalSort() - test.That(t, out, test.ShouldResemble, []Name{ - NewName(apiA, "E"), - NewName(apiA, "D"), - NewName(apiA, "C"), - NewName(apiA, "B"), - NewName(apiA, "A"), - }) -} - -func TestResourceGraphMergeAdd(t *testing.T) { - cfgA := []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - } - cfgB := []fakeComponent{ - { - Name: NewName(apiA, "D"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "E"), - DependsOn: []Name{NewName(apiA, "D")}, - }, - { - Name: NewName(apiA, "F"), - DependsOn: []Name{NewName(apiA, "E")}, - }, - } - gA := NewGraph() - test.That(t, gA, test.ShouldNotBeNil) - for _, component := range cfgA { - test.That(t, gA.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gA.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - out := gA.TopologicalSort() - test.That(t, out, test.ShouldResemble, []Name{ - NewName(apiA, "C"), - NewName(apiA, "B"), - NewName(apiA, "A"), - }) - gB := NewGraph() - test.That(t, gB, test.ShouldNotBeNil) - for _, component := range cfgB { - test.That(t, gB.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gB.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - out = gB.TopologicalSort() - test.That(t, out, test.ShouldResemble, []Name{ - NewName(apiA, "F"), - NewName(apiA, "E"), - NewName(apiA, "D"), - }) - test.That(t, gA.MergeAdd(gB), test.ShouldBeNil) - test.That(t, gA.AddChild(NewName(apiA, "D"), - NewName(apiA, "C")), test.ShouldBeNil) - out = gA.TopologicalSort() - test.That(t, out, test.ShouldResemble, []Name{ - NewName(apiA, "F"), - NewName(apiA, "E"), - NewName(apiA, "D"), - NewName(apiA, "C"), - NewName(apiA, "B"), - NewName(apiA, "A"), - }) -} - -func TestResourceGraphMergeRemove(t *testing.T) { - cfgA := []fakeComponent{ - { - Name: NewName(apiA, "1"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "2"), - DependsOn: []Name{NewName(apiA, "1")}, - }, - { - Name: NewName(apiA, "3"), - DependsOn: []Name{ - NewName(apiA, "1"), - NewName(apiA, "11"), - }, - }, - { - Name: NewName(apiA, "4"), - DependsOn: []Name{NewName(apiA, "2")}, - }, - { - Name: NewName(apiA, "5"), - DependsOn: []Name{NewName(apiA, "4")}, - }, - { - Name: NewName(apiA, "6"), - DependsOn: []Name{NewName(apiA, "4")}, - }, - { - Name: NewName(apiA, "7"), - DependsOn: []Name{NewName(apiA, "4")}, - }, - { - Name: NewName(apiA, "8"), - DependsOn: []Name{ - NewName(apiA, "3"), - NewName(apiA, "2"), - }, - }, - { - Name: NewName(apiA, "9"), - DependsOn: []Name{NewName(apiA, "8")}, - }, - { - Name: NewName(apiA, "10"), - DependsOn: []Name{ - NewName(apiA, "12"), - NewName(apiA, "8"), - }, - }, - { - Name: NewName(apiA, "11"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "12"), - DependsOn: []Name{NewName(apiA, "11")}, - }, - { - Name: NewName(apiA, "13"), - DependsOn: []Name{NewName(apiA, "11")}, - }, - { - Name: NewName(apiA, "14"), - DependsOn: []Name{NewName(apiA, "11")}, - }, - } - gA := NewGraph() - test.That(t, gA, test.ShouldNotBeNil) - for _, component := range cfgA { - test.That(t, gA.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gA.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - out := gA.TopologicalSort() - test.That(t, newResourceNameSet(out[0:7]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "5"), - NewName(apiA, "6"), - NewName(apiA, "7"), - NewName(apiA, "9"), - NewName(apiA, "10"), - NewName(apiA, "13"), - NewName(apiA, "14"), - }...)) - test.That(t, newResourceNameSet(out[7:10]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "4"), - NewName(apiA, "8"), - NewName(apiA, "12"), - }...)) - test.That(t, newResourceNameSet(out[10:12]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "2"), - NewName(apiA, "3"), - }...)) - test.That(t, newResourceNameSet(out[12:14]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "1"), - NewName(apiA, "11"), - }...)) - removalList := []Name{ - NewName(apiA, "5"), - NewName(apiA, "7"), - NewName(apiA, "12"), - NewName(apiA, "2"), - NewName(apiA, "13"), - } - gB := NewGraph() - for _, comp := range removalList { - gC, err := gA.SubGraphFrom(comp) - test.That(t, err, test.ShouldBeNil) - gB.MergeAdd(gC) - } - gA.MarkForRemoval(gB) - test.That(t, gA.RemoveMarked(), test.ShouldHaveLength, 10) - - out = gA.TopologicalSort() - test.That(t, len(out), test.ShouldEqual, 4) - test.That(t, newResourceNameSet(out[0:2]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "14"), - NewName(apiA, "3"), - }...)) - test.That(t, newResourceNameSet(out[2:4]...), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "11"), - NewName(apiA, "1"), - }...)) -} - -func newResourceNameSet(resourceNames ...Name) map[Name]*GraphNode { - set := make(map[Name]*GraphNode, len(resourceNames)) - for _, val := range resourceNames { - set[val] = &GraphNode{} - } - return set -} - -func TestResourceGraphFindNodeByName(t *testing.T) { - cfgA := []fakeComponent{ - { - Name: NewName(APINamespaceRDK.WithComponentType("aapi"), "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(APINamespaceRDK.WithComponentType("aapi"), "B"), - DependsOn: []Name{NewName(APINamespaceRDK.WithComponentType("aapi"), "A")}, - }, - { - Name: NewName(APINamespaceRDK.WithComponentType("aapi"), "C"), - DependsOn: []Name{NewName(APINamespaceRDK.WithComponentType("aapi"), "B")}, - }, - } - gA := NewGraph() - test.That(t, gA, test.ShouldNotBeNil) - for _, component := range cfgA { - test.That(t, gA.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gA.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - names := gA.findNodesByShortName("A") - test.That(t, names, test.ShouldHaveLength, 1) - names = gA.findNodesByShortName("B") - test.That(t, names, test.ShouldHaveLength, 1) - names = gA.findNodesByShortName("C") - test.That(t, names, test.ShouldHaveLength, 1) - names = gA.findNodesByShortName("D") - test.That(t, names, test.ShouldHaveLength, 0) -} - -var cfgA = []fakeComponent{ - { - Name: NewName(apiA, "A"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "D"), - DependsOn: []Name{ - NewName(apiA, "A"), - NewName(apiA, "B"), - }, - }, - { - Name: NewName(apiA, "E"), - DependsOn: []Name{NewName(apiA, "D")}, - }, - { - Name: NewName(apiA, "F"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "G"), - DependsOn: []Name{NewName(apiA, "F")}, - }, - { - Name: NewName(apiA, "H"), - DependsOn: []Name{NewName(apiA, "F")}, - }, -} - -func TestResourceGraphReplaceNodesParents(t *testing.T) { - gA := NewGraph() - test.That(t, gA, test.ShouldNotBeNil) - for _, component := range cfgA { - test.That(t, gA.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gA.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - out := gA.TopologicalSort() - test.That(t, newResourceNameSet(out[0:4]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "G"), - NewName(apiA, "H"), - NewName(apiA, "E"), - NewName(apiA, "C"), - }...)) - test.That(t, newResourceNameSet(out[4:6]...), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "F"), - NewName(apiA, "D"), - }...)) - test.That(t, newResourceNameSet(out[6]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "B"), - }...)) - test.That(t, newResourceNameSet(out[7]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "A"), - }...)) - - cfgB := []fakeComponent{ - { - Name: NewName(apiA, "F"), - DependsOn: []Name{}, - }, - { - Name: NewName(apiA, "B"), - DependsOn: []Name{ - NewName(apiA, "A"), - NewName(apiA, "F"), - }, - }, - { - Name: NewName(apiA, "C"), - DependsOn: []Name{NewName(apiA, "B")}, - }, - { - Name: NewName(apiA, "D"), - DependsOn: []Name{NewName(apiA, "A")}, - }, - { - Name: NewName(apiA, "G"), - DependsOn: []Name{NewName(apiA, "C")}, - }, - { - Name: NewName(apiA, "H"), - DependsOn: []Name{NewName(apiA, "D")}, - }, - } - gB := NewGraph() - test.That(t, gB, test.ShouldNotBeNil) - for _, component := range cfgB { - test.That(t, gB.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gB.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - for n := range gB.nodes { - test.That(t, gA.ReplaceNodesParents(n, gB), test.ShouldBeNil) - } - out = gA.TopologicalSort() - test.That(t, newResourceNameSet(out[0:3]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "G"), - NewName(apiA, "H"), - NewName(apiA, "E"), - }...)) - test.That(t, newResourceNameSet(out[3:5]...), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "C"), - NewName(apiA, "D"), - }...)) - test.That(t, newResourceNameSet(out[5]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "B"), - }...)) - test.That(t, newResourceNameSet(out[6:8]...), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "A"), - NewName(apiA, "F"), - }...)) - - cfgC := []fakeComponent{ - { - Name: NewName(apiA, "W"), - DependsOn: []Name{}, - }, - } - gC := NewGraph() - test.That(t, gC, test.ShouldNotBeNil) - for _, component := range cfgC { - test.That(t, gC.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gC.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - test.That(t, gA.ReplaceNodesParents(NewName(apiA, "W"), gC), test.ShouldNotBeNil) -} - -func TestResourceGraphCopyNodeAndChildren(t *testing.T) { - gA := NewGraph() - test.That(t, gA, test.ShouldNotBeNil) - for _, component := range cfgA { - test.That(t, gA.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - test.That(t, gA.AddChild(component.Name, dep), test.ShouldBeNil) - } - } - gB := NewGraph() - test.That(t, gB, test.ShouldNotBeNil) - test.That(t, gB.CopyNodeAndChildren(NewName(apiA, "F"), gA), test.ShouldBeNil) - out := gB.TopologicalSort() - test.That(t, newResourceNameSet(out[0:2]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "G"), - NewName(apiA, "H"), - }...)) - test.That(t, newResourceNameSet(out[2]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "F"), - }...)) - - test.That(t, gB.CopyNodeAndChildren(NewName(apiA, "D"), gA), test.ShouldBeNil) - out = gB.TopologicalSort() - test.That(t, newResourceNameSet(out[0:3]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "G"), - NewName(apiA, "H"), - NewName(apiA, "E"), - }...)) - test.That(t, newResourceNameSet(out[3:5]...), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "F"), - NewName(apiA, "D"), - }...)) - - for n := range gA.nodes { - test.That(t, gB.CopyNodeAndChildren(n, gA), test.ShouldBeNil) - } - out = gB.TopologicalSort() - test.That(t, newResourceNameSet(out[0:4]...), test.ShouldResemble, - newResourceNameSet([]Name{ - NewName(apiA, "G"), - NewName(apiA, "H"), - NewName(apiA, "E"), - NewName(apiA, "C"), - }...)) - test.That(t, newResourceNameSet(out[4:6]...), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "F"), - NewName(apiA, "D"), - }...)) - test.That(t, newResourceNameSet(out[6]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "B"), - }...)) - test.That(t, newResourceNameSet(out[7]), test.ShouldResemble, newResourceNameSet([]Name{ - NewName(apiA, "A"), - }...)) -} - -func TestResourceGraphRandomRemoval(t *testing.T) { - g := NewGraph() - test.That(t, g, test.ShouldNotBeNil) - for _, component := range commonCfg { - test.That(t, g.AddNode(component.Name, &GraphNode{}), test.ShouldBeNil) - for _, dep := range component.DependsOn { - err := g.AddChild(component.Name, dep) - test.That(t, err, test.ShouldBeNil) - } - } - - name := NewName(apiA, "B") - - for _, c := range commonCfg { - if c.Name == name { - continue - } - for _, dep := range c.DependsOn { - if dep == name { - test.That(t, g.GetAllParentsOf(c.Name), test.ShouldContain, name) - break - } - } - } - - g.remove(name) - test.That(t, g.GetAllParentsOf(name), test.ShouldBeEmpty) - test.That(t, g.GetAllChildrenOf(name), test.ShouldBeEmpty) -} - -func TestResourceGraphMarkForRemoval(t *testing.T) { - g := NewGraph() - - test.That(t, g, test.ShouldNotBeNil) - for _, component := range commonCfg { - res := &someResource{Named: component.Name.AsNamed()} - test.That(t, g.AddNode(component.Name, NewConfiguredGraphNode( - Config{}, - res, - DefaultModelFamily.WithModel("foo"), - )), test.ShouldBeNil) - for _, dep := range component.DependsOn { - err := g.AddChild(component.Name, dep) - test.That(t, err, test.ShouldBeNil) - } - } - - name := NewName(apiA, "B") - - for _, c := range commonCfg { - if c.Name == name { - continue - } - for _, dep := range c.DependsOn { - if dep == name { - test.That(t, g.GetAllParentsOf(c.Name), test.ShouldContain, name) - break - } - } - } - - subG, err := g.SubGraphFrom(name) - test.That(t, err, test.ShouldBeNil) - g.MarkForRemoval(subG) - - toClose := g.RemoveMarked() - test.That(t, toClose, test.ShouldHaveLength, 5) - namesToClose := make(map[Name]struct{}, len(toClose)) - for _, res := range toClose { - namesToClose[res.Name()] = struct{}{} - } - test.That(t, namesToClose, test.ShouldResemble, map[Name]struct{}{ - NewName(apiA, "B"): {}, - NewName(apiA, "F"): {}, - NewName(apiA, "D"): {}, - NewName(apiA, "C"): {}, - NewName(apiA, "E"): {}, - }) - - test.That(t, g.GetAllParentsOf(name), test.ShouldBeEmpty) - test.That(t, g.GetAllChildrenOf(name), test.ShouldBeEmpty) -} - -func TestResourceGraphClock(t *testing.T) { - g := NewGraph() - - test.That(t, g.CurrLogicalClockValue(), test.ShouldEqual, 0) - - name1 := NewName(apiA, "a") - name2 := NewName(apiA, "b") - node1 := &GraphNode{} - test.That(t, g.AddNode(name1, node1), test.ShouldBeNil) - test.That(t, node1.UpdatedAt(), test.ShouldEqual, 0) - node2 := &GraphNode{} - test.That(t, g.AddNode(name1, node2), test.ShouldBeNil) - test.That(t, node1.UpdatedAt(), test.ShouldEqual, 0) - test.That(t, node2.UpdatedAt(), test.ShouldEqual, 0) - n, ok := g.Node(name1) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, n, test.ShouldNotEqual, node2) // see docs of AddNode/GraphNode.replace - test.That(t, n, test.ShouldEqual, node1) // see docs of AddNode/GraphNode.replace - - res1 := &someResource{Named: name1.AsNamed()} - node1.SwapResource(res1, DefaultModelFamily.WithModel("foo")) - test.That(t, g.CurrLogicalClockValue(), test.ShouldEqual, 1) - test.That(t, node1.UpdatedAt(), test.ShouldEqual, 1) - test.That(t, node2.UpdatedAt(), test.ShouldEqual, 0) - node1.SwapResource(res1, DefaultModelFamily.WithModel("foo")) - test.That(t, g.CurrLogicalClockValue(), test.ShouldEqual, 2) - test.That(t, node1.UpdatedAt(), test.ShouldEqual, 2) - - node2 = &GraphNode{} - test.That(t, g.AddNode(name2, node2), test.ShouldBeNil) - node2.SwapResource(res1, DefaultModelFamily.WithModel("foo")) - test.That(t, g.CurrLogicalClockValue(), test.ShouldEqual, 3) - test.That(t, node1.UpdatedAt(), test.ShouldEqual, 2) - test.That(t, node2.UpdatedAt(), test.ShouldEqual, 3) -} - -func TestResourceGraphLastReconfigured(t *testing.T) { - g := NewGraph() - - name1 := NewName(apiA, "a") - node1 := &GraphNode{} - test.That(t, g.AddNode(name1, node1), test.ShouldBeNil) - // Assert that uninitialized node has a nil lastReconfigured value. - test.That(t, node1.LastReconfigured(), test.ShouldBeNil) - - res1 := &someResource{Named: name1.AsNamed()} - node1.SwapResource(res1, DefaultModelFamily.WithModel("foo")) - lr := node1.LastReconfigured() - test.That(t, lr, test.ShouldNotBeNil) - // Assert that after SwapResource, node's lastReconfigured time is between - // 10s ago and now. - test.That(t, *lr, test.ShouldHappenBetween, - time.Now().Add(-10*time.Second), time.Now()) - - // Mock a mutation with another SwapResource. Assert that lastReconfigured - // value changed. - node1.SwapResource(res1, DefaultModelFamily.WithModel("foo")) - newLR := node1.LastReconfigured() - test.That(t, newLR, test.ShouldNotBeNil) - // Assert that after another SwapResource, node's lastReconfigured time is - // after old lr value and between 10s ago and now. - test.That(t, *newLR, test.ShouldHappenAfter, *lr) - test.That(t, *newLR, test.ShouldHappenBetween, - time.Now().Add(-10*time.Second), time.Now()) -} - -func TestResourceGraphResolveDependencies(t *testing.T) { - logger := logging.NewTestLogger(t) - g := NewGraph() - test.That(t, g.ResolveDependencies(logger), test.ShouldBeNil) - - name1 := NewName(APINamespaceRDK.WithComponentType("aapi"), "a") - node1 := NewUnconfiguredGraphNode(Config{}, []string{"a", "b", "c", "d"}) - test.That(t, g.AddNode(name1, node1), test.ShouldBeNil) - err := g.ResolveDependencies(logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, name1.String()) - test.That(t, err.Error(), test.ShouldContainSubstring, "depend on itself") - test.That(t, node1.UnresolvedDependencies(), test.ShouldResemble, []string{"a", "b", "c", "d"}) - node1.setUnresolvedDependencies("b", "c", "d") - - name2 := NewName(APINamespaceRDK.WithComponentType("aapi"), "b") - node2 := NewUnconfiguredGraphNode(Config{}, []string{"z"}) - test.That(t, g.AddNode(name2, node2), test.ShouldBeNil) - - test.That(t, g.ResolveDependencies(logger), test.ShouldBeNil) - test.That(t, node1.UnresolvedDependencies(), test.ShouldResemble, []string{"c", "d"}) - test.That(t, node2.UnresolvedDependencies(), test.ShouldResemble, []string{"z"}) - - name3 := NewName(APINamespaceRDK.WithComponentType("aapi"), "rem1:c") - node3 := NewUnconfiguredGraphNode(Config{}, []string{"z"}) - name4 := NewName(APINamespaceRDK.WithComponentType("aapi"), "rem2:c") - node4 := NewUnconfiguredGraphNode(Config{}, []string{"z"}) - test.That(t, g.AddNode(name3, node3), test.ShouldBeNil) - test.That(t, g.AddNode(name4, node4), test.ShouldBeNil) - - err = g.ResolveDependencies(logger) - test.That(t, err.Error(), test.ShouldContainSubstring, "conflicting names") - test.That(t, err.Error(), test.ShouldContainSubstring, name1.String()) - test.That(t, err.Error(), test.ShouldContainSubstring, name3.String()) - test.That(t, err.Error(), test.ShouldContainSubstring, name4.String()) - - test.That(t, node1.UnresolvedDependencies(), test.ShouldResemble, []string{"c", "d"}) - test.That(t, node2.UnresolvedDependencies(), test.ShouldResemble, []string{"z"}) - test.That(t, node3.UnresolvedDependencies(), test.ShouldResemble, []string{"z"}) - test.That(t, node4.UnresolvedDependencies(), test.ShouldResemble, []string{"z"}) - - test.That(t, node1.hasUnresolvedDependencies(), test.ShouldBeTrue) - test.That(t, node2.hasUnresolvedDependencies(), test.ShouldBeTrue) - test.That(t, node3.hasUnresolvedDependencies(), test.ShouldBeTrue) - test.That(t, node4.hasUnresolvedDependencies(), test.ShouldBeTrue) - - g.remove(name3) - test.That(t, g.ResolveDependencies(logger), test.ShouldBeNil) - - test.That(t, node1.UnresolvedDependencies(), test.ShouldResemble, []string{"d"}) - test.That(t, node2.UnresolvedDependencies(), test.ShouldResemble, []string{"z"}) - test.That(t, node4.UnresolvedDependencies(), test.ShouldResemble, []string{"z"}) - - test.That(t, node1.hasUnresolvedDependencies(), test.ShouldBeTrue) - test.That(t, node2.hasUnresolvedDependencies(), test.ShouldBeTrue) - test.That(t, node4.hasUnresolvedDependencies(), test.ShouldBeTrue) - - name5 := NewName(APINamespaceRDK.WithComponentType("aapi"), "z") - node5 := NewUnconfiguredGraphNode(Config{}, []string{"rdk:component:foo/bar", "d"}) - test.That(t, g.AddNode(name5, node5), test.ShouldBeNil) - test.That(t, g.ResolveDependencies(logger), test.ShouldBeNil) - - test.That(t, node1.UnresolvedDependencies(), test.ShouldResemble, []string{"d"}) - test.That(t, node2.UnresolvedDependencies(), test.ShouldBeEmpty) - test.That(t, node4.UnresolvedDependencies(), test.ShouldBeEmpty) - test.That(t, node5.UnresolvedDependencies(), test.ShouldResemble, []string{"d"}) - - test.That(t, node1.hasUnresolvedDependencies(), test.ShouldBeTrue) - test.That(t, node2.hasUnresolvedDependencies(), test.ShouldBeFalse) - test.That(t, node4.hasUnresolvedDependencies(), test.ShouldBeFalse) - test.That(t, node5.hasUnresolvedDependencies(), test.ShouldBeTrue) - - name6 := NewName(APINamespaceRDK.WithComponentType("aapi"), "d") - node6 := NewUnconfiguredGraphNode(Config{}, []string{}) - test.That(t, g.AddNode(name6, node6), test.ShouldBeNil) - test.That(t, g.ResolveDependencies(logger), test.ShouldBeNil) - test.That(t, node1.UnresolvedDependencies(), test.ShouldBeEmpty) - test.That(t, node2.UnresolvedDependencies(), test.ShouldBeEmpty) - test.That(t, node4.UnresolvedDependencies(), test.ShouldBeEmpty) - test.That(t, node5.UnresolvedDependencies(), test.ShouldBeEmpty) - test.That(t, node6.UnresolvedDependencies(), test.ShouldBeEmpty) - - test.That(t, node1.hasUnresolvedDependencies(), test.ShouldBeFalse) - test.That(t, node2.hasUnresolvedDependencies(), test.ShouldBeFalse) - test.That(t, node4.hasUnresolvedDependencies(), test.ShouldBeFalse) - test.That(t, node5.hasUnresolvedDependencies(), test.ShouldBeFalse) - test.That(t, node6.hasUnresolvedDependencies(), test.ShouldBeFalse) -} - -type someResource struct { - Named - TriviallyReconfigurable - TriviallyCloseable -} diff --git a/resource/resource_registry_test.go b/resource/resource_registry_test.go deleted file mode 100644 index d8b52c489d0..00000000000 --- a/resource/resource_registry_test.go +++ /dev/null @@ -1,344 +0,0 @@ -package resource_test - -import ( - "context" - "errors" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/jhump/protoreflect/grpcreflect" - pb "go.viam.com/api/robot/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/arm/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" -) - -var ( - button = "button" - acme = resource.NewName(resource.APINamespace("acme").WithComponentType(button), "button1") - nav = "navigation" - testService = resource.NewName(resource.APINamespaceRDK.WithComponentType(nav), "nav1") -) - -func TestComponentRegistry(t *testing.T) { - logger := logging.NewTestLogger(t) - rf := func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (arm.Arm, error) { - return &fake.Arm{Named: conf.ResourceName().AsNamed()}, nil - } - model := resource.Model{Name: "x"} - test.That(t, func() { - resource.Register(acme.API, model, resource.Registration[arm.Arm, resource.NoNativeConfig]{}) - }, test.ShouldPanic) - resource.Register(acme.API, model, resource.Registration[arm.Arm, resource.NoNativeConfig]{Constructor: rf}) - - resInfo, ok := resource.LookupRegistration(acme.API, model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resInfo, test.ShouldNotBeNil) - _, ok = resource.LookupRegistration(acme.API, resource.Model{Name: "z"}) - test.That(t, ok, test.ShouldBeFalse) - res, err := resInfo.Constructor(context.Background(), nil, resource.Config{Name: "foo"}, logger) - test.That(t, err, test.ShouldBeNil) - resArm, err := resource.AsType[arm.Arm](res) - test.That(t, err, test.ShouldBeNil) - test.That(t, resArm.Name().Name, test.ShouldEqual, "foo") - - resource.Deregister(acme.API, model) - _, ok = resource.LookupRegistration(acme.API, model) - test.That(t, ok, test.ShouldBeFalse) - - modelName2 := resource.DefaultServiceModel - test.That(t, func() { - resource.Register(testService.API, modelName2, resource.Registration[arm.Arm, resource.NoNativeConfig]{}) - }, test.ShouldPanic) - resource.Register(testService.API, modelName2, resource.Registration[arm.Arm, resource.NoNativeConfig]{Constructor: rf}) - - resInfo, ok = resource.LookupRegistration(testService.API, modelName2) - test.That(t, resInfo, test.ShouldNotBeNil) - test.That(t, ok, test.ShouldBeTrue) - _, ok = resource.LookupRegistration(testService.API, resource.DefaultModelFamily.WithModel("z")) - test.That(t, ok, test.ShouldBeFalse) - res, err = resInfo.Constructor(context.Background(), nil, resource.Config{Name: "bar"}, logger) - test.That(t, err, test.ShouldBeNil) - resArm, err = resource.AsType[arm.Arm](res) - test.That(t, err, test.ShouldBeNil) - test.That(t, resArm.Name().Name, test.ShouldEqual, "bar") - - resource.Deregister(testService.API, modelName2) - _, ok = resource.LookupRegistration(testService.API, modelName2) - test.That(t, ok, test.ShouldBeFalse) -} - -func TestResourceAPIRegistry(t *testing.T) { - statf := func(context.Context, arm.Arm) (interface{}, error) { - return nil, errors.New("one") - } - var capColl resource.APIResourceCollection[arm.Arm] - - sf := func(apiResColl resource.APIResourceCollection[arm.Arm]) interface{} { - capColl = apiResColl - return 5 - } - rcf := func(_ context.Context, _ rpc.ClientConn, _ string, name resource.Name, _ logging.Logger) (arm.Arm, error) { - return capColl.Resource(name.ShortName()) - } - - test.That(t, func() { - resource.RegisterAPI(acme.API, resource.APIRegistration[arm.Arm]{ - Status: statf, - RPCServiceServerConstructor: sf, - RPCServiceDesc: &pb.RobotService_ServiceDesc, - }) - }, test.ShouldPanic) - test.That(t, func() { - resource.RegisterAPIWithAssociation(acme.API, resource.APIRegistration[arm.Arm]{ - Status: statf, - RPCServiceServerConstructor: sf, - RPCServiceDesc: &pb.RobotService_ServiceDesc, - }, resource.AssociatedConfigRegistration[resource.AssociatedConfig]{}) - }, test.ShouldPanic) - resource.RegisterAPI(acme.API, resource.APIRegistration[arm.Arm]{ - Status: statf, - RPCServiceServerConstructor: sf, - RPCServiceHandler: pb.RegisterRobotServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.RobotService_ServiceDesc, - }) - apiInfo, ok, err := resource.LookupAPIRegistration[arm.Arm](acme.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, apiInfo, test.ShouldNotBeNil) - _, err = apiInfo.Status(nil, &fake.Arm{Named: arm.Named("foo").AsNamed()}) - test.That(t, err, test.ShouldBeError, errors.New("one")) - coll, err := resource.NewAPIResourceCollection(arm.API, map[resource.Name]arm.Arm{ - arm.Named("foo"): &fake.Arm{Named: arm.Named("foo").AsNamed()}, - }) - test.That(t, err, test.ShouldBeNil) - svcServer := apiInfo.RPCServiceServerConstructor(coll) - test.That(t, svcServer, test.ShouldNotBeNil) - test.That(t, apiInfo.RPCClient, test.ShouldBeNil) - - api2 := resource.APINamespace("acme2").WithComponentType(button) - _, ok, err = resource.LookupAPIRegistration[arm.Arm](api2) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeFalse) - - resource.RegisterAPI(api2, resource.APIRegistration[arm.Arm]{ - RPCServiceServerConstructor: sf, - RPCClient: rcf, - RPCServiceDesc: &pb.RobotService_ServiceDesc, - RPCServiceHandler: pb.RegisterRobotServiceHandlerFromEndpoint, - }) - apiInfo, ok, err = resource.LookupAPIRegistration[arm.Arm](api2) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, apiInfo.Status, test.ShouldBeNil) - svcServer = apiInfo.RPCServiceServerConstructor(coll) - test.That(t, svcServer, test.ShouldNotBeNil) - res, err := apiInfo.RPCClient(nil, nil, "", arm.Named("foo"), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res.Name().Name, test.ShouldEqual, "foo") - test.That(t, apiInfo.RPCServiceDesc, test.ShouldEqual, &pb.RobotService_ServiceDesc) - - reflectSvcDesc, err := grpcreflect.LoadServiceDescriptor(apiInfo.RPCServiceDesc) - test.That(t, err, test.ShouldBeNil) - test.That(t, apiInfo.ReflectRPCServiceDesc, test.ShouldResemble, reflectSvcDesc) - - api3 := resource.APINamespace("acme3").WithComponentType(button) - _, ok, err = resource.LookupAPIRegistration[arm.Arm](api3) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeFalse) - - resource.RegisterAPI(api3, resource.APIRegistration[arm.Arm]{RPCClient: rcf}) - apiInfo, ok, err = resource.LookupAPIRegistration[arm.Arm](api3) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, apiInfo, test.ShouldNotBeNil) - res, err = apiInfo.RPCClient(nil, nil, "", arm.Named("foo"), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, res.Name().Name, test.ShouldEqual, "foo") - - api4 := resource.APINamespace("acme4").WithComponentType(button) - _, ok, err = resource.LookupAPIRegistration[arm.Arm](api4) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeFalse) - test.That(t, func() { - resource.RegisterAPI(api4, resource.APIRegistration[arm.Arm]{ - RPCServiceServerConstructor: sf, - RPCClient: rcf, - RPCServiceHandler: pb.RegisterRobotServiceHandlerFromEndpoint, - }) - }, test.ShouldPanic) - test.That(t, func() { - resource.RegisterAPIWithAssociation(api4, resource.APIRegistration[arm.Arm]{ - RPCServiceServerConstructor: sf, - RPCClient: rcf, - RPCServiceHandler: pb.RegisterRobotServiceHandlerFromEndpoint, - }, resource.AssociatedConfigRegistration[resource.AssociatedConfig]{}) - }, test.ShouldPanic) - - resource.DeregisterAPI(api3) - _, ok, err = resource.LookupAPIRegistration[arm.Arm](api3) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeFalse) -} - -type mockAssociatedConfig struct { - Field1 string `json:"field1"` - capName resource.Name -} - -func (st *mockAssociatedConfig) Equals(other resource.AssociatedConfig) bool { - st2, err := utils.AssertType[*mockAssociatedConfig](other) - if err != nil { - return false - } - return st.Field1 == st2.Field1 && st.capName == st2.capName -} - -func (st *mockAssociatedConfig) UpdateResourceNames(updater func(old resource.Name) resource.Name) { - st.capName = updater(arm.Named("foo")) -} - -func (st *mockAssociatedConfig) Link(conf *resource.Config) { - copySt := st - conf.AssociatedAttributes = make(map[resource.Name]resource.AssociatedConfig) - conf.AssociatedAttributes[st.capName] = copySt -} - -func TestResourceAPIRegistryWithAssociation(t *testing.T) { - statf := func(context.Context, arm.Arm) (interface{}, error) { - return nil, errors.New("one") - } - sf := func(apiResColl resource.APIResourceCollection[arm.Arm]) interface{} { - return nil - } - - someName := resource.NewName(resource.APINamespace(uuid.NewString()).WithComponentType(button), "button1") - resource.RegisterAPIWithAssociation(someName.API, resource.APIRegistration[arm.Arm]{ - Status: statf, - RPCServiceServerConstructor: sf, - RPCServiceHandler: pb.RegisterRobotServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.RobotService_ServiceDesc, - }, resource.AssociatedConfigRegistration[*mockAssociatedConfig]{}) - reg, ok := resource.LookupAssociatedConfigRegistration(someName.API) - test.That(t, ok, test.ShouldBeTrue) - assoc, err := reg.AttributeMapConverter(utils.AttributeMap{"field1": "hey"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, assoc.(*mockAssociatedConfig).Field1, test.ShouldEqual, "hey") - test.That(t, assoc.(*mockAssociatedConfig).capName, test.ShouldResemble, resource.Name{}) - assoc.UpdateResourceNames(func(n resource.Name) resource.Name { - return arm.Named(n.String()) // odd but whatever - }) - test.That(t, assoc.(*mockAssociatedConfig).capName, test.ShouldResemble, arm.Named(arm.Named("foo").String())) - cfg := &resource.Config{} - assoc.Link(cfg) - test.That(t, assoc.Equals(cfg.AssociatedAttributes[assoc.(*mockAssociatedConfig).capName]), test.ShouldBeTrue) -} - -func TestDiscoveryFunctions(t *testing.T) { - df := func(ctx context.Context, logger logging.Logger) (interface{}, error) { - return []resource.Discovery{}, nil - } - validAPIQuery := resource.NewDiscoveryQuery(acme.API, resource.Model{Name: "some model"}) - _, ok := resource.LookupRegistration(validAPIQuery.API, validAPIQuery.Model) - test.That(t, ok, test.ShouldBeFalse) - - rf := func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (arm.Arm, error) { - return &fake.Arm{Named: conf.ResourceName().AsNamed()}, nil - } - - resource.Register(validAPIQuery.API, validAPIQuery.Model, resource.Registration[arm.Arm, resource.NoNativeConfig]{ - Constructor: rf, - Discover: df, - }) - - reg, ok := resource.LookupRegistration(validAPIQuery.API, validAPIQuery.Model) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, reg.Discover, test.ShouldEqual, df) -} - -func TestTransformAttributeMap(t *testing.T) { - type myType struct { - A string `json:"a"` - B string `json:"b"` - Attributes map[string]string `json:"attributes"` - } - - attrs := utils.AttributeMap{ - "a": "1", - "b": "2", - "c": "3", - "d": "4", - "e": 5, - } - transformed, err := resource.TransformAttributeMap[*myType](attrs) - test.That(t, err, test.ShouldBeNil) - test.That(t, transformed, test.ShouldResemble, &myType{ - A: "1", - B: "2", - Attributes: map[string]string{ - "c": "3", - "d": "4", - }, - }) - - transformed, err = resource.TransformAttributeMap[*myType](attrs) - test.That(t, err, test.ShouldBeNil) - test.That(t, transformed, test.ShouldResemble, &myType{ - A: "1", - B: "2", - Attributes: map[string]string{ - "c": "3", - "d": "4", - }, - }) - - type myExtendedType struct { - A string `json:"a"` - B string `json:"b"` - Attributes utils.AttributeMap `json:"attributes"` - } - - transformedExt, err := resource.TransformAttributeMap[*myExtendedType](attrs) - test.That(t, err, test.ShouldBeNil) - test.That(t, transformedExt, test.ShouldResemble, &myExtendedType{ - A: "1", - B: "2", - Attributes: utils.AttributeMap{ - "c": "3", - "d": "4", - "e": 5, - }, - }) - - transformedExt, err = resource.TransformAttributeMap[*myExtendedType](attrs) - test.That(t, err, test.ShouldBeNil) - test.That(t, transformedExt, test.ShouldResemble, &myExtendedType{ - A: "1", - B: "2", - Attributes: utils.AttributeMap{ - "c": "3", - "d": "4", - "e": 5, - }, - }) -} - -func TestDependencyNotReadyError(t *testing.T) { - toe := &resource.DependencyNotReadyError{"toe", errors.New("turf toe")} - foot := &resource.DependencyNotReadyError{"foot", toe} - leg := &resource.DependencyNotReadyError{"leg", foot} - human := &resource.DependencyNotReadyError{"human", leg} - - test.That(t, strings.Count(human.Error(), "\\"), test.ShouldEqual, 0) - test.That(t, human.PrettyPrint(), test.ShouldEqual, `Dependency "human" is not ready yet - - Because "leg" is not ready yet - - Because "foot" is not ready yet - - Because "toe" is not ready yet - - Because "turf toe"`) -} diff --git a/resource/resource_test.go b/resource/resource_test.go deleted file mode 100644 index a154c365e88..00000000000 --- a/resource/resource_test.go +++ /dev/null @@ -1,717 +0,0 @@ -package resource_test - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/arm/fake" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" -) - -func TestResourceType(t *testing.T) { - for _, tc := range []struct { - TestName string - Namespace resource.APINamespace - Type string - Expected resource.APIType - Err string - }{ - { - "missing namespace", - "", - resource.APITypeComponentName, - resource.APIType{Name: resource.APITypeComponentName}, - "namespace field for resource missing or invalid", - }, - { - "missing type", - resource.APINamespaceRDK, - "", - resource.APIType{Namespace: resource.APINamespaceRDK}, - "type field for resource missing or invalid", - }, - - { - "reserved character in resource type", - "rd:k", - resource.APITypeComponentName, - resource.APIType{Namespace: "rd:k", Name: resource.APITypeComponentName}, - "reserved character : used", - }, - { - "reserved charater in namespace", - resource.APINamespaceRDK, - "compon:ent", - resource.APIType{Namespace: resource.APINamespaceRDK, Name: "compon:ent"}, - "reserved character : used", - }, - { - "all fields included", - resource.APINamespaceRDK, - resource.APITypeComponentName, - resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - "", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed := tc.Namespace.WithType(tc.Type) - test.That(t, observed, test.ShouldResemble, tc.Expected) - err := observed.Validate() - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestResourceAPI(t *testing.T) { - for _, tc := range []struct { - TestName string - Namespace resource.APINamespace - Type string - SubtypeName string - Expected resource.API - Err string - }{ - { - "missing namespace", - "", - resource.APITypeComponentName, - arm.SubtypeName, - resource.API{ - Type: resource.APIType{ - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - "namespace field for resource missing or invalid", - }, - { - "missing type", - resource.APINamespaceRDK, - "", - arm.SubtypeName, - resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - }, - SubtypeName: arm.SubtypeName, - }, - "type field for resource missing or invalid", - }, - { - "missing subtype", - resource.APINamespaceRDK, - resource.APITypeComponentName, - "", - resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - }, - "subtype field for resource missing or invalid", - }, - { - "reserved character in subtype name", - resource.APINamespaceRDK, - resource.APITypeComponentName, - "sub:type", - resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: "sub:type", - }, - "reserved character : used", - }, - { - "all fields included", - resource.APINamespaceRDK, - resource.APITypeComponentName, - arm.SubtypeName, - resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - "", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed := tc.Namespace.WithType(tc.Type).WithSubtype(tc.SubtypeName) - test.That(t, observed, test.ShouldResemble, tc.Expected) - err := observed.Validate() - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestResourceNameNew(t *testing.T) { - for _, tc := range []struct { - TestName string - Namespace resource.APINamespace - Type string - SubtypeName string - Name string - Expected resource.Name - }{ - { - "missing name", - resource.APINamespaceRDK, - resource.APITypeComponentName, - arm.SubtypeName, - "", - resource.Name{ - API: resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: arm.SubtypeName, - }, - Name: "", - }, - }, - { - "all fields included", - resource.APINamespaceRDK, - resource.APITypeComponentName, - arm.SubtypeName, - "arm1", - resource.Name{ - API: resource.API{ - Type: resource.APIType{Namespace: resource.APINamespaceRDK, Name: resource.APITypeComponentName}, - SubtypeName: arm.SubtypeName, - }, - Name: "arm1", - }, - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed := resource.NewName(tc.Namespace.WithType(tc.Type).WithSubtype(tc.SubtypeName), tc.Name) - test.That(t, observed, test.ShouldResemble, tc.Expected) - }) - } -} - -func TestResourceNameNewFromString(t *testing.T) { - for _, tc := range []struct { - TestName string - Name string - Expected resource.Name - Err string - }{ - { - "malformed name", - "rdk/components/arm/arm1", - resource.Name{}, - "string \"rdk/components/arm/arm1\" is not a fully qualified resource name", - }, - { - "too many colons", - "rdk::component::arm/arm1", - resource.Name{}, - "string \"rdk::component::arm/arm1\" is not a fully qualified resource name", - }, - { - "too few colons", - "rdk.component.arm/arm1", - resource.Name{}, - "string \"rdk.component.arm/arm1\" is not a fully qualified resource name", - }, - { - "simple name", - "arm1", - resource.Name{}, - "string \"arm1\" is not a fully qualified resource name", - }, - { - "short name", - "robot1:robot2:robot3:arm1", - resource.Name{}, - "string \"robot1:robot2:robot3:arm1\" is not a fully qualified resource name", - }, - { - "missing name", - "rdk:component:arm/", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - Name: "", - }, - "", - }, - { - "all fields included", - arm.Named("arm1").String(), - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - Name: "arm1", - }, - "", - }, - { - "all fields included 2", - "rdk:component:movement_sensor/movementsensor1", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: movementsensor.SubtypeName, - }, - Name: "movementsensor1", - }, - "", - }, - { - "with remotes", - "rdk:component:movement_sensor/remote1:movementsensor1", - resource.Name{ - Remote: "remote1", - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: movementsensor.SubtypeName, - }, - Name: "movementsensor1", - }, - "", - }, - { - "with remotes 2", - "rdk:component:movement_sensor/remote1:remote2:movementsensor1", - resource.Name{ - Remote: "remote1:remote2", - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: movementsensor.SubtypeName, - }, - Name: "movementsensor1", - }, - "", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed, err := resource.NewFromString(tc.Name) - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, observed, test.ShouldResemble, tc.Expected) - test.That(t, observed.String(), test.ShouldResemble, tc.Name) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestResourceNameStrings(t *testing.T) { - for _, tc := range []struct { - TestName string - Name resource.Name - ExpectedFullName string - }{ - { - "all fields included", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - Name: "arm1", - }, - arm.Named("arm1").String(), - }, - { - "missing subtype", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - }, - Name: "arm1", - }, - "rdk:component:/arm1", - }, - { - "missing name", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - }, - "rdk:component:arm/", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - test.That(t, tc.Name.String(), test.ShouldEqual, tc.ExpectedFullName) - }) - } -} - -func TestResourceNameValidate(t *testing.T) { - for _, tc := range []struct { - Name string - NewResource resource.Name - Err string - }{ - { - "missing namespace", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - Name: "arm1", - }, - "namespace field for resource missing or invalid", - }, - { - "missing type", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - }, - SubtypeName: arm.SubtypeName, - }, - Name: "arm1", - }, - "type field for resource missing or invalid", - }, - { - "missing subtype", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - }, - Name: "arm1", - }, - "subtype field for resource missing or invalid", - }, - { - "missing name", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - }, - "name field for resource is empty", - }, - { - "all fields included", - resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: arm.SubtypeName, - }, - Name: "arm1", - }, - "", - }, - } { - t.Run(tc.Name, func(t *testing.T) { - err := tc.NewResource.Validate() - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestRemoteResource(t *testing.T) { - n, err := resource.NewFromString("rdk:component:movement_sensor/movementsensor1") - test.That(t, err, test.ShouldBeNil) - test.That(t, n, test.ShouldResemble, resource.Name{ - API: resource.API{ - Type: resource.APIType{ - Namespace: resource.APINamespaceRDK, - Name: resource.APITypeComponentName, - }, - SubtypeName: movementsensor.SubtypeName, - }, - Name: "movementsensor1", - }) - - test.That(t, n.ContainsRemoteNames(), test.ShouldBeFalse) - - n1 := n.PrependRemote("remote1") - - test.That(t, n1.ContainsRemoteNames(), test.ShouldBeTrue) - test.That(t, n1.Remote, test.ShouldResemble, "remote1") - test.That(t, n1.String(), test.ShouldResemble, "rdk:component:movement_sensor/remote1:movementsensor1") - - test.That(t, n1, test.ShouldNotResemble, n) - - n2 := n1.PrependRemote("remote2") - - test.That(t, n2.ContainsRemoteNames(), test.ShouldBeTrue) - test.That(t, n2.Remote, test.ShouldResemble, "remote2:remote1") - test.That(t, n2.String(), test.ShouldResemble, "rdk:component:movement_sensor/remote2:remote1:movementsensor1") - - n3 := n2.PopRemote() - test.That(t, n3.ContainsRemoteNames(), test.ShouldBeTrue) - test.That(t, n3.Remote, test.ShouldResemble, "remote1") - test.That(t, n3, test.ShouldResemble, n1) - test.That(t, n3.String(), test.ShouldResemble, "rdk:component:movement_sensor/remote1:movementsensor1") - - n4 := n3.PopRemote() - test.That(t, n4.ContainsRemoteNames(), test.ShouldBeFalse) - test.That(t, n4.Remote, test.ShouldResemble, "") - test.That(t, n4, test.ShouldResemble, n) - test.That(t, n4.String(), test.ShouldResemble, "rdk:component:movement_sensor/movementsensor1") - - resourceAPI := resource.APINamespace("test").WithComponentType("mycomponent") - n5 := resource.NewName(resourceAPI, "test") - test.That(t, n5.String(), test.ShouldResemble, "test:component:mycomponent/test") - n5 = resource.NewName(resourceAPI, "") - test.That(t, n5.String(), test.ShouldResemble, "test:component:mycomponent/") - n5 = resource.NewName(resourceAPI, "remote1:test") - test.That(t, n5.String(), test.ShouldResemble, "test:component:mycomponent/remote1:test") - n5 = resource.NewName(resourceAPI, "remote2:remote1:test") - test.That(t, n5.String(), test.ShouldResemble, "test:component:mycomponent/remote2:remote1:test") - n5 = resource.NewName(resourceAPI, "remote1:") - test.That(t, n5.String(), test.ShouldResemble, "test:component:mycomponent/remote1:") - n5 = resource.NewName(resourceAPI, "remote2:remote1:") - test.That(t, n5.String(), test.ShouldResemble, "test:component:mycomponent/remote2:remote1:") -} - -func TestNewPossibleRDKServiceAPIFromString(t *testing.T) { - for _, tc := range []struct { - TestName string - StrAPI string - Expected resource.API - Err string - }{ - { - "valid", - "rdk:component:arm", - arm.API, - "", - }, - { - "valid with special characters and numbers", - "acme_corp1:test-collection99:api_a2", - resource.API{ - Type: resource.APIType{Namespace: "acme_corp1", Name: "test-collection99"}, - SubtypeName: "api_a2", - }, - "", - }, - { - "invalid with slash", - "acme/corp:test:subtypeA", - resource.API{}, - "not a valid api name", - }, - { - "invalid with caret", - "acme:test:subtype^A", - resource.API{}, - "not a valid api name", - }, - { - "missing field", - "acme:test", - resource.API{}, - "not a valid api name", - }, - { - "empty namespace", - ":test:subtypeA", - resource.API{}, - "not a valid api name", - }, - { - "empty family", - "acme::subtypeA", - resource.API{}, - "not a valid api name", - }, - { - "empty name", - "acme:test::", - resource.API{}, - "not a valid api name", - }, - { - "extra field", - "acme:test:subtypeA:fail", - resource.API{}, - "not a valid api name", - }, - { - "mistaken resource name", - "acme:test:subtypeA/fail", - resource.API{}, - "not a valid api name", - }, - { - "valid nested json", - `{"namespace": "acme", "type": "test", "subtype": "subtypeB"}`, - resource.API{ - Type: resource.APIType{Namespace: "acme", Name: "test"}, - SubtypeName: "subtypeB", - }, - "not a valid api name", - }, - { - "invalid nested json type", - `{"namespace": "acme", "type": "te^st", "subtype": "subtypeB"}`, - resource.API{}, - "not a valid api name", - }, - { - "invalid nested json namespace", - `{"namespace": "$acme", "type": "test", "subtype": "subtypeB"}`, - resource.API{}, - "not a valid api name", - }, - { - "invalid nested json subtype", - `{"namespace": "acme", "type": "test", "subtype": "subtype#B"}`, - resource.API{}, - "not a valid api name", - }, - { - "missing nested json field", - `{"namespace": "acme", "name": "subtype#B"}`, - resource.API{}, - "not a valid api name", - }, - { - "single name", - `hello`, - resource.APINamespaceRDK.WithServiceType("hello"), - "", - }, - { - "double name", - `uh:hello`, - resource.API{}, - "not a valid api name", - }, - } { - t.Run(tc.TestName, func(t *testing.T) { - observed, err := resource.NewPossibleRDKServiceAPIFromString(tc.StrAPI) - if tc.Err == "" { - test.That(t, err, test.ShouldBeNil) - test.That(t, observed.Validate(), test.ShouldBeNil) - test.That(t, observed, test.ShouldResemble, tc.Expected) - test.That(t, observed.String(), test.ShouldResemble, tc.Expected.String()) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tc.Err) - } - }) - } -} - -func TestDependenciesLookup(t *testing.T) { - deps := resource.Dependencies{} - - armName := arm.Named("foo") - _, err := deps.Lookup(armName) - test.That(t, err, test.ShouldBeError, resource.DependencyNotFoundError(armName)) - remoteArmName := arm.Named("robot1:foo") - _, err = deps.Lookup(remoteArmName) - test.That(t, err, test.ShouldBeError, resource.DependencyNotFoundError(remoteArmName)) - - logger := logging.NewTestLogger(t) - someArm, err := fake.NewArm(context.Background(), nil, resource.Config{ConvertedAttributes: &fake.Config{}}, logger) - test.That(t, err, test.ShouldBeNil) - deps[armName] = someArm - - t.Log("adding an arm by just its name should allow it to be looked up by that same name") - res, err := deps.Lookup(armName) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, someArm) - - t.Log("but not the remote name since its too specific") - _, err = deps.Lookup(remoteArmName) - test.That(t, err, test.ShouldBeError, resource.DependencyNotFoundError(remoteArmName)) - - deps = resource.Dependencies{} - deps[remoteArmName] = someArm - - t.Log("adding an arm by its remote name should allow it to be looked up by the same remote name") - res, err = deps.Lookup(remoteArmName) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, someArm) - - t.Log("as well as just the arm name since it is not specific") - res, err = deps.Lookup(armName) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldEqual, someArm) - - remoteArmName2 := arm.Named("robot2:foo") - deps[remoteArmName2] = someArm - t.Log("but not if there are two remote names with the same naked name") - _, err = deps.Lookup(armName) - test.That(t, err, test.ShouldBeError, utils.NewRemoteResourceClashError(armName.Name)) - - sensorName := movementsensor.Named("foo") - _, err = deps.Lookup(sensorName) - test.That(t, err, test.ShouldBeError, resource.DependencyNotFoundError(sensorName)) - - remoteSensorName := movementsensor.Named("robot1:foo") - _, err = deps.Lookup(remoteSensorName) - test.That(t, err, test.ShouldBeError, resource.DependencyNotFoundError(remoteSensorName)) -} diff --git a/resource/response_metadata_test.go b/resource/response_metadata_test.go deleted file mode 100644 index 831ffbe8bc5..00000000000 --- a/resource/response_metadata_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package resource - -import ( - "testing" - "time" - - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - "google.golang.org/protobuf/types/known/timestamppb" -) - -func TestResponseToProto(t *testing.T) { - ts := time.UnixMilli(12345) - metadata := ResponseMetadata{CapturedAt: ts} - proto := metadata.AsProto() - test.That(t, proto.CapturedAt.AsTime(), test.ShouldEqual, ts) -} - -func TestResponseFromProto(t *testing.T) { - ts := ×tamppb.Timestamp{Seconds: 12, Nanos: 345000000} - proto := &commonpb.ResponseMetadata{CapturedAt: ts} - metadata := ResponseMetadataFromProto(proto) - test.That(t, metadata.CapturedAt, test.ShouldEqual, time.UnixMilli(12345)) -} diff --git a/resource/verify_main_test.go b/resource/verify_main_test.go deleted file mode 100644 index 4e33ac14b10..00000000000 --- a/resource/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package resource - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/rimage/color_cluster_test.go b/rimage/color_cluster_test.go deleted file mode 100644 index 293d77c6096..00000000000 --- a/rimage/color_cluster_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package rimage - -import ( - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" -) - -func doTest(t *testing.T, fn string, numClusters int) { - checkSkipDebugTest(t) - t.Helper() - img, err := NewImageFromFile(artifact.MustPath("rimage/" + fn)) - test.That(t, err, test.ShouldBeNil) - - clusters, err := ClusterFromImage(img, numClusters) - test.That(t, err, test.ShouldBeNil) - - res := ClusterImage(clusters, img) - err = WriteImageToFile(t.TempDir()+"/"+fn, res) - test.That(t, err, test.ShouldBeNil) -} - -func TestCluster1(t *testing.T) { - doTest(t, "warped-board-1605543525.png", 4) -} - -func TestCluster2(t *testing.T) { - doTest(t, "chess-segment2.png", 3) -} diff --git a/rimage/color_test.go b/rimage/color_test.go deleted file mode 100644 index 6e829674f62..00000000000 --- a/rimage/color_test.go +++ /dev/null @@ -1,529 +0,0 @@ -package rimage - -import ( - "image" - "image/color" - "math" - "os" - "strings" - "testing" - - "github.com/lucasb-eyer/go-colorful" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" -) - -func _checkAllDifferent(t *testing.T, colors []Color) { - t.Helper() - for i, c1 := range colors { - for j, c2 := range colors { - d := c1.Distance(c2) - if i == j { - test.That(t, d, test.ShouldEqual, 0) - } else { - test.That(t, d, test.ShouldBeGreaterThanOrEqualTo, 1) - } - } - } -} - -func _checkAllSame(t *testing.T, colors []Color) { - t.Helper() - _checkAllClose(t, colors, 1.0) -} - -func _checkAllClose(t *testing.T, colors []Color, maxDistance float64) { - t.Helper() - numErrors := 0 - for _, c1 := range colors { - for _, c2 := range colors { - if !_assertClose(t, c1, c2, maxDistance) { - numErrors++ - test.That(t, numErrors, test.ShouldBeLessThanOrEqualTo, 20) - } - } - } -} - -func _testColorFailure(t *testing.T, a, b Color, threshold float64, comparison string) { - t.Helper() - d := a.distanceDebug(b, true) - t.Fatalf("%v(%s) %v(%s) difference should be %s %f, but is %f https://www.viam.com/color.html?#1=%s&2=%s", - a, a.Hex(), b, b.Hex(), comparison, threshold, d, a.Hex(), b.Hex()) -} - -func _assertCloseHex(t *testing.T, a, b string, threshold float64) bool { - t.Helper() - aa := NewColorFromHexOrPanic(a) - bb := NewColorFromHexOrPanic(b) - - return _assertClose(t, aa, bb, threshold) -} - -func _assertClose(t *testing.T, a, b Color, threshold float64) bool { - t.Helper() - if d := a.Distance(b); d < threshold { - return true - } - - _testColorFailure(t, a, b, threshold, "<") - return false -} - -func _assertNotCloseHex(t *testing.T, a, b string, threshold float64) bool { - t.Helper() - aa := NewColorFromHexOrPanic(a) - bb := NewColorFromHexOrPanic(b) - - if d := aa.Distance(bb); d > threshold { - return true - } - - _testColorFailure(t, aa, bb, threshold, ">") - return false -} - -func _assertSame(t *testing.T, a, b Color) { - t.Helper() - if d := a.Distance(b); d < 1 { - return - } - _testColorFailure(t, a, b, 1, "<") -} - -func _assertNotSame(t *testing.T, a, b Color) { - t.Helper() - if d := a.Distance(b); d > 1 { - return - } - _testColorFailure(t, a, b, 1, ">") -} - -func TestColorHSVColorConversion(t *testing.T) { - c, err := colorful.Hex("#ff0000") - test.That(t, err, test.ShouldBeNil) - test.That(t, c.Hex(), test.ShouldEqual, "#ff0000") - r, g, b := c.RGB255() - test.That(t, r, test.ShouldEqual, 255) - test.That(t, g, test.ShouldEqual, 0) - test.That(t, b, test.ShouldEqual, 0) - - H, S, V := c.Hsv() - c2 := colorful.Hsv(H, S, V) - test.That(t, c2.Hex(), test.ShouldEqual, c.Hex()) - - test.That(t, c.Hex(), test.ShouldEqual, Red.Hex()) - test.That(t, Red.Hex(), test.ShouldEqual, "#ff0000") - - c5, ok := colorful.MakeColor(Red) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, c5.Hex(), test.ShouldEqual, Red.Hex()) - - c6Hex := "#123456" - c6, err := NewColorFromHex(c6Hex) - test.That(t, err, test.ShouldBeNil) - test.That(t, c6Hex, test.ShouldEqual, c6.Hex()) -} - -func TestColorBits(t *testing.T) { - c := newcolor(1, 2, 3, 10001, 5, 6) - r, g, b := c.RGB255() - h, s, v := c.hsv() - test.That(t, int(r), test.ShouldEqual, 1) - test.That(t, int(g), test.ShouldEqual, 2) - test.That(t, int(b), test.ShouldEqual, 3) - test.That(t, int(h), test.ShouldEqual, 10001) - test.That(t, int(s), test.ShouldEqual, 5) - test.That(t, int(v), test.ShouldEqual, 6) -} - -func TestColorRoundTrip(t *testing.T) { - c := NewColor(17, 83, 133) - c2 := NewColorFromColor(c) - test.That(t, c2.Hex(), test.ShouldEqual, c.Hex()) - test.That(t, c2.Hex(), test.ShouldEqual, "#115385") - - c2 = NewColorFromColor(color.RGBA{17, 83, 133, 255}) - test.That(t, c2.Hex(), test.ShouldEqual, c.Hex()) - - c2 = NewColorFromColor(color.NRGBA{17, 83, 133, 255}) - test.That(t, c2.Hex(), test.ShouldEqual, c.Hex()) -} - -func TestColorHSVDistanceSanityCheckDiff(t *testing.T) { - data := [][]float64{ - {0.0, 0.5, 0.5}, - {0.2, 0.5, 0.3}, - {0.5, 0.2, 0.3}, - {0.0, 0.9, 0.1}, - {0.9, 0.1, 0.2}, - } - - for _, x := range data { - d := _loopedDiff(x[0], x[1]) - test.That(t, math.Abs(d-x[2]), test.ShouldBeLessThanOrEqualTo, .0001) - } -} - -func TestColorHSVDistanceSanityCheck(t *testing.T) { - _, s, v := Red.hsv() - test.That(t, s, test.ShouldEqual, 255) - test.That(t, v, test.ShouldEqual, 255) - h, _, _ := Green.hsv() - test.That(t, h, test.ShouldEqual, 21845) - - d := White.Distance(Gray) - test.That(t, d, test.ShouldBeGreaterThanOrEqualTo, 1) - - _checkAllDifferent(t, Colors) -} - -func TestColorHSVDistanceSanityCheck2(t *testing.T) { - // check rotating aroudn 360 - _assertSame(t, NewColorFromHSV(190, 1.0, 1.0), NewColorFromHSV(195, 1.0, 1.0)) - _assertSame(t, NewColorFromHSV(355, 1.0, 1.0), NewColorFromHSV(359, 1.0, 1.0)) - _assertSame(t, NewColorFromHSV(359, 1.0, 1.0), NewColorFromHSV(1, 1.0, 1.0)) - - // in the same hue, check value diff - _assertSame(t, NewColorFromHSV(180, .5, 0), NewColorFromHSV(180, .5, .05)) - _assertSame(t, NewColorFromHSV(180, .5, 0), NewColorFromHSV(180, .5, .1)) - _assertNotSame(t, NewColorFromHSV(180, .5, 0), NewColorFromHSV(180, .5, .15)) - - _assertSame(t, NewColorFromHSV(180, .5, .09), NewColorFromHSV(180, .5, .05)) - _assertSame(t, NewColorFromHSV(180, .5, .09), NewColorFromHSV(180, .5, .10)) - _assertSame(t, NewColorFromHSV(180, .5, .09), NewColorFromHSV(180, .5, .15)) - - // in a dark value, hue shouldn't matter - _assertSame(t, NewColorFromHSV(180, .5, .09), NewColorFromHSV(0, .5, .09)) - - // grays - _assertSame(t, NewColorFromHSV(180, 0, .5), NewColorFromHSV(180, .05, .5)) - _assertSame(t, NewColorFromHSV(180, 0, .5), NewColorFromHSV(180, .1, .5)) - _assertNotSame(t, NewColorFromHSV(180, 0, .5), NewColorFromHSV(180, .15, .5)) - - _assertSame(t, NewColorFromHSV(180, .09, .5), NewColorFromHSV(180, .05, .5)) - _assertSame(t, NewColorFromHSV(180, .09, .5), NewColorFromHSV(180, .1, .5)) - _assertSame(t, NewColorFromHSV(180, .09, .5), NewColorFromHSV(180, .15, .5)) - - // in the lower left quadrant, how much hue difference is ok - _assertSame(t, NewColorFromHSV(180, .4, .4), NewColorFromHSV(175, .4, .4)) - _assertSame(t, NewColorFromHSV(180, .4, .4), NewColorFromHSV(170, .4, .4)) - _assertNotSame(t, NewColorFromHSV(180, .4, .4), NewColorFromHSV(150, .4, .4)) - - // in the upper right quadrant, how much hue difference is ok - _assertSame(t, NewColorFromHSV(180, .8, .8), NewColorFromHSV(175, .8, .8)) - _assertSame(t, NewColorFromHSV(180, .8, .8), NewColorFromHSV(173, .8, .8)) - _assertNotSame(t, NewColorFromHSV(180, .8, .8), NewColorFromHSV(165, .8, .8)) - - // a black vs dark blue case - _assertNotSame(t, NewColorFromHSV(50, .6, .08), NewColorFromHSV(210, 1.0, .18)) -} - -func TestColorHSVDistanceBlacks1(t *testing.T) { - data := []Color{ - NewColorFromHexOrPanic("#020300"), - NewColorFromHexOrPanic("#010101"), - NewColor(17, 23, 11), - NewColor(23, 13, 11), - NewColor(11, 23, 21), - NewColor(11, 17, 23), - NewColor(11, 11, 23), - NewColor(19, 11, 23), - NewColor(23, 11, 20), - NewColor(23, 11, 16), - NewColor(23, 11, 13), - } - - _assertSame(t, data[0], data[1]) - - _checkAllSame(t, data) -} - -func TestColorHSVDistanceDarks(t *testing.T) { - veryDarkBlue := NewColorFromHexOrPanic("#0a1a1f") - mostlyDarkBlue := NewColorFromHexOrPanic("#09202d") - - d := veryDarkBlue.Distance(mostlyDarkBlue) - test.That(t, d, test.ShouldBeLessThanOrEqualTo, 1) - - mostlyDarkBlue2 := NewColorFromHexOrPanic("#093051") - blackish := NewColorFromHexOrPanic("#201b0e") - - d = mostlyDarkBlue2.Distance(blackish) - test.That(t, d, test.ShouldBeGreaterThanOrEqualTo, 1) - - veryDarkBlue = NewColorFromHexOrPanic("#11314c") - - d = mostlyDarkBlue2.Distance(veryDarkBlue) - test.That(t, d, test.ShouldBeLessThanOrEqualTo, 1) -} - -func TestColorRatioOffFrom135Finish(t *testing.T) { - data := [][]float64{ - {.000, 0.50}, - {.125, 0.75}, - {.250, 1.00}, - {.375, 0.75}, - {.500, 0.50}, - {.625, 0.25}, - {.750, 0.00}, - {.875, 0.25}, - {1.00, 0.50}, - } - - for _, d := range data { - res := _ratioOffFrom135Finish(d[0]) - test.That(t, res, test.ShouldEqual, d[1]) - } -} - -func TestColorRatioOffFrom135(t *testing.T) { - data := [][]float64{ - {1.0, 1.0, 1.0}, // a 45 degree angle is "bad" so should be 1 - {-1.0, -1.0, 1.0}, - {-1.0, 1.0, 0.0}, - {1.0, -1.0, 0.0}, - - {0.0, 1.0, 0.5}, - {0.0, -1.0, 0.5}, - {-1.0, 0.0, 0.5}, - {1.0, 0.0, 0.5}, - } - - for _, x := range data { - res := _ratioOffFrom135(x[0], x[1]) - test.That(t, res, test.ShouldEqual, x[2]) - } -} - -func TestColorHSVDistanceChess1(t *testing.T) { - x1 := NewColor(158, 141, 112) - x2 := NewColor(176, 154, 101) - - xd := x1.Distance(x2) - test.That(t, xd, test.ShouldBeGreaterThanOrEqualTo, 1) - - w1 := NewColor(132, 120, 75) - w2 := NewColor(184, 159, 110) - _assertNotSame(t, w1, w2) // note: i changed this as i was trying to force something that shouldn't be true - - x1 = NewColorFromHexOrPanic("#8d836a") - x2 = NewColorFromHexOrPanic("#8e7e51") - _assertNotSame(t, x1, x2) -} - -func TestColorHSVDistanceChess2(t *testing.T) { - data := []Color{ - NewColor(5, 51, 85), - NewColor(158, 141, 112), - NewColor(176, 154, 101), - NewColor(19, 17, 9), - } - _checkAllDifferent(t, data) -} - -func TestColorHSVDistanceChess3(t *testing.T) { - t.Parallel() - pieceColor, err := NewColorFromHex("#8e7e51") - test.That(t, err, test.ShouldBeNil) - - harbinger := NewColorFromHexOrPanic("#a49470") - distance := pieceColor.Distance(harbinger) - test.That(t, distance, test.ShouldBeGreaterThanOrEqualTo, 1) - - harbinger = NewColorFromHexOrPanic("#857657") - distance = pieceColor.Distance(harbinger) - test.That(t, distance, test.ShouldBeGreaterThanOrEqualTo, 1) - - allColors, err := readColorsFromFile(artifact.MustPath("rimage/hsvdistancechess3.txt")) - test.That(t, err, test.ShouldBeNil) - - for _, myColor := range allColors { - distance := pieceColor.Distance(myColor) - test.That(t, distance, test.ShouldBeGreaterThanOrEqualTo, 1) - } - - _checkAllClose(t, allColors, 2) -} - -func readColorsFromFile(fn string) ([]Color, error) { - raw, err := os.ReadFile(fn) - if err != nil { - return nil, err - } - - all := []Color{} - for _, squareColor := range strings.Split(string(raw), "\n") { - squareColor = strings.TrimSpace(squareColor) - if len(squareColor) == 0 { - continue - } - myColor, err := NewColorFromHex(squareColor) - if err != nil { - return nil, err - } - all = append(all, myColor) - } - - return all, nil -} - -func TestColorHSVDistanceChessA(t *testing.T) { - _assertNotCloseHex(t, "#8c9173", "#7b7e6c", 1.0) - _assertNotCloseHex(t, "#909571", "#83876f", .99) // I "broke" this when changing H,S,V to smaller types, thing it's ok - _assertNotCloseHex(t, "#0d1e2a", "#0e273f", 1.0) - _assertNotCloseHex(t, "#041726", "#031e39", 1.0) -} - -func TestColorHSVDistanceChessB(t *testing.T) { - a := NewColorFromHexOrPanic("#828263") - b := NewColorFromHexOrPanic("#868363") - _assertSame(t, a, b) -} - -func TestColorHSVDistanceRandom1(t *testing.T) { - test.That(t, _assertCloseHex(t, "#182b2b", "#0f2725", 1.2), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#2f433c", "#283e3d", 1.1), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#001b3d", "#002856", 1.1), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#393330", "#291f1f", 1.0), test.ShouldBeTrue) - - test.That(t, _assertCloseHex(t, "#282737", "#261f2d", 1.2), test.ShouldBeTrue) - test.That(t, _assertNotCloseHex(t, "#282737", "#261f2d", 0.9), test.ShouldBeTrue) - - test.That(t, _assertCloseHex(t, "#1b3351", "#1d233c", 1.2), test.ShouldBeTrue) - - test.That(t, _assertCloseHex(t, "#303330", "#202825", 1.1), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#000204", "#162320", 1.1), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#1d252f", "#192326", 1.1), test.ShouldBeTrue) - - test.That(t, _assertCloseHex(t, "#013b74", "#0e2f53", 1.0), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#022956", "#0f284a", 1.0), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#001d35", "#071723", 1.1), test.ShouldBeTrue) - test.That(t, _assertCloseHex(t, "#747373", "#595863", 1.07), test.ShouldBeTrue) - - test.That(t, _assertNotCloseHex(t, "#515445", "#524e4d", 1.1), test.ShouldBeTrue) - test.That(t, _assertNotCloseHex(t, "#9fa59c", "#adc3c5", 1.1), test.ShouldBeTrue) - test.That(t, _assertNotCloseHex(t, "#adc3c5", "#9ab0a7", 1.02), test.ShouldBeTrue) - - test.That(t, _assertNotCloseHex(t, "#adc3c5", "#aaaca0", 1.2), test.ShouldBeTrue) - test.That(t, _assertNotCloseHex(t, "#adc3c5", "#abafa2", 1.2), test.ShouldBeTrue) - test.That(t, _assertNotCloseHex(t, "#031c31", "#002b64", 1.2), test.ShouldBeTrue) - - test.That(t, _assertCloseHex(t, "#9cb7ab", "#adc3c5", 1.11), test.ShouldBeTrue) // shiny - test.That(t, _assertCloseHex(t, "#899d96", "#adc3c5", 1.125), test.ShouldBeTrue) // shiny - - test.That(t, _assertNotCloseHex(t, "#958f8f", "#2b2928", 3), test.ShouldBeTrue) // gray vs black - test.That(t, _assertNotCloseHex(t, "#5e5b5b", "#2b2928", 3), test.ShouldBeTrue) // gray vs black - test.That(t, _assertNotCloseHex(t, "#3d3c3c", "#2b2928", 1.0), test.ShouldBeTrue) // pretty dark gray vs black - - test.That(t, _assertNotCloseHex(t, "#807c79", "#5f5b5a", 1.05), test.ShouldBeTrue) - - test.That(t, _assertCloseHex(t, "#4b494c", "#423c3a", 1.25), test.ShouldBeTrue) - - test.That(t, _assertCloseHex(t, "#202320", "#262626", .8), test.ShouldBeTrue) -} - -func TestColorConvert(t *testing.T) { - // estimate of max error we're ok with based on conversion lossyness - okError := float64(math.MaxUint16) * (1 - math.Pow(255.0/256.0, 4)) - - testRoundTrip := func(c color.Color) { - cc := NewColorFromColor(c) - - r1, g1, b1, a1 := c.RGBA() - r2, g2, b2, a2 := cc.RGBA() - - test.That(t, r2, test.ShouldAlmostEqual, r1, okError) - test.That(t, g2, test.ShouldAlmostEqual, g1, okError) - test.That(t, b2, test.ShouldAlmostEqual, b1, okError) - test.That(t, a2, test.ShouldAlmostEqual, a1, okError) - } - - testRoundTrip(Red) - - testRoundTrip(color.NRGBA{17, 50, 124, 255}) - - testRoundTrip(color.RGBA{17, 50, 124, 255}) - - testRoundTrip(color.YCbCr{17, 50, 124}) - testRoundTrip(color.YCbCr{17, 50, 1}) - testRoundTrip(color.YCbCr{17, 50, 255}) - testRoundTrip(color.YCbCr{112, 50, 124}) -} - -func TestHSVConvert(t *testing.T) { - tt := func(c color.Color) { - me := NewColorFromColor(c) - them, ok := colorful.MakeColor(c) - test.That(t, ok, test.ShouldBeTrue) - - h1, s1, v1 := me.HsvNormal() - h2, s2, v2 := them.Hsv() - - test.That(t, h1, test.ShouldAlmostEqual, h2, .1) - test.That(t, s1, test.ShouldAlmostEqual, s2, .01) - test.That(t, v1, test.ShouldAlmostEqual, v2, .01) - } - - tt(color.NRGBA{128, 128, 128, 255}) - tt(color.NRGBA{128, 200, 225, 255}) - tt(color.NRGBA{200, 128, 32, 255}) - tt(color.NRGBA{31, 213, 200, 255}) - - for r := 0; r <= 255; r += 32 { - for g := 0; g <= 255; g += 32 { - for b := 0; b <= 255; b += 32 { - tt(color.NRGBA{uint8(r), uint8(g), uint8(b), 255}) - } - } - } -} - -func TestColorSegment1(t *testing.T) { - checkSkipDebugTest(t) - img, err := NewImageFromFile(artifact.MustPath("rimage/chess-segment1.png")) - test.That(t, err, test.ShouldBeNil) - - all := []Color{} - - for x := 0; x < img.Width(); x++ { - for y := 0; y < img.Height(); y++ { - c := img.Get(image.Point{x, y}) - all = append(all, c) - } - } - - clusters, err := ClusterHSV(all, 4) - test.That(t, err, test.ShouldBeNil) - - diffs := ColorDiffs{} - - for x, a := range clusters { - for y := x + 1; y < len(clusters); y++ { - if x == y { - continue - } - b := clusters[y] - - diffs.Add(a, b) - } - } - - outDir := t.TempDir() - logging.NewTestLogger(t).Debugf("out dir: %q", outDir) - err = diffs.WriteTo(outDir + "/foo.html") - test.That(t, err, test.ShouldBeNil) - - out := NewImage(img.Width(), img.Height()) - for x := 0; x < img.Width(); x++ { - for y := 0; y < img.Height(); y++ { - c := img.Get(image.Point{x, y}) - _, cc, _ := c.Closest(clusters) - out.Set(image.Point{x, y}, cc) - } - } - - out.WriteTo(outDir + "/foo.png") -} diff --git a/rimage/convkernel_test.go b/rimage/convkernel_test.go deleted file mode 100644 index b820405af12..00000000000 --- a/rimage/convkernel_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package rimage - -import ( - "testing" - - "go.viam.com/test" -) - -func TestNewKernel(t *testing.T) { - k, err := NewKernel(5, 5) - // test creation - test.That(t, err, test.ShouldBeNil) - test.That(t, k.Width, test.ShouldEqual, 5) - test.That(t, k.Height, test.ShouldEqual, 5) - // test content - test.That(t, k.Content[0], test.ShouldResemble, []float64{0, 0, 0, 0, 0}) - test.That(t, k.At(4, 4), test.ShouldEqual, 0) - test.That(t, k.At(3, 2), test.ShouldEqual, 0) - // test set - k.Set(3, 2, 1) - test.That(t, k.At(3, 2), test.ShouldEqual, 1) - // test AbsSum - k.Set(1, 2, -1) - test.That(t, k.AbSum(), test.ShouldEqual, 2) - // test Normalize - normalized := k.Normalize() - test.That(t, normalized.At(3, 2), test.ShouldEqual, 0.5) - - k2, err := NewKernel(-1, 5) - // test error - test.That(t, err, test.ShouldNotBeNil) - test.That(t, k2, test.ShouldBeNil) - - // test in normalize sum = 0 - k3, err := NewKernel(5, 5) - test.That(t, err, test.ShouldBeNil) - normalized3 := k3.Normalize() - test.That(t, normalized3, test.ShouldNotBeNil) -} diff --git a/rimage/convolution_helpers_test.go b/rimage/convolution_helpers_test.go deleted file mode 100644 index b210c01ae04..00000000000 --- a/rimage/convolution_helpers_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package rimage - -import ( - "image" - "image/color" - "testing" - - "go.viam.com/test" - "gonum.org/v1/gonum/mat" -) - -func TestPaddingFloat64(t *testing.T) { - img := mat.NewDense(10, 10, nil) - for i := 0; i < 10; i++ { - for j := 0; j < 10; j++ { - img.Set(i, j, float64(i*i+j*j)) - } - } - nRows, nCols := img.Dims() - test.That(t, nRows, test.ShouldEqual, 10) - test.That(t, nCols, test.ShouldEqual, 10) - padded1, err := PaddingFloat64(img, image.Point{3, 3}, image.Point{1, 1}, BorderConstant) - test.That(t, err, test.ShouldBeNil) - paddedHeight1, paddedWidth1 := padded1.Dims() - test.That(t, paddedHeight1, test.ShouldEqual, 12) - test.That(t, paddedWidth1, test.ShouldEqual, 12) - test.That(t, padded1.At(0, 0), test.ShouldEqual, 0.) - test.That(t, padded1.At(11, 11), test.ShouldEqual, 0.) -} - -func TestPaddingGray(t *testing.T) { - rect := image.Rect(0, 0, 10, 12) - img := image.NewGray(rect) - for x := 0; x < 10; x++ { - for y := 0; y < 12; y++ { - img.Set(x, y, color.Gray{uint8(x*x + y*y)}) - } - } - // testing constant padding - paddedConstant, err := PaddingGray(img, image.Point{3, 3}, image.Point{1, 1}, BorderConstant) - test.That(t, err, test.ShouldBeNil) - rect2 := paddedConstant.Bounds() - test.That(t, rect2.Max.X, test.ShouldEqual, 12) - test.That(t, rect2.Max.Y, test.ShouldEqual, 14) - test.That(t, paddedConstant.At(0, 0), test.ShouldResemble, color.Gray{0}) - test.That(t, paddedConstant.At(11, 13), test.ShouldResemble, color.Gray{0}) - // testing reflect padding - paddedReflect, err := PaddingGray(img, image.Point{3, 3}, image.Point{1, 1}, BorderReflect) - test.That(t, err, test.ShouldBeNil) - rect3 := paddedConstant.Bounds() - test.That(t, rect3.Max.X, test.ShouldEqual, 12) - test.That(t, rect3.Max.Y, test.ShouldEqual, 14) - test.That(t, paddedReflect.At(0, 0), test.ShouldResemble, color.Gray{2}) - test.That(t, paddedReflect.At(11, 13), test.ShouldResemble, color.Gray{164}) - // testing replicate padding - paddedReplicate, err := PaddingGray(img, image.Point{3, 3}, image.Point{1, 1}, BorderReplicate) - test.That(t, err, test.ShouldBeNil) - rect4 := paddedConstant.Bounds() - test.That(t, rect4.Max.X, test.ShouldEqual, 12) - test.That(t, rect4.Max.Y, test.ShouldEqual, 14) - test.That(t, paddedReplicate.At(0, 0), test.ShouldResemble, color.Gray{1}) - test.That(t, paddedReplicate.At(11, 13), test.ShouldResemble, color.Gray{202}) -} diff --git a/rimage/convolution_test.go b/rimage/convolution_test.go deleted file mode 100644 index aacd22defd1..00000000000 --- a/rimage/convolution_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package rimage - -import ( - "image" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" -) - -func TestConvolveGray(t *testing.T) { - t.Parallel() - // test that image test files are in artifacts - im, err := NewImageFromFile(artifact.MustPath("rimage/binary_image.jpg")) - test.That(t, err, test.ShouldBeNil) - gt, err := NewImageFromFile(artifact.MustPath("rimage/sobelx.png")) - test.That(t, err, test.ShouldBeNil) - // Create a new grayscale image - bounds := im.Bounds() - w, h := bounds.Max.X, bounds.Max.Y - imGray := image.NewGray(image.Rect(0, 0, w, h)) - for x := 0; x < w; x++ { - for y := 0; y < h; y++ { - imGray.Set(x, y, im.At(x, y)) - } - } - // Create a new grayscale image for GT - boundsGT := im.Bounds() - wGT, hGT := boundsGT.Max.X, boundsGT.Max.Y - imGTGray := image.NewGray(image.Rect(0, 0, wGT, hGT)) - for x := 0; x < wGT; x++ { - for y := 0; y < hGT; y++ { - imGTGray.Set(x, y, gt.At(x, y)) - } - } - // convolve image - kernel := GetSobelX() - convolved, err := ConvolveGray(imGray, &kernel, image.Point{1, 1}, 0) - test.That(t, err, test.ShouldBeNil) - // check size - test.That(t, convolved.Rect.Max.X, test.ShouldEqual, w) - test.That(t, convolved.Rect.Max.Y, test.ShouldEqual, h) - // compare 2 non zero pixel values - test.That(t, convolved.At(97, 47), test.ShouldResemble, imGTGray.At(97, 47)) - test.That(t, convolved.At(536, 304), test.ShouldResemble, imGTGray.At(536, 304)) -} - -func TestConvolveGrayFloat64(t *testing.T) { - t.Parallel() - // test that image test files are in artifacts - im, err := NewImageFromFile(artifact.MustPath("rimage/binary_image.jpg")) - bounds := im.Bounds() - w, h := bounds.Max.X, bounds.Max.Y - test.That(t, err, test.ShouldBeNil) - // convert to gray float - imGray := ConvertColorImageToLuminanceFloat(im) - x, y := 100, 200 - r, g, b, _ := im.GetXY(x, y).RGBA() - test.That(t, imGray.At(y, x), test.ShouldEqual, uint8(r)) - test.That(t, imGray.At(y, x), test.ShouldEqual, uint8(g)) - test.That(t, imGray.At(y, x), test.ShouldEqual, uint8(b)) - // load gt image - gt, err := NewImageFromFile(artifact.MustPath("rimage/sobelx.png")) - test.That(t, err, test.ShouldBeNil) - // convert to gray float - gtGray := ConvertColorImageToLuminanceFloat(gt) - x1, y1 := 350, 120 - r1, g1, b1, _ := gt.GetXY(x1, y1).RGBA() - test.That(t, gtGray.At(y1, x1), test.ShouldEqual, uint8(r1)) - test.That(t, gtGray.At(y1, x1), test.ShouldEqual, uint8(g1)) - test.That(t, gtGray.At(y1, x1), test.ShouldEqual, uint8(b1)) - - kernel := GetSobelX() - convolved, err := ConvolveGrayFloat64(imGray, &kernel) - test.That(t, err, test.ShouldBeNil) - // check size - nRows, nCols := convolved.Dims() - test.That(t, nCols, test.ShouldEqual, w) - test.That(t, nRows, test.ShouldEqual, h) - // compare 2 non zero pixel values - test.That(t, convolved.At(47, 97), test.ShouldResemble, gtGray.At(47, 97)) // 0 < val < 255 - test.That(t, convolved.At(304, 536), test.ShouldEqual, -1) // val < 0 - not clamped -} - -func TestGetSobelY(t *testing.T) { - k := GetSobelY() - test.That(t, k.Height, test.ShouldEqual, 3) - test.That(t, k.Width, test.ShouldEqual, 3) - test.That(t, k.At(0, 0), test.ShouldEqual, -1) - test.That(t, k.At(0, 1), test.ShouldEqual, -2) - test.That(t, k.At(0, 2), test.ShouldEqual, -1) - test.That(t, k.At(1, 0), test.ShouldEqual, 0) -} - -func TestGetBlur3(t *testing.T) { - k := GetBlur3() - test.That(t, k.Height, test.ShouldEqual, 3) - test.That(t, k.Width, test.ShouldEqual, 3) - test.That(t, k.At(0, 0), test.ShouldEqual, 1) - test.That(t, k.At(0, 1), test.ShouldEqual, 1) - test.That(t, k.At(0, 2), test.ShouldEqual, 1) - test.That(t, k.At(1, 0), test.ShouldEqual, 1) -} - -func TestGetGaussian3(t *testing.T) { - k := GetGaussian3() - test.That(t, k.Height, test.ShouldEqual, 3) - test.That(t, k.Width, test.ShouldEqual, 3) - test.That(t, k.At(0, 0), test.ShouldEqual, 1) - test.That(t, k.At(0, 1), test.ShouldEqual, 2) - test.That(t, k.At(0, 2), test.ShouldEqual, 1) - test.That(t, k.At(1, 0), test.ShouldEqual, 2) - normalized := (&k).Normalize() - test.That(t, normalized.Height, test.ShouldEqual, 3) - test.That(t, normalized.Width, test.ShouldEqual, 3) - test.That(t, normalized.At(0, 0), test.ShouldEqual, 1./16.) - test.That(t, normalized.At(0, 1), test.ShouldEqual, 2./16.) - test.That(t, normalized.At(0, 2), test.ShouldEqual, 1./16.) - test.That(t, normalized.At(1, 0), test.ShouldEqual, 2./16.) -} - -func TestGetGaussian5(t *testing.T) { - k := GetGaussian5() - test.That(t, k.Height, test.ShouldEqual, 5) - test.That(t, k.Width, test.ShouldEqual, 5) - - normalized := (&k).Normalize() - test.That(t, normalized.Height, test.ShouldEqual, 5) - test.That(t, normalized.Width, test.ShouldEqual, 5) - test.That(t, normalized.At(0, 0), test.ShouldEqual, 1./273.) - test.That(t, normalized.At(0, 1), test.ShouldEqual, 4./273.) - test.That(t, normalized.At(0, 2), test.ShouldEqual, 7./273.) - test.That(t, normalized.At(1, 0), test.ShouldEqual, 4./273.) - - test.That(t, k.At(0, 0), test.ShouldEqual, 1) - test.That(t, k.At(0, 1), test.ShouldEqual, 4) - test.That(t, k.At(0, 2), test.ShouldEqual, 7) - test.That(t, k.At(1, 0), test.ShouldEqual, 4) -} diff --git a/rimage/depth_map_test.go b/rimage/depth_map_test.go deleted file mode 100644 index 300d65ea0a8..00000000000 --- a/rimage/depth_map_test.go +++ /dev/null @@ -1,401 +0,0 @@ -package rimage - -import ( - "bufio" - "bytes" - "context" - "image" - "image/color" - "image/png" - "math" - "math/rand" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" -) - -func TestRawDepthMap(t *testing.T) { - t.Parallel() - m, err := ParseRawDepthMap(artifact.MustPath("rimage/board2.dat.gz")) - test.That(t, err, test.ShouldBeNil) - - test.That(t, m.Width(), test.ShouldEqual, 1280) - test.That(t, m.Height(), test.ShouldEqual, 720) - origHeight := m.GetDepth(300, 300) - test.That(t, origHeight, test.ShouldEqual, 749) - - buf := bytes.Buffer{} - _, err = WriteRawDepthMapTo(m, &buf) - test.That(t, err, test.ShouldBeNil) - - m, err = ReadDepthMap(bufio.NewReader(&buf)) - test.That(t, err, test.ShouldBeNil) - test.That(t, m.Width(), test.ShouldEqual, 1280) - test.That(t, m.Height(), test.ShouldEqual, 720) - origHeight = m.GetDepth(300, 300) - test.That(t, origHeight, test.ShouldEqual, 749) - - fn := t.TempDir() + "/board2-rt.dat.gz" - - err = WriteRawDepthMapToFile(m, fn) - test.That(t, err, test.ShouldBeNil) - - m, err = ParseRawDepthMap(fn) - test.That(t, err, test.ShouldBeNil) - test.That(t, m.Width(), test.ShouldEqual, 1280) - test.That(t, m.Height(), test.ShouldEqual, 720) - origHeight = m.GetDepth(300, 300) - test.That(t, origHeight, test.ShouldEqual, 749) -} - -func TestDepthMap(t *testing.T) { - t.Parallel() - m, err := NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - - test.That(t, m.Width(), test.ShouldEqual, 1280) - test.That(t, m.Height(), test.ShouldEqual, 720) - origHeight := m.GetDepth(300, 300) - test.That(t, origHeight, test.ShouldEqual, 749) - - buf := bytes.Buffer{} - err = m.WriteToBuf(&buf) - test.That(t, err, test.ShouldBeNil) - - img, _, err := image.Decode(bufio.NewReader(&buf)) - test.That(t, err, test.ShouldBeNil) - m, err = ConvertImageToDepthMap(context.Background(), img) - test.That(t, err, test.ShouldBeNil) - test.That(t, m.Width(), test.ShouldEqual, 1280) - test.That(t, m.Height(), test.ShouldEqual, 720) - origHeight = m.GetDepth(300, 300) - test.That(t, origHeight, test.ShouldEqual, 749) - - fn := t.TempDir() + "/board2-rt.png" - - err = WriteImageToFile(fn, m) - test.That(t, err, test.ShouldBeNil) - - m, err = NewDepthMapFromFile(context.Background(), fn) - test.That(t, err, test.ShouldBeNil) - test.That(t, m.Width(), test.ShouldEqual, 1280) - test.That(t, m.Height(), test.ShouldEqual, 720) - origHeight = m.GetDepth(300, 300) - test.That(t, origHeight, test.ShouldEqual, 749) -} - -func TestCloneDepthMap(t *testing.T) { - t.Parallel() - m, err := NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - - mm := m.Clone() - for y := 0; y < m.Height(); y++ { - for x := 0; x < m.Width(); x++ { - test.That(t, mm.GetDepth(x, y), test.ShouldResemble, m.GetDepth(x, y)) - } - } - mm.Set(0, 0, Depth(5000)) - test.That(t, mm.GetDepth(0, 0), test.ShouldNotResemble, m.GetDepth(0, 0)) -} - -func TestDepthMapNewFormat(t *testing.T) { - m, err := ParseRawDepthMap(artifact.MustPath("rimage/depthformat2.dat.gz")) - test.That(t, err, test.ShouldBeNil) - - test.That(t, m.width, test.ShouldEqual, 1280) - test.That(t, m.height, test.ShouldEqual, 720) - - numZero := 0 - - for x := 0; x < m.width; x++ { - d := m.GetDepth(x, m.height-1) - if d == 0 { - numZero++ - } else { - test.That(t, d, test.ShouldBeBetween, 100, 5000) - } - } - - test.That(t, numZero, test.ShouldBeBetween, 0, m.width) -} - -// 1 2 5 3 1 // 1 2 2 4 6 -// 3 4 -- 90 cw -> 6 4 2 // 3 4 -- 90 ccw -> 1 3 5 -// 5 6 // 5 6. -func TestDepthRotate90(t *testing.T) { - dm := NewEmptyDepthMap(2, 3) - dm.Set(0, 0, 1) - dm.Set(1, 0, 2) - dm.Set(0, 1, 3) - dm.Set(1, 1, 4) - dm.Set(0, 2, 5) - dm.Set(1, 2, 6) - - dm2 := dm.Rotate90(true) - test.That(t, dm2.Height(), test.ShouldEqual, 2) - test.That(t, dm2.Width(), test.ShouldEqual, 3) - test.That(t, dm2.GetDepth(0, 0), test.ShouldEqual, Depth(5)) - test.That(t, dm2.GetDepth(2, 1), test.ShouldEqual, Depth(2)) - dm3 := dm.Rotate90(false) - test.That(t, dm3.Height(), test.ShouldEqual, 2) - test.That(t, dm3.Width(), test.ShouldEqual, 3) - test.That(t, dm3.GetDepth(0, 0), test.ShouldEqual, Depth(2)) - test.That(t, dm3.GetDepth(2, 1), test.ShouldEqual, Depth(5)) -} - -func TestToGray16Picture(t *testing.T) { - t.Parallel() - iwd, err := newImageWithDepth( - context.Background(), - artifact.MustPath("rimage/board2.png"), artifact.MustPath("rimage/board2.dat.gz"), false, - ) - test.That(t, err, test.ShouldBeNil) - gimg := iwd.Depth.ToGray16Picture() - - test.That(t, gimg.Bounds().Max.X, test.ShouldEqual, iwd.Depth.Width()) - test.That(t, gimg.Bounds().Max.Y, test.ShouldEqual, iwd.Depth.Height()) - - file, err := os.Create(t.TempDir() + "/board2_gray.png") - test.That(t, err, test.ShouldBeNil) - defer file.Close() - png.Encode(file, gimg) -} - -func makeImagesForSubImageTest(ori, crop image.Rectangle) (*Image, *Image) { - oriWidth, oriHeight := ori.Max.X-ori.Min.X, ori.Max.Y-ori.Min.Y - overlap := ori.Intersect(crop) - cropWidth, cropHeight := overlap.Max.X-overlap.Min.X, overlap.Max.Y-overlap.Min.Y - oriData := make([]Color, 0, oriWidth*oriHeight) - cropData := make([]Color, 0, cropWidth*cropHeight) - i := Color(0) - for y := ori.Min.Y; y < ori.Max.Y; y++ { - for x := ori.Min.X; x < ori.Max.X; x++ { - oriData = append(oriData, i) - if x >= overlap.Min.X && x < overlap.Max.X && y >= overlap.Min.Y && y < overlap.Max.Y { - cropData = append(cropData, i) - } - i++ - } - } - if crop.Empty() { - return &Image{data: oriData, width: oriWidth, height: oriHeight}, &Image{} - } - return &Image{data: oriData, width: oriWidth, height: oriHeight}, &Image{data: cropData, width: cropWidth, height: cropHeight} -} - -func makeDepthMapsForSubImageTest(ori, crop image.Rectangle) (*DepthMap, *DepthMap) { - oriWidth, oriHeight := ori.Max.X-ori.Min.X, ori.Max.Y-ori.Min.Y - overlap := ori.Intersect(crop) - cropWidth, cropHeight := overlap.Max.X-overlap.Min.X, overlap.Max.Y-overlap.Min.Y - oriData := []Depth{} - cropData := []Depth{} - i := Depth(0) - for y := ori.Min.Y; y < ori.Max.Y; y++ { - for x := ori.Min.X; x < ori.Max.X; x++ { - oriData = append(oriData, i) - if x >= overlap.Min.X && x < overlap.Max.X && y >= overlap.Min.Y && y < overlap.Max.Y { - cropData = append(cropData, i) - } - i++ - } - } - if crop.Empty() { - return &DepthMap{data: oriData, width: oriWidth, height: oriHeight}, NewEmptyDepthMap(0, 0) - } - return &DepthMap{width: oriWidth, height: oriHeight, data: oriData}, &DepthMap{width: cropWidth, height: cropHeight, data: cropData} -} - -func TestSubImage(t *testing.T) { - type subImages struct{ Original, Crop image.Rectangle } - tests := []subImages{ - {image.Rect(0, 0, 100, 75), image.Rect(0, 0, 100, 75)}, // crop of the same size - {image.Rect(0, 0, 100, 75), image.Rect(0, 0, 10, 5)}, // crop upper left - {image.Rect(0, 0, 100, 75), image.Rect(90, 70, 100, 75)}, // crop lower right - {image.Rect(0, 0, 100, 75), image.Rect(30, 40, 35, 45)}, // crop middle - {image.Rect(0, 0, 100, 75), image.Rect(0, 0, 100, 2)}, // crop top - {image.Rect(0, 0, 100, 75), image.Rect(0, 72, 100, 75)}, // crop bottom - {image.Rect(0, 0, 100, 75), image.Rect(98, 0, 100, 75)}, // crop right - {image.Rect(0, 0, 100, 75), image.Rect(0, 0, 2, 75)}, // crop left - {image.Rect(0, 0, 100, 75), image.Rect(95, 70, 105, 80)}, // crop is not a full subset - {image.Rect(0, 0, 100, 75), image.Rect(200, 200, 300, 300)}, // out of bounds - {image.Rect(0, 0, 100, 75), image.Rectangle{}}, // empty - } - for _, rec := range tests { - originalImg, expectedCrop := makeImagesForSubImageTest(rec.Original, rec.Crop) - crop := originalImg.SubImage(rec.Crop) - test.That(t, crop, test.ShouldResemble, expectedCrop) - } - for _, rec := range tests { - originalDM, expectedCrop := makeDepthMapsForSubImageTest(rec.Original, rec.Crop) - crop := originalDM.SubImage(rec.Crop) - test.That(t, crop, test.ShouldResemble, expectedCrop) - } -} - -func BenchmarkDepthMapRotate90(b *testing.B) { - dm, err := ParseRawDepthMap(artifact.MustPath("rimage/depthformat2.dat.gz")) - test.That(b, err, test.ShouldBeNil) - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - dm.Rotate90(true) - } -} - -func BenchmarkDepthMapRotate180(b *testing.B) { - dm, err := ParseRawDepthMap(artifact.MustPath("rimage/depthformat2.dat.gz")) - test.That(b, err, test.ShouldBeNil) - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - dm.Rotate180() - } -} - -func TestDepthMapStats(t *testing.T) { - dm := NewEmptyDepthMap(3, 3) - for x := 0; x < 3; x++ { - for y := 0; y < 3; y++ { - dm.Set(x, y, Depth((x*10)+y)) - } - } - - d, a := dm.AverageDepthAndStats(image.Point{1, 1}, 0) - test.That(t, d, test.ShouldEqual, 11.0) - test.That(t, a, test.ShouldEqual, 0.0) - - d, a = dm.AverageDepthAndStats(image.Point{1, 1}, 1) - test.That(t, d, test.ShouldEqual, 12.375) - test.That(t, a, test.ShouldEqual, 6.46875) - - d, a = dm.AverageDepthAndStats(image.Point{3, 3}, 1) - test.That(t, d, test.ShouldEqual, 22.0) - test.That(t, a, test.ShouldEqual, 0.0) - - img := dm.InterestingPixels(5) - test.That(t, img.GrayAt(1, 1).Y, test.ShouldEqual, uint8(255)) - - img = dm.InterestingPixels(10) - test.That(t, img.GrayAt(1, 1).Y, test.ShouldEqual, uint8(0)) -} - -func TestDepthMap_ConvertDepthMapToLuminanceFloat(t *testing.T) { - iwd, err := newImageWithDepth( - context.Background(), - artifact.MustPath("rimage/board2.png"), artifact.MustPath("rimage/board2.dat.gz"), false, - ) - test.That(t, err, test.ShouldBeNil) - fimg := iwd.Depth.ConvertDepthMapToLuminanceFloat() - nRows, nCols := fimg.Dims() - // test dimensions - test.That(t, nCols, test.ShouldEqual, iwd.Depth.Width()) - test.That(t, nRows, test.ShouldEqual, iwd.Depth.Height()) - // test values - // select random pixel - x, y := rand.Intn(nCols), rand.Intn(nRows) - test.That(t, fimg.At(y, x), test.ShouldEqual, float64(iwd.Depth.GetDepth(x, y))) -} - -func TestDepthColorModel(t *testing.T) { - dm := NewEmptyDepthMap(1, 1) - // DepthMap Color model should convert to Gray16 - gray := color.Gray16{Y: 5} - convGray := dm.ColorModel().Convert(gray) - test.That(t, convGray, test.ShouldHaveSameTypeAs, gray) - test.That(t, convGray.(color.Gray16).Y, test.ShouldEqual, gray.Y) - // test Gray8 - gray8 := color.Gray{Y: math.MaxUint8} - convGray = dm.ColorModel().Convert(gray8) - test.That(t, convGray, test.ShouldHaveSameTypeAs, gray) - test.That(t, convGray.(color.Gray16).Y, test.ShouldEqual, math.MaxUint16) - gray8 = color.Gray{Y: 24} // copies the 8 bits, to 16 bits: 0001 1000 -> 0001 1000 0001 1000 - convGray = dm.ColorModel().Convert(gray8) - test.That(t, convGray, test.ShouldHaveSameTypeAs, gray) - test.That(t, convGray.(color.Gray16).Y, test.ShouldEqual, 6168) - // do it directly in binary for clarity - gray8 = color.Gray{Y: 0b01101100} - convGray = dm.ColorModel().Convert(gray8) - test.That(t, convGray, test.ShouldHaveSameTypeAs, gray) - test.That(t, convGray.(color.Gray16).Y, test.ShouldEqual, 0b0110110001101100) - // test max value - maxGray := color.Gray16{Y: math.MaxUint16} - convGray = dm.ColorModel().Convert(maxGray) - test.That(t, convGray, test.ShouldHaveSameTypeAs, gray) - test.That(t, convGray.(color.Gray16).Y, test.ShouldEqual, maxGray.Y) - // 8 bit color gets copied into the next byte - rgba8 := color.NRGBA{24, 24, 24, math.MaxUint8} - convGray = dm.ColorModel().Convert(rgba8) - test.That(t, convGray, test.ShouldHaveSameTypeAs, gray) - test.That(t, convGray.(color.Gray16).Y, test.ShouldEqual, 6168) -} - -func TestViamDepthMap(t *testing.T) { - // create various types of depth representations - width := 10 - height := 20 - dm := NewEmptyDepthMap(width, height) - g16 := image.NewGray16(image.Rect(0, 0, width, height)) - g8 := image.NewGray(image.Rect(0, 0, width, height)) - for x := 0; x < width; x++ { - for y := 0; y < height; y++ { - dm.Set(x, y, Depth(x*y)) - g16.SetGray16(x, y, color.Gray16{uint16(x * y)}) - g8.SetGray(x, y, color.Gray{uint8(x * y)}) - } - } - // write dm to a viam type buffer - buf := &bytes.Buffer{} - byt, err := WriteViamDepthMapTo(dm, buf) - test.That(t, err, test.ShouldBeNil) - test.That(t, byt, test.ShouldEqual, (3*8 + 2*width*height)) // 3 bytes for header, 2 bytes per pixel - // write gray16 to a viam type buffer - buf16 := &bytes.Buffer{} - byt16, err := WriteViamDepthMapTo(g16, buf16) - test.That(t, err, test.ShouldBeNil) - test.That(t, byt16, test.ShouldEqual, (3*8 + 2*width*height)) // 3 bytes for header, 2 bytes per pixel - // gray should fail - buf8 := &bytes.Buffer{} - _, err = WriteViamDepthMapTo(g8, buf8) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot convert image type") - // read from a viam type buffers and compare to original - dm2, err := ReadDepthMap(buf) - test.That(t, err, test.ShouldBeNil) - test.That(t, dm2, test.ShouldResemble, dm) - dm3, err := ReadDepthMap(buf16) - test.That(t, err, test.ShouldBeNil) - test.That(t, dm3, test.ShouldResemble, dm) -} - -func TestDepthMapEncoding(t *testing.T) { - m, err := NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/fakeDM.vnd.viam.dep")) - test.That(t, err, test.ShouldBeNil) - // Test values at points of DepthMap - // This example DepthMap (fakeDM) was made such that Depth(x,y) = x*y - test.That(t, m.Width(), test.ShouldEqual, 20) - test.That(t, m.Height(), test.ShouldEqual, 10) - testPt1 := m.GetDepth(13, 3) - test.That(t, testPt1, test.ShouldEqual, 39) - testPt2 := m.GetDepth(10, 6) - test.That(t, testPt2, test.ShouldEqual, 60) - - // Save DepthMap BYTES to a file - outDir := t.TempDir() - saveTo := outDir + "/grayboard_bytes.vnd.viam.dep" - err = WriteRawDepthMapToFile(m, saveTo) - test.That(t, err, test.ShouldBeNil) - - newM, err := NewDepthMapFromFile(context.Background(), saveTo) - test.That(t, err, test.ShouldBeNil) - test.That(t, newM.Bounds().Dx(), test.ShouldEqual, 20) - test.That(t, newM.Bounds().Dy(), test.ShouldEqual, 10) - testPtA := newM.GetDepth(13, 3) - test.That(t, testPtA, test.ShouldEqual, 39) - testPtB := newM.GetDepth(10, 6) - test.That(t, testPtB, test.ShouldEqual, 60) -} diff --git a/rimage/depth_processing_test.go b/rimage/depth_processing_test.go deleted file mode 100644 index d0cd927ef50..00000000000 --- a/rimage/depth_processing_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package rimage - -import ( - "testing" - - "github.com/golang/geo/r2" - "go.viam.com/test" -) - -type rangeArrayHelper struct { - dim int - expected []int -} - -func TestRangeArray(t *testing.T) { - cases := []rangeArrayHelper{ - {5, []int{-2, -1, 0, 1, 2}}, - {4, []int{-2, -1, 0, 1}}, - {3, []int{-1, 0, 1}}, - {2, []int{-1, 0}}, - {1, []int{0}}, - {0, []int{}}, - {-2, []int{}}, - } - for _, c := range cases { - got := makeRangeArray(c.dim) - test.That(t, c.expected, test.ShouldResemble, got) - } -} - -func TestStructuringElement(t *testing.T) { - expected := &DepthMap{3, 3, []Depth{0, 1, 0, 1, 1, 1, 0, 1, 0}} - got := makeStructuringElement(3) - test.That(t, expected, test.ShouldResemble, got) -} - -func TestInterpolations(t *testing.T) { - dm := NewEmptyDepthMap(2, 2) - dm.Set(0, 0, 1) - dm.Set(1, 0, 2) - dm.Set(0, 1, 3) - dm.Set(1, 1, 4) - - pt := r2.Point{0.25, 0.25} - d := NearestNeighborDepth(pt, dm) - test.That(t, *d, test.ShouldEqual, Depth(1)) - d = BilinearInterpolationDepth(pt, dm) // 1.75 - test.That(t, *d, test.ShouldEqual, Depth(2)) - - pt = r2.Point{0.25, 0.75} - d = NearestNeighborDepth(pt, dm) - test.That(t, *d, test.ShouldEqual, Depth(3)) - d = BilinearInterpolationDepth(pt, dm) // 2.75 - test.That(t, *d, test.ShouldEqual, Depth(3)) - - pt = r2.Point{0.5, 0.5} - d = NearestNeighborDepth(pt, dm) - test.That(t, *d, test.ShouldEqual, Depth(4)) - d = BilinearInterpolationDepth(pt, dm) // 2.5 - test.That(t, *d, test.ShouldEqual, Depth(3)) - - pt = r2.Point{1.0, 1.0} - d = NearestNeighborDepth(pt, dm) - test.That(t, *d, test.ShouldEqual, Depth(4)) - d = BilinearInterpolationDepth(pt, dm) // 4 - test.That(t, *d, test.ShouldEqual, Depth(4)) - - pt = r2.Point{1.1, 1.0} - d = NearestNeighborDepth(pt, dm) - test.That(t, d, test.ShouldBeNil) - d = BilinearInterpolationDepth(pt, dm) - test.That(t, d, test.ShouldBeNil) -} diff --git a/rimage/depthadapter/depth_map_pc_test.go b/rimage/depthadapter/depth_map_pc_test.go deleted file mode 100644 index 3e957dfbf95..00000000000 --- a/rimage/depthadapter/depth_map_pc_test.go +++ /dev/null @@ -1,100 +0,0 @@ -//go:build !no_cgo - -package depthadapter_test - -import ( - "context" - "sync" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/depthadapter" - "go.viam.com/rdk/rimage/transform" -) - -// Intrinsics for the Intel 515 used to capture rimage/board2.dat.gz. -func genIntrinsics() *transform.PinholeCameraIntrinsics { - return &transform.PinholeCameraIntrinsics{ - Width: 1024, - Height: 768, - Fx: 821.32642889, - Fy: 821.68607359, - Ppx: 494.95941428, - Ppy: 370.70529534, - } -} - -func TestDMPointCloudAdapter(t *testing.T) { - m, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - - adapter := depthadapter.ToPointCloud(m, genIntrinsics()) - test.That(t, adapter, test.ShouldNotBeNil) - test.That(t, adapter.Size(), test.ShouldEqual, 812049) - - // Test Uncached Iterate - var xTotalUncached, yTotalUncached, zTotalUncached float64 - adapter.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - xTotalUncached += p.X - yTotalUncached += p.Y - zTotalUncached += p.Z - return true - }) - - // Test Cached Iterate - var xTotalCached, yTotalCached, zToatlCached float64 - adapter.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - xTotalCached += p.X - yTotalCached += p.Y - zToatlCached += p.Z - return true - }) - test.That(t, xTotalCached, test.ShouldAlmostEqual, xTotalUncached) - test.That(t, yTotalCached, test.ShouldAlmostEqual, yTotalUncached) - test.That(t, zToatlCached, test.ShouldAlmostEqual, zTotalUncached) -} - -func TestDMPointCloudAdapterRace(t *testing.T) { - m, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - - baseAdapter := depthadapter.ToPointCloud(m, genIntrinsics()) - raceAdapter := depthadapter.ToPointCloud(m, genIntrinsics()) - - var wg sync.WaitGroup - wg.Add(2) - - utils.PanicCapturingGo(func() { - meta := raceAdapter.MetaData() - test.That(t, meta, test.ShouldNotBeNil) - defer wg.Done() - }) - utils.PanicCapturingGo(func() { - defer wg.Done() - raceAdapter.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - return true - }) - }) - - wg.Wait() - test.That(t, raceAdapter.MetaData(), test.ShouldNotBeNil) - - // Make sure that all points in the base adapter are in the race adapter - baseAdapter.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - _, isin := raceAdapter.At(p.X, p.Y, p.Z) - test.That(t, isin, test.ShouldBeTrue) - return true - }) - // And Vice Versa - raceAdapter.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - _, isin := baseAdapter.At(p.X, p.Y, p.Z) - test.That(t, isin, test.ShouldBeTrue) - return true - }) -} diff --git a/rimage/filters_test.go b/rimage/filters_test.go deleted file mode 100644 index 331dcb92e53..00000000000 --- a/rimage/filters_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package rimage - -import ( - "testing" - - "go.viam.com/test" -) - -func TestSavitskyGolay(t *testing.T) { - expected := [][]float64{ - {-0.0742857142857142, 0.01142857142857133, 0.040000000000000036, 0.01142857142857142, -0.0742857142857143}, - {0.011428571428571434, 0.09714285714285709, 0.1257142857142857, 0.09714285714285716, 0.011428571428571496}, - {0.040000000000000015, 0.12571428571428572, 0.1542857142857143, 0.12571428571428575, 0.04000000000000007}, - {0.011428571428571434, 0.09714285714285714, 0.1257142857142857, 0.09714285714285714, 0.011428571428571439}, - {-0.0742857142857143, 0.01142857142857143, 0.03999999999999997, 0.011428571428571392, -0.07428571428571429}, - } - got, err := savitskyGolayKernel(2, 2) - test.That(t, err, test.ShouldBeNil) - for i := 0; i < 5; i++ { - for j := 0; j < 5; j++ { - test.That(t, expected[i][j], test.ShouldAlmostEqual, got[i][j]) - } - } -} diff --git a/rimage/gradient_test.go b/rimage/gradient_test.go deleted file mode 100644 index e36f04e558e..00000000000 --- a/rimage/gradient_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package rimage - -import ( - "context" - "image" - "image/png" - "math" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" -) - -func writePicture(img image.Image, p string) error { - file, err := os.Create(p) - if err != nil { - return err - } - defer file.Close() - png.Encode(file, img) - return nil -} - -func TestVectorFieldToDenseAndBack(t *testing.T) { - width, height := 200, 100 - vf := MakeEmptyVectorField2D(width, height) - for x := 0; x < width; x++ { - for y := 0; y < height; y++ { - mag, dir := getMagnitudeAndDirection(float64(x), float64(y)) - vf.Set(x, y, Vec2D{mag, dir}) - } - } - // turn into mat.Dense - magMat := vf.MagnitudeField() - dirMat := vf.DirectionField() - for x := 0; x < width; x++ { - for y := 0; y < height; y++ { - p := image.Point{x, y} - test.That(t, magMat.At(y, x), test.ShouldEqual, vf.Get(p).Magnitude()) - test.That(t, dirMat.At(y, x), test.ShouldEqual, vf.Get(p).Direction()) - } - } - // turn back into VectorField2D - vf2, err := VectorField2DFromDense(magMat, dirMat) - test.That(t, err, test.ShouldBeNil) - test.That(t, vf2, test.ShouldResemble, &vf) -} - -func TestSobelFilter(t *testing.T) { - outDir := t.TempDir() - logging.NewTestLogger(t).Debugf("out dir: %q", outDir) - // circle.png is 300x200 canvas, circle is 150 pixels in diameter, centered at (150,100) - dm, err := NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/circle.png")) - test.That(t, err, test.ShouldBeNil) - - gradients := SobelDepthGradient(dm) - test.That(t, gradients.Height(), test.ShouldEqual, dm.Height()) - test.That(t, gradients.Width(), test.ShouldEqual, dm.Width()) - img := gradients.DirectionPicture() - err = writePicture(img, outDir+"/circle_gradient.png") - test.That(t, err, test.ShouldBeNil) - // reminder: left-handed coordinate system. +x is right, +y is down. - // (223,100) is right edge of circle - test.That(t, radZeroTo2Pi(gradients.GetVec2D(223, 100).Direction()), test.ShouldEqual, math.Pi) - // (150,173) is bottom edge of circle - test.That(t, radZeroTo2Pi(gradients.GetVec2D(150, 173).Direction()), test.ShouldEqual, 3.*math.Pi/2.) - // (76,100) is left edge of circle - test.That(t, radZeroTo2Pi(gradients.GetVec2D(76, 100).Direction()), test.ShouldEqual, 0) - // (150,27) is top edge of circle - test.That(t, radZeroTo2Pi(gradients.GetVec2D(150, 27).Direction()), test.ShouldEqual, math.Pi/2.) -} - -func BenchmarkSobelFilter(b *testing.B) { - dm, err := NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/shelf_grayscale.png")) - test.That(b, err, test.ShouldBeNil) - for i := 0; i < b.N; i++ { - _ = SobelDepthGradient(dm) - } -} diff --git a/rimage/gray_image_helpers_test.go b/rimage/gray_image_helpers_test.go deleted file mode 100644 index 5df4371c256..00000000000 --- a/rimage/gray_image_helpers_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package rimage - -import ( - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" -) - -func TestGrayAndAvg(t *testing.T) { - t.Parallel() - im, _ := NewImageFromFile(artifact.MustPath("calibrate/chess3.jpeg")) - im2, _ := MultiplyGrays(MakeGray(im), MakeGray(im)) - got := GetGrayAvg(im2) - test.That(t, got, test.ShouldEqual, 12122) - - im, _ = NewImageFromFile(artifact.MustPath("calibrate/chess2.jpeg")) - im2, _ = MultiplyGrays(MakeGray(im), MakeGray(im)) - got = GetGrayAvg(im2) - test.That(t, got, test.ShouldEqual, 11744) - - im, _ = NewImageFromFile(artifact.MustPath("calibrate/chess1.jpeg")) - im2, _ = MultiplyGrays(MakeGray(im), MakeGray(im)) - got = GetGrayAvg(im2) - test.That(t, got, test.ShouldEqual, 11839) -} diff --git a/rimage/image_file_test.go b/rimage/image_file_test.go deleted file mode 100644 index b73220803b9..00000000000 --- a/rimage/image_file_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package rimage - -import ( - "bytes" - "context" - "image" - "image/png" - "os" - "testing" - - libjpeg "github.com/viam-labs/go-libjpeg/jpeg" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/utils" -) - -func TestPngEncodings(t *testing.T) { - t.Parallel() - openBytes, err := os.ReadFile(artifact.MustPath("rimage/opencv_encoded_image.png")) - test.That(t, err, test.ShouldBeNil) - openGray16, err := png.Decode(bytes.NewReader(openBytes)) - test.That(t, err, test.ShouldBeNil) - goBytes, err := os.ReadFile(artifact.MustPath("rimage/go_encoded_image.png")) - test.That(t, err, test.ShouldBeNil) - goGray16, err := png.Decode(bytes.NewReader(goBytes)) - test.That(t, err, test.ShouldBeNil) - test.That(t, openBytes, test.ShouldNotResemble, goBytes) // go and openCV encode PNGs differently - test.That(t, openGray16, test.ShouldResemble, goGray16) // but they decode to the same image - var goEncodedOpenCVBytes bytes.Buffer - err = png.Encode(&goEncodedOpenCVBytes, openGray16) - test.That(t, err, test.ShouldBeNil) - test.That(t, goEncodedOpenCVBytes.Bytes(), test.ShouldResemble, goBytes) -} - -func TestDecodeImage(t *testing.T) { - img := image.NewNRGBA(image.Rect(0, 0, 4, 8)) - img.Set(3, 3, Red) - - var buf bytes.Buffer - test.That(t, png.Encode(&buf, img), test.ShouldBeNil) - - t.Run("lazy", func(t *testing.T) { - decoded, err := DecodeImage(context.Background(), buf.Bytes(), utils.WithLazyMIMEType(utils.MimeTypePNG)) - test.That(t, err, test.ShouldBeNil) - test.That(t, decoded, test.ShouldHaveSameTypeAs, &LazyEncodedImage{}) - decodedLazy := decoded.(*LazyEncodedImage) - test.That(t, decodedLazy.RawData(), test.ShouldResemble, buf.Bytes()) - test.That(t, decodedLazy.Bounds(), test.ShouldResemble, img.Bounds()) - }) -} - -func TestEncodeImage(t *testing.T) { - img := image.NewNRGBA(image.Rect(0, 0, 4, 8)) - img.Set(3, 3, Red) - - var buf bytes.Buffer - test.That(t, png.Encode(&buf, img), test.ShouldBeNil) - - var bufJPEG bytes.Buffer - test.That(t, libjpeg.Encode(&bufJPEG, img, jpegEncoderOptions), test.ShouldBeNil) - - t.Run("lazy", func(t *testing.T) { - // fast - lazyImg := NewLazyEncodedImage(buf.Bytes(), "hehe") - encoded, err := EncodeImage(context.Background(), lazyImg, "hehe") - test.That(t, err, test.ShouldBeNil) - test.That(t, encoded, test.ShouldResemble, buf.Bytes()) - - lazyImg = NewLazyEncodedImage(buf.Bytes(), "hehe") - encoded, err = EncodeImage(context.Background(), lazyImg, utils.WithLazyMIMEType("hehe")) - test.That(t, err, test.ShouldBeNil) - test.That(t, encoded, test.ShouldResemble, buf.Bytes()) - }) - t.Run("jpeg from png", func(t *testing.T) { - lazyImg := NewLazyEncodedImage(buf.Bytes(), utils.MimeTypePNG) - encoded, err := EncodeImage(context.Background(), lazyImg, utils.MimeTypeJPEG) - test.That(t, err, test.ShouldBeNil) - test.That(t, encoded, test.ShouldResemble, bufJPEG.Bytes()) - }) -} - -func TestRawRGBAEncodingDecoding(t *testing.T) { - img := image.NewNRGBA(image.Rect(0, 0, 4, 8)) - img.Set(3, 3, Red) - - encodedImgBytes, err := EncodeImage(context.Background(), img, utils.MimeTypeRawRGBA) - test.That(t, err, test.ShouldBeNil) - reader := bytes.NewReader(encodedImgBytes) - - conf, header, err := image.DecodeConfig(reader) - test.That(t, err, test.ShouldBeNil) - test.That(t, header, test.ShouldEqual, "vnd.viam.rgba") - test.That(t, conf.Width, test.ShouldEqual, img.Bounds().Dx()) - test.That(t, conf.Height, test.ShouldEqual, img.Bounds().Dy()) - - // decode with image package - reader = bytes.NewReader(encodedImgBytes) - imgDecoded, header, err := image.Decode(reader) - test.That(t, err, test.ShouldBeNil) - test.That(t, header, test.ShouldEqual, "vnd.viam.rgba") - test.That(t, imgDecoded.Bounds(), test.ShouldResemble, img.Bounds()) - test.That(t, imgDecoded.At(3, 6), test.ShouldResemble, img.At(3, 6)) - test.That(t, imgDecoded.At(1, 3), test.ShouldResemble, img.At(1, 3)) - - decodedImg, err := DecodeImage(context.Background(), encodedImgBytes, utils.MimeTypeRawRGBA) - test.That(t, err, test.ShouldBeNil) - test.That(t, decodedImg.Bounds(), test.ShouldResemble, img.Bounds()) - imgR, imgG, imgB, imgA := img.At(3, 3).RGBA() - decodedImgR, decodedImgG, decodedImgB, decodedImgA := decodedImg.At(3, 3).RGBA() - test.That(t, imgR, test.ShouldResemble, decodedImgR) - test.That(t, imgG, test.ShouldResemble, decodedImgG) - test.That(t, imgB, test.ShouldResemble, decodedImgB) - test.That(t, imgA, test.ShouldResemble, decodedImgA) -} - -func TestRawDepthEncodingDecoding(t *testing.T) { - img := NewEmptyDepthMap(4, 8) - for x := 0; x < 4; x++ { - for y := 0; y < 8; y++ { - img.Set(x, y, Depth(x*y)) - } - } - encodedImgBytes, err := EncodeImage(context.Background(), img, utils.MimeTypeRawDepth) - test.That(t, err, test.ShouldBeNil) - reader := bytes.NewReader(encodedImgBytes) - - // decode Header - conf, header, err := image.DecodeConfig(reader) - test.That(t, err, test.ShouldBeNil) - test.That(t, header, test.ShouldEqual, "vnd.viam.dep") - test.That(t, conf.Width, test.ShouldEqual, img.Bounds().Dx()) - test.That(t, conf.Height, test.ShouldEqual, img.Bounds().Dy()) - - // decode with image package - reader = bytes.NewReader(encodedImgBytes) - imgDecoded, header, err := image.Decode(reader) - test.That(t, err, test.ShouldBeNil) - test.That(t, header, test.ShouldEqual, "vnd.viam.dep") - test.That(t, imgDecoded.Bounds(), test.ShouldResemble, img.Bounds()) - test.That(t, imgDecoded.At(2, 3), test.ShouldResemble, img.At(2, 3)) - test.That(t, imgDecoded.At(1, 0), test.ShouldResemble, img.At(1, 0)) - - // decode with rimage package - decodedImg, err := DecodeImage(context.Background(), encodedImgBytes, utils.MimeTypeRawDepth) - test.That(t, err, test.ShouldBeNil) - test.That(t, decodedImg.Bounds(), test.ShouldResemble, img.Bounds()) - decodedDm, ok := decodedImg.(*DepthMap) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, decodedDm.GetDepth(2, 3), test.ShouldEqual, img.GetDepth(2, 3)) - test.That(t, decodedDm.GetDepth(1, 0), test.ShouldEqual, img.GetDepth(1, 0)) -} diff --git a/rimage/image_processing_test.go b/rimage/image_processing_test.go deleted file mode 100644 index 4740223c63c..00000000000 --- a/rimage/image_processing_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package rimage - -import ( - "fmt" - "image" - "math" - "testing" - - "github.com/golang/geo/r2" - "github.com/lucasb-eyer/go-colorful" - "go.viam.com/test" - "go.viam.com/utils/artifact" -) - -func TestCanny1(t *testing.T) { - t.Parallel() - doCannyTest(t, "canny1") -} - -func TestCanny2(t *testing.T) { - t.Parallel() - doCannyTest(t, "canny2") -} - -func doCannyTest(t *testing.T, root string) { - t.Helper() - img, err := NewImageFromFile(artifact.MustPath(fmt.Sprintf("rimage/%s.png", root))) - test.That(t, err, test.ShouldBeNil) - - goodOnes := 0 - - for a := 0.01; a <= .05; a += .005 { - outfn := fmt.Sprintf(t.TempDir()+"/%s-%v.png", root, int(1000*a)) - t.Log(outfn) - - out, err := SimpleEdgeDetection(img, a, 3.0) - test.That(t, err, test.ShouldBeNil) - - err = WriteImageToFile(outfn, out) - test.That(t, err, test.ShouldBeNil) - - bad := false - - for x := 50; x <= 750; x += 100 { - for y := 50; y <= 750; y += 100 { - spots := CountBrightSpots(out, image.Point{x, y}, 25, 255) - - if y < 200 || y > 600 { - if spots < 100 { - bad = true - t.Logf("\t%v,%v %v\n", x, y, spots) - } - } else { - if spots > 90 { - bad = true - t.Logf("\t%v,%v %v\n", x, y, spots) - } - } - - if bad { - break - } - } - - if bad { - break - } - } - - if bad { - continue - } - - goodOnes++ - - break - } - - test.That(t, goodOnes, test.ShouldNotEqual, 0) -} - -func TestCloneImage(t *testing.T) { - t.Parallel() - img, err := readImageFromFile(artifact.MustPath("rimage/canny1.png")) - test.That(t, err, test.ShouldBeNil) - - // Image path - i := ConvertImage(img) - ii := CloneImage(i) - for y := 0; y < ii.Height(); y++ { - for x := 0; x < ii.Width(); x++ { - test.That(t, ii.GetXY(x, y), test.ShouldResemble, i.GetXY(x, y)) - } - } - ii.SetXY(0, 0, Red) - test.That(t, ii.GetXY(0, 0), test.ShouldNotResemble, i.GetXY(0, 0)) - - // ImageWithDepth path - j := convertToImageWithDepth(img) - ii = CloneImage(j) - for y := 0; y < ii.Height(); y++ { - for x := 0; x < ii.Width(); x++ { - test.That(t, ii.GetXY(x, y), test.ShouldResemble, i.GetXY(x, y)) - } - } - ii.SetXY(0, 0, Red) - test.That(t, ii.GetXY(0, 0), test.ShouldNotResemble, i.GetXY(0, 0)) -} - -func BenchmarkConvertImage(b *testing.B) { - img, err := readImageFromFile(artifact.MustPath("rimage/canny1.png")) - test.That(b, err, test.ShouldBeNil) - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - ConvertImage(img) - } -} - -func TestConvertYCbCr(t *testing.T) { - orig, err := readImageFromFile(artifact.MustPath("rimage/canny1.png")) - test.That(t, err, test.ShouldBeNil) - - var yuvImg image.YCbCr - ImageToYCbCrForTesting(&yuvImg, orig) - - err = WriteImageToFile(t.TempDir()+"/canny1-ycbcr.png", &yuvImg) - test.That(t, err, test.ShouldBeNil) - - c1, b1 := colorful.MakeColor(orig.At(100, 100)) - c2, b2 := colorful.MakeColor(yuvImg.At(100, 100)) - test.That(t, b1 || b2, test.ShouldBeTrue) - - test.That(t, c2.Hex(), test.ShouldEqual, c1.Hex()) -} - -func BenchmarkConvertImageYCbCr(b *testing.B) { - orig, err := readImageFromFile(artifact.MustPath("rimage/canny1.png")) - test.That(b, err, test.ShouldBeNil) - - var yuvImg image.YCbCr - ImageToYCbCrForTesting(&yuvImg, orig) - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - ConvertImage(&yuvImg) - } -} - -func TestColorInterpolation(t *testing.T) { - img := NewImage(2, 2) - img.SetXY(0, 0, NewColorFromHSV(30, 0, 0)) - img.SetXY(1, 0, NewColorFromHSV(40, .5, .5)) - img.SetXY(0, 1, NewColorFromHSV(50, .5, .5)) - img.SetXY(1, 1, NewColorFromHSV(60, 1.0, 1.0)) - - pt := r2.Point{0.5, 0.5} - c := BilinearInterpolationColor(pt, img) - h, s, v := c.HsvNormal() - test.That(t, math.Abs(h-45), test.ShouldBeLessThan, .01) - test.That(t, math.Abs(s-0.5), test.ShouldBeLessThan, .01) - test.That(t, math.Abs(v-0.5), test.ShouldBeLessThan, .01) - c = NearestNeighborColor(pt, img) - test.That(t, *c, test.ShouldResemble, NewColorFromHSV(60, 1.0, 1.0)) - - pt = r2.Point{0.75, 0.25} - c = BilinearInterpolationColor(pt, img) - h, s, v = c.HsvNormal() - test.That(t, math.Abs(h-42.46), test.ShouldBeLessThan, .01) - test.That(t, math.Abs(s-0.5), test.ShouldBeLessThan, .01) - test.That(t, math.Abs(v-0.5), test.ShouldBeLessThan, .01) - c = NearestNeighborColor(pt, img) - test.That(t, *c, test.ShouldResemble, NewColorFromHSV(40, 0.5, 0.5)) - - pt = r2.Point{1.0, 1.0} - c = BilinearInterpolationColor(pt, img) - h, s, v = c.HsvNormal() - test.That(t, math.Abs(h-60), test.ShouldBeLessThan, .01) - test.That(t, math.Abs(s-1.), test.ShouldBeLessThan, .01) - test.That(t, math.Abs(v-1.), test.ShouldBeLessThan, .01) - c = NearestNeighborColor(pt, img) - test.That(t, *c, test.ShouldResemble, NewColorFromHSV(60, 1.0, 1.0)) - - pt = r2.Point{1.1, 1.0} - c = BilinearInterpolationColor(pt, img) - test.That(t, c, test.ShouldBeNil) - c = NearestNeighborColor(pt, img) - test.That(t, c, test.ShouldBeNil) -} - -func TestCanny(t *testing.T) { - imgOriginal, err := readImageFromFile(artifact.MustPath("rimage/canny_test_1.jpg")) - test.That(t, err, test.ShouldBeNil) - img := ConvertImage(imgOriginal) - test.That(t, err, test.ShouldBeNil) - - gtOriginal, err := readImageFromFile(artifact.MustPath("rimage/test_canny.png")) - gt := ConvertImage(gtOriginal) - test.That(t, err, test.ShouldBeNil) - - cannyDetector := NewCannyDericheEdgeDetector() - edgesMat, _ := cannyDetector.DetectEdges(img, 0.5) - edges := ConvertImage(edgesMat) - test.That(t, len(gt.data), test.ShouldEqual, len(edges.data)) - test.That(t, gt.data, test.ShouldResemble, edges.data) -} - -func TestCannyBlocks(t *testing.T) { - // load test image and GT - imgOriginal, err := readImageFromFile(artifact.MustPath("rimage/edge_test_image.png")) - test.That(t, err, test.ShouldBeNil) - img := ConvertImage(imgOriginal) - test.That(t, err, test.ShouldBeNil) - gtGradient, err := readImageFromFile(artifact.MustPath("rimage/edge_test_gradient.png")) - gtGrad := ConvertImage(gtGradient) - test.That(t, err, test.ShouldBeNil) - gtNonMaxSup, err := readImageFromFile(artifact.MustPath("rimage/edge_test_nms.png")) - gtNms := ConvertImage(gtNonMaxSup) - test.That(t, err, test.ShouldBeNil) - // Compute forward gradient - imgGradient, _ := ForwardGradient(img, 0., false) - magData := imgGradient.Magnitude.RawMatrix().Data - magDataInt := make([]Color, len(magData)) - for idx := 0; idx < len(magData); idx++ { - magDataInt[idx] = NewColor(uint8(math.Round(magData[idx])), uint8(math.Round(magData[idx])), uint8(math.Round(magData[idx]))) - } - magOut := Image{ - data: magDataInt, - } - - // NMS - nms, _ := GradientNonMaximumSuppressionC8(imgGradient.Magnitude, imgGradient.Direction) - nmsData := nms.RawMatrix().Data - nmsDataInt := make([]Color, len(nmsData)) - for idx := 0; idx < len(magData); idx++ { - nmsDataInt[idx] = NewColor(uint8(math.Round(nmsData[idx])), uint8(math.Round(nmsData[idx])), uint8(math.Round(nmsData[idx]))) - } - nmsOut := Image{ - data: nmsDataInt, - } - - // run tests - tests := []struct { - testName string - dataOut, dataGT []Color - }{ - {"gradient", magOut.data, gtGrad.data}, - {"nms", nmsOut.data, gtNms.data}, - } - for _, tt := range tests { - t.Run(tt.testName, func(t *testing.T) { - test.That(t, len(tt.dataOut), test.ShouldEqual, len(tt.dataGT)) - test.That(t, tt.dataOut, test.ShouldResemble, tt.dataGT) - }) - } -} diff --git a/rimage/image_with_depth_test.go b/rimage/image_with_depth_test.go deleted file mode 100644 index d709b9c2ffd..00000000000 --- a/rimage/image_with_depth_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package rimage - -import ( - "context" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" -) - -func TestCloneImageWithDepth(t *testing.T) { - t.Parallel() - iwd, err := newImageWithDepth( - context.Background(), - artifact.MustPath("rimage/board1.png"), - artifact.MustPath("rimage/board1.dat.gz"), - true, - ) - test.That(t, err, test.ShouldBeNil) - - ii := cloneToImageWithDepth(iwd) - for y := 0; y < ii.Height(); y++ { - for x := 0; x < ii.Width(); x++ { - test.That(t, ii.Depth.GetDepth(x, y), test.ShouldResemble, iwd.Depth.GetDepth(x, y)) - test.That(t, ii.Color.GetXY(x, y), test.ShouldResemble, iwd.Color.GetXY(x, y)) - } - } - test.That(t, ii.IsAligned(), test.ShouldEqual, iwd.IsAligned()) -} - -func TestImageToDepthMap(t *testing.T) { - t.Parallel() - iwd, err := newImageWithDepth(context.Background(), - artifact.MustPath("rimage/board2.png"), artifact.MustPath("rimage/board2.dat.gz"), false) - test.That(t, err, test.ShouldBeNil) - // convert to gray16 image - depthImage := iwd.Depth.ToGray16Picture() - // convert back - dmFromImage, err := ConvertImageToDepthMap(context.Background(), depthImage) - test.That(t, err, test.ShouldBeNil) - // tests - test.That(t, iwd.Depth.Height(), test.ShouldEqual, dmFromImage.Height()) - test.That(t, iwd.Depth.Width(), test.ShouldEqual, dmFromImage.Width()) - test.That(t, iwd.Depth, test.ShouldResemble, dmFromImage) -} - -func TestConvertToDepthMap(t *testing.T) { - t.Parallel() - iwd, err := newImageWithDepth(context.Background(), - artifact.MustPath("rimage/board2.png"), artifact.MustPath("rimage/board2.dat.gz"), false) - test.That(t, err, test.ShouldBeNil) - // convert to gray16 image - depthImage := iwd.Depth.ToGray16Picture() - - // case 1 - dm1, err := ConvertImageToDepthMap(context.Background(), iwd) - test.That(t, err, test.ShouldBeNil) - test.That(t, iwd.Depth, test.ShouldEqual, dm1) - // case 2 - dm2, err := ConvertImageToDepthMap(context.Background(), depthImage) - test.That(t, err, test.ShouldBeNil) - test.That(t, iwd.Depth, test.ShouldResemble, dm2) - // default - should return error - badType := iwd.Color - _, err = ConvertImageToDepthMap(context.Background(), badType) - test.That(t, err, test.ShouldNotBeNil) -} diff --git a/rimage/lazy_encoded_test.go b/rimage/lazy_encoded_test.go deleted file mode 100644 index 0727e0e2a00..00000000000 --- a/rimage/lazy_encoded_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package rimage - -import ( - "bytes" - "image" - "image/png" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/utils" -) - -func TestLazyEncodedImage(t *testing.T) { - img := image.NewNRGBA(image.Rect(0, 0, 4, 8)) - img.Set(3, 3, Red) - - var buf bytes.Buffer - test.That(t, png.Encode(&buf, img), test.ShouldBeNil) - - imgLazy := NewLazyEncodedImage(buf.Bytes(), utils.MimeTypePNG) - - test.That(t, imgLazy.(*LazyEncodedImage).MIMEType(), test.ShouldEqual, utils.MimeTypePNG) - test.That(t, NewColorFromColor(imgLazy.At(0, 0)), test.ShouldEqual, Black) - test.That(t, NewColorFromColor(imgLazy.At(3, 3)), test.ShouldEqual, Red) - test.That(t, imgLazy.Bounds(), test.ShouldResemble, img.Bounds()) - test.That(t, imgLazy.ColorModel(), test.ShouldResemble, img.ColorModel()) - - img2, err := png.Decode(bytes.NewBuffer(imgLazy.(*LazyEncodedImage).RawData())) - test.That(t, err, test.ShouldBeNil) - test.That(t, img2, test.ShouldResemble, img) - - // a bad image though :( - imgLazy = NewLazyEncodedImage([]byte{1, 2, 3}, utils.MimeTypePNG) - - test.That(t, imgLazy.(*LazyEncodedImage).MIMEType(), test.ShouldEqual, utils.MimeTypePNG) - test.That(t, func() { imgLazy.Bounds() }, test.ShouldPanic) - test.That(t, func() { imgLazy.ColorModel() }, test.ShouldPanicWith, image.ErrFormat) - test.That(t, func() { NewColorFromColor(imgLazy.At(0, 0)) }, test.ShouldPanicWith, image.ErrFormat) - test.That(t, func() { NewColorFromColor(imgLazy.At(4, 4)) }, test.ShouldPanicWith, image.ErrFormat) - - imgLazy = NewLazyEncodedImage([]byte{1, 2, 3}, "weeeee") - - test.That(t, imgLazy.(*LazyEncodedImage).MIMEType(), test.ShouldEqual, "weeeee") - test.That(t, func() { imgLazy.Bounds() }, test.ShouldPanic) - test.That(t, func() { imgLazy.ColorModel() }, test.ShouldPanic) - test.That(t, func() { NewColorFromColor(imgLazy.At(0, 0)) }, test.ShouldPanic) - test.That(t, func() { NewColorFromColor(imgLazy.At(4, 4)) }, test.ShouldPanic) -} diff --git a/rimage/point_utils_test.go b/rimage/point_utils_test.go deleted file mode 100644 index 5ffea6a3485..00000000000 --- a/rimage/point_utils_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package rimage - -import ( - "image" - "math" - "testing" - - "github.com/golang/geo/r2" - "go.viam.com/test" -) - -func TestPointDistance(t *testing.T) { - test.That(t, PointDistance(image.Point{0, 3}, image.Point{4, 0}), test.ShouldEqual, 5.0) -} - -func TestPointCenter(t *testing.T) { - all := []image.Point{ - {0, 0}, - {2, 0}, - {0, 2}, - {2, 2}, - } - - test.That(t, Center(all, 1000), test.ShouldResemble, image.Point{1, 1}) - - all = append(all, image.Point{100, 100}) - - test.That(t, Center(all, 1000), test.ShouldResemble, image.Point{50, 50}) - test.That(t, Center(all, 48), test.ShouldResemble, image.Point{1, 1}) -} - -func TestPointAngle(t *testing.T) { - test.That(t, PointAngle(image.Point{0, 0}, image.Point{1, 0}), test.ShouldEqual, 0.0) - test.That(t, PointAngle(image.Point{0, 0}, image.Point{1, 1}), test.ShouldEqual, math.Pi/4) - test.That(t, PointAngle(image.Point{0, 0}, image.Point{-1, -1}), test.ShouldEqual, math.Pi/4-math.Pi) -} - -func TestPointBoundingBox(t *testing.T) { - r := BoundingBox([]image.Point{ - {100, 100}, - {200, 200}, - {50, 50}, - {1000, 1000}, - {1, 1}, - }) - - test.That(t, r.Min, test.ShouldResemble, image.Point{1, 1}) - test.That(t, r.Max, test.ShouldResemble, image.Point{1000, 1000}) -} - -func TestR2ToImage(t *testing.T) { - imagePoint := image.Point{2, 3} - - test.That(t, R2PointToImagePoint(r2.Point{2.36, 3.004}), test.ShouldResemble, imagePoint) - test.That(t, R2PointToImagePoint(r2.Point{2.5, 3.5}), test.ShouldNotEqual, imagePoint) - - imageRect := image.Rect(-2, 1, 5, 8) - r2Rect := r2.RectFromPoints(r2.Point{-2.1, 1.44}, r2.Point{5.33, 8.49}) - - test.That(t, R2RectToImageRect(r2Rect), test.ShouldResemble, imageRect) - - r2Rect2 := r2.RectFromPoints(r2.Point{-2.5, 1.5}, r2.Point{5.0, 8.49}) - - test.That(t, R2RectToImageRect(r2Rect2), test.ShouldNotEqual, imageRect) - test.That(t, R2RectToImageRect(r2Rect2), test.ShouldResemble, image.Rect(-3, 2, 5, 8)) - - resultImageRect := imageRect.Add(imagePoint) - resultR2Rect := TranslateR2Rect(r2Rect, r2.Point{2., 3.}) - - test.That(t, R2RectToImageRect(resultR2Rect), test.ShouldResemble, resultImageRect) -} - -func TestSliceVecsToXsYs(t *testing.T) { - pts := []r2.Point{ - {0, 0}, - {1, 2}, - {3, 4}, - } - xs, ys := SliceVecsToXsYs(pts) - test.That(t, xs[0], test.ShouldEqual, 0) - test.That(t, xs[1], test.ShouldEqual, 1) - test.That(t, xs[2], test.ShouldEqual, 3) - test.That(t, ys[0], test.ShouldEqual, 0) - test.That(t, ys[1], test.ShouldEqual, 2) - test.That(t, ys[2], test.ShouldEqual, 4) -} - -func TestAreCollinear(t *testing.T) { - a := r2.Point{0, 0} - b := r2.Point{1, 1} - c := r2.Point{2, 2} - d := r2.Point{0, 2} - - areColl := AreCollinear(a, b, c, 0.1) - test.That(t, areColl, test.ShouldBeTrue) - areColl = AreCollinear(a, b, d, 0.1) - test.That(t, areColl, test.ShouldBeFalse) -} diff --git a/rimage/preprocessing_test.go b/rimage/preprocessing_test.go deleted file mode 100644 index e1802938313..00000000000 --- a/rimage/preprocessing_test.go +++ /dev/null @@ -1,203 +0,0 @@ -package rimage - -import ( - "context" - "image" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" -) - -// Smoothing with Morphological filters. -type smoothTestHelper struct{} - -func (h *smoothTestHelper) Process( - t *testing.T, pCtx *ProcessorContext, fn string, img, img2 image.Image, logger logging.Logger, -) error { - t.Helper() - var err error - dm, err := ConvertImageToDepthMap(context.Background(), img) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, MaxDepth), "depth") - - // use Opening smoothing - // kernel size 3, 1 iteration - openedDM, err := OpeningMorph(dm, 3, 1) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(openedDM.ToPrettyPicture(0, MaxDepth), "depth-opened") - - // use Closing smoothing - // size 3, 1 iteration - closedDM1, err := ClosingMorph(dm, 3, 1) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(closedDM1.ToPrettyPicture(0, MaxDepth), "depth-closed-3-1") - // size 3, 3 iterations - closedDM2, err := ClosingMorph(dm, 3, 3) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(closedDM2.ToPrettyPicture(0, MaxDepth), "depth-closed-3-3") - // size 5, 1 iteration - closedDM3, err := ClosingMorph(dm, 5, 1) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(closedDM3.ToPrettyPicture(0, MaxDepth), "depth-closed-5-1") - - return nil -} - -func TestSmoothGripper(t *testing.T) { - d := NewMultipleImageTestDebugger(t, "align/gripper1/depth", "*.png", "") - err := d.Process(t, &smoothTestHelper{}) - test.That(t, err, test.ShouldBeNil) -} - -// Canny Edge Detection for depth maps. -type cannyTestHelper struct{} - -func (h *cannyTestHelper) Process( - t *testing.T, pCtx *ProcessorContext, fn string, img, img2 image.Image, logger logging.Logger, -) error { - t.Helper() - var err error - cannyColor := NewCannyDericheEdgeDetector() - cannyDepth := NewCannyDericheEdgeDetectorWithParameters(0.85, 0.33, false) - - colorImg := ConvertImage(img) - depthImg, err := ConvertImageToDepthMap(context.Background(), img2) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(depthImg.ToPrettyPicture(0, MaxDepth), "depth-ii") - - // edges no preprocessing - colEdges, err := cannyColor.DetectEdges(colorImg, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(colEdges, "color-edges-nopreprocess") - - dmEdges, err := cannyDepth.DetectDepthEdges(depthImg, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(dmEdges, "depth-edges-nopreprocess") - - // cleaned - CleanDepthMap(depthImg) - dmCleanedEdges, err := cannyDepth.DetectDepthEdges(depthImg, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(depthImg.ToPrettyPicture(0, 500), "depth-cleaned-near") // near - pCtx.GotDebugImage(depthImg.ToPrettyPicture(500, 4000), "depth-cleaned-middle") // middle - pCtx.GotDebugImage(depthImg.ToPrettyPicture(4000, MaxDepth), "depth-cleaned-far") // far - pCtx.GotDebugImage(dmCleanedEdges, "depth-edges-cleaned") - - // morphological - closedDM, err := ClosingMorph(depthImg, 5, 1) - test.That(t, err, test.ShouldBeNil) - dmClosedEdges, err := cannyDepth.DetectDepthEdges(closedDM, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(closedDM.ToPrettyPicture(0, MaxDepth), "depth-closed-5-1") - pCtx.GotDebugImage(dmClosedEdges, "depth-edges-preprocess-1") - - // color code the distances of the missing data - pCtx.GotDebugImage(drawAverageHoleDepth(closedDM), "hole-depths") - - // filled - morphed := makeImageWithDepth(colorImg, closedDM, true) - morphed.Depth, err = FillDepthMap(morphed.Depth, morphed.Color) - test.That(t, err, test.ShouldBeNil) - closedDM = morphed.Depth - filledEdges, err := cannyDepth.DetectDepthEdges(closedDM, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(closedDM.ToPrettyPicture(0, MaxDepth), "depth-holes-filled") - pCtx.GotDebugImage(filledEdges, "depth-edges-filled") - - // smoothed - smoothDM, err := GaussianSmoothing(closedDM, 1) - test.That(t, err, test.ShouldBeNil) - dmSmoothedEdges, err := cannyDepth.DetectDepthEdges(smoothDM, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(smoothDM.ToPrettyPicture(0, MaxDepth), "depth-smoothed") - pCtx.GotDebugImage(dmSmoothedEdges, "depth-edges-smoothed") - - // bilateral smoothed - bilateralDM, err := JointBilateralSmoothing(closedDM, 1, 500) - test.That(t, err, test.ShouldBeNil) - dmBilateralEdges, err := cannyDepth.DetectDepthEdges(bilateralDM, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(bilateralDM.ToPrettyPicture(0, MaxDepth), "depth-bilateral") - pCtx.GotDebugImage(dmBilateralEdges, "depth-edges-bilateral") - - // savitsky-golay smoothed - validPoints := MissingDepthData(closedDM) - sgDM, err := SavitskyGolaySmoothing(closedDM, validPoints, 3, 3) - test.That(t, err, test.ShouldBeNil) - sgEdges, err := cannyDepth.DetectDepthEdges(sgDM, 0.0) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(sgDM.ToPrettyPicture(0, MaxDepth), "depth-savitskygolay") - pCtx.GotDebugImage(sgEdges, "depth-edges-savitskygolay") - - return nil -} - -func TestDepthPreprocessCanny(t *testing.T) { - d := NewMultipleImageTestDebugger(t, "depthpreprocess/color", "*.png", "depthpreprocess/depth") - err := d.Process(t, &cannyTestHelper{}) - test.That(t, err, test.ShouldBeNil) -} - -// Depth pre-processing pipeline. -type preprocessTestHelper struct{} - -func (h *preprocessTestHelper) Process( - t *testing.T, pCtx *ProcessorContext, fn string, img, img2 image.Image, logger logging.Logger, -) error { - t.Helper() - var err error - colorImg := ConvertImage(img) - depthImg, err := ConvertImageToDepthMap(context.Background(), img2) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(depthImg.ToPrettyPicture(0, MaxDepth), "depth-raw") - pCtx.GotDebugImage(Overlay(colorImg, depthImg), "raw-overlay") - - missingDepth := MissingDepthData(depthImg) - pCtx.GotDebugImage(missingDepth, "depth-raw-missing-data") - - preprocessedImg, err := PreprocessDepthMap(depthImg, colorImg) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(preprocessedImg.ToPrettyPicture(0, MaxDepth), "depth-preprocessed") - - missingPreprocessDepth := MissingDepthData(preprocessedImg) - pCtx.GotDebugImage(missingPreprocessDepth, "depth-preprocessed-missing-data") - - return nil -} - -func TestDepthPreprocess(t *testing.T) { - d := NewMultipleImageTestDebugger(t, "depthpreprocess/color", "*.png", "depthpreprocess/depth") - err := d.Process(t, &preprocessTestHelper{}) - test.That(t, err, test.ShouldBeNil) -} - -// drawAverageHoleDepth is a debugging function to see the depth calculated by averageDepthAroundHole. -func drawAverageHoleDepth(dm *DepthMap) *Image { - red, green, blue := NewColor(255, 0, 0), NewColor(0, 255, 0), NewColor(0, 0, 255) - img := NewImage(dm.Width(), dm.Height()) - validData := MissingDepthData(dm) - missingData := invertGrayImage(validData) - holeMap := segmentBinaryImage(missingData) - for _, seg := range holeMap { - borderPoints := getPointsOnHoleBorder(seg, dm) - avgDepth := averageDepthInSegment(borderPoints, dm) - var c Color - switch { - case avgDepth < 500.0: - c = red - case avgDepth >= 500.0 && avgDepth < 4000.0: - c = green - default: - c = blue - } - for pt := range seg { - img.Set(pt, c) - } - } - return img -} diff --git a/rimage/transform/brown_conrady_test.go b/rimage/transform/brown_conrady_test.go deleted file mode 100644 index fb34c0cfa8c..00000000000 --- a/rimage/transform/brown_conrady_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package transform - -import ( - "testing" - - "go.viam.com/test" -) - -func TestBrownConradyCheckValid(t *testing.T) { - t.Run("nil &BrownConrady{} are invalid", func(t *testing.T) { - var nilBrownConradyPtr *BrownConrady - err := nilBrownConradyPtr.CheckValid() - expected := "BrownConrady shaped distortion_parameters not provided: invalid distortion_parameters" - test.That(t, err.Error(), test.ShouldContainSubstring, expected) - }) - - t.Run("non nil &BrownConrady{} are valid", func(t *testing.T) { - distortionsA := &BrownConrady{} - test.That(t, distortionsA.CheckValid(), test.ShouldBeNil) - }) -} diff --git a/rimage/transform/calibrate_pinhole_extrinsics_test.go b/rimage/transform/calibrate_pinhole_extrinsics_test.go deleted file mode 100644 index 744b31dede1..00000000000 --- a/rimage/transform/calibrate_pinhole_extrinsics_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package transform - -import ( - "encoding/json" - "fmt" - "io" - "os" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -var intel515ParamsPath = utils.ResolveFile("rimage/transform/data/intel515_parameters.json") - -func TestExtrinsicCalibration(t *testing.T) { - logger := logging.NewTestLogger(t) - // get a file with known extrinsic parameters and make expected pose - cam, err := NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - expRotation := cam.ExtrinsicD2C.Orientation().RotationMatrix() - expTranslation := cam.ExtrinsicD2C.Point() - - // get points and intrinsics from test file - jsonFile, err := os.Open(utils.ResolveFile("rimage/transform/data/example_extrinsic_calib.json")) - test.That(t, err, test.ShouldBeNil) - defer jsonFile.Close() - - byteValue, err := io.ReadAll(jsonFile) - test.That(t, err, test.ShouldBeNil) - - extConf := &ExtrinsicCalibrationConfig{} - err = json.Unmarshal(byteValue, extConf) - test.That(t, err, test.ShouldBeNil) - - // create the optimization problem - prob, err := BuildExtrinsicOptProblem(extConf) - test.That(t, err, test.ShouldBeNil) - pose, err := RunPinholeExtrinsicCalibration(prob, logger) - test.That(t, err, test.ShouldBeNil) - translation := pose.Point() - rotation := pose.Orientation() - - // only test to 3 digits for found translation and rotation - test.That(t, fmt.Sprintf("%.3f", translation.X), test.ShouldEqual, fmt.Sprintf("%.3f", expTranslation.X)) - test.That(t, fmt.Sprintf("%.3f", translation.Y), test.ShouldEqual, fmt.Sprintf("%.3f", expTranslation.Y)) - test.That(t, fmt.Sprintf("%.3f", translation.Z), test.ShouldEqual, fmt.Sprintf("%.3f", expTranslation.Z)) - q, expq := rotation.Quaternion(), expRotation.Quaternion() - test.That(t, fmt.Sprintf("%.3f", q.Real), test.ShouldEqual, fmt.Sprintf("%.3f", expq.Real)) - test.That(t, fmt.Sprintf("%.3f", q.Imag), test.ShouldEqual, fmt.Sprintf("%.3f", expq.Imag)) - test.That(t, fmt.Sprintf("%.3f", q.Jmag), test.ShouldEqual, fmt.Sprintf("%.3f", expq.Jmag)) - test.That(t, fmt.Sprintf("%.3f", q.Kmag), test.ShouldEqual, fmt.Sprintf("%.3f", expq.Kmag)) -} diff --git a/rimage/transform/cam_poses_test.go b/rimage/transform/cam_poses_test.go deleted file mode 100644 index b1471db646a..00000000000 --- a/rimage/transform/cam_poses_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package transform - -import ( - "math" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" -) - -func TestGetCorrectCameraPose(t *testing.T) { - logger := logging.NewTestLogger(t) - gt := readJSONGroundTruth(logger) - - pts1 := convert2DSliceToVectorSlice(gt.Pts1) - pts2 := convert2DSliceToVectorSlice(gt.Pts2) - K := convert2DSliceToDense(gt.K) - rows, cols := K.Dims() - test.That(t, rows, test.ShouldEqual, 3) - test.That(t, cols, test.ShouldEqual, 3) - test.That(t, len(pts1), test.ShouldEqual, len(pts2)) - // test pose does not return error - pose, err := EstimateNewPose(pts1, pts2, K) - test.That(t, err, test.ShouldBeNil) - // test dimensions of pose matrix: 3x4 - nRows, nCols := pose.PoseMat.Dims() - test.That(t, nRows, test.ShouldEqual, 3) - test.That(t, nCols, test.ShouldEqual, 4) - // test dimensions of rotation matrix: 3x3 - nRowsR, nColsR := pose.Rotation.Dims() - test.That(t, nRowsR, test.ShouldEqual, 3) - test.That(t, nColsR, test.ShouldEqual, 3) - // test values for 3d translation vector - test.That(t, pose.Translation.At(2, 0), test.ShouldAlmostEqual, -0.9946075890134962) - test.That(t, pose.Translation.At(1, 0), test.ShouldBeLessThan, 0.05) - test.That(t, pose.Translation.At(0, 0), test.ShouldBeLessThan, 0.1) - // test diagonal elements of rotation matrix - test.That(t, math.Abs(pose.Rotation.At(0, 0)), test.ShouldBeBetween, 0.98, 1.0) - test.That(t, math.Abs(pose.Rotation.At(1, 1)), test.ShouldBeBetween, 0.99, 1.0) - test.That(t, math.Abs(pose.Rotation.At(2, 2)), test.ShouldBeBetween, 0.97, 1.0) - - // test Pose function - poseSpatialMath, err := pose.Pose() - test.That(t, err, test.ShouldBeNil) - t1 := poseSpatialMath.Point() - test.That(t, math.Abs(t1.X-pose.Translation.At(0, 0)), test.ShouldBeLessThan, 0.0000001) - test.That(t, math.Abs(t1.Y-pose.Translation.At(1, 0)), test.ShouldBeLessThan, 0.0000001) - test.That(t, math.Abs(t1.Z-pose.Translation.At(2, 0)), test.ShouldBeLessThan, 0.0000001) - rot := poseSpatialMath.Orientation().RotationMatrix() - test.That(t, math.Abs(rot.At(0, 0)-pose.Rotation.At(0, 0)), test.ShouldBeLessThan, 0.0000001) -} diff --git a/rimage/transform/camera_matrix_test.go b/rimage/transform/camera_matrix_test.go deleted file mode 100644 index 26eab425418..00000000000 --- a/rimage/transform/camera_matrix_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package transform - -import ( - "context" - "image" - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" -) - -func TestPC1(t *testing.T) { - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board2.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - - // get camera matrix parameters - cameraMatrices, err := NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - - pcCrop, err := cameraMatrices.RGBDToPointCloud(img, dm, image.Rectangle{image.Point{30, 30}, image.Point{50, 50}}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pcCrop.Size(), test.ShouldEqual, 1) - - // error -- too many rectangles - _, err = cameraMatrices.RGBDToPointCloud(img, dm, image.Rectangle{image.Point{30, 30}, image.Point{50, 50}}, image.Rectangle{}) - test.That(t, err.Error(), test.ShouldContainSubstring, "more than one cropping rectangle") - - pc, err := cameraMatrices.RGBDToPointCloud(img, dm) - test.That(t, err, test.ShouldBeNil) - - file, err := os.OpenFile(t.TempDir()+"/x.pcd", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o755) - test.That(t, err, test.ShouldBeNil) - defer file.Close() - - pointcloud.ToPCD(pc, file, pointcloud.PCDAscii) -} - -func TestCameraMatrixTo3D(t *testing.T) { - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board2.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - - // get and set camera matrix parameters - cameraMatrices, err := NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - - // test To3D - testPoint := image.Point{0, 0} - vec, err := cameraMatrices.ImagePointTo3DPoint(testPoint, dm.Get(testPoint)) - test.That(t, err, test.ShouldBeNil) - test.That(t, vec.Z, test.ShouldEqual, float64(dm.Get(testPoint))) - // out of bounds - panic - testPoint = image.Point{img.Width(), img.Height()} - test.That(t, func() { cameraMatrices.ImagePointTo3DPoint(testPoint, dm.Get(testPoint)) }, test.ShouldPanic) -} diff --git a/rimage/transform/camera_system_test.go b/rimage/transform/camera_system_test.go deleted file mode 100644 index 3b0b56198d8..00000000000 --- a/rimage/transform/camera_system_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package transform - -import ( - "context" - "image" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/rimage" -) - -func TestParallelProjection(t *testing.T) { - pp := ParallelProjection{} - - // load the images - img, err := rimage.NewImageFromFile(artifact.MustPath("rimage/board2.png")) - test.That(t, err, test.ShouldBeNil) - img2, err := rimage.NewImageFromFile(artifact.MustPath("rimage/circle.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - // no image error - _, err = pp.RGBDToPointCloud(nil, dm) - test.That(t, err, test.ShouldBeError, errors.New("no rgb image to project to pointcloud")) - // no depth error - _, err = pp.RGBDToPointCloud(img, nil) - test.That(t, err, test.ShouldBeError, errors.New("no depth map to project to pointcloud")) - // not the same size - _, err = pp.RGBDToPointCloud(img2, dm) - test.That(t, err.Error(), test.ShouldContainSubstring, "rgb image and depth map are not the same size") - - // parallel projection - vec3d, err := pp.ImagePointTo3DPoint(image.Point{4, 5}, 10) - test.That(t, err, test.ShouldBeNil) - test.That(t, vec3d, test.ShouldResemble, r3.Vector{4, 5, 10}) - - pc, err := pp.RGBDToPointCloud(img, dm) - test.That(t, err, test.ShouldBeNil) - data, got := pc.At(140, 500, float64(dm.GetDepth(140, 500))) - test.That(t, got, test.ShouldBeTrue) - test.That(t, rimage.NewColorFromColor(data.Color()), test.ShouldResemble, img.GetXY(140, 500)) - - pc2, err := pp.RGBDToPointCloud(img, dm, image.Rectangle{image.Point{130, 490}, image.Point{150, 510}}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc2.Size(), test.ShouldEqual, 400) - data, got = pc2.At(140, 500, float64(dm.GetDepth(140, 500))) - test.That(t, got, test.ShouldBeTrue) - test.That(t, rimage.NewColorFromColor(data.Color()), test.ShouldResemble, img.GetXY(140, 500)) - - _, err = pp.RGBDToPointCloud(img, dm, image.Rectangle{image.Point{130, 490}, image.Point{150, 510}}, image.Rectangle{}) - test.That(t, err.Error(), test.ShouldContainSubstring, "more than one cropping rectangle") - - img3, dm3, err := pp.PointCloudToRGBD(pc) - test.That(t, err, test.ShouldBeNil) - test.That(t, img3.GetXY(140, 500), test.ShouldResemble, img.GetXY(140, 500)) - test.That(t, dm3.GetDepth(140, 500), test.ShouldResemble, dm.GetDepth(140, 500)) -} diff --git a/rimage/transform/cmd/extrinsic_calibration/main_test.go b/rimage/transform/cmd/extrinsic_calibration/main_test.go deleted file mode 100644 index 8c03039b7bf..00000000000 --- a/rimage/transform/cmd/extrinsic_calibration/main_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "encoding/json" - "math" - "math/rand" - "os" - "testing" - - "github.com/golang/geo/r2" - "github.com/golang/geo/r3" - "go.viam.com/test" - "gonum.org/v1/gonum/optimize" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -type mockPose struct{} - -func (m mockPose) Point() r3.Vector { - return r3.Vector{} -} - -func (m mockPose) Orientation() spatialmath.Orientation { - return spatialmath.NewOrientationVector() -} - -func TestMainCalibrate(t *testing.T) { - outDir := t.TempDir() - logger := logging.NewTestLogger(t) - - // get a file with known extrinsic parameters - camera, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(utils.ResolveFile("rimage/transform/data/intel515_parameters.json")) - test.That(t, err, test.ShouldBeNil) - - // create many points from a known extrinsic file - calibConfig := createInputConfig(camera, 100) - // writes bytes to temporary file - b, err := json.MarshalIndent(calibConfig, "", " ") - test.That(t, err, test.ShouldBeNil) - err = os.WriteFile(outDir+"/test.json", b, 0o644) - test.That(t, err, test.ShouldBeNil) - - mockCalibrationFn := func(_ *optimize.Problem, _ logging.Logger) (spatialmath.Pose, error) { - return mockPose{}, nil - } - - // read from temp file and process - calibrate(outDir+"/test.json", logger, mockCalibrationFn) -} - -func createInputConfig(c *transform.DepthColorIntrinsicsExtrinsics, n int) *transform.ExtrinsicCalibrationConfig { - depthH, depthW := float64(c.DepthCamera.Height), float64(c.DepthCamera.Width) - colorPoints := make([]r2.Point, n) - depthPoints := make([]r3.Vector, n) - for i := 0; i < n; i++ { - dx := math.Round(rand.Float64() * depthW) - dy := math.Round(rand.Float64() * depthH) - dz := math.Round(rand.Float64()*2450.) + 50.0 // always want at least 50 mm distance - depthPoints[i] = r3.Vector{dx, dy, dz} - cx, cy, _ := c.DepthPixelToColorPixel(dx, dy, dz) - colorPoints[i] = r2.Point{cx, cy} - } - conf := &transform.ExtrinsicCalibrationConfig{ - ColorPoints: colorPoints, - DepthPoints: depthPoints, - ColorIntrinsics: c.ColorCamera, - DepthIntrinsics: c.DepthCamera, - } - return conf -} diff --git a/rimage/transform/cmd/extrinsic_calibration/verify_main_test.go b/rimage/transform/cmd/extrinsic_calibration/verify_main_test.go deleted file mode 100644 index ca3e9069299..00000000000 --- a/rimage/transform/cmd/extrinsic_calibration/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/rimage/transform/homographies_test.go b/rimage/transform/homographies_test.go deleted file mode 100644 index e6e29ea0d1a..00000000000 --- a/rimage/transform/homographies_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package transform - -import ( - "math" - "testing" - - "github.com/golang/geo/r2" - "go.viam.com/test" - "gonum.org/v1/gonum/mat" -) - -// CreateRotationMatrix creates a 2x2 rotation matrix with given angle in radians. -func CreateRotationMatrix(angle float64) *mat.Dense { - r := mat.NewDense(2, 2, nil) - r.Set(0, 0, math.Cos(angle)) - r.Set(0, 1, -math.Sin(angle)) - r.Set(1, 0, math.Sin(angle)) - r.Set(1, 1, math.Cos(angle)) - - return r -} - -func SlicesXsYsToPoints(points [][]float64) []r2.Point { - pts := make([]r2.Point, len(points)) - for i, pt := range points { - x := pt[0] - y := pt[1] - pts[i] = r2.Point{x, y} - } - return pts -} - -func repeatedSlice(value float64, n int) []float64 { - arr := make([]float64, n) - for i := 0; i < n; i++ { - arr[i] = value - } - return arr -} - -func TestGeometricDistance(t *testing.T) { - pt1 := r2.Point{0, 0} - pt2 := r2.Point{1, 0} - // h = Id, distance should be 1 - h1 := mat.NewDense(3, 3, nil) - h1.Set(0, 0, 1) - h1.Set(1, 1, 1) - h1.Set(2, 2, 1) - d1 := geometricDistance(pt1, pt2, h1) - test.That(t, d1, test.ShouldEqual, 1.0) - // rotation -pi/2 - h2 := mat.NewDense(3, 3, nil) - h2.Set(0, 1, 1) - h2.Set(1, 0, -1) - h2.Set(2, 2, 1) - d2 := geometricDistance(pt1, pt2, h2) - test.That(t, d2, test.ShouldEqual, 1.0) - // rotation -pi/2 - h3 := mat.NewDense(3, 3, nil) - h3.Set(0, 1, 1) - h3.Set(1, 0, -1) - h3.Set(2, 2, 1) - pt3 := r2.Point{1, 0} - d3 := geometricDistance(pt3, pt2, h3) - test.That(t, d3, test.ShouldEqual, 1.4142135623730951) -} diff --git a/rimage/transform/homography_parameters_test.go b/rimage/transform/homography_parameters_test.go deleted file mode 100644 index 3cbf5ef892d..00000000000 --- a/rimage/transform/homography_parameters_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package transform - -import ( - "math" - "testing" - - "github.com/golang/geo/r2" - "go.viam.com/test" - "gonum.org/v1/gonum/floats" - "gonum.org/v1/gonum/mat" - - "go.viam.com/rdk/utils" -) - -func TestEstimateHomographyFrom8Points(t *testing.T) { - h := []float64{ - 7.82502613e-01, -9.71005496e-02, 9.73247024e+00, - 9.71005496e-02, 7.82502613e-01, 7.26666735e+00, - 8.96533720e-04, -9.39239890e-04, 1.00000000e+00, - } - homography := mat.NewDense(3, 3, h) - pts1 := []r2.Point{{0., 0.}, {25., 0.}, {0., 25.}, {25., 25.}} - pts2 := []r2.Point{ - {9.7324705, 7.2666674}, - {28.65283, 9.481666}, - {7.4806085, 27.474358}, - {26.896238, 29.288015}, - } - H, _ := EstimateExactHomographyFrom8Points(pts1, pts2, false) - test.That(t, mat.EqualApprox(H.matrix, homography, 0.00001), test.ShouldBeTrue) - pts3 := []r2.Point{} - h1, err1 := EstimateExactHomographyFrom8Points(pts1, pts3, false) - test.That(t, h1, test.ShouldBeNil) - test.That(t, err1, test.ShouldBeError) - h2, err2 := EstimateExactHomographyFrom8Points(pts3, pts2, false) - test.That(t, h2, test.ShouldBeNil) - test.That(t, err2, test.ShouldBeError) -} - -func TestEstimateHomographyRANSAC(t *testing.T) { - dim := 2 - grid := make([]float64, 8) - floats.Span(grid, 0, 7) - pts := utils.Single(dim, grid) - pts1 := SlicesXsYsToPoints(pts) - - pts2 := make([]r2.Point, len(pts1)) - for i := 0; i < len(pts1); i++ { - pts2[i] = r2.Point{pts1[i].X + 2, pts1[i].Y + 3} - } - h, _, _ := EstimateHomographyRANSAC(pts1, pts2, 0.5, 2000) - // homography should be close to - // [[1 0 2], - // [0 1 3], - // [0 0 1]] - test.That(t, h.At(0, 0), test.ShouldAlmostEqual, 1, 0.001) - test.That(t, h.At(1, 1), test.ShouldAlmostEqual, 1, 0.001) - test.That(t, h.At(0, 1), test.ShouldAlmostEqual, 0, 0.001) - test.That(t, h.At(1, 0), test.ShouldAlmostEqual, 0, 0.001) - test.That(t, h.At(0, 2), test.ShouldAlmostEqual, 2, 0.001) - test.That(t, h.At(1, 2), test.ShouldAlmostEqual, 3, 0.001) - test.That(t, h.At(2, 0), test.ShouldAlmostEqual, 0, 0.001) - test.That(t, h.At(2, 1), test.ShouldAlmostEqual, 0, 0.001) - test.That(t, h.At(2, 2), test.ShouldAlmostEqual, 1, 0.001) -} - -func TestEstimateLeastSquaresHomography(t *testing.T) { - // create a rotation as a simple homography - h := CreateRotationMatrix(math.Pi / 8.) - // create grid of points - x := make([]float64, 9) - floats.Span(x, 0, 200) - pts1Slice := utils.Single(2, x) - pts1 := mat.NewDense(len(pts1Slice), len(pts1Slice[0]), nil) - for i, pt := range pts1Slice { - pts1.Set(i, 0, pt[0]) - pts1.Set(i, 1, pt[1]) - } - // rotate point with H - r, c := pts1.Dims() - pts2 := mat.NewDense(c, r, nil) - pts2.Mul(h, pts1.T()) - b := pts2.T() - pts3 := mat.DenseCopyOf(b) - // estimate homography with least squares method - estH, _ := EstimateLeastSquaresHomography(pts1, pts3) - // check that 2x2 block are close to each other - test.That(t, estH.At(0, 0), test.ShouldAlmostEqual, h.At(0, 0), 0.001) - test.That(t, estH.At(0, 1), test.ShouldAlmostEqual, h.At(0, 1), 0.001) - test.That(t, estH.At(1, 0), test.ShouldAlmostEqual, h.At(1, 0), 0.001) - test.That(t, estH.At(1, 1), test.ShouldAlmostEqual, h.At(1, 1), 0.001) - - // test translation (2,2) - pts1Copy := mat.DenseCopyOf(pts1) - - pts4Data := pts1Copy.RawMatrix().Data - vals := repeatedSlice(2., r*c) - floats.Add(pts4Data, vals) - pts4 := mat.NewDense(r, c, pts4Data) - estH2, _ := EstimateLeastSquaresHomography(pts1, pts4) - - // check that 2x2 block are close to identity - test.That(t, estH2.At(0, 0), test.ShouldAlmostEqual, 1.0, 0.001) - test.That(t, estH2.At(0, 1), test.ShouldBeLessThanOrEqualTo, 0.001) - test.That(t, estH2.At(1, 0), test.ShouldBeLessThanOrEqualTo, 0.001) - test.That(t, estH2.At(1, 1), test.ShouldAlmostEqual, 1.0, 0.001) - // check that translation terms are close to sqrt(2) / 4. (from homography decomposition formula) - test.That(t, estH2.At(0, 2), test.ShouldAlmostEqual, 0.3535533905932738, 0.01) - test.That(t, estH2.At(1, 2), test.ShouldAlmostEqual, 0.3535533905932738, 0.01) - // test that translation terms are equal tx = ty - test.That(t, estH2.At(1, 2), test.ShouldAlmostEqual, estH2.At(0, 2), 0.01) -} diff --git a/rimage/transform/homography_test.go b/rimage/transform/homography_test.go deleted file mode 100644 index 7e995f00695..00000000000 --- a/rimage/transform/homography_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package transform - -import ( - "context" - "image" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" -) - -type homographyTestHelper struct { - params *DepthColorHomography - proj *PinholeCameraIntrinsics -} - -func (h *homographyTestHelper) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - img, img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - var err error - im := rimage.ConvertImage(img) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), img2) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "depth_homography") - - imgFixed, dmFixed, err := h.params.AlignColorAndDepthImage(im, dm) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(imgFixed, "color-fixed_homography") - pCtx.GotDebugImage(dmFixed.ToPrettyPicture(0, rimage.MaxDepth), "depth-fixed_homography") - - pCtx.GotDebugImage(rimage.Overlay(imgFixed, dmFixed), "overlay_homography") - - // get pointcloud - pc, err := h.proj.RGBDToPointCloud(imgFixed, dmFixed) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(pc, "aligned-pointcloud_homography") - - // go back to image and depth map - roundTripImg, roundTripDm, err := h.proj.PointCloudToRGBD(pc) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(roundTripImg, "from-pointcloud-color") - pCtx.GotDebugImage(roundTripDm.ToPrettyPicture(0, rimage.MaxDepth), "from-pointcloud-depth") - - return nil -} - -func TestNewHomography(t *testing.T) { - _, err := NewHomography([]float64{}) - test.That(t, err, test.ShouldBeError, errors.New("input to NewHomography must have length of 9. Has length of 0")) - - vals := []float64{ - 2.32700501e-01, - -8.33535395e-03, - -3.61894025e+01, - -1.90671303e-03, - 2.35303232e-01, - 8.38582614e+00, - -6.39101664e-05, - -4.64582754e-05, - 1.00000000e+00, - } - _, err = NewHomography(vals) - test.That(t, err, test.ShouldBeNil) -} - -func TestDepthColorHomography(t *testing.T) { - intrinsics := &PinholeCameraIntrinsics{ // color camera intrinsic parameters - Width: 1024, - Height: 768, - Fx: 821.32642889, - Fy: 821.68607359, - Ppx: 494.95941428, - Ppy: 370.70529534, - } - conf := &RawDepthColorHomography{ - Homography: []float64{ - 2.32700501e-01, - -8.33535395e-03, - -3.61894025e+01, - -1.90671303e-03, - 2.35303232e-01, - 8.38582614e+00, - -6.39101664e-05, - -4.64582754e-05, - 1.00000000e+00, - }, - DepthToColor: false, - RotateDepth: -90, - } - - dch, err := NewDepthColorHomography(conf) - test.That(t, err, test.ShouldBeNil) - d := rimage.NewMultipleImageTestDebugger(t, "transform/homography/color", "*.png", "transform/homography/depth") - err = d.Process(t, &homographyTestHelper{dch, intrinsics}) - test.That(t, err, test.ShouldBeNil) -} diff --git a/rimage/transform/image_align_test.go b/rimage/transform/image_align_test.go deleted file mode 100644 index bc0e338caf8..00000000000 --- a/rimage/transform/image_align_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package transform - -import ( - "image" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" -) - -type alignImageHelper struct { - name string - config AlignConfig - expectedColorOutput []image.Point - expectedDepthOutput []image.Point -} - -func Abs(x int) int { - if x < 0 { - return -x - } - return x -} - -func makeTestCases() []alignImageHelper { - cases := []alignImageHelper{ - { - name: "base_case", - config: AlignConfig{ - ColorInputSize: image.Point{120, 240}, - ColorWarpPoints: []image.Point{{29, 82}, {61, 48}}, - DepthInputSize: image.Point{200, 100}, - DepthWarpPoints: []image.Point{{15, 57}, {47, 23}}, - OutputSize: image.Point{50, 50}, - }, - expectedColorOutput: rimage.ArrayToPoints([]image.Point{{14, 25}, {119, 124}}), - expectedDepthOutput: rimage.ArrayToPoints([]image.Point{{0, 0}, {105, 99}}), - }, - { - name: "rotated case", - config: AlignConfig{ - ColorInputSize: image.Point{120, 240}, - ColorWarpPoints: []image.Point{{29, 82}, {61, 48}}, - DepthInputSize: image.Point{100, 200}, - DepthWarpPoints: []image.Point{{42, 15}, {76, 47}}, - OutputSize: image.Point{50, 50}, - }, - expectedColorOutput: rimage.ArrayToPoints([]image.Point{{14, 25}, {119, 124}}), - expectedDepthOutput: rotatePoints(rimage.ArrayToPoints([]image.Point{{0, 0}, {99, 105}})), - }, - { - name: "scaled case", - config: AlignConfig{ - ColorInputSize: image.Point{120, 240}, - ColorWarpPoints: []image.Point{{29, 82}, {61, 48}}, - DepthInputSize: image.Point{150, 75}, - DepthWarpPoints: []image.Point{{11, 43}, {35, 17}}, - OutputSize: image.Point{50, 50}, - }, - expectedColorOutput: rimage.ArrayToPoints([]image.Point{{14, 25}, {119, 124}}), - expectedDepthOutput: rimage.ArrayToPoints([]image.Point{{0, 0}, {79, 74}}), - }, - { - name: "scaled+rotated case", - config: AlignConfig{ - ColorInputSize: image.Point{120, 240}, - ColorWarpPoints: []image.Point{{29, 82}, {61, 48}}, - DepthInputSize: image.Point{75, 150}, - DepthWarpPoints: []image.Point{{31, 11}, {57, 35}}, - OutputSize: image.Point{50, 50}, - }, - expectedColorOutput: rimage.ArrayToPoints([]image.Point{{14, 25}, {119, 124}}), - expectedDepthOutput: rotatePoints(rimage.ArrayToPoints([]image.Point{{0, 0}, {74, 79}})), - }, - } - return cases -} - -func expectedImageAlignOutput(t *testing.T, a alignImageHelper, logger logging.Logger) { - t.Helper() - colorOutput, depthOutput, err := ImageAlign( - a.config.ColorInputSize, - a.config.ColorWarpPoints, - a.config.DepthInputSize, - a.config.DepthWarpPoints, - logger, - ) - test.That(t, err, test.ShouldBeNil) - // If scaling changes expected pixel boundaries by 1 pixel, that can be explained by rounding - for i := range colorOutput { - Xdiff := Abs(colorOutput[i].X - a.expectedColorOutput[i].X) - Ydiff := Abs(colorOutput[i].Y - a.expectedColorOutput[i].Y) - test.That(t, Xdiff, test.ShouldBeLessThanOrEqualTo, 1) - test.That(t, Ydiff, test.ShouldBeLessThanOrEqualTo, 1) - } - for i := range depthOutput { - Xdiff := Abs(depthOutput[i].X - a.expectedDepthOutput[i].X) - Ydiff := Abs(depthOutput[i].Y - a.expectedDepthOutput[i].Y) - test.That(t, Xdiff, test.ShouldBeLessThanOrEqualTo, 1) - test.That(t, Ydiff, test.ShouldBeLessThanOrEqualTo, 1) - } -} - -func TestAlignImage(t *testing.T) { - cases := makeTestCases() - for _, c := range cases { - logger := logging.NewTestLogger(t) - t.Run(c.name, func(t *testing.T) { - expectedImageAlignOutput(t, c, logger) - }) - } -} diff --git a/rimage/transform/pinhole_camera_parameters_test.go b/rimage/transform/pinhole_camera_parameters_test.go deleted file mode 100644 index 2ebd90a4f5e..00000000000 --- a/rimage/transform/pinhole_camera_parameters_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package transform - -import ( - "context" - "image" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -func TestIntrinsicsJSON(t *testing.T) { - colorIntrinsics, err := NewPinholeCameraIntrinsicsFromJSONFile( - utils.ResolveFile("rimage/transform/data/intel515_color_camera.json"), - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, colorIntrinsics.Height, test.ShouldEqual, 720) - test.That(t, colorIntrinsics.Width, test.ShouldEqual, 1280) - test.That(t, colorIntrinsics.Fx, test.ShouldEqual, 900.538) - test.That(t, colorIntrinsics.Fy, test.ShouldEqual, 900.818) -} - -func TestDepthColorIntrinsicsExtrinsics(t *testing.T) { - sensorParams, err := NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - // check depth sensor parameters values - depthIntrinsics := sensorParams.DepthCamera - test.That(t, depthIntrinsics.Height, test.ShouldEqual, 768) - test.That(t, depthIntrinsics.Width, test.ShouldEqual, 1024) - test.That(t, depthIntrinsics.Fx, test.ShouldEqual, 734.938) - test.That(t, depthIntrinsics.Fy, test.ShouldEqual, 735.516) - - // check color sensor parameters values - colorIntrinsics := sensorParams.ColorCamera - test.That(t, colorIntrinsics.Height, test.ShouldEqual, 720) - test.That(t, colorIntrinsics.Width, test.ShouldEqual, 1280) - test.That(t, colorIntrinsics.Fx, test.ShouldEqual, 900.538) - test.That(t, colorIntrinsics.Fy, test.ShouldEqual, 900.818) - - // check sensorParams sensor parameters values - gtRotation, err := spatialmath.NewRotationMatrix([]float64{ - 0.999958, -0.00838489, 0.00378392, - 0.00824708, 0.999351, 0.0350734, - -0.00407554, -0.0350407, 0.999378, - }) - test.That(t, err, test.ShouldBeNil) - gtTranslation := r3.Vector{-0.000828434, 0.0139185, -0.0033418} - - rotationMatrix := sensorParams.ExtrinsicD2C.Orientation().RotationMatrix() - translationVector := sensorParams.ExtrinsicD2C.Point() - test.That(t, spatialmath.OrientationAlmostEqual(rotationMatrix, gtRotation), test.ShouldBeTrue) - test.That(t, translationVector.X, test.ShouldAlmostEqual, gtTranslation.X) - test.That(t, translationVector.Y, test.ShouldAlmostEqual, gtTranslation.Y) - test.That(t, translationVector.Z, test.ShouldAlmostEqual, gtTranslation.Z) -} - -func TestTransformPointToPoint(t *testing.T) { - dcie := NewEmptyDepthColorIntrinsicsExtrinsics() - x1, y1, z1 := 0., 0., 1. - t1 := r3.Vector{0, 0, 1} - // Get rigid body transform between Depth and RGB sensor - dcie.ExtrinsicD2C = spatialmath.NewPoseFromPoint(t1) - x2, y2, z2 := dcie.TransformPointToPoint(x1, y1, z1) - test.That(t, x2, test.ShouldEqual, 0.) - test.That(t, y2, test.ShouldEqual, 0.) - test.That(t, z2, test.ShouldEqual, 2.) - - t2 := r3.Vector{0, 2, 0} - dcie.ExtrinsicD2C = spatialmath.NewPoseFromPoint(t2) - x3, y3, z3 := dcie.TransformPointToPoint(x1, y1, z1) - test.That(t, x3, test.ShouldEqual, 0.) - test.That(t, y3, test.ShouldEqual, 2.) - test.That(t, z3, test.ShouldEqual, 1.) - // Rotation in the (z,x) plane of 90 degrees - rot, err := spatialmath.NewRotationMatrix([]float64{0, 0, 1, 0, 1, 0, -1, 0, 0}) - test.That(t, err, test.ShouldBeNil) - dcie.ExtrinsicD2C = spatialmath.NewPose(t2, rot) - x4, y4, z4 := dcie.TransformPointToPoint(x1, y1, z1) - test.That(t, x4, test.ShouldAlmostEqual, 1.) - test.That(t, y4, test.ShouldAlmostEqual, 2.) - test.That(t, z4, test.ShouldAlmostEqual, 0.) -} - -func TestUndistortImage(t *testing.T) { - params800 := &PinholeCameraIntrinsics{ - Width: 800, - Height: 600, - Fx: 887.07855759, - Fy: 886.579955, - Ppx: 382.80075175, - Ppy: 302.75546742, - } - distortion800 := &BrownConrady{ - RadialK1: -0.42333866, - RadialK2: 0.25696641, - TangentialP1: 0.00142052, - TangentialP2: -0.00116427, - RadialK3: -0.06468911, - } - pinhole800 := &PinholeCameraModel{PinholeCameraIntrinsics: params800, Distortion: distortion800} - params1280 := &PinholeCameraIntrinsics{ - Width: 1280, - Height: 720, - Fx: 1067.68786, - Fy: 1067.64416, - Ppx: 629.229310, - Ppy: 387.990797, - } - distortion1280 := &BrownConrady{ - RadialK1: -4.27329870e-01, - RadialK2: 2.41688942e-01, - TangentialP1: 9.33797688e-04, - TangentialP2: -2.65675762e-04, - RadialK3: -6.51379008e-02, - } - pinhole1280 := &PinholeCameraModel{PinholeCameraIntrinsics: params1280, Distortion: distortion1280} - // nil input - _, err := pinhole800.UndistortImage(nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "input image is nil") - // wrong size error - img1280, err := rimage.NewImageFromFile(artifact.MustPath("transform/undistort/distorted_1280x720.jpg")) - test.That(t, err, test.ShouldBeNil) - _, err = pinhole800.UndistortImage(img1280) - test.That(t, err.Error(), test.ShouldContainSubstring, "img dimension and intrinsics don't match") - - outDir := t.TempDir() - // correct undistortion - // 800x600 - img800, err := rimage.NewImageFromFile(artifact.MustPath("transform/undistort/distorted_800x600.jpg")) - test.That(t, err, test.ShouldBeNil) - corrected800, err := pinhole800.UndistortImage(img800) - test.That(t, err, test.ShouldBeNil) - err = rimage.WriteImageToFile(outDir+"/corrected_800x600.jpg", corrected800) - test.That(t, err, test.ShouldBeNil) - // 1280x720 - corrected1280, err := pinhole1280.UndistortImage(img1280) - test.That(t, err, test.ShouldBeNil) - err = rimage.WriteImageToFile(outDir+"/corrected_1280x720.jpg", corrected1280) - test.That(t, err, test.ShouldBeNil) -} - -func TestUndistortDepthMap(t *testing.T) { - params := &PinholeCameraIntrinsics{ // not the real intrinsic parameters of the depth map - Width: 1280, - Height: 720, - Fx: 1067.68786, - Fy: 1067.64416, - Ppx: 629.229310, - Ppy: 387.990797, - } - distortion := &BrownConrady{ - RadialK1: 0., - RadialK2: 0., - TangentialP1: 0., - TangentialP2: 0., - RadialK3: 0., - } - var cameraModel PinholeCameraModel - cameraModel.PinholeCameraIntrinsics = params - cameraModel.Distortion = distortion - pinhole := &cameraModel - // nil input - _, err := pinhole.UndistortDepthMap(nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "input DepthMap is nil") - - // wrong size error - dmWrong, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("transform/align-test-1615761793.png")) - test.That(t, err, test.ShouldBeNil) - _, err = pinhole.UndistortDepthMap(dmWrong) - test.That(t, err.Error(), test.ShouldContainSubstring, "img dimension and intrinsics don't match") - - // correct undistortion - img, err := rimage.NewDepthMapFromFile( - context.Background(), artifact.MustPath("rimage/board2_gray.png")) - test.That(t, err, test.ShouldBeNil) - corrected, err := pinhole.UndistortDepthMap(img) - test.That(t, err, test.ShouldBeNil) - // should not have changed the values at all, as distortion parameters are all 0 - test.That(t, corrected.GetDepth(200, 300), test.ShouldEqual, img.GetDepth(200, 300)) - test.That(t, corrected.GetDepth(0, 0), test.ShouldEqual, img.GetDepth(0, 0)) - test.That(t, corrected.GetDepth(1279, 719), test.ShouldEqual, img.GetDepth(1279, 719)) -} - -func TestGetCameraMatrix(t *testing.T) { - intrinsics := &PinholeCameraIntrinsics{ - Width: 0, - Height: 0, - Fx: 50, - Fy: 55, - Ppx: 320, - Ppy: 160, - } - intrinsicsK := intrinsics.GetCameraMatrix() - test.That(t, intrinsicsK, test.ShouldNotBeNil) - test.That(t, intrinsicsK.At(0, 0), test.ShouldEqual, intrinsics.Fx) - test.That(t, intrinsicsK.At(1, 1), test.ShouldEqual, intrinsics.Fy) - test.That(t, intrinsicsK.At(0, 2), test.ShouldEqual, intrinsics.Ppx) - test.That(t, intrinsicsK.At(1, 2), test.ShouldEqual, intrinsics.Ppy) - test.That(t, intrinsicsK.At(2, 2), test.ShouldEqual, 1) -} - -func TestNilIntrinsics(t *testing.T) { - var nilIntrinsics *PinholeCameraIntrinsics - test.That(t, func() { nilIntrinsics.CheckValid() }, test.ShouldNotPanic) - test.That(t, func() { nilIntrinsics.GetCameraMatrix() }, test.ShouldNotPanic) - test.That(t, func() { nilIntrinsics.PixelToPoint(0.0, 0.0, 0.0) }, test.ShouldNotPanic) - test.That(t, func() { nilIntrinsics.PointToPixel(0.0, 0.0, 0.0) }, test.ShouldNotPanic) - test.That(t, func() { nilIntrinsics.ImagePointTo3DPoint(image.Point{}, rimage.Depth(0)) }, test.ShouldNotPanic) - test.That(t, func() { nilIntrinsics.RGBDToPointCloud(&rimage.Image{}, &rimage.DepthMap{}) }, test.ShouldNotPanic) - test.That(t, func() { nilIntrinsics.PointCloudToRGBD(pointcloud.PointCloud(nil)) }, test.ShouldNotPanic) -} diff --git a/rimage/transform/two_view_geom_test.go b/rimage/transform/two_view_geom_test.go deleted file mode 100644 index a027935531e..00000000000 --- a/rimage/transform/two_view_geom_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package transform - -import ( - "encoding/json" - "io" - "os" - "testing" - - "github.com/golang/geo/r2" - "go.viam.com/test" - "go.viam.com/utils/artifact" - "gonum.org/v1/gonum/mat" - - "go.viam.com/rdk/logging" -) - -type poseGroundTruth struct { - Pts1 [][]float64 `json:"pts1"` - Pts2 [][]float64 `json:"pts2"` - R [][]float64 `json:"rot"` // TODO(RSDK-568): unit? - T [][]float64 `json:"translation"` // TODO(RSDK-568): unit? - K [][]float64 `json:"cam_mat"` - F [][]float64 `json:"fundamental_matrix"` -} - -func convert2DSliceToVectorSlice(points [][]float64) []r2.Point { - vecs := make([]r2.Point, len(points)) - for i, pt := range points { - vecs[i] = r2.Point{ - X: pt[0], - Y: pt[1], - } - } - return vecs -} - -func convert2DSliceToDense(data [][]float64) *mat.Dense { - m := len(data) - n := len(data[0]) - out := mat.NewDense(m, n, nil) - for i, row := range data { - out.SetRow(i, row) - } - return out -} - -func readJSONGroundTruth(logger logging.Logger) *poseGroundTruth { - // Open jsonFile - jsonFile, err := os.Open(artifact.MustPath("rimage/matched_kps.json")) - if err != nil { - return nil - } - logger.Info("Ground Truth json file successfully loaded") - defer jsonFile.Close() - // read our opened jsonFile as a byte array. - byteValue, _ := io.ReadAll(jsonFile) - - // initialize poseGroundTruth - var gt poseGroundTruth - - // unmarshal byteArray - json.Unmarshal(byteValue, >) - return > -} - -func TestComputeFundamentalMatrix(t *testing.T) { - logger := logging.NewTestLogger(t) - gt := readJSONGroundTruth(logger) - pts1 := convert2DSliceToVectorSlice(gt.Pts1) - pts2 := convert2DSliceToVectorSlice(gt.Pts2) - F2, err := ComputeFundamentalMatrixAllPoints(pts1, pts2, true) - test.That(t, err, test.ShouldBeNil) - // test that x2^T @ F @ x1 approx 0 - var res1, res2 mat.Dense - v1 := mat.NewDense(3, 1, []float64{pts1[0].X, pts1[0].Y, 1}) - v2 := mat.NewDense(1, 3, []float64{pts2[0].X, pts2[0].Y, 1}) - res1.Mul(F2, v1) - res2.Mul(v2, &res1) - test.That(t, res2.At(0, 0), test.ShouldBeLessThan, 0.01) - // essential matrix - K := convert2DSliceToDense(gt.K) - E, err := GetEssentialMatrixFromFundamental(K, K, F2) - test.That(t, err, test.ShouldBeNil) - // test that xHat2^T @ E @ xHat1 approx 0, with xHat = K^-1 @ x - eNorm := mat.Norm(E, 2) - E.Scale(1./eNorm, E) - var res3, res4 mat.Dense - var Kinv, x1Hat, x2Hat mat.Dense - err = Kinv.Inverse(K) - test.That(t, err, test.ShouldBeNil) - x1Hat.Mul(&Kinv, v1) - x2Hat.Mul(&Kinv, transposeDense(v2)) - x2HatT := transposeDense(&x2Hat) - res3.Mul(E, &x1Hat) - res4.Mul(x2HatT, &res3) - test.That(t, res4.At(0, 0), test.ShouldBeLessThan, 0.0001) -} diff --git a/rimage/transform/verify_main_test.go b/rimage/transform/verify_main_test.go deleted file mode 100644 index d5e16f69bda..00000000000 --- a/rimage/transform/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package transform - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/rimage/transform/warp_point_parameters_test.go b/rimage/transform/warp_point_parameters_test.go deleted file mode 100644 index 06245ea40c1..00000000000 --- a/rimage/transform/warp_point_parameters_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package transform - -import ( - "context" - "image" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" -) - -func TestRGBDToPointCloud(t *testing.T) { - logger := logging.NewTestLogger(t) - img, err := rimage.NewImageFromFile(artifact.MustPath("transform/align-test-1615761793_color.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("transform/align-test-1615761793.png")) - test.That(t, err, test.ShouldBeNil) - - // from experimentation - config := &AlignConfig{ - ColorInputSize: image.Point{1024, 768}, - ColorWarpPoints: []image.Point{{604, 575}, {695, 115}}, - DepthInputSize: image.Point{224, 171}, - DepthWarpPoints: []image.Point{{89, 109}, {206, 132}}, - OutputSize: image.Point{448, 342}, - OutputOrigin: image.Point{227, 160}, - } - dct, err := NewDepthColorWarpTransforms(config, logger) - test.That(t, err, test.ShouldBeNil) - - // align images first - col, dm, err := dct.AlignColorAndDepthImage(img, dm) - test.That(t, err, test.ShouldBeNil) - // project - pc, err := dct.RGBDToPointCloud(col, dm) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc, test.ShouldNotBeNil) - // crop - pcCrop, err := dct.RGBDToPointCloud(col, dm, image.Rectangle{image.Point{20, 20}, image.Point{40, 40}}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pcCrop.Size(), test.ShouldEqual, 400) - // crop error - _, err = dct.RGBDToPointCloud(col, dm, image.Rectangle{image.Point{20, 20}, image.Point{40, 40}}, image.Rectangle{}) - test.That(t, err.Error(), test.ShouldContainSubstring, "more than one cropping rectangle") - - // image with depth with depth missing should return error - img, err = rimage.NewImageFromFile(artifact.MustPath("transform/align-test-1615761793_color.png")) - test.That(t, err, test.ShouldBeNil) - - pcBad, err := dct.RGBDToPointCloud(img, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, pcBad, test.ShouldBeNil) -} - -func TestWarpPointsTo3D(t *testing.T) { - logger := logging.NewTestLogger(t) - img, err := rimage.NewImageFromFile(artifact.MustPath("transform/align-test-1615761793_color.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("transform/align-test-1615761793.png")) - test.That(t, err, test.ShouldBeNil) - - // from experimentation - config := &AlignConfig{ - ColorInputSize: image.Point{1024, 768}, - ColorWarpPoints: []image.Point{{604, 575}, {695, 115}}, - DepthInputSize: image.Point{224, 171}, - DepthWarpPoints: []image.Point{{89, 109}, {206, 132}}, - OutputSize: image.Point{448, 342}, - OutputOrigin: image.Point{227, 160}, - } - testPoint := image.Point{0, 0} - dct, err := NewDepthColorWarpTransforms(config, logger) - test.That(t, err, test.ShouldBeNil) - // align the images - img, dm, err = dct.AlignColorAndDepthImage(img, dm) - test.That(t, err, test.ShouldBeNil) - // Check to see if the origin point on the pointcloud transformed correctly - vec, err := dct.ImagePointTo3DPoint(config.OutputOrigin, dm.Get(config.OutputOrigin)) - test.That(t, err, test.ShouldBeNil) - test.That(t, vec.X, test.ShouldEqual, 0.0) - test.That(t, vec.Y, test.ShouldEqual, 0.0) - // test out To3D - vec, err = dct.ImagePointTo3DPoint(testPoint, dm.Get(testPoint)) - test.That(t, err, test.ShouldBeNil) - test.That(t, vec.Z, test.ShouldEqual, float64(dm.Get(testPoint))) - // out of bounds - panic - testPoint = image.Point{img.Width(), img.Height()} - test.That(t, func() { dct.ImagePointTo3DPoint(testPoint, dm.Get(testPoint)) }, test.ShouldPanic) -} diff --git a/rimage/verify_main_test.go b/rimage/verify_main_test.go deleted file mode 100644 index 548b003eacc..00000000000 --- a/rimage/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package rimage - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/rimage/warp_test.go b/rimage/warp_test.go deleted file mode 100644 index ac5fb752e08..00000000000 --- a/rimage/warp_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package rimage - -import ( - "image" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - "gonum.org/v1/gonum/mat" -) - -func increasingArray(start, inc float64, total int) []float64 { - data := make([]float64, total) - for i := 0; i < total; i++ { - data[i] = start + float64(i)*inc - } - return data -} - -func TestWarp1(t *testing.T) { - size := 5 - - src := []image.Point{ - {1, 1}, - {size - 1, 1}, - {1, size - 1}, - {size - 1, size - 1}, - } - dst := []image.Point{ - {0, 0}, - {size, 0}, - {0, size}, - {size, size}, - } - - m2 := GetPerspectiveTransform(src, dst) - r, c := m2.Dims() - test.That(t, r, test.ShouldEqual, 3) - test.That(t, c, test.ShouldEqual, 3) - test.That(t, m2.At(0, 0), test.ShouldAlmostEqual, 0.5999999999999999, .01) - - input := mat.NewDense(size, size, increasingArray(0, 1, size*size)) - - res := mat.NewDense(size, size, nil) - Warp(&WarpMatrixConnector{input, res}, m2) - - test.That(t, res.At(0, 0), test.ShouldAlmostEqual, 6.0, .01) - test.That(t, res.At(4, 4), test.ShouldAlmostEqual, 20.4, .01) -} - -func TestWarp2(t *testing.T) { - t.Parallel() - img, err := NewImageFromFile(artifact.MustPath("rimage/canny1.png")) - test.That(t, err, test.ShouldBeNil) - - size := 800 - - m := GetPerspectiveTransform( - []image.Point{ - {100, 100}, - {700, 100}, - {100, 700}, - {700, 700}, - }, - []image.Point{ - {0, 0}, - {size, 0}, - {0, size}, - {size, size}, - }) - - out := WarpImage(img, m, image.Point{size, size}) - - err = WriteImageToFile(t.TempDir()+"/canny1-warped.png", out) - test.That(t, err, test.ShouldBeNil) -} - -func BenchmarkWarp(b *testing.B) { - img, err := NewImageFromFile(artifact.MustPath("rimage/canny1.png")) - test.That(b, err, test.ShouldBeNil) - - size := 800 - - m := GetPerspectiveTransform( - []image.Point{ - {100, 100}, - {700, 100}, - {100, 700}, - {700, 700}, - }, - []image.Point{ - {0, 0}, - {size, 0}, - {0, size}, - {size, size}, - }) - - b.ResetTimer() - - for n := 0; n < b.N; n++ { - WarpImage(img, m, image.Point{size, size}) - } -} - -func TestWarpInvert(t *testing.T) { - toSlice := func(m mat.Matrix) []float64 { - a := []float64{} - for x := 0; x < 3; x++ { - for y := 0; y < 3; y++ { - a = append(a, m.At(x, y)) - } - } - return a - } - - doTest := func(inSlice, correct []float64) { - input := mat.NewDense(3, 3, inSlice) - output := toSlice(invert(input)) - test.That(t, output, test.ShouldHaveLength, len(correct)) - for i := 0; i < len(correct); i++ { - test.That(t, output[i], test.ShouldAlmostEqual, correct[i], .01) - } - } - - doTest( - []float64{1.66, 0, -1.66, 0, 1.66, -1.66, 0, 0, 1}, - []float64{0.6, 0, 1, 0, 0.6, 1.0, 0, 0, 1}, - ) - - doTest( - []float64{1.3333333333333333, 0, -133.3333, 0, 1.3333, -133.333, -0, -0, 1}, - []float64{0.75, 0, 100, 0, 0.75, 100, 0, 0, 1}, - ) -} - -func TestWarpSmall1(t *testing.T) { - // this is mostly making sure this test actually runs - // as it requires a non-standard matrix invert - img, err := readImageFromFile(artifact.MustPath("rimage/warpsmall1.jpg")) - test.That(t, err, test.ShouldBeNil) - - outputSize := image.Point{100, 100} - x := WarpImage(img, GetPerspectiveTransform( - []image.Point{{0, 170}, {0, 0}, {223, 0}, {223, 170}}, - ArrayToPoints([]image.Point{{0, 0}, {outputSize.X - 1, outputSize.Y - 1}}), - ), outputSize) - - err = WriteImageToFile(t.TempDir()+"/warpsmall1.png", x) - test.That(t, err, test.ShouldBeNil) -} diff --git a/robot/client/client_session_test.go b/robot/client/client_session_test.go deleted file mode 100644 index 753adce3877..00000000000 --- a/robot/client/client_session_test.go +++ /dev/null @@ -1,763 +0,0 @@ -package client_test - -import ( - "context" - "fmt" - "io" - "sync" - "testing" - "time" - - "github.com/google/uuid" - "github.com/pkg/errors" - "go.viam.com/test" - echopb "go.viam.com/utils/proto/rpc/examples/echoresource/v1" - "go.viam.com/utils/rpc" - "go.viam.com/utils/testutils" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/client" - "go.viam.com/rdk/robot/web" - "go.viam.com/rdk/session" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/testutils/robottestutils" -) - -/* -The client session tests here are fairly complicated because they make heavy use of dependency injection -in order to mimic the server side very deliberately in order to introduce failures that would be hard -with the actual production code. As a result, you'll find the server analogue to this to be much simpler -to reason about and in fact it ends up covering many similar cases but ones that are not as important to -client behavior. -*/ - -var ( - someTargetName1 = resource.NewName(resource.APINamespace("rdk").WithType("bar").WithSubtype("baz"), "barf") - someTargetName2 = resource.NewName(resource.APINamespace("rdk").WithType("bar").WithSubtype("baz"), "barfy") -) - -var echoAPI = resource.APINamespaceRDK.WithComponentType("echo") - -func init() { - resource.RegisterAPI(echoAPI, resource.APIRegistration[resource.Resource]{ - RPCServiceServerConstructor: func(apiResColl resource.APIResourceCollection[resource.Resource]) interface{} { - return &echoServer{coll: apiResColl} - }, - RPCServiceHandler: echopb.RegisterEchoResourceServiceHandlerFromEndpoint, - RPCServiceDesc: &echopb.EchoResourceService_ServiceDesc, - RPCClient: func( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, - ) (resource.Resource, error) { - return NewClientFromConn(ctx, conn, remoteName, name, logger), nil - }, - }) - resource.RegisterComponent( - echoAPI, - resource.DefaultModelFamily.WithModel("fake"), - resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - panic("never construct") - }, - }, - ) -} - -func TestClientSessionOptions(t *testing.T) { - t.Parallel() - ctx := context.Background() - for _, webrtcDisabled := range []bool{false, true} { - for _, sessionsDisabled := range []bool{false, true} { - for _, withRemoteName := range []bool{false, true} { - webrtcDisabledCopy := webrtcDisabled - withRemoteNameCopy := withRemoteName - sessionsDisabledCopy := sessionsDisabled - - t.Run( - fmt.Sprintf( - "webrtc disabled=%t,with remote name=%t,sessions disabled=%t", - webrtcDisabledCopy, - withRemoteNameCopy, - sessionsDisabledCopy, - ), - func(t *testing.T) { - t.Parallel() - - logger := logging.NewTestLogger(t) - - sessMgr := &sessionManager{} - arbName := resource.NewName(echoAPI, "woo") - injectRobot := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return []resource.Name{arbName} }, - ResourceByNameFunc: func(name resource.Name) (resource.Resource, error) { - return &dummyEcho{Named: arbName.AsNamed()}, nil - }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - LoggerFunc: func() logging.Logger { return logger }, - SessMgr: sessMgr, - } - - svc := web.New(injectRobot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - var opts []client.RobotClientOption - if sessionsDisabledCopy { - opts = append(opts, client.WithDisableSessions()) - } - if withRemoteNameCopy { - opts = append(opts, client.WithRemoteName("rem1")) - } - if webrtcDisabledCopy { - opts = append(opts, client.WithDialOptions(rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - Disable: true, - }))) - } - roboClient, err := client.New(ctx, addr, logger, opts...) - test.That(t, err, test.ShouldBeNil) - - injectRobot.Mu.Lock() - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - session.SafetyMonitorResourceName(ctx, someTargetName1) - return []robot.Status{}, nil - } - injectRobot.Mu.Unlock() - - var capMu sync.Mutex - var startCalled int - var findCalled int - var capOwnerID string - var capID uuid.UUID - var associateCount int - var storedID uuid.UUID - var storedResourceName resource.Name - - sess1 := session.New(context.Background(), "ownerID", 5*time.Second, func(id uuid.UUID, resourceName resource.Name) { - capMu.Lock() - associateCount++ - storedID = id - storedResourceName = resourceName - capMu.Unlock() - }) - nextCtx := session.ToContext(ctx, sess1) - - sessMgr.mu.Lock() - sessMgr.StartFunc = func(ctx context.Context, ownerID string) (*session.Session, error) { - capMu.Lock() - startCalled++ - capOwnerID = ownerID - capMu.Unlock() - return sess1, nil - } - sessMgr.FindByIDFunc = func(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - if id != sess1.ID() { - return nil, errors.New("session id mismatch") - } - capMu.Lock() - findCalled++ - capID = id - capOwnerID = ownerID - capMu.Unlock() - sess1.Heartbeat(ctx) // gotta keep session alive - return sess1, nil - } - sessMgr.mu.Unlock() - - resp, err := roboClient.Status(nextCtx, []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 0) - - if sessionsDisabledCopy { - // wait for any kind of heartbeat - time.Sleep(2 * time.Second) - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 0) - test.That(t, findCalled, test.ShouldEqual, 0) - capMu.Unlock() - } else { - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 1) - test.That(t, findCalled, test.ShouldEqual, 0) - - if webrtcDisabledCopy { - test.That(t, capOwnerID, test.ShouldEqual, "") - } else { - test.That(t, capOwnerID, test.ShouldNotEqual, "") - } - capMu.Unlock() - - startAt := time.Now() - testutils.WaitForAssertionWithSleep(t, time.Second, 10, func(tb testing.TB) { - tb.Helper() - - capMu.Lock() - defer capMu.Unlock() - test.That(tb, findCalled, test.ShouldBeGreaterThanOrEqualTo, 5) - test.That(tb, capID, test.ShouldEqual, sess1.ID()) - - if webrtcDisabledCopy { - test.That(tb, capOwnerID, test.ShouldEqual, "") - } else { - test.That(tb, capOwnerID, test.ShouldNotEqual, "") - } - }) - // testing against time but fairly generous range - test.That(t, time.Since(startAt), test.ShouldBeBetween, 4*time.Second, 7*time.Second) - } - - capMu.Lock() - if withRemoteNameCopy { - test.That(t, associateCount, test.ShouldEqual, 1) - test.That(t, storedID, test.ShouldEqual, sess1.ID()) - test.That(t, storedResourceName, test.ShouldResemble, someTargetName1.PrependRemote("rem1")) - } else { - test.That(t, associateCount, test.ShouldEqual, 0) - } - capMu.Unlock() - - echoRes, err := roboClient.ResourceByName(arbName) - test.That(t, err, test.ShouldBeNil) - echoClient := echoRes.(*dummyClient).client - - echoMultiClient, err := echoClient.EchoResourceMultiple(nextCtx, &echopb.EchoResourceMultipleRequest{ - Name: arbName.Name, - Message: "doesnotmatter", - }) - test.That(t, err, test.ShouldBeNil) - _, err = echoMultiClient.Recv() // EOF; okay - test.That(t, err, test.ShouldBeError, io.EOF) - - err = roboClient.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - capMu.Lock() - if withRemoteNameCopy { - test.That(t, associateCount, test.ShouldEqual, 2) - test.That(t, storedID, test.ShouldEqual, sess1.ID()) - test.That(t, storedResourceName, test.ShouldResemble, someTargetName2.PrependRemote("rem1")) - } else { - test.That(t, associateCount, test.ShouldEqual, 0) - } - capMu.Unlock() - - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - } - } - } -} - -func TestClientSessionExpiration(t *testing.T) { - t.Parallel() - ctx := context.Background() - for _, webrtcDisabled := range []bool{false, true} { - webrtcDisabledCopy := webrtcDisabled - - t.Run( - fmt.Sprintf( - "webrtc disabled=%t", - webrtcDisabledCopy, - ), - func(t *testing.T) { - t.Parallel() - - logger := logging.NewTestLogger(t) - - sessMgr := &sessionManager{} - arbName := resource.NewName(echoAPI, "woo") - - var dummyEcho1 dummyEcho - injectRobot := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return []resource.Name{arbName} }, - ResourceByNameFunc: func(name resource.Name) (resource.Resource, error) { - return &dummyEcho1, nil - }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - LoggerFunc: func() logging.Logger { return logger }, - SessMgr: sessMgr, - } - - svc := web.New(injectRobot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - var opts []client.RobotClientOption - if webrtcDisabledCopy { - opts = append(opts, client.WithDialOptions(rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - Disable: true, - }))) - } - roboClient, err := client.New(ctx, addr, logger, opts...) - test.That(t, err, test.ShouldBeNil) - - injectRobot.Mu.Lock() - var capSessID uuid.UUID - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - sess, ok := session.FromContext(ctx) - if !ok { - panic("expected session") - } - capSessID = sess.ID() - return []robot.Status{}, nil - } - injectRobot.Mu.Unlock() - - var capMu sync.Mutex - var startCalled int - var findCalled int - - sess1 := session.New(context.Background(), "ownerID", 5*time.Second, nil) - sess2 := session.New(context.Background(), "ownerID", 5*time.Second, nil) - sess3 := session.New(context.Background(), "ownerID", 5*time.Second, nil) - sessions := []*session.Session{sess1, sess2, sess3} - nextCtx := session.ToContext(ctx, sess1) - - sessMgr.mu.Lock() - sessMgr.StartFunc = func(ctx context.Context, ownerID string) (*session.Session, error) { - logger.Debug("start session requested") - capMu.Lock() - if startCalled != 0 && findCalled < 5 { - logger.Debug("premature start session") - return nil, errors.New("premature restart") - } - startCalled++ - findCalled = 0 - sess := sessions[startCalled-1] - capMu.Unlock() - - // like a restart - sessMgr.expired = false - logger.Debug("start session started") - return sess, nil - } - sessMgr.FindByIDFunc = func(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - capMu.Lock() - findCalled++ - if startCalled == 1 && findCalled >= 5 { // expired until restart - capMu.Unlock() - logger.Debug("enough heartbeats once; expire the session") - return nil, session.ErrNoSession - } - if startCalled == 2 && findCalled >= 5 { // expired until restart - capMu.Unlock() - logger.Debug("enough heartbeats twice; expire the session") - return nil, session.ErrNoSession - } - sess := sessions[startCalled-1] - if id != sess.ID() { - return nil, errors.New("session id mismatch") - } - capMu.Unlock() - sess.Heartbeat(ctx) // gotta keep session alive - return sess, nil - } - sessMgr.mu.Unlock() - - resp, err := roboClient.Status(nextCtx, []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 0) - - injectRobot.Mu.Lock() - test.That(t, capSessID, test.ShouldEqual, sess1.ID()) - injectRobot.Mu.Unlock() - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 1) - test.That(t, findCalled, test.ShouldEqual, 0) - capMu.Unlock() - - startAt := time.Now() - testutils.WaitForAssertionWithSleep(t, time.Second, 10, func(tb testing.TB) { - tb.Helper() - - capMu.Lock() - defer capMu.Unlock() - test.That(tb, findCalled, test.ShouldBeGreaterThanOrEqualTo, 5) - }) - // testing against time but fairly generous range - test.That(t, time.Since(startAt), test.ShouldBeBetween, 4*time.Second, 7*time.Second) - - sessMgr.mu.Lock() - sessMgr.expired = true - sessMgr.mu.Unlock() - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 1) - capMu.Unlock() - - logger.Debug("now call status which should work with a restarted session") - resp, err = roboClient.Status(nextCtx, []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 0) - - injectRobot.Mu.Lock() - test.That(t, capSessID, test.ShouldEqual, sess2.ID()) - injectRobot.Mu.Unlock() - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 2) - capMu.Unlock() - - testutils.WaitForAssertionWithSleep(t, time.Second, 10, func(tb testing.TB) { - tb.Helper() - - capMu.Lock() - defer capMu.Unlock() - test.That(tb, findCalled, test.ShouldBeGreaterThanOrEqualTo, 5) - }) - sessMgr.mu.Lock() - sessMgr.expired = true - sessMgr.mu.Unlock() - - echoRes, err := roboClient.ResourceByName(arbName) - test.That(t, err, test.ShouldBeNil) - echoClient := echoRes.(*dummyClient).client - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 2) - capMu.Unlock() - - echoMultiClient, err := echoClient.EchoResourceMultiple(nextCtx, &echopb.EchoResourceMultipleRequest{ - Name: arbName.Name, - Message: "doesnotmatter", - }) - test.That(t, err, test.ShouldBeNil) - _, err = echoMultiClient.Recv() // EOF; okay - test.That(t, err, test.ShouldBeError, io.EOF) - - dummyEcho1.mu.Lock() - test.That(t, dummyEcho1.capSessID, test.ShouldEqual, sess3.ID()) - dummyEcho1.mu.Unlock() - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 3) - capMu.Unlock() - - err = roboClient.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - } -} - -func TestClientSessionResume(t *testing.T) { - t.Parallel() - ctx := context.Background() - for _, webrtcDisabled := range []bool{false, true} { - webrtcDisabledCopy := webrtcDisabled - - t.Run( - fmt.Sprintf( - "webrtc disabled=%t", - webrtcDisabledCopy, - ), - func(t *testing.T) { - t.Parallel() - - logger := logging.NewTestLogger(t) - - sessMgr := &sessionManager{} - injectRobot := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return []resource.Name{} }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - LoggerFunc: func() logging.Logger { return logger }, - SessMgr: sessMgr, - } - - svc := web.New(injectRobot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - var opts []client.RobotClientOption - if webrtcDisabledCopy { - opts = append(opts, client.WithDialOptions(rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - Disable: true, - }))) - } - roboClient, err := client.New(ctx, addr, logger, opts...) - test.That(t, err, test.ShouldBeNil) - - var capMu sync.Mutex - var startCalled int - var findCalled int - - sess1 := session.New(context.Background(), "ownerID", 5*time.Second, nil) - nextCtx := session.ToContext(ctx, sess1) - - sessMgr.mu.Lock() - sessMgr.StartFunc = func(ctx context.Context, ownerID string) (*session.Session, error) { - logger.Debug("start session requested") - capMu.Lock() - startCalled++ - findCalled = 0 - capMu.Unlock() - return sess1, nil - } - sessMgr.FindByIDFunc = func(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - if id != sess1.ID() { - return nil, errors.New("session id mismatch") - } - capMu.Lock() - findCalled++ - capMu.Unlock() - sess1.Heartbeat(ctx) // gotta keep session alive - return sess1, nil - } - sessMgr.mu.Unlock() - - injectRobot.Mu.Lock() - var capSessID uuid.UUID - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - sess, ok := session.FromContext(ctx) - if !ok { - panic("expected session") - } - capSessID = sess.ID() - return []robot.Status{}, nil - } - injectRobot.Mu.Unlock() - - resp, err := roboClient.Status(nextCtx, []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 0) - - testutils.WaitForAssertionWithSleep(t, time.Second, 10, func(tb testing.TB) { - tb.Helper() - capMu.Lock() - defer capMu.Unlock() - test.That(tb, findCalled, test.ShouldBeGreaterThanOrEqualTo, 5) - }) - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 1) - capMu.Unlock() - - errFindCalled := make(chan struct{}) - sessMgr.mu.Lock() - sessMgr.FindByIDFunc = func(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - close(errFindCalled) - return nil, status.New(codes.Unavailable, "disconnected or something").Err() - } - sessMgr.mu.Unlock() - - <-errFindCalled - time.Sleep(time.Second) - - sessMgr.mu.Lock() - sessMgr.FindByIDFunc = func(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - if id != sess1.ID() { - return nil, errors.New("session id mismatch") - } - capMu.Lock() - findCalled++ - capMu.Unlock() - sess1.Heartbeat(ctx) // gotta keep session alive - return sess1, nil - } - sessMgr.mu.Unlock() - - resp, err = roboClient.Status(nextCtx, []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 0) - - capMu.Lock() - test.That(t, startCalled, test.ShouldEqual, 1) - capMu.Unlock() - - injectRobot.Mu.Lock() - test.That(t, capSessID, test.ShouldEqual, sess1.ID()) - injectRobot.Mu.Unlock() - - err = roboClient.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - } -} - -// we don't want everyone making an inject of this, so let's keep it here for now. -type sessionManager struct { - mu sync.Mutex - StartFunc func(ctx context.Context, ownerID string) (*session.Session, error) - FindByIDFunc func(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) - expired bool -} - -func (mgr *sessionManager) Start(ctx context.Context, ownerID string) (*session.Session, error) { - mgr.mu.Lock() - defer mgr.mu.Unlock() - return mgr.StartFunc(ctx, ownerID) -} - -func (mgr *sessionManager) All() []*session.Session { - panic("unimplemented") -} - -func (mgr *sessionManager) FindByID(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - mgr.mu.Lock() - defer mgr.mu.Unlock() - return mgr.FindByIDFunc(ctx, id, ownerID) -} - -func (mgr *sessionManager) AssociateResource(id uuid.UUID, resourceName resource.Name) { - panic("unimplemented") -} - -func (mgr *sessionManager) Close() { -} - -func (mgr *sessionManager) ServerInterceptors() session.ServerInterceptors { - return session.ServerInterceptors{ - // this is required for expiration tests which pull session info via interceptor - UnaryServerInterceptor: mgr.UnaryServerInterceptor, - StreamServerInterceptor: mgr.StreamServerInterceptor, - } -} - -func (mgr *sessionManager) sessionFromMetadata(ctx context.Context) (context.Context, error) { - meta, ok := metadata.FromIncomingContext(ctx) - if !ok { - return ctx, nil - } - - values := meta.Get(session.IDMetadataKey) - switch len(values) { - case 0: - return ctx, nil - case 1: - mgr.mu.Lock() - if mgr.expired { - mgr.mu.Unlock() - return nil, session.ErrNoSession - } - mgr.mu.Unlock() - sessID, err := uuid.Parse(values[0]) - if err != nil { - return nil, err - } - sess := session.NewWithID(ctx, sessID, "", time.Minute, nil) - return session.ToContext(ctx, sess), nil - default: - return nil, errors.New("found more than one session id in metadata") - } -} - -func (mgr *sessionManager) UnaryServerInterceptor( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, -) (interface{}, error) { - ctx, err := mgr.sessionFromMetadata(ctx) - if err != nil { - return nil, err - } - return handler(ctx, req) -} - -// StreamServerInterceptor associates the current session (if present) in the current context before -// passing it to the stream response handler. -func (mgr *sessionManager) StreamServerInterceptor( - srv interface{}, - ss grpc.ServerStream, - info *grpc.StreamServerInfo, - handler grpc.StreamHandler, -) error { - ctx, err := mgr.sessionFromMetadata(ss.Context()) - if err != nil { - return err - } - return handler(srv, &ssStreamContextWrapper{ss, ctx}) -} - -type ssStreamContextWrapper struct { - grpc.ServerStream - ctx context.Context -} - -func (w ssStreamContextWrapper) Context() context.Context { - return w.ctx -} - -// NewClientFromConn constructs a new client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) resource.Resource { - c := echopb.NewEchoResourceServiceClient(conn) - return &dummyClient{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - } -} - -type dummyClient struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - name string - client echopb.EchoResourceServiceClient -} - -type dummyEcho struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - mu sync.Mutex - capSessID uuid.UUID -} - -type echoServer struct { - echopb.UnimplementedEchoResourceServiceServer - coll resource.APIResourceCollection[resource.Resource] -} - -func (srv *echoServer) EchoResourceMultiple( - req *echopb.EchoResourceMultipleRequest, - server echopb.EchoResourceService_EchoResourceMultipleServer, -) error { - sess, ok := session.FromContext(server.Context()) - if ok { - res, err := srv.coll.Resource(req.Name) - if err != nil { - return err - } - typed, err := resource.AsType[*dummyEcho](res) - if err != nil { - return err - } - typed.mu.Lock() - typed.capSessID = sess.ID() - typed.mu.Unlock() - } - - session.SafetyMonitorResourceName(server.Context(), someTargetName2) - return nil -} diff --git a/robot/client/client_test.go b/robot/client/client_test.go deleted file mode 100644 index feee46e0d8b..00000000000 --- a/robot/client/client_test.go +++ /dev/null @@ -1,2049 +0,0 @@ -package client - -import ( - "bytes" - "context" - "fmt" - "image" - "image/png" - "io" - "math" - "net" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/edaniels/golog" - "github.com/go-viper/mapstructure/v2" - "github.com/golang/geo/r3" - "github.com/google/uuid" - "github.com/jhump/protoreflect/grpcreflect" - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - armpb "go.viam.com/api/component/arm/v1" - basepb "go.viam.com/api/component/base/v1" - boardpb "go.viam.com/api/component/board/v1" - camerapb "go.viam.com/api/component/camera/v1" - gripperpb "go.viam.com/api/component/gripper/v1" - inputcontrollerpb "go.viam.com/api/component/inputcontroller/v1" - motorpb "go.viam.com/api/component/motor/v1" - sensorpb "go.viam.com/api/component/sensor/v1" - servopb "go.viam.com/api/component/servo/v1" - pb "go.viam.com/api/robot/v1" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/rpc" - gotestutils "go.viam.com/utils/testutils" - "gonum.org/v1/gonum/num/quat" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/reflection" - "google.golang.org/grpc/status" - - "go.viam.com/rdk/cloud" - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/config" - "go.viam.com/rdk/gostream" - rgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/robot/server" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" - rutils "go.viam.com/rdk/utils" -) - -var emptyResources = []resource.Name{ - arm.Named("arm1"), - base.Named("base1"), - board.Named("board1"), - board.Named("board3"), - camera.Named("camera1"), - gripper.Named("gripper1"), -} - -var finalResources = []resource.Name{ - arm.Named("arm2"), - arm.Named("arm3"), - base.Named("base2"), - base.Named("base3"), - board.Named("board2"), - board.Named("board3"), - camera.Named("camera2"), - camera.Named("camera3"), - gripper.Named("gripper2"), - gripper.Named("gripper3"), - motor.Named("motor2"), - motor.Named("motor3"), - servo.Named("servo2"), - servo.Named("servo3"), -} - -var pose1 = spatialmath.NewZeroPose() - -type mockRPCSubtypesUnimplemented struct { - pb.UnimplementedRobotServiceServer - ResourceNamesFunc func(*pb.ResourceNamesRequest) (*pb.ResourceNamesResponse, error) -} - -func (ms *mockRPCSubtypesUnimplemented) ResourceNames( - ctx context.Context, req *pb.ResourceNamesRequest, -) (*pb.ResourceNamesResponse, error) { - return ms.ResourceNamesFunc(req) -} - -type mockRPCSubtypesImplemented struct { - mockRPCSubtypesUnimplemented - ResourceNamesFunc func(*pb.ResourceNamesRequest) (*pb.ResourceNamesResponse, error) -} - -func (ms *mockRPCSubtypesImplemented) ResourceRPCSubtypes( - ctx context.Context, _ *pb.ResourceRPCSubtypesRequest, -) (*pb.ResourceRPCSubtypesResponse, error) { - return &pb.ResourceRPCSubtypesResponse{}, nil -} - -func (ms *mockRPCSubtypesImplemented) ResourceNames( - ctx context.Context, req *pb.ResourceNamesRequest, -) (*pb.ResourceNamesResponse, error) { - return ms.ResourceNamesFunc(req) -} - -var resourceFunc1 = func(*pb.ResourceNamesRequest) (*pb.ResourceNamesResponse, error) { - board1 := board.Named("board1") - rNames := []*commonpb.ResourceName{ - { - Namespace: string(board1.API.Type.Namespace), - Type: board1.API.Type.Name, - Subtype: board1.API.SubtypeName, - Name: board1.Name, - }, - } - return &pb.ResourceNamesResponse{Resources: rNames}, nil -} - -var resourceFunc2 = func(*pb.ResourceNamesRequest) (*pb.ResourceNamesResponse, error) { - board1 := board.Named("board1") - board2 := board.Named("board2") - rNames := []*commonpb.ResourceName{ - { - Namespace: string(board1.API.Type.Namespace), - Type: board1.API.Type.Name, - Subtype: board1.API.SubtypeName, - Name: board1.Name, - }, - { - Namespace: string(board2.API.Type.Namespace), - Type: board2.API.Type.Name, - Subtype: board2.API.SubtypeName, - Name: board2.Name, - }, - } - return &pb.ResourceNamesResponse{Resources: rNames}, nil -} - -func makeRPCServer(logger golog.Logger, option rpc.ServerOption) (rpc.Server, net.Listener, error) { - err := errors.New("failed to make rpc server") - var addr string - var listener net.Listener - var server rpc.Server - - for i := 0; i < 10; i++ { - port, err := utils.TryReserveRandomPort() - if err != nil { - continue - } - - addr = fmt.Sprint("localhost:", port) - listener, err = net.Listen("tcp", addr) - if err != nil { - continue - } - - server, err = rpc.NewServer(logger, option) - if err != nil { - continue - } - return server, listener, nil - } - return nil, nil, err -} - -func TestUnimplementedRPCSubtypes(t *testing.T) { - var client1 *RobotClient // test implemented - var client2 *RobotClient // test unimplemented - ctx1, cancel := context.WithTimeout(context.Background(), time.Second*1) - defer cancel() - ctx2, cancel := context.WithTimeout(context.Background(), time.Second*1) - defer cancel() - logger1 := logging.NewTestLogger(t) - logger2 := logging.NewTestLogger(t) - - rpcServer1, listener1, err := makeRPCServer(logger1.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - rpcServer2, listener2, err := makeRPCServer(logger2.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, rpcServer2.Stop(), test.ShouldBeNil) - }() - defer func() { - test.That(t, rpcServer1.Stop(), test.ShouldBeNil) - }() - - implementedService := mockRPCSubtypesImplemented{ - ResourceNamesFunc: resourceFunc1, - } - - unimplementedService := mockRPCSubtypesUnimplemented{ - ResourceNamesFunc: resourceFunc1, - } - - err = rpcServer1.RegisterServiceServer( - ctx1, - &pb.RobotService_ServiceDesc, - &implementedService, - pb.RegisterRobotServiceHandlerFromEndpoint, - ) - test.That(t, err, test.ShouldBeNil) - - err = rpcServer2.RegisterServiceServer( - ctx2, - &pb.RobotService_ServiceDesc, - &unimplementedService, - pb.RegisterRobotServiceHandlerFromEndpoint) - test.That(t, err, test.ShouldBeNil) - - go func() { - test.That(t, rpcServer1.Serve(listener1), test.ShouldBeNil) - }() - go func() { - test.That(t, rpcServer2.Serve(listener2), test.ShouldBeNil) - }() - - client1, err = New( - ctx1, - listener1.Addr().String(), - logger1, - ) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, client1.Close(ctx1), test.ShouldBeNil) - }() - test.That(t, client1.Connected(), test.ShouldBeTrue) - test.That(t, client1.rpcSubtypesUnimplemented, test.ShouldBeFalse) - - client2, err = New( - ctx2, - listener2.Addr().String(), - logger2, - ) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, client2.Close(ctx2), test.ShouldBeNil) - }() - test.That(t, client2.Connected(), test.ShouldBeTrue) - test.That(t, client2.rpcSubtypesUnimplemented, test.ShouldBeTrue) - - // verify that the unimplemented check does not affect calls to ResourceNames - test.That(t, len(client2.ResourceNames()), test.ShouldEqual, 1) - _, err = client2.ResourceByName(board.Named("board1")) - test.That(t, err, test.ShouldBeNil) - - // still unimplemented, but with two resources - unimplementedService.ResourceNamesFunc = resourceFunc2 - err = client2.Refresh(ctx2) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(client2.ResourceNames()), test.ShouldEqual, 2) - _, err = client2.ResourceByName(board.Named("board2")) - test.That(t, err, test.ShouldBeNil) -} - -func TestStatusClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - listener2, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer1 := grpc.NewServer() - gServer2 := grpc.NewServer() - resourcesFunc := func() []resource.Name { - return []resource.Name{ - arm.Named("arm1"), - base.Named("base1"), - board.Named("board1"), - camera.Named("camera1"), - gripper.Named("gripper1"), - input.Named("inputController1"), - motor.Named("motor1"), - motor.Named("motor2"), - sensor.Named("sensor1"), - servo.Named("servo1"), - } - } - - // TODO(RSDK-882): will update this so that this is not necessary - frameSystemConfigFunc := func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - - injectRobot1 := &inject.Robot{ - FrameSystemConfigFunc: frameSystemConfigFunc, - ResourceNamesFunc: resourcesFunc, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - injectRobot2 := &inject.Robot{ - FrameSystemConfigFunc: frameSystemConfigFunc, - ResourceNamesFunc: resourcesFunc, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - pb.RegisterRobotServiceServer(gServer1, server.New(injectRobot1)) - pb.RegisterRobotServiceServer(gServer2, server.New(injectRobot2)) - - injectArm := &inject.Arm{} - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return pose1, nil - } - - injectBoard := &inject.Board{} - - injectCamera := &inject.Camera{} - img := image.NewNRGBA(image.Rect(0, 0, 4, 4)) - var imgBuf bytes.Buffer - test.That(t, png.Encode(&imgBuf, img), test.ShouldBeNil) - - var imageReleased bool - var imageReleasedMu sync.Mutex - injectCamera.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader(gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - imageReleasedMu.Lock() - imageReleased = true - imageReleasedMu.Unlock() - return img, func() {}, nil - })), nil - } - - injectInputDev := &inject.InputController{} - injectInputDev.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - return []input.Control{input.AbsoluteX, input.ButtonStart}, nil - } - - injectGripper := &inject.Gripper{} - var gripperOpenCalled bool - injectGripper.OpenFunc = func(ctx context.Context, extra map[string]interface{}) error { - gripperOpenCalled = true - return nil - } - var gripperGrabCalled bool - injectGripper.GrabFunc = func(ctx context.Context, extra map[string]interface{}) (bool, error) { - gripperGrabCalled = true - return true, nil - } - - injectServo := &inject.Servo{} - var capServoAngle uint32 - injectServo.MoveFunc = func(ctx context.Context, angle uint32, extra map[string]interface{}) error { - capServoAngle = angle - return nil - } - injectServo.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) { - return 5, nil - } - - armSvc1, err := resource.NewAPIResourceCollection(arm.API, map[resource.Name]arm.Arm{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&armpb.ArmService_ServiceDesc, arm.NewRPCServiceServer(armSvc1)) - - armSvc2, err := resource.NewAPIResourceCollection(arm.API, map[resource.Name]arm.Arm{arm.Named("arm1"): injectArm}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&armpb.ArmService_ServiceDesc, arm.NewRPCServiceServer(armSvc2)) - - baseSvc, err := resource.NewAPIResourceCollection(base.API, map[resource.Name]base.Base{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&basepb.BaseService_ServiceDesc, base.NewRPCServiceServer(baseSvc)) - - baseSvc2, err := resource.NewAPIResourceCollection(base.API, map[resource.Name]base.Base{base.Named("base1"): &inject.Base{}}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&basepb.BaseService_ServiceDesc, base.NewRPCServiceServer(baseSvc2)) - - boardSvc1, err := resource.NewAPIResourceCollection(board.API, map[resource.Name]board.Board{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&boardpb.BoardService_ServiceDesc, board.NewRPCServiceServer(boardSvc1)) - - boardSvc2, err := resource.NewAPIResourceCollection(board.API, map[resource.Name]board.Board{board.Named("board1"): injectBoard}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&boardpb.BoardService_ServiceDesc, board.NewRPCServiceServer(boardSvc2)) - - cameraSvc1, err := resource.NewAPIResourceCollection(camera.API, map[resource.Name]camera.Camera{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&camerapb.CameraService_ServiceDesc, camera.NewRPCServiceServer(cameraSvc1)) - - cameraSvc2, err := resource.NewAPIResourceCollection(camera.API, map[resource.Name]camera.Camera{camera.Named("camera1"): injectCamera}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&camerapb.CameraService_ServiceDesc, camera.NewRPCServiceServer(cameraSvc2)) - - gripperSvc1, err := resource.NewAPIResourceCollection(gripper.API, map[resource.Name]gripper.Gripper{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&gripperpb.GripperService_ServiceDesc, gripper.NewRPCServiceServer(gripperSvc1)) - - gripperSvc2, err := resource.NewAPIResourceCollection(gripper.API, - map[resource.Name]gripper.Gripper{gripper.Named("gripper1"): injectGripper}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&gripperpb.GripperService_ServiceDesc, gripper.NewRPCServiceServer(gripperSvc2)) - - inputControllerSvc1, err := resource.NewAPIResourceCollection(input.API, map[resource.Name]input.Controller{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&inputcontrollerpb.InputControllerService_ServiceDesc, input.NewRPCServiceServer(inputControllerSvc1)) - - inputControllerSvc2, err := resource.NewAPIResourceCollection( - input.API, map[resource.Name]input.Controller{input.Named("inputController1"): injectInputDev}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&inputcontrollerpb.InputControllerService_ServiceDesc, input.NewRPCServiceServer(inputControllerSvc2)) - - motorSvc, err := resource.NewAPIResourceCollection(motor.API, map[resource.Name]motor.Motor{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&motorpb.MotorService_ServiceDesc, motor.NewRPCServiceServer(motorSvc)) - - motorSvc2, err := resource.NewAPIResourceCollection(motor.API, - map[resource.Name]motor.Motor{motor.Named("motor1"): &inject.Motor{}, motor.Named("motor2"): &inject.Motor{}}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&motorpb.MotorService_ServiceDesc, motor.NewRPCServiceServer(motorSvc2)) - - servoSvc, err := resource.NewAPIResourceCollection(servo.API, map[resource.Name]servo.Servo{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&servopb.ServoService_ServiceDesc, servo.NewRPCServiceServer(servoSvc)) - - servoSvc2, err := resource.NewAPIResourceCollection(servo.API, map[resource.Name]servo.Servo{servo.Named("servo1"): injectServo}) - test.That(t, err, test.ShouldBeNil) - gServer2.RegisterService(&servopb.ServoService_ServiceDesc, servo.NewRPCServiceServer(servoSvc2)) - - sensorSvc, err := resource.NewAPIResourceCollection(sensor.API, map[resource.Name]sensor.Sensor{}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&sensorpb.SensorService_ServiceDesc, sensor.NewRPCServiceServer(sensorSvc)) - - go gServer1.Serve(listener1) - defer gServer1.Stop() - go gServer2.Serve(listener2) - defer gServer2.Stop() - - // failing - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = New(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - - o1 := &spatialmath.OrientationVectorDegrees{OX: 0, OY: 0, OZ: 1.0000000000000002, Theta: 7} - o1Cfg, err := spatialmath.NewOrientationConfig(o1) - test.That(t, err, test.ShouldBeNil) - - cfg := config.Config{ - Components: []resource.Config{ - { - Name: "a", - API: arm.API, - Frame: &referenceframe.LinkConfig{ - Parent: "b", - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Orientation: o1Cfg, - }, - }, - { - Name: "b", - API: base.API, - }, - }, - } - injectRobot1.ConfigFunc = func() *config.Config { - return &cfg - } - - client, err := New(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - arm1, err := arm.FromRobot(client, "arm1") - test.That(t, err, test.ShouldBeNil) - _, err = arm1.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - _, err = arm1.JointPositions(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - err = arm1.MoveToPosition(context.Background(), spatialmath.NewPoseFromPoint(r3.Vector{X: 1}), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - err = arm1.MoveToJointPositions(context.Background(), &armpb.JointPositions{Values: []float64{1}}, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - _, err = base.FromRobot(client, "base1") - test.That(t, err, test.ShouldBeNil) - - board1, err := board.FromRobot(client, "board1") - test.That(t, err, test.ShouldBeNil) - test.That(t, board1, test.ShouldNotBeNil) - pin, err := board1.GPIOPinByName("pin") - test.That(t, err, test.ShouldBeNil) - _, err = pin.Get(context.Background(), nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - camera1, err := camera.FromRobot(client, "camera1") - test.That(t, err, test.ShouldBeNil) - _, _, err = camera.ReadImage(context.Background(), camera1) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - gripper1, err := gripper.FromRobot(client, "gripper1") - test.That(t, err, test.ShouldBeNil) - err = gripper1.Open(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - _, err = gripper1.Grab(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - motor1, err := motor.FromRobot(client, "motor1") - test.That(t, err, test.ShouldBeNil) - err = motor1.SetPower(context.Background(), 0, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - err = motor1.GoFor(context.Background(), 0, 0, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - sensorDevice, err := sensor.FromRobot(client, "sensor1") - test.That(t, err, test.ShouldBeNil) - _, err = sensorDevice.Readings(context.Background(), make(map[string]interface{})) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - servo1, err := servo.FromRobot(client, "servo1") - test.That(t, err, test.ShouldBeNil) - err = servo1.Move(context.Background(), 5, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - _, err = servo1.Position(context.Background(), nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - resource1, err := client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = resource1.(arm.Arm).EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - _, err = resource1.(arm.Arm).JointPositions(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - err = resource1.(arm.Arm).MoveToPosition(context.Background(), spatialmath.NewPoseFromPoint(r3.Vector{X: 1}), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - err = resource1.(arm.Arm).MoveToJointPositions(context.Background(), &armpb.JointPositions{Values: []float64{1}}, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - // working - client, err = New(context.Background(), listener2.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, func() { client.RemoteByName("remote1") }, test.ShouldPanic) - - arm1, err = arm.FromRobot(client, "arm1") - test.That(t, err, test.ShouldBeNil) - pos, err := arm1.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostEqual(pos, pose1), test.ShouldBeTrue) - - _, err = base.FromRobot(client, "base1") - test.That(t, err, test.ShouldBeNil) - - _, err = board.FromRobot(client, "board1") - test.That(t, err, test.ShouldBeNil) - - camera1, err = camera.FromRobot(client, "camera1") - test.That(t, err, test.ShouldBeNil) - ctx := gostream.WithMIMETypeHint(context.Background(), rutils.MimeTypeRawRGBA) - frame, _, err := camera.ReadImage(ctx, camera1) - test.That(t, err, test.ShouldBeNil) - compVal, _, err := rimage.CompareImages(img, frame) - test.That(t, err, test.ShouldBeNil) - test.That(t, compVal, test.ShouldEqual, 0) // exact copy, no color conversion - imageReleasedMu.Lock() - test.That(t, imageReleased, test.ShouldBeTrue) - imageReleasedMu.Unlock() - - gripper1, err = gripper.FromRobot(client, "gripper1") - test.That(t, err, test.ShouldBeNil) - err = gripper1.Open(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, gripperOpenCalled, test.ShouldBeTrue) - test.That(t, gripperGrabCalled, test.ShouldBeFalse) - - inputDev, err := input.FromRobot(client, "inputController1") - test.That(t, err, test.ShouldBeNil) - controlList, err := inputDev.Controls(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, controlList, test.ShouldResemble, []input.Control{input.AbsoluteX, input.ButtonStart}) - - motor1, err = motor.FromRobot(client, "motor1") - test.That(t, err, test.ShouldBeNil) - test.That(t, motor1, test.ShouldNotBeNil) - - motor2, err := motor.FromRobot(client, "motor2") - test.That(t, err, test.ShouldBeNil) - test.That(t, motor2, test.ShouldNotBeNil) - - servo1, err = servo.FromRobot(client, "servo1") - test.That(t, err, test.ShouldBeNil) - err = servo1.Move(context.Background(), 4, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, capServoAngle, test.ShouldEqual, 4) - - currentVal, err := servo1.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, currentVal, test.ShouldEqual, 5) - - resource1, err = client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - pos, err = resource1.(arm.Arm).EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostEqual(pos, pose1), test.ShouldBeTrue) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestClientRefresh(t *testing.T) { - logger := logging.NewTestLogger(t) - - listener := gotestutils.ReserveRandomListener(t) - gServer := grpc.NewServer() - injectRobot := &inject.Robot{} - - var mu sync.RWMutex - dur := 100 * time.Millisecond - - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - go gServer.Serve(listener) - defer func() { - mu.Lock() - gServer.Stop() - mu.Unlock() - }() - t.Run("run with same reconnectTime and checkConnectedTime", func(t *testing.T) { - calledEnough := make(chan struct{}) - var callCountAPIs int - var callCountNames int - - mu.Lock() - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { - mu.Lock() - defer mu.Unlock() - callCountAPIs++ - if callCountAPIs == 6 { - close(calledEnough) - } - return nil - } - injectRobot.ResourceNamesFunc = func() []resource.Name { - mu.Lock() - defer mu.Unlock() - callCountNames++ - return emptyResources - } - mu.Unlock() - - start := time.Now() - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithRefreshEvery(dur), - WithCheckConnectedEvery(dur), - WithReconnectEvery(dur), - ) - test.That(t, err, test.ShouldBeNil) - // block here until ResourceNames is called 6 times - <-calledEnough - test.That(t, time.Since(start), test.ShouldBeGreaterThanOrEqualTo, 5*dur) - test.That(t, time.Since(start), test.ShouldBeLessThanOrEqualTo, 10*dur) - test.That(t, callCountAPIs, test.ShouldEqual, 6) - test.That(t, callCountNames, test.ShouldEqual, 6) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }) - - t.Run("run with different reconnectTime and checkConnectedTime", func(t *testing.T) { - calledEnough := make(chan struct{}) - var callCountAPIs int - var callCountNames int - - mu.Lock() - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { - mu.Lock() - defer mu.Unlock() - callCountAPIs++ - if callCountAPIs == 7 { - close(calledEnough) - } - return nil - } - injectRobot.ResourceNamesFunc = func() []resource.Name { - mu.Lock() - defer mu.Unlock() - callCountNames++ - return emptyResources - } - mu.Unlock() - - start := time.Now() - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithRefreshEvery(dur), - WithCheckConnectedEvery(dur*2), - WithReconnectEvery(dur), - ) - test.That(t, err, test.ShouldBeNil) - // block here until ResourceNames is called 7 times - <-calledEnough - test.That(t, time.Since(start), test.ShouldBeGreaterThanOrEqualTo, 3*dur) - test.That(t, time.Since(start), test.ShouldBeLessThanOrEqualTo, 7*dur) - test.That(t, callCountAPIs, test.ShouldEqual, 7) - test.That(t, callCountNames, test.ShouldEqual, 7) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }) - - t.Run("refresh tests", func(t *testing.T) { - mu.Lock() - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return finalResources } - mu.Unlock() - client, _ := New( - context.Background(), - listener.Addr().String(), - logger, - ) - - armNames := []resource.Name{arm.Named("arm2"), arm.Named("arm3")} - baseNames := []resource.Name{base.Named("base2"), base.Named("base3")} - - test.That(t, client.RemoteNames(), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(arm.NamesFromRobot(client)...), - test.ShouldResemble, - utils.NewStringSet(testutils.ExtractNames(armNames...)...), - ) - test.That(t, - utils.NewStringSet(base.NamesFromRobot(client)...), - test.ShouldResemble, - utils.NewStringSet(testutils.ExtractNames(baseNames...)...), - ) - - test.That(t, testutils.NewResourceNameSet(client.ResourceNames()...), test.ShouldResemble, testutils.NewResourceNameSet( - finalResources...)) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - - mu.Lock() - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return emptyResources } - mu.Unlock() - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - ) - test.That(t, err, test.ShouldBeNil) - - armNames = []resource.Name{arm.Named("arm1")} - baseNames = []resource.Name{base.Named("base1")} - - test.That(t, client.RemoteNames(), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(arm.NamesFromRobot(client)...), - test.ShouldResemble, - utils.NewStringSet(testutils.ExtractNames(armNames...)...), - ) - test.That(t, - utils.NewStringSet(base.NamesFromRobot(client)...), - test.ShouldResemble, - utils.NewStringSet(testutils.ExtractNames(baseNames...)...), - ) - - test.That(t, testutils.NewResourceNameSet(client.ResourceNames()...), test.ShouldResemble, testutils.NewResourceNameSet( - emptyResources...)) - - mu.Lock() - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return finalResources } - mu.Unlock() - test.That(t, client.Refresh(context.Background()), test.ShouldBeNil) - - armNames = []resource.Name{arm.Named("arm2"), arm.Named("arm3")} - baseNames = []resource.Name{base.Named("base2"), base.Named("base3")} - - test.That(t, client.RemoteNames(), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(arm.NamesFromRobot(client)...), - test.ShouldResemble, - utils.NewStringSet(testutils.ExtractNames(armNames...)...), - ) - test.That(t, - utils.NewStringSet(base.NamesFromRobot(client)...), - test.ShouldResemble, - utils.NewStringSet(testutils.ExtractNames(baseNames...)...), - ) - - test.That(t, testutils.NewResourceNameSet(client.ResourceNames()...), test.ShouldResemble, testutils.NewResourceNameSet( - finalResources...)) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }) -} - -func TestClientDisconnect(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer := grpc.NewServer() - injectRobot := &inject.Robot{} - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{arm.Named("arm1")} - } - - // TODO(RSDK-882): will update this so that this is not necessary - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - - go gServer.Serve(listener) - - start := time.Now() - - test.That(t, err, test.ShouldBeNil) - - dur := 100 * time.Millisecond - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithCheckConnectedEvery(dur), - WithReconnectEvery(2*dur), - ) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }() - - test.That(t, client.Connected(), test.ShouldBeTrue) - test.That(t, len(client.ResourceNames()), test.ShouldEqual, 1) - _, err = client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - - gServer.Stop() - test.That(t, <-client.Changed(), test.ShouldBeTrue) - test.That(t, client.Connected(), test.ShouldBeFalse) - timeSinceStart := time.Since(start) - test.That(t, timeSinceStart, test.ShouldBeBetweenOrEqual, dur, 4*dur) - test.That(t, len(client.ResourceNames()), test.ShouldEqual, 0) - _, err = client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeError, client.checkConnected()) -} - -func TestClientUnaryDisconnectHandler(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - - var unaryStatusCallReceived bool - justOneUnaryStatusCall := grpc.ChainUnaryInterceptor( - func( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, - ) (interface{}, error) { - if strings.HasSuffix(info.FullMethod, "RobotService/GetStatus") { - if unaryStatusCallReceived { - return nil, status.Error(codes.Unknown, io.ErrClosedPipe.Error()) - } - unaryStatusCallReceived = true - } - var resp interface{} - return resp, nil - }, - ) - gServer := grpc.NewServer(justOneUnaryStatusCall) - - injectRobot := &inject.Robot{} - injectRobot.StatusFunc = func(ctx context.Context, rs []resource.Name) ([]robot.Status, error) { - return []robot.Status{}, nil - } - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - - go gServer.Serve(listener) - - never := -1 * time.Second - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithCheckConnectedEvery(never), - WithReconnectEvery(never), - ) - test.That(t, err, test.ShouldBeNil) - - t.Run("unary call to connected remote", func(t *testing.T) { - t.Helper() - - client.connected.Store(false) - _, err = client.Status(context.Background(), []resource.Name{}) - test.That(t, status.Code(err), test.ShouldEqual, codes.Unavailable) - test.That(t, err.Error(), test.ShouldContainSubstring, fmt.Sprintf("not connected to remote robot at %s", listener.Addr().String())) - test.That(t, unaryStatusCallReceived, test.ShouldBeFalse) - client.connected.Store(true) - }) - - t.Run("unary call to disconnected remote", func(t *testing.T) { - t.Helper() - - _, err = client.Status(context.Background(), []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, unaryStatusCallReceived, test.ShouldBeTrue) - }) - - t.Run("unary call to undetected disconnected remote", func(t *testing.T) { - test.That(t, unaryStatusCallReceived, test.ShouldBeTrue) - _, err = client.Status(context.Background(), []resource.Name{}) - test.That(t, status.Code(err), test.ShouldEqual, codes.Unavailable) - test.That(t, err.Error(), test.ShouldContainSubstring, fmt.Sprintf("not connected to remote robot at %s", listener.Addr().String())) - }) - - defer func() { - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }() - gServer.Stop() -} - -func TestClientStreamDisconnectHandler(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - - var streamStatusCallReceived bool - interceptStreamStatusCall := grpc.ChainStreamInterceptor( - func( - srv interface{}, - ss grpc.ServerStream, - info *grpc.StreamServerInfo, - handler grpc.StreamHandler, - ) error { - if strings.HasSuffix(info.FullMethod, "RobotService/StreamStatus") { - streamStatusCallReceived = true - } - return handler(srv, ss) - }, - ) - - gServer := grpc.NewServer(interceptStreamStatusCall) - - injectRobot := &inject.Robot{} - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return nil } - injectRobot.StatusFunc = func(ctx context.Context, rs []resource.Name) ([]robot.Status, error) { - return []robot.Status{}, nil - } - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - - go gServer.Serve(listener) - - never := -1 * time.Second - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithCheckConnectedEvery(never), - WithReconnectEvery(never), - ) - test.That(t, err, test.ShouldBeNil) - - t.Run("stream call to disconnected remote", func(t *testing.T) { - t.Helper() - - client.connected.Store(false) - _, err = client.client.StreamStatus(context.Background(), &pb.StreamStatusRequest{}) - test.That(t, status.Code(err), test.ShouldEqual, codes.Unavailable) - test.That(t, err.Error(), test.ShouldContainSubstring, fmt.Sprintf("not connected to remote robot at %s", listener.Addr().String())) - test.That(t, streamStatusCallReceived, test.ShouldBeFalse) - client.connected.Store(true) - }) - - t.Run("stream call to connected remote", func(t *testing.T) { - t.Helper() - - ssc, err := client.client.StreamStatus(context.Background(), &pb.StreamStatusRequest{}) - test.That(t, err, test.ShouldBeNil) - ssc.Recv() - test.That(t, streamStatusCallReceived, test.ShouldBeTrue) - }) - - t.Run("receive call from stream of disconnected remote", func(t *testing.T) { - t.Helper() - - ssc, err := client.client.StreamStatus(context.Background(), &pb.StreamStatusRequest{}) - test.That(t, err, test.ShouldBeNil) - - client.connected.Store(false) - _, err = ssc.Recv() - test.That(t, status.Code(err), test.ShouldEqual, codes.Unavailable) - test.That(t, err.Error(), test.ShouldContainSubstring, fmt.Sprintf("not connected to remote robot at %s", listener.Addr().String())) - client.connected.Store(true) - }) - - defer func() { - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }() - gServer.Stop() -} - -type mockType struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable -} - -func TestClientReconnect(t *testing.T) { - someAPI := resource.APINamespace("acme").WithComponentType(uuid.New().String()) - var called int64 - resource.RegisterAPI( - someAPI, - resource.APIRegistration[resource.Resource]{ - RPCClient: func( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, - ) (resource.Resource, error) { - atomic.AddInt64(&called, 1) - return &mockType{Named: name.AsNamed()}, nil - }, - }, - ) - - logger := logging.NewTestLogger(t) - - var listener net.Listener = gotestutils.ReserveRandomListener(t) - gServer := grpc.NewServer() - injectRobot := &inject.Robot{} - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - thing1Name := resource.NewName(someAPI, "thing1") - injectRobot.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{arm.Named("arm1"), thing1Name} - } - - // TODO(RSDK-882): will update this so that this is not necessary - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - - injectArm := &inject.Arm{} - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return pose1, nil - } - - armSvc2, err := resource.NewAPIResourceCollection(arm.API, map[resource.Name]arm.Arm{arm.Named("arm1"): injectArm}) - test.That(t, err, test.ShouldBeNil) - gServer.RegisterService(&armpb.ArmService_ServiceDesc, arm.NewRPCServiceServer(armSvc2)) - - go gServer.Serve(listener) - - dur := 100 * time.Millisecond - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithCheckConnectedEvery(dur), - WithReconnectEvery(dur), - ) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }() - - test.That(t, len(client.ResourceNames()), test.ShouldEqual, 2) - _, err = client.ResourceByName(thing1Name) - test.That(t, err, test.ShouldBeNil) - a, err := client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = a.(arm.Arm).EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, atomic.LoadInt64(&called), test.ShouldEqual, 1) - - gServer.Stop() - - test.That(t, <-client.Changed(), test.ShouldBeTrue) - test.That(t, len(client.ResourceNames()), test.ShouldEqual, 0) - _, err = client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeError, client.checkConnected()) - - gServer2 := grpc.NewServer() - pb.RegisterRobotServiceServer(gServer2, server.New(injectRobot)) - gServer2.RegisterService(&armpb.ArmService_ServiceDesc, arm.NewRPCServiceServer(armSvc2)) - - // Note: There's a slight chance this test can fail if someone else - // claims the port we just released by closing the server. - listener, err = net.Listen("tcp", listener.Addr().String()) - test.That(t, err, test.ShouldBeNil) - go gServer2.Serve(listener) - defer gServer2.Stop() - - test.That(t, <-client.Changed(), test.ShouldBeTrue) - test.That(t, client.Connected(), test.ShouldBeTrue) - test.That(t, len(client.ResourceNames()), test.ShouldEqual, 2) - _, err = client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = a.(arm.Arm).EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, atomic.LoadInt64(&called), test.ShouldEqual, 1) -} - -func TestClientRefreshNoReconfigure(t *testing.T) { - someAPI := resource.APINamespace("acme").WithComponentType(uuid.New().String()) - var called int64 - resource.RegisterAPI( - someAPI, - resource.APIRegistration[resource.Resource]{ - RPCClient: func( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, - ) (resource.Resource, error) { - atomic.AddInt64(&called, 1) - return &mockType{Named: name.AsNamed()}, nil - }, - }, - ) - - logger := logging.NewTestLogger(t) - - var listener net.Listener = gotestutils.ReserveRandomListener(t) - gServer := grpc.NewServer() - injectRobot := &inject.Robot{} - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - thing1Name := resource.NewName(someAPI, "thing1") - - var callCount int - calledEnough := make(chan struct{}) - - allow := make(chan struct{}) - injectRobot.ResourceNamesFunc = func() []resource.Name { - if callCount == 1 { - <-allow - } - if callCount == 5 { - close(calledEnough) - } - callCount++ - - return []resource.Name{arm.Named("arm1"), thing1Name} - } - - go gServer.Serve(listener) - defer gServer.Stop() - - dur := 100 * time.Millisecond - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithRefreshEvery(dur), - ) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }() - - close(allow) - <-calledEnough - - test.That(t, len(client.ResourceNames()), test.ShouldEqual, 2) - - _, err = client.ResourceByName(thing1Name) - test.That(t, err, test.ShouldBeNil) - - test.That(t, atomic.LoadInt64(&called), test.ShouldEqual, 1) -} - -func TestClientDialerOption(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer := grpc.NewServer() - - go gServer.Serve(listener) - defer gServer.Stop() - - td := &testutils.TrackingDialer{Dialer: rpc.NewCachedDialer()} - ctx := rpc.ContextWithDialer(context.Background(), td) - client1, err := New(ctx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, td.NewConnections, test.ShouldEqual, 3) - - client2, err := New(ctx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, td.NewConnections, test.ShouldEqual, 3) - - err = client1.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - err = client2.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestClientResources(t *testing.T) { - injectRobot := &inject.Robot{} - - desc1, err := grpcreflect.LoadServiceDescriptor(&pb.RobotService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - desc2, err := grpcreflect.LoadServiceDescriptor(&armpb.ArmService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - respWith := []resource.RPCAPI{ - { - API: resource.APINamespace("acme").WithComponentType("huwat"), - Desc: desc1, - }, - { - API: resource.APINamespace("acme").WithComponentType("wat"), - Desc: desc2, - }, - } - - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return respWith } - injectRobot.ResourceNamesFunc = func() []resource.Name { return finalResources } - - gServer := grpc.NewServer() - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - logger := logging.NewTestLogger(t) - - go gServer.Serve(listener) - - client, err := New(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - // no reflection - resources, rpcAPIs, err := client.resources(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, resources, test.ShouldResemble, finalResources) - test.That(t, rpcAPIs, test.ShouldBeEmpty) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - gServer.Stop() - - // with reflection - gServer = grpc.NewServer() - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - reflection.Register(gServer) - test.That(t, err, test.ShouldBeNil) - listener, err = net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - go gServer.Serve(listener) - defer gServer.Stop() - - client, err = New(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - resources, rpcAPIs, err = client.resources(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, resources, test.ShouldResemble, finalResources) - - test.That(t, rpcAPIs, test.ShouldHaveLength, len(respWith)) - for idx, rpcType := range rpcAPIs { - otherT := respWith[idx] - test.That(t, rpcType.API, test.ShouldResemble, otherT.API) - test.That(t, rpcType.Desc.AsProto(), test.ShouldResemble, otherT.Desc.AsProto()) - } - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestClientDiscovery(t *testing.T) { - injectRobot := &inject.Robot{} - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { - return finalResources - } - q := resource.DiscoveryQuery{movementsensor.Named("foo").API, resource.DefaultModelFamily.WithModel("something")} - injectRobot.DiscoverComponentsFunc = func(ctx context.Context, keys []resource.DiscoveryQuery) ([]resource.Discovery, error) { - return []resource.Discovery{{ - Query: q, - Results: map[string]interface{}{"abc": []float64{1.2, 2.3, 3.4}}, - }}, nil - } - - gServer := grpc.NewServer() - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - logger := logging.NewTestLogger(t) - - go gServer.Serve(listener) - defer gServer.Stop() - - client, err := New(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - resp, err := client.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{q}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 1) - test.That(t, resp[0].Query, test.ShouldResemble, q) - test.That(t, resp[0].Results, test.ShouldResemble, map[string]interface{}{"abc": []interface{}{1.2, 2.3, 3.4}}) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func ensurePartsAreEqual(part, otherPart *referenceframe.FrameSystemPart) error { - if part.FrameConfig.Name() != otherPart.FrameConfig.Name() { - return errors.Errorf("part had name %s while other part had name %s", part.FrameConfig.Name(), otherPart.FrameConfig.Name()) - } - frameConfig := part.FrameConfig - otherFrameConfig := otherPart.FrameConfig - if frameConfig.Parent() != otherFrameConfig.Parent() { - return errors.Errorf("part had parent %s while other part had parent %s", frameConfig.Parent(), otherFrameConfig.Parent()) - } - if !spatialmath.R3VectorAlmostEqual(frameConfig.Pose().Point(), otherFrameConfig.Pose().Point(), 1e-8) { - return errors.New("translations of parts not equal") - } - - orient := frameConfig.Pose().Orientation() - otherOrient := otherFrameConfig.Pose().Orientation() - - switch { - case orient == nil && otherOrient != nil: - if !spatialmath.QuaternionAlmostEqual(otherOrient.Quaternion(), quat.Number{1, 0, 0, 0}, 1e-5) { - return errors.New("orientations of parts not equal") - } - case otherOrient == nil: - return errors.New("orientation not returned for other part") - case !spatialmath.OrientationAlmostEqual(orient, otherOrient): - return errors.New("orientations of parts not equal") - } - return nil -} - -func TestClientConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - listener2, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - workingServer := grpc.NewServer() - failingServer := grpc.NewServer() - - resourcesFunc := func() []resource.Name { return []resource.Name{} } - workingRobot := &inject.Robot{ - ResourceNamesFunc: resourcesFunc, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - failingRobot := &inject.Robot{ - ResourceNamesFunc: resourcesFunc, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - - o1 := &spatialmath.R4AA{Theta: math.Pi / 2, RZ: 1} - o1Cfg, err := spatialmath.NewOrientationConfig(o1) - test.That(t, err, test.ShouldBeNil) - - l1 := &referenceframe.LinkConfig{ - ID: "frame1", - Parent: referenceframe.World, - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Orientation: o1Cfg, - Geometry: &spatialmath.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, - } - lif1, err := l1.ParseConfig() - test.That(t, err, test.ShouldBeNil) - l2 := &referenceframe.LinkConfig{ - ID: "frame2", - Parent: "frame1", - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Geometry: &spatialmath.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, - } - lif2, err := l2.ParseConfig() - test.That(t, err, test.ShouldBeNil) - - fsConfigs := []*referenceframe.FrameSystemPart{ - { - FrameConfig: lif1, - }, - { - FrameConfig: lif2, - }, - } - - workingRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{Parts: fsConfigs}, nil - } - - configErr := errors.New("failed to retrieve config") - failingRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return nil, configErr - } - - pb.RegisterRobotServiceServer(workingServer, server.New(workingRobot)) - pb.RegisterRobotServiceServer(failingServer, server.New(failingRobot)) - - go workingServer.Serve(listener1) - defer workingServer.Stop() - - ctx := context.Background() - - t.Run("Failing client due to cancellation", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(ctx) - cancel() - _, err = New(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - workingFSClient, err := New(ctx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client test config for working frame service", func(t *testing.T) { - config, err := workingFSClient.FrameSystemConfig(ctx) - test.That(t, err, test.ShouldBeNil) - err = ensurePartsAreEqual(fsConfigs[0], config.Parts[0]) - test.That(t, err, test.ShouldBeNil) - err = ensurePartsAreEqual(fsConfigs[1], config.Parts[1]) - test.That(t, err, test.ShouldBeNil) - }) - - err = workingFSClient.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - t.Run("dialed client test config for working frame service", func(t *testing.T) { - workingDialedClient, err := New(ctx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - config, err := workingDialedClient.FrameSystemConfig(ctx) - test.That(t, err, test.ShouldBeNil) - err = ensurePartsAreEqual(fsConfigs[0], config.Parts[0]) - test.That(t, err, test.ShouldBeNil) - err = ensurePartsAreEqual(fsConfigs[1], config.Parts[1]) - test.That(t, err, test.ShouldBeNil) - err = workingDialedClient.Close(ctx) - test.That(t, err, test.ShouldBeNil) - }) - - go failingServer.Serve(listener2) - defer failingServer.Stop() - - failingFSClient, err := New(ctx, listener2.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client test config for failing frame service", func(t *testing.T) { - frameSystemParts, err := failingFSClient.FrameSystemConfig(ctx) - test.That(t, frameSystemParts, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - }) - - err = failingFSClient.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - t.Run("dialed client test config for failing frame service with failing config", func(t *testing.T) { - failingDialedClient, err := New(ctx, listener2.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - parts, err := failingDialedClient.FrameSystemConfig(ctx) - test.That(t, parts, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - - err = failingDialedClient.Close(ctx) - test.That(t, err, test.ShouldBeNil) - }) -} - -func TestClientStatus(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - listener2, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer := grpc.NewServer() - gServer2 := grpc.NewServer() - - injectRobot := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return []resource.Name{} }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - injectRobot2 := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return []resource.Name{} }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - pb.RegisterRobotServiceServer(gServer2, server.New(injectRobot2)) - - go gServer.Serve(listener1) - defer gServer.Stop() - - go gServer2.Serve(listener2) - defer gServer2.Stop() - - t.Run("failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = New(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - t.Run("working status service", func(t *testing.T) { - client, err := New(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - gLastReconfigured, err := time.Parse("2006-01-02 15:04:05", "1998-04-30 19:08:00") - test.That(t, err, test.ShouldBeNil) - gStatus := robot.Status{ - Name: movementsensor.Named("gps"), - LastReconfigured: gLastReconfigured, - Status: map[string]interface{}{"efg": []string{"hello"}}, - } - aLastReconfigured, err := time.Parse("2006-01-02 15:04:05", "2011-11-11 00:00:00") - test.That(t, err, test.ShouldBeNil) - aStatus := robot.Status{ - Name: arm.Named("arm"), - LastReconfigured: aLastReconfigured, - Status: struct{}{}, - } - statusMap := map[resource.Name]robot.Status{ - gStatus.Name: gStatus, - aStatus.Name: aStatus, - } - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - statuses := make([]robot.Status, 0, len(resourceNames)) - for _, n := range resourceNames { - statuses = append(statuses, statusMap[n]) - } - return statuses, nil - } - expected := map[resource.Name]interface{}{ - gStatus.Name: map[string]interface{}{"efg": []interface{}{"hello"}}, - aStatus.Name: map[string]interface{}{}, - } - expectedLRs := map[resource.Name]time.Time{ - gStatus.Name: gLastReconfigured, - aStatus.Name: aLastReconfigured, - } - resp, err := client.Status(context.Background(), []resource.Name{aStatus.Name}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 1) - test.That(t, resp[0].Status, test.ShouldResemble, expected[resp[0].Name]) - test.That(t, resp[0].LastReconfigured, test.ShouldResemble, expectedLRs[resp[0].Name]) - - result := struct{}{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &result}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(resp[0].Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldResemble, aStatus.Status) - - resp, err = client.Status(context.Background(), []resource.Name{gStatus.Name, aStatus.Name}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 2) - - observed := map[resource.Name]interface{}{ - resp[0].Name: resp[0].Status, - resp[1].Name: resp[1].Status, - } - observedLRs := map[resource.Name]time.Time{ - resp[0].Name: resp[0].LastReconfigured, - resp[1].Name: resp[1].LastReconfigured, - } - test.That(t, observed, test.ShouldResemble, expected) - test.That(t, observedLRs, test.ShouldResemble, expectedLRs) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("failing status client", func(t *testing.T) { - client2, err := New(context.Background(), listener2.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - passedErr := errors.New("can't get status") - injectRobot2.StatusFunc = func(ctx context.Context, status []resource.Name) ([]robot.Status, error) { - return nil, passedErr - } - _, err = client2.Status(context.Background(), []resource.Name{}) - test.That(t, err.Error(), test.ShouldContainSubstring, passedErr.Error()) - - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - }) -} - -func TestForeignResource(t *testing.T) { - injectRobot := &inject.Robot{} - - desc1, err := grpcreflect.LoadServiceDescriptor(&pb.RobotService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - desc2, err := grpcreflect.LoadServiceDescriptor(&armpb.ArmService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - subtype1 := resource.APINamespace("acme").WithComponentType("huwat") - subtype2 := resource.APINamespace("acme").WithComponentType("wat") - respWith := []resource.RPCAPI{ - { - API: resource.APINamespace("acme").WithComponentType("huwat"), - Desc: desc1, - }, - { - API: resource.APINamespace("acme").WithComponentType("wat"), - Desc: desc2, - }, - } - - respWithResources := []resource.Name{ - arm.Named("arm1"), - resource.NewName(subtype1, "thing1"), - resource.NewName(subtype2, "thing2"), - } - - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return respWith } - injectRobot.ResourceNamesFunc = func() []resource.Name { return respWithResources } - // TODO(RSDK-882): will update this so that this is not necessary - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - - gServer := grpc.NewServer() - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - reflection.Register(gServer) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - logger := logging.NewTestLogger(t) - - go gServer.Serve(listener) - defer gServer.Stop() - - client, err := New(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - res1, err := client.ResourceByName(respWithResources[0]) - test.That(t, err, test.ShouldBeNil) - test.That(t, res1, test.ShouldImplement, (*arm.Arm)(nil)) - - res2, err := client.ResourceByName(respWithResources[1]) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldHaveSameTypeAs, (*rgrpc.ForeignResource)(nil)) - test.That(t, res2.(*rgrpc.ForeignResource).Name(), test.ShouldResemble, respWithResources[1]) - - res3, err := client.ResourceByName(respWithResources[2]) - test.That(t, err, test.ShouldBeNil) - test.That(t, res3, test.ShouldHaveSameTypeAs, (*rgrpc.ForeignResource)(nil)) - test.That(t, res3.(*rgrpc.ForeignResource).Name(), test.ShouldResemble, respWithResources[2]) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestNewRobotClientRefresh(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer := grpc.NewServer() - injectRobot := &inject.Robot{} - var callCount int - - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { - callCount++ - return emptyResources - } - - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - - go gServer.Serve(listener) - defer gServer.Stop() - - dur := -100 * time.Millisecond - client, err := New( - context.Background(), - listener.Addr().String(), - logger, - WithRefreshEvery(dur), - ) - - test.That(t, err, test.ShouldBeNil) - test.That(t, client, test.ShouldNotBeNil) - test.That(t, callCount, test.ShouldEqual, 1) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - callCount = 0 - dur = 0 - client, err = New( - context.Background(), - listener.Addr().String(), - logger, - WithRefreshEvery(dur), - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, client, test.ShouldNotBeNil) - test.That(t, callCount, test.ShouldEqual, 1) - - gServer.Stop() - test.That(t, <-client.Changed(), test.ShouldBeTrue) - test.That(t, client.Connected(), test.ShouldBeFalse) - err = client.Refresh(context.Background()) - test.That(t, err.Error(), test.ShouldContainSubstring, "not connected to remote robot") - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestClientStopAll(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer1 := grpc.NewServer() - resourcesFunc := func() []resource.Name { return []resource.Name{} } - stopAllCalled := false - injectRobot1 := &inject.Robot{ - ResourceNamesFunc: resourcesFunc, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - StopAllFunc: func(ctx context.Context, extra map[resource.Name]map[string]interface{}) error { - stopAllCalled = true - return nil - }, - } - pb.RegisterRobotServiceServer(gServer1, server.New(injectRobot1)) - - go gServer1.Serve(listener1) - defer gServer1.Stop() - - client, err := New(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - err = client.StopAll(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, stopAllCalled, test.ShouldBeTrue) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestRemoteClientMatch(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer1 := grpc.NewServer() - validResources := []resource.Name{arm.Named("remote:arm1")} - injectRobot1 := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return validResources }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - - // TODO(RSDK-882): will update this so that this is not necessary - injectRobot1.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - pb.RegisterRobotServiceServer(gServer1, server.New(injectRobot1)) - - injectArm := &inject.Arm{} - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return pose1, nil - } - - armSvc1, err := resource.NewAPIResourceCollection(arm.API, map[resource.Name]arm.Arm{arm.Named("remote:arm1"): injectArm}) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&armpb.ArmService_ServiceDesc, arm.NewRPCServiceServer(armSvc1)) - - go gServer1.Serve(listener1) - defer gServer1.Stop() - - // working - dur := 100 * time.Millisecond - client, err := New( - context.Background(), - listener1.Addr().String(), - logger, - WithRefreshEvery(dur), - ) - test.That(t, err, test.ShouldBeNil) - - resource1, err := client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, client.resourceClients[arm.Named("remote:arm1")], test.ShouldEqual, resource1) - pos, err := resource1.(arm.Arm).EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostEqual(pos, pose1), test.ShouldBeTrue) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestRemoteClientDuplicate(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer1 := grpc.NewServer() - validResources := []resource.Name{arm.Named("remote1:arm1"), arm.Named("remote2:arm1")} - injectRobot1 := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return validResources }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - pb.RegisterRobotServiceServer(gServer1, server.New(injectRobot1)) - - injectArm := &inject.Arm{} - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return pose1, nil - } - - armSvc1, err := resource.NewAPIResourceCollection(arm.API, map[resource.Name]arm.Arm{ - arm.Named("remote1:arm1"): injectArm, - arm.Named("remote2:arm1"): injectArm, - }) - test.That(t, err, test.ShouldBeNil) - gServer1.RegisterService(&armpb.ArmService_ServiceDesc, arm.NewRPCServiceServer(armSvc1)) - - go gServer1.Serve(listener1) - defer gServer1.Stop() - - // working - dur := 100 * time.Millisecond - client, err := New( - context.Background(), - listener1.Addr().String(), - logger, - WithRefreshEvery(dur), - ) - test.That(t, err, test.ShouldBeNil) - - _, err = client.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(arm.Named("arm1"))) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestClientOperationIntercept(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - - injectRobot := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return []resource.Name{} }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - - gServer := grpc.NewServer() - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - - go gServer.Serve(listener1) - defer gServer.Stop() - - ctx := context.Background() - var fakeArgs interface{} - fakeManager := operation.NewManager(logger) - ctx, done := fakeManager.Create(ctx, "fake", fakeArgs) - defer done() - fakeOp := operation.Get(ctx) - test.That(t, fakeOp, test.ShouldNotBeNil) - - client, err := New(ctx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - meta, ok := metadata.FromIncomingContext(ctx) - test.That(t, ok, test.ShouldBeTrue) - receivedOpID, err := operation.GetOrCreateFromMetadata(meta) - test.That(t, err, test.ShouldBeNil) - test.That(t, receivedOpID.String(), test.ShouldEqual, fakeOp.ID.String()) - return []robot.Status{}, nil - } - - resp, err := client.Status(ctx, []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 0) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestGetUnknownResource(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - - injectRobot := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return []resource.Name{arm.Named("myArm")} }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - - // TODO(RSDK-882): will update this so that this is not necessary - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - - gServer := grpc.NewServer() - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - - go gServer.Serve(listener1) - defer gServer.Stop() - - client, err := New(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - // grabbing known resource is fine - myArm, err := client.ResourceByName(arm.Named("myArm")) - test.That(t, err, test.ShouldBeNil) - test.That(t, myArm, test.ShouldNotBeNil) - - // grabbing unknown resource returns error - _, err = client.ResourceByName(base.Named("notABase")) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(base.Named("notABase"))) - - err = client.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestLoggingInterceptor(t *testing.T) { - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - - // A server with the logging interceptor looks for some values in the grpc request metadata and - // will call unary functions with a modified context. - gServer := grpc.NewServer(grpc.ChainUnaryInterceptor(logging.UnaryServerInterceptor)) - injectRobot := &inject.Robot{ - // Needed for client connect. Not important to the test. - ResourceNamesFunc: func() []resource.Name { return []resource.Name{arm.Named("myArm")} }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - - // Hijack the `StatusFunc` for testing the reception of debug metadata via the - // logging/distributed tracing interceptor. - StatusFunc: func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - switch len(resourceNames) { - case 0: - // The status call with a nil `resourceNames` signals there should be no debug - // information on the context. - if logging.IsDebugMode(ctx) || logging.GetName(ctx) != "" { - return nil, fmt.Errorf("Bad context. DebugMode? %v Name: %v", logging.IsDebugMode(ctx), logging.GetName(ctx)) - } - case 1: - // The status call with a `resourceNames` of length 1 signals there should be debug - // information with `oliver`. - if !logging.IsDebugMode(ctx) || logging.GetName(ctx) != "oliver" { - return nil, fmt.Errorf("Bad context. DebugMode? %v Name: %v", logging.IsDebugMode(ctx), logging.GetName(ctx)) - } - default: - return nil, fmt.Errorf("Bad resource names: %v", resourceNames) - } - - return nil, nil - }, - } - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - - go gServer.Serve(listener) - defer gServer.Stop() - - // Clients by default have an interceptor that serializes context debug information as grpc - // metadata. - client, err := New(context.Background(), listener.Addr().String(), logging.NewTestLogger(t)) - test.That(t, err, test.ShouldBeNil) - defer client.Close(context.Background()) - - // The status call with a nil `resourceNames` signals there should be no debug information on - // the context. - _, err = client.Status(context.Background(), []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - - // The status call with a `resourceNames` of length 1 signals there should be debug information - // with `oliver`. - _, err = client.Status(logging.EnableDebugModeWithKey(context.Background(), "oliver"), []resource.Name{{}}) - test.That(t, err, test.ShouldBeNil) -} - -func TestCloudMetadata(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - gServer := grpc.NewServer() - - injectCloudMD := cloud.Metadata{ - LocationID: "the-location", - PrimaryOrgID: "the-primary-org", - MachineID: "the-machine", - MachinePartID: "the-robot-part", - } - injectRobot := &inject.Robot{ - ResourceNamesFunc: func() []resource.Name { return nil }, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - CloudMetadataFunc: func(ctx context.Context) (cloud.Metadata, error) { - return injectCloudMD, nil - }, - } - // TODO(RSDK-882): will update this so that this is not necessary - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) - - go gServer.Serve(listener) - defer gServer.Stop() - - client, err := New(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - }() - - md, err := client.CloudMetadata(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, md, test.ShouldResemble, injectCloudMD) -} diff --git a/robot/client/verify_main_test.go b/robot/client/verify_main_test.go deleted file mode 100644 index f0c62676fc5..00000000000 --- a/robot/client/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package client - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/robot/framesystem/framesystem_test.go b/robot/framesystem/framesystem_test.go deleted file mode 100644 index 38bc14c982d..00000000000 --- a/robot/framesystem/framesystem_test.go +++ /dev/null @@ -1,275 +0,0 @@ -package framesystem_test - -import ( - "context" - "math" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - - _ "go.viam.com/rdk/components/register" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - robotimpl "go.viam.com/rdk/robot/impl" - _ "go.viam.com/rdk/services/register" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -func TestEmptyConfigFrameService(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - r, err := robotimpl.New(ctx, &config.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - fsCfg, err := r.FrameSystemConfig(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, fsCfg.Parts, test.ShouldHaveLength, 0) - fs, err := referenceframe.NewFrameSystem("test", fsCfg.Parts, fsCfg.AdditionalTransforms) - test.That(t, err, test.ShouldBeNil) - test.That(t, fs.FrameNames(), test.ShouldHaveLength, 0) -} - -func TestNewFrameSystemFromConfig(t *testing.T) { - o1 := &spatialmath.R4AA{Theta: math.Pi / 2, RZ: 1} - o1Cfg, err := spatialmath.NewOrientationConfig(o1) - test.That(t, err, test.ShouldBeNil) - - l1 := &referenceframe.LinkConfig{ - ID: "frame1", - Parent: referenceframe.World, - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Orientation: o1Cfg, - Geometry: &spatialmath.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, - } - lif1, err := l1.ParseConfig() - test.That(t, err, test.ShouldBeNil) - - l2 := &referenceframe.LinkConfig{ - ID: "frame2", - Parent: "frame1", - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - } - lif2, err := l2.ParseConfig() - test.That(t, err, test.ShouldBeNil) - - parts := []*referenceframe.FrameSystemPart{ - { - FrameConfig: lif1, - }, - { - FrameConfig: lif2, - }, - } - frameSys, err := referenceframe.NewFrameSystem("test", parts, []*referenceframe.LinkInFrame{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, frameSys, test.ShouldNotBeNil) - frame1 := frameSys.Frame("frame1") - frame1Origin := frameSys.Frame("frame1_origin") - frame2 := frameSys.Frame("frame2") - frame2Origin := frameSys.Frame("frame2_origin") - - resFrame, err := frameSys.Parent(frame2) - test.That(t, err, test.ShouldBeNil) - test.That(t, resFrame, test.ShouldResemble, frame2Origin) - resFrame, err = frameSys.Parent(frame2Origin) - test.That(t, err, test.ShouldBeNil) - test.That(t, resFrame, test.ShouldResemble, frame1) - resFrame, err = frameSys.Parent(frame1) - test.That(t, err, test.ShouldBeNil) - test.That(t, resFrame, test.ShouldResemble, frame1Origin) - resFrame, err = frameSys.Parent(frame1Origin) - test.That(t, err, test.ShouldBeNil) - test.That(t, resFrame, test.ShouldResemble, frameSys.World()) -} - -func TestNewFrameSystemFromConfigWithTransforms(t *testing.T) { - // use robot/impl/data/fake.json as config input - ctx := context.Background() - emptyIn := []referenceframe.Input{} - zeroIn := []referenceframe.Input{{Value: 0.0}} - blankPos := make(map[string][]referenceframe.Input) - blankPos["pieceArm"] = zeroIn - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), rdkutils.ResolveFile("robot/impl/data/fake.json"), logger) - test.That(t, err, test.ShouldBeNil) - - r, err := robotimpl.New(context.Background(), cfg, logger) - test.That(t, err, test.ShouldBeNil) - defer r.Close(context.Background()) - fsCfg, err := r.FrameSystemConfig(ctx) - test.That(t, err, test.ShouldBeNil) - - // use fake registrations to have a FrameSystem return - testPose := spatialmath.NewPose( - r3.Vector{X: 1., Y: 2., Z: 3.}, - &spatialmath.R4AA{Theta: math.Pi / 2, RX: 0., RY: 1., RZ: 0.}, - ) - - fsCfg.AdditionalTransforms = []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame("pieceArm", testPose, "frame1", nil), - referenceframe.NewLinkInFrame("pieceGripper", testPose, "frame2", nil), - referenceframe.NewLinkInFrame("frame2", testPose, "frame2a", nil), - referenceframe.NewLinkInFrame("frame2", testPose, "frame2c", nil), - referenceframe.NewLinkInFrame(referenceframe.World, testPose, "frame3", nil), - } - - fs, err := referenceframe.NewFrameSystem("test", fsCfg.Parts, fsCfg.AdditionalTransforms) - test.That(t, err, test.ShouldBeNil) - // 4 frames defined + 5 from transforms, 18 frames when including the offset, - test.That(t, len(fs.FrameNames()), test.ShouldEqual, 18) - - // see if all frames are present and if their frames are correct - test.That(t, fs.Frame("world"), test.ShouldNotBeNil) - - t.Log("pieceArm") - test.That(t, fs.Frame("pieceArm"), test.ShouldNotBeNil) - pose, err := fs.Frame("pieceArm").Transform(zeroIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{500, 0, 300}), test.ShouldBeTrue) - - t.Log("pieceArm_origin") - test.That(t, fs.Frame("pieceArm_origin"), test.ShouldNotBeNil) - pose, err = fs.Frame("pieceArm_origin").Transform(emptyIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{500, 500, 1000}), test.ShouldBeTrue) - - t.Log("pieceGripper") - test.That(t, fs.Frame("pieceGripper"), test.ShouldNotBeNil) - pose, err = fs.Frame("pieceGripper").Transform(emptyIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{}), test.ShouldBeTrue) - - t.Log("pieceGripper_origin") - test.That(t, fs.Frame("pieceGripper_origin"), test.ShouldNotBeNil) - pose, err = fs.Frame("pieceGripper_origin").Transform(emptyIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{}), test.ShouldBeTrue) - - t.Log("movement_sensor2") - test.That(t, fs.Frame("movement_sensor2"), test.ShouldNotBeNil) - pose, err = fs.Frame("movement_sensor2").Transform(emptyIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{}), test.ShouldBeTrue) - - t.Log("movement_sensor2_origin") - test.That(t, fs.Frame("movement_sensor2_origin"), test.ShouldNotBeNil) - pose, err = fs.Frame("movement_sensor2_origin").Transform(emptyIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{}), test.ShouldBeTrue) - - t.Log("cameraOver") - test.That(t, fs.Frame("cameraOver"), test.ShouldNotBeNil) - pose, err = fs.Frame("cameraOver").Transform(emptyIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{}), test.ShouldBeTrue) - - t.Log("cameraOver_origin") - test.That(t, fs.Frame("cameraOver_origin"), test.ShouldNotBeNil) - pose, err = fs.Frame("cameraOver_origin").Transform(emptyIn) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Point().ApproxEqual(r3.Vector{2000, 500, 1300}), test.ShouldBeTrue) - - t.Log("movement_sensor1") - test.That(t, fs.Frame("movement_sensor1"), test.ShouldBeNil) // movement_sensor1 is not registered - - // There is a point at (1500, 500, 1300) in the world referenceframe. See if it transforms correctly in each referenceframe. - worldPose := referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewPoseFromPoint(r3.Vector{1500, 500, 1300})) - armPt := r3.Vector{500, 0, 0} - tf, err := fs.Transform(blankPos, worldPose, "pieceArm") - test.That(t, err, test.ShouldBeNil) - transformPose, _ := tf.(*referenceframe.PoseInFrame) - test.That(t, transformPose.Pose().Point().ApproxEqual(armPt), test.ShouldBeTrue) - - sensorPt := r3.Vector{500, 0, 0} - tf, err = fs.Transform(blankPos, worldPose, "movement_sensor2") - test.That(t, err, test.ShouldBeNil) - transformPose, _ = tf.(*referenceframe.PoseInFrame) - test.That(t, transformPose.Pose().Point().ApproxEqual(sensorPt), test.ShouldBeTrue) - - gripperPt := r3.Vector{500, 0, 0} - tf, err = fs.Transform(blankPos, worldPose, "pieceGripper") - test.That(t, err, test.ShouldBeNil) - transformPose, _ = tf.(*referenceframe.PoseInFrame) - test.That(t, transformPose.Pose().Point().ApproxEqual(gripperPt), test.ShouldBeTrue) - - cameraPt := r3.Vector{500, 0, 0} - tf, err = fs.Transform(blankPos, worldPose, "cameraOver") - test.That(t, err, test.ShouldBeNil) - transformPose, _ = tf.(*referenceframe.PoseInFrame) - test.That(t, spatialmath.R3VectorAlmostEqual(transformPose.Pose().Point(), cameraPt, 1e-8), test.ShouldBeTrue) - - // go from camera point to gripper point - cameraPose := referenceframe.NewPoseInFrame("cameraOver", spatialmath.NewPoseFromPoint(cameraPt)) - tf, err = fs.Transform(blankPos, cameraPose, "pieceGripper") - test.That(t, err, test.ShouldBeNil) - transformPose, _ = tf.(*referenceframe.PoseInFrame) - test.That(t, spatialmath.R3VectorAlmostEqual(transformPose.Pose().Point(), gripperPt, 1e-8), test.ShouldBeTrue) -} - -func TestNewFrameSystemFromBadConfig(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - testCases := []struct { - name string - num string - err error - }{ - {"no world node", "2", referenceframe.ErrNoWorldConnection}, - {"frame named world", "3", errors.Errorf("cannot give frame system part the name %s", referenceframe.World)}, - {"parent field empty", "4", errors.New("parent field in frame config for part \"cameraOver\" is empty")}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - cfg, err := config.Read(ctx, rdkutils.ResolveFile("robot/impl/data/fake_wrongconfig"+tc.num+".json"), logger) - test.That(t, err, test.ShouldBeNil) - r, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - defer r.Close(ctx) - fsCfg, err := r.FrameSystemConfig(ctx) - if err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - return - } - _, err = referenceframe.NewFrameSystem(tc.num, fsCfg.Parts, fsCfg.AdditionalTransforms) - test.That(t, err, test.ShouldBeError, tc.err) - }) - } - - cfg, err := config.Read(ctx, rdkutils.ResolveFile("robot/impl/data/fake.json"), logger) - test.That(t, err, test.ShouldBeNil) - r, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - defer r.Close(ctx) - - testPose := spatialmath.NewPose(r3.Vector{X: 1., Y: 2., Z: 3.}, &spatialmath.R4AA{Theta: math.Pi / 2, RX: 0., RY: 1., RZ: 0.}) - - t.Run("frame missing parent", func(t *testing.T) { - transforms := []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame("pieceArm", testPose, "frame1", nil), - referenceframe.NewLinkInFrame("noParent", testPose, "frame2", nil), - } - fsCfg, err := r.FrameSystemConfig(ctx) - test.That(t, err, test.ShouldBeNil) - fsCfg.AdditionalTransforms = transforms - fs, err := referenceframe.NewFrameSystem("", fsCfg.Parts, fsCfg.AdditionalTransforms) - test.That(t, err, test.ShouldBeError, referenceframe.NewParentFrameMissingError("frame2", "noParent")) - test.That(t, fs, test.ShouldBeNil) - }) - - t.Run("empty string frame name", func(t *testing.T) { - transforms := []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame("pieceArm", testPose, "", nil), - } - fsCfg, err := r.FrameSystemConfig(ctx) - test.That(t, err, test.ShouldBeNil) - fsCfg.AdditionalTransforms = transforms - fs, err := referenceframe.NewFrameSystem("", fsCfg.Parts, fsCfg.AdditionalTransforms) - test.That(t, err, test.ShouldBeError, referenceframe.ErrEmptyStringFrameName) - test.That(t, fs, test.ShouldBeNil) - }) -} diff --git a/robot/impl/discovery_test.go b/robot/impl/discovery_test.go deleted file mode 100644 index be3cfae5500..00000000000 --- a/robot/impl/discovery_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package robotimpl - -import ( - "context" - "testing" - - "github.com/pkg/errors" - modulepb "go.viam.com/api/module/v1" - "go.viam.com/test" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - rtestutils "go.viam.com/rdk/testutils" -) - -func setupLocalRobotWithFakeConfig(t *testing.T) robot.LocalRobot { - t.Helper() - - logger := logging.NewTestLogger(t) - ctx := context.Background() - cfg, err := config.Read(ctx, "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - return setupLocalRobot(t, ctx, cfg, logger) -} - -var ( - workingAPI = resource.APINamespace("acme").WithComponentType("working-discovery") - workingModel = resource.DefaultModelFamily.WithModel("workingModel") - workingQ = resource.NewDiscoveryQuery(workingAPI, workingModel) - - failAPI = resource.APINamespace("acme").WithComponentType("failing-discovery") - failModel = resource.DefaultModelFamily.WithModel("failModel") - failQ = resource.NewDiscoveryQuery(failAPI, failModel) - - noDiscoverModel = resource.DefaultModelFamily.WithModel("nodiscoverModel") - noDiscoverQ = resource.DiscoveryQuery{failAPI, noDiscoverModel} - - modManagerAPI = resource.NewAPI("rdk-internal", "service", "module-manager") - modManagerModel = resource.NewModel("rdk-internal", "builtin", "module-manager") - modManagerQ = resource.NewDiscoveryQuery(modManagerAPI, modManagerModel) - - missingQ = resource.NewDiscoveryQuery(failAPI, resource.DefaultModelFamily.WithModel("missing")) - - workingDiscovery = map[string]interface{}{"position": "up"} - errFailed = errors.New("can't get discovery") -) - -func init() { - resource.Register(workingQ.API, workingQ.Model, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (resource.Resource, error) { - return nil, errors.New("no") - }, - Discover: func(ctx context.Context, logger logging.Logger) (interface{}, error) { - return workingDiscovery, nil - }, - }) - - resource.Register(failQ.API, failQ.Model, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, - ) (resource.Resource, error) { - return nil, errors.New("no") - }, - Discover: func(ctx context.Context, logger logging.Logger) (interface{}, error) { - return nil, errFailed - }, - }) -} - -func TestDiscovery(t *testing.T) { - t.Run("not found", func(t *testing.T) { - r := setupLocalRobotWithFakeConfig(t) - discoveries, err := r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{missingQ}) - test.That(t, discoveries, test.ShouldBeEmpty) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("no Discover", func(t *testing.T) { - r := setupLocalRobotWithFakeConfig(t) - discoveries, err := r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{noDiscoverQ}) - test.That(t, err, test.ShouldBeNil) - test.That(t, discoveries, test.ShouldBeEmpty) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("failing Discover", func(t *testing.T) { - r := setupLocalRobotWithFakeConfig(t) - _, err := r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{failQ}) - test.That(t, err, test.ShouldBeError, &resource.DiscoverError{failQ}) - }) - - t.Run("working Discover", func(t *testing.T) { - r := setupLocalRobotWithFakeConfig(t) - discoveries, err := r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{workingQ}) - test.That(t, err, test.ShouldBeNil) - test.That(t, discoveries, test.ShouldResemble, []resource.Discovery{{Query: workingQ, Results: workingDiscovery}}) - }) - - t.Run("duplicated working Discover", func(t *testing.T) { - r := setupLocalRobotWithFakeConfig(t) - discoveries, err := r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{workingQ, workingQ, workingQ}) - test.That(t, err, test.ShouldBeNil) - test.That(t, discoveries, test.ShouldResemble, []resource.Discovery{{Query: workingQ, Results: workingDiscovery}}) - }) - - t.Run("working and missing Discover", func(t *testing.T) { - r := setupLocalRobotWithFakeConfig(t) - discoveries, err := r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{workingQ, missingQ}) - test.That(t, err, test.ShouldBeNil) - test.That(t, discoveries, test.ShouldResemble, []resource.Discovery{{Query: workingQ, Results: workingDiscovery}}) - }) - - t.Run("internal module manager Discover", func(t *testing.T) { - r := setupLocalRobotWithFakeConfig(t) - ctx := context.Background() - - // test with empty modmanager - discoveries, err := r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{modManagerQ}) - test.That(t, err, test.ShouldBeNil) - expectedHandlerMap := map[string]modulepb.HandlerMap{} - expectedDiscovery := moduleManagerDiscoveryResult{ - ResourceHandles: expectedHandlerMap, - } - test.That(t, discoveries, test.ShouldResemble, []resource.Discovery{{Query: modManagerQ, Results: expectedDiscovery}}) - - // add modules - complexPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - simplePath := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "simple", - ExePath: simplePath, - }, - { - Name: "complex", - ExePath: complexPath, - }, - }, - } - r.Reconfigure(ctx, cfg) - - // rerun discovery expecting a full tree of resources - discoveries, err = r.DiscoverComponents(context.Background(), []resource.DiscoveryQuery{modManagerQ}) - test.That(t, err, test.ShouldBeNil) - test.That(t, discoveries, test.ShouldHaveLength, 1) - modManagerDiscovery, ok := discoveries[0].Results.(moduleManagerDiscoveryResult) - test.That(t, ok, test.ShouldBeTrue) - resourceHandles := modManagerDiscovery.ResourceHandles - test.That(t, resourceHandles, test.ShouldHaveLength, 2) - //nolint:govet // we copy an internal lock -- it is okay - simpleHandles, ok := resourceHandles["simple"] - test.That(t, ok, test.ShouldBeTrue) - test.That(t, simpleHandles.Handlers, test.ShouldHaveLength, 1) - test.That(t, simpleHandles.Handlers[0].Models, test.ShouldResemble, []string{"acme:demo:mycounter"}) - test.That(t, simpleHandles.Handlers[0].Subtype.Subtype.Namespace, test.ShouldResemble, "rdk") - test.That(t, simpleHandles.Handlers[0].Subtype.Subtype.Type, test.ShouldResemble, "component") - test.That(t, simpleHandles.Handlers[0].Subtype.Subtype.Subtype, test.ShouldResemble, "generic") - - //nolint:govet // we copy an internal lock -- it is okay - complexHandles, ok := resourceHandles["complex"] - test.That(t, ok, test.ShouldBeTrue) - // confirm that complex handlers are also present in the map - test.That(t, len(complexHandles.Handlers), test.ShouldBeGreaterThan, 1) - }) -} diff --git a/robot/impl/local_robot.go b/robot/impl/local_robot.go index 53694bef06b..f901b9089c7 100644 --- a/robot/impl/local_robot.go +++ b/robot/impl/local_robot.go @@ -359,11 +359,7 @@ func newWithResources( logger logging.Logger, opts ...Option, ) (robot.LocalRobot, error) { - var rOpts options var err error - for _, opt := range opts { - opt.apply(&rOpts) - } closeCtx, cancel := context.WithCancel(ctx) r := &localRobot{ @@ -377,14 +373,13 @@ func newWithResources( }, logger, ), - operations: operation.NewManager(logger), - logger: logger, - closeContext: closeCtx, - cancelBackgroundWorkers: cancel, - triggerConfig: make(chan struct{}), - configTicker: nil, - revealSensitiveConfigDiffs: rOpts.revealSensitiveConfigDiffs, - cloudConnSvc: icloud.NewCloudConnectionService(cfg.Cloud, logger), + operations: operation.NewManager(logger), + logger: logger, + closeContext: closeCtx, + cancelBackgroundWorkers: cancel, + triggerConfig: make(chan struct{}), + configTicker: nil, + cloudConnSvc: icloud.NewCloudConnectionService(cfg.Cloud, logger), } r.mostRecentCfg.Store(config.Config{}) var heartbeatWindow time.Duration @@ -427,7 +422,7 @@ func newWithResources( // we assume these never appear in our configs and as such will not be removed from the // resource graph - r.webSvc = web.New(r, logger, rOpts.webOptions...) + r.webSvc = web.New(r, logger) r.frameSvc, err = framesystem.New(ctx, resource.Dependencies{}, logger) if err != nil { return nil, err @@ -463,9 +458,6 @@ func newWithResources( } homeDir := config.ViamDotDir - if rOpts.viamHomeDir != "" { - homeDir = rOpts.viamHomeDir - } // Once web service is started, start module manager r.manager.startModuleManager( closeCtx, diff --git a/robot/impl/local_robot_test.go b/robot/impl/local_robot_test.go deleted file mode 100644 index 8053e461834..00000000000 --- a/robot/impl/local_robot_test.go +++ /dev/null @@ -1,3378 +0,0 @@ -package robotimpl - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "math" - "net" - "os" - "path" - "strings" - "testing" - "time" - - "github.com/go-viper/mapstructure/v2" - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.uber.org/zap" - // registers all components. - commonpb "go.viam.com/api/common/v1" - armpb "go.viam.com/api/component/arm/v1" - pb "go.viam.com/api/robot/v1" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/pexec" - "go.viam.com/utils/rpc" - "go.viam.com/utils/testutils" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "go.viam.com/rdk/cloud" - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/arm/fake" - "go.viam.com/rdk/components/audioinput" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/encoder" - fakeencoder "go.viam.com/rdk/components/encoder/fake" - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/components/motor" - fakemotor "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/components/movementsensor" - _ "go.viam.com/rdk/components/register" - "go.viam.com/rdk/config" - "go.viam.com/rdk/examples/customresources/apis/gizmoapi" - "go.viam.com/rdk/examples/customresources/apis/summationapi" - rgrpc "go.viam.com/rdk/grpc" - internalcloud "go.viam.com/rdk/internal/cloud" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/client" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/robot/packages" - putils "go.viam.com/rdk/robot/packages/testutils" - "go.viam.com/rdk/robot/server" - weboptions "go.viam.com/rdk/robot/web/options" - "go.viam.com/rdk/services/datamanager" - "go.viam.com/rdk/services/datamanager/builtin" - genericservice "go.viam.com/rdk/services/generic" - "go.viam.com/rdk/services/motion" - motionBuiltin "go.viam.com/rdk/services/motion/builtin" - "go.viam.com/rdk/services/navigation" - _ "go.viam.com/rdk/services/register" - "go.viam.com/rdk/services/sensors" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/spatialmath" - rtestutils "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/testutils/robottestutils" - rutils "go.viam.com/rdk/utils" -) - -var fakeModel = resource.DefaultModelFamily.WithModel("fake") - -func TestConfig1(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/cfgtest1.json", logger) - test.That(t, err, test.ShouldBeNil) - - r := setupLocalRobot(t, context.Background(), cfg, logger) - - c1, err := camera.FromRobot(r, "c1") - test.That(t, err, test.ShouldBeNil) - test.That(t, c1.Name(), test.ShouldResemble, camera.Named("c1")) - pic, _, err := camera.ReadImage(context.Background(), c1) - test.That(t, err, test.ShouldBeNil) - - bounds := pic.Bounds() - - test.That(t, bounds.Max.X, test.ShouldBeGreaterThanOrEqualTo, 32) -} - -func TestConfigFake(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - setupLocalRobot(t, context.Background(), cfg, logger) -} - -// this serves as a test for updateWeakDependents as the web service defines a weak -// dependency on all resources. -func TestConfigRemote(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - o1 := &spatialmath.R4AA{math.Pi / 2., 0, 0, 1} - o1Cfg, err := spatialmath.NewOrientationConfig(o1) - test.That(t, err, test.ShouldBeNil) - - remoteConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "foo", - API: base.API, - Model: fakeModel, - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - }, - }, - { - Name: "myParentIsRemote", - API: base.API, - Model: fakeModel, - Frame: &referenceframe.LinkConfig{ - Parent: "foo:cameraOver", - }, - }, - }, - Services: []resource.Config{}, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr, - Frame: &referenceframe.LinkConfig{ - Parent: "foo", - Translation: r3.Vector{100, 200, 300}, - Orientation: o1Cfg, - }, - }, - { - Name: "bar", - Address: addr, - }, - { - Name: "squee", - Address: addr, - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - Translation: r3.Vector{100, 200, 300}, - Orientation: o1Cfg, - }, - }, - }, - } - - ctx2 := context.Background() - r2 := setupLocalRobot(t, ctx2, remoteConfig, logger) - - expected := []resource.Name{ - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("squee:pieceArm"), - arm.Named("foo:pieceArm"), - arm.Named("bar:pieceArm"), - base.Named("foo"), - base.Named("myParentIsRemote"), - camera.Named("squee:cameraOver"), - camera.Named("foo:cameraOver"), - camera.Named("bar:cameraOver"), - audioinput.Named("squee:mic1"), - audioinput.Named("foo:mic1"), - audioinput.Named("bar:mic1"), - movementsensor.Named("squee:movement_sensor1"), - movementsensor.Named("foo:movement_sensor1"), - movementsensor.Named("bar:movement_sensor1"), - movementsensor.Named("squee:movement_sensor2"), - movementsensor.Named("foo:movement_sensor2"), - movementsensor.Named("bar:movement_sensor2"), - gripper.Named("squee:pieceGripper"), - gripper.Named("foo:pieceGripper"), - gripper.Named("bar:pieceGripper"), - motion.Named("squee:builtin"), - sensors.Named("squee:builtin"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - motion.Named("bar:builtin"), - sensors.Named("bar:builtin"), - } - - resources2 := r2.ResourceNames() - - test.That( - t, - rtestutils.NewResourceNameSet(resources2...), - test.ShouldResemble, - rtestutils.NewResourceNameSet(expected...), - ) - - expectedRemotes := []string{"squee", "foo", "bar"} - remotes2 := r2.RemoteNames() - - test.That( - t, utils.NewStringSet(remotes2...), - test.ShouldResemble, - utils.NewStringSet(expectedRemotes...), - ) - - arm1Name := arm.Named("bar:pieceArm") - arm1, err := r2.ResourceByName(arm1Name) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1.Name(), test.ShouldResemble, arm1Name) - pos1, err := arm1.(arm.Arm).EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - arm2, err := r2.ResourceByName(arm.Named("foo:pieceArm")) - test.That(t, err, test.ShouldBeNil) - pos2, err := arm2.(arm.Arm).EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.PoseAlmostCoincident(pos1, pos2), test.ShouldBeTrue) - - statuses, err := r2.Status( - context.Background(), - []resource.Name{ - movementsensor.Named("squee:movement_sensor1"), - movementsensor.Named("foo:movement_sensor1"), - movementsensor.Named("bar:movement_sensor1"), - }, - ) - test.That(t, err, test.ShouldBeNil) - - expectedStatusLength := 3 - test.That(t, len(statuses), test.ShouldEqual, expectedStatusLength) - - for idx := 0; idx < expectedStatusLength; idx++ { - test.That(t, statuses[idx].Status, test.ShouldResemble, map[string]interface{}{}) - // Assert that last reconfigured values are within last hour (remote - // recently configured all three resources). - lr := statuses[idx].LastReconfigured - test.That(t, lr, test.ShouldHappenBetween, - time.Now().Add(-1*time.Hour), time.Now()) - } - - statuses, err = r2.Status( - context.Background(), - []resource.Name{arm.Named("squee:pieceArm"), arm.Named("foo:pieceArm"), arm.Named("bar:pieceArm")}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 3) - - armStatus := &armpb.Status{ - EndPosition: &commonpb.Pose{X: 500, Z: 300, OZ: 1}, - JointPositions: &armpb.JointPositions{Values: []float64{0.0}}, - } - convMap := &armpb.Status{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(statuses[0].Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, armStatus) - - convMap = &armpb.Status{} - decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(statuses[1].Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, armStatus) - - convMap = &armpb.Status{} - decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(statuses[2].Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, armStatus) - - cfg2 := r2.Config() - // Components should only include local components. - test.That(t, len(cfg2.Components), test.ShouldEqual, 2) - - fsConfig, err := r2.FrameSystemConfig(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, fsConfig.Parts, test.ShouldHaveLength, 12) -} - -func TestConfigRemoteWithAuth(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - for _, tc := range []struct { - Case string - Managed bool - EntityName string - }{ - {Case: "unmanaged and default host"}, - {Case: "unmanaged and specific host", EntityName: "something-different"}, - {Case: "managed and default host", Managed: true}, - {Case: "managed and specific host", Managed: true, EntityName: "something-different"}, - } { - t.Run(tc.Case, func(t *testing.T) { - ctx := context.Background() - r := setupLocalRobot(t, ctx, cfg, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - options.Managed = tc.Managed - options.FQDN = tc.EntityName - options.LocalFQDN = primitive.NewObjectID().Hex() - apiKey := "sosecret" - locationSecret := "locsosecret" - - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: rpc.CredentialsTypeAPIKey, - Config: rutils.AttributeMap{ - "key": apiKey, - }, - }, - { - Type: rutils.CredentialsTypeRobotLocationSecret, - Config: rutils.AttributeMap{ - "secret": locationSecret, - }, - }, - } - - if tc.Managed { - options.BakedAuthEntity = "blah" - options.BakedAuthCreds = rpc.Credentials{Type: "blah"} - } - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - entityName := tc.EntityName - if entityName == "" { - entityName = options.LocalFQDN - } - - remoteConfig := &config.Config{ - Debug: true, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr, - Auth: config.RemoteAuth{ - Managed: tc.Managed, - }, - }, - { - Name: "bar", - Address: addr, - Auth: config.RemoteAuth{ - Managed: tc.Managed, - }, - }, - }, - } - - setupLocalRobot(t, context.Background(), remoteConfig, logger) - - remoteConfig.Remotes[0].Auth.Credentials = &rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey, - } - remoteConfig.Remotes[1].Auth.Credentials = &rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret, - } - - var r2 robot.LocalRobot - if tc.Managed { - remoteConfig.Remotes[0].Auth.Entity = "wrong" - setupLocalRobot(t, context.Background(), remoteConfig, logger) - - remoteConfig.AllowInsecureCreds = true - - r3 := setupLocalRobot(t, context.Background(), remoteConfig, logger) - remoteBot, ok := r3.RemoteByName("foo") - test.That(t, ok, test.ShouldBeFalse) - test.That(t, remoteBot, test.ShouldBeNil) - - remoteConfig.Remotes[0].Auth.Entity = entityName - remoteConfig.Remotes[1].Auth.Entity = entityName - test.That(t, setupLocalRobot(t, context.Background(), remoteConfig, logger).Close(context.Background()), test.ShouldBeNil) - - ctx2 := context.Background() - remoteConfig.Remotes[0].Address = options.LocalFQDN - if tc.EntityName != "" { - remoteConfig.Remotes[1].Address = options.FQDN - } - r2 = setupLocalRobot(t, ctx2, remoteConfig, logger) - } else { - setupLocalRobot(t, context.Background(), remoteConfig, logger) - - remoteConfig.AllowInsecureCreds = true - - test.That(t, setupLocalRobot(t, context.Background(), remoteConfig, logger).Close(context.Background()), test.ShouldBeNil) - - ctx2 := context.Background() - remoteConfig.Remotes[0].Address = options.LocalFQDN - r2 = setupLocalRobot(t, ctx2, remoteConfig, logger) - - _, err = r2.ResourceByName(motion.Named(resource.DefaultServiceName)) - test.That(t, err, test.ShouldBeNil) - } - - test.That(t, r2, test.ShouldNotBeNil) - - expected := []resource.Name{ - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("bar:pieceArm"), - arm.Named("foo:pieceArm"), - audioinput.Named("bar:mic1"), - audioinput.Named("foo:mic1"), - camera.Named("bar:cameraOver"), - camera.Named("foo:cameraOver"), - movementsensor.Named("bar:movement_sensor1"), - movementsensor.Named("foo:movement_sensor1"), - movementsensor.Named("bar:movement_sensor2"), - movementsensor.Named("foo:movement_sensor2"), - gripper.Named("bar:pieceGripper"), - gripper.Named("foo:pieceGripper"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - motion.Named("bar:builtin"), - sensors.Named("bar:builtin"), - } - - resources2 := r2.ResourceNames() - - test.That( - t, - rtestutils.NewResourceNameSet(resources2...), - test.ShouldResemble, - rtestutils.NewResourceNameSet(expected...), - ) - - remotes2 := r2.RemoteNames() - expectedRemotes := []string{"bar", "foo"} - - test.That( - t, utils.NewStringSet(remotes2...), - test.ShouldResemble, - utils.NewStringSet(expectedRemotes...), - ) - - statuses, err := r2.Status( - context.Background(), []resource.Name{movementsensor.Named("bar:movement_sensor1"), movementsensor.Named("foo:movement_sensor1")}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 2) - test.That(t, statuses[0].Status, test.ShouldResemble, map[string]interface{}{}) - test.That(t, statuses[1].Status, test.ShouldResemble, map[string]interface{}{}) - - statuses, err = r2.Status( - context.Background(), []resource.Name{arm.Named("bar:pieceArm"), arm.Named("foo:pieceArm")}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 2) - - armStatus := &armpb.Status{ - EndPosition: &commonpb.Pose{X: 500, Z: 300, OZ: 1}, - JointPositions: &armpb.JointPositions{Values: []float64{0.0}}, - } - convMap := &armpb.Status{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(statuses[0].Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, armStatus) - - convMap = &armpb.Status{} - decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(statuses[1].Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, armStatus) - }) - } -} - -func TestConfigRemoteWithTLSAuth(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - - altName := primitive.NewObjectID().Hex() - cert, certFile, keyFile, certPool, err := testutils.GenerateSelfSignedCertificate("somename", altName) - test.That(t, err, test.ShouldBeNil) - t.Cleanup(func() { - os.Remove(certFile) - os.Remove(keyFile) - }) - - leaf, err := x509.ParseCertificate(cert.Certificate[0]) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - options.Network.TLSConfig = &tls.Config{ - RootCAs: certPool, - ClientCAs: certPool, - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS12, - ClientAuth: tls.VerifyClientCertIfGiven, - } - options.Auth.TLSAuthEntities = leaf.DNSNames - options.Managed = true - options.FQDN = altName - locationSecret := "locsosecret" - - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: rutils.CredentialsTypeRobotLocationSecret, - Config: rutils.AttributeMap{ - "secret": locationSecret, - }, - }, - } - - options.BakedAuthEntity = "blah" - options.BakedAuthCreds = rpc.Credentials{Type: "blah"} - - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - remoteTLSConfig := options.Network.TLSConfig.Clone() - remoteTLSConfig.Certificates = nil - remoteTLSConfig.ServerName = "somename" - remoteConfig := &config.Config{ - Debug: true, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr, - Auth: config.RemoteAuth{ - Managed: true, - }, - }, - }, - Network: config.NetworkConfig{ - NetworkConfigData: config.NetworkConfigData{ - TLSConfig: remoteTLSConfig, - }, - }, - } - - setupLocalRobot(t, context.Background(), remoteConfig, logger) - - // use secret - remoteConfig.Remotes[0].Auth.Credentials = &rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret, - } - test.That(t, setupLocalRobot(t, context.Background(), remoteConfig, logger).Close(context.Background()), test.ShouldBeNil) - - // use cert - remoteTLSConfig.Certificates = []tls.Certificate{cert} - test.That(t, setupLocalRobot(t, context.Background(), remoteConfig, logger).Close(context.Background()), test.ShouldBeNil) - - // use cert with mDNS - remoteConfig.Remotes[0].Address = options.FQDN - test.That(t, setupLocalRobot(t, context.Background(), remoteConfig, logger).Close(context.Background()), test.ShouldBeNil) - - // use signaling creds - remoteConfig.Remotes[0].Address = addr - remoteConfig.Remotes[0].Auth.Credentials = nil - remoteConfig.Remotes[0].Auth.SignalingServerAddress = addr - remoteConfig.Remotes[0].Auth.SignalingAuthEntity = options.FQDN - remoteConfig.Remotes[0].Auth.SignalingCreds = &rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret, - } - test.That(t, setupLocalRobot(t, context.Background(), remoteConfig, logger).Close(context.Background()), test.ShouldBeNil) - - // use cert with mDNS while signaling present - ctx2 := context.Background() - remoteConfig.Remotes[0].Auth.SignalingCreds = &rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret + "bad", - } - remoteConfig.Remotes[0].Address = options.FQDN - r2 := setupLocalRobot(t, ctx2, remoteConfig, logger) - - expected := []resource.Name{ - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("foo:pieceArm"), - audioinput.Named("foo:mic1"), - camera.Named("foo:cameraOver"), - movementsensor.Named("foo:movement_sensor1"), - movementsensor.Named("foo:movement_sensor2"), - gripper.Named("foo:pieceGripper"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - } - - resources2 := r2.ResourceNames() - - test.That( - t, - rtestutils.NewResourceNameSet(resources2...), - test.ShouldResemble, - rtestutils.NewResourceNameSet(expected...), - ) - - remotes2 := r2.RemoteNames() - expectedRemotes := []string{"foo"} - - test.That( - t, utils.NewStringSet(remotes2...), - test.ShouldResemble, - utils.NewStringSet(expectedRemotes...), - ) - - statuses, err := r2.Status(context.Background(), []resource.Name{movementsensor.Named("foo:movement_sensor1")}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 1) - test.That(t, statuses[0].Status, test.ShouldResemble, map[string]interface{}{}) - - statuses, err = r2.Status(context.Background(), []resource.Name{arm.Named("foo:pieceArm")}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 1) - - armStatus := &armpb.Status{ - EndPosition: &commonpb.Pose{X: 500, Z: 300, OZ: 1}, - JointPositions: &armpb.JointPositions{Values: []float64{0.0}}, - } - convMap := &armpb.Status{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(statuses[0].Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, armStatus) -} - -type dummyArm struct { - arm.Arm - stopCount int - extra map[string]interface{} - channel chan struct{} -} - -func (da *dummyArm) Name() resource.Name { - return arm.Named("bad") -} - -func (da *dummyArm) MoveToPosition( - ctx context.Context, - pose spatialmath.Pose, - extra map[string]interface{}, -) error { - return nil -} - -func (da *dummyArm) MoveToJointPositions(ctx context.Context, positionDegs *armpb.JointPositions, extra map[string]interface{}) error { - return nil -} - -func (da *dummyArm) JointPositions(ctx context.Context, extra map[string]interface{}) (*armpb.JointPositions, error) { - return nil, errors.New("fake error") -} - -func (da *dummyArm) Stop(ctx context.Context, extra map[string]interface{}) error { - da.stopCount++ - da.extra = extra - return nil -} - -func (da *dummyArm) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - close(da.channel) - <-ctx.Done() - return nil, ctx.Err() -} - -func (da *dummyArm) Close(ctx context.Context) error { - return nil -} - -func TestStopAll(t *testing.T) { - logger := logging.NewTestLogger(t) - channel := make(chan struct{}) - - model := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - dummyArm1 := dummyArm{channel: channel} - dummyArm2 := dummyArm{channel: channel} - resource.RegisterComponent( - arm.API, - model, - resource.Registration[arm.Arm, resource.NoNativeConfig]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (arm.Arm, error) { - if conf.Name == "arm1" { - return &dummyArm1, nil - } - return &dummyArm2, nil - }}) - - armConfig := fmt.Sprintf(`{ - "components": [ - { - "model": "%[1]s", - "name": "arm1", - "type": "arm" - }, - { - "model": "%[1]s", - "name": "arm2", - "type": "arm" - } - ] - } - `, model.String()) - defer func() { - resource.Deregister(arm.API, model) - }() - - cfg, err := config.FromReader(context.Background(), "", strings.NewReader(armConfig), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - r := setupLocalRobot(t, ctx, cfg, logger) - - test.That(t, dummyArm1.stopCount, test.ShouldEqual, 0) - test.That(t, dummyArm2.stopCount, test.ShouldEqual, 0) - - test.That(t, dummyArm1.extra, test.ShouldBeNil) - test.That(t, dummyArm2.extra, test.ShouldBeNil) - - err = r.StopAll(ctx, map[resource.Name]map[string]interface{}{arm.Named("arm2"): {"foo": "bar"}}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, dummyArm1.stopCount, test.ShouldEqual, 1) - test.That(t, dummyArm2.stopCount, test.ShouldEqual, 1) - - test.That(t, dummyArm1.extra, test.ShouldBeNil) - test.That(t, dummyArm2.extra, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - - // Test OPID cancellation - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - conn, err := rgrpc.Dial(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - arm1, err := arm.NewClientFromConn(ctx, conn, "somerem", arm.Named("arm1"), logger) - test.That(t, err, test.ShouldBeNil) - - foundOPID := false - stopAllErrCh := make(chan error, 1) - go func() { - <-channel - for _, opid := range r.OperationManager().All() { - if opid.Method == "/viam.component.arm.v1.ArmService/DoCommand" { - foundOPID = true - stopAllErrCh <- r.StopAll(ctx, nil) - } - } - }() - _, err = arm1.DoCommand(ctx, map[string]interface{}{}) - s, isGRPCErr := status.FromError(err) - test.That(t, isGRPCErr, test.ShouldBeTrue) - test.That(t, s.Code(), test.ShouldEqual, codes.Canceled) - - stopAllErr := <-stopAllErrCh - test.That(t, foundOPID, test.ShouldBeTrue) - test.That(t, stopAllErr, test.ShouldBeNil) -} - -type dummyBoard struct { - board.Board - closeCount int -} - -func (db *dummyBoard) Name() resource.Name { - return board.Named("bad") -} - -func (db *dummyBoard) AnalogNames() []string { - return nil -} - -func (db *dummyBoard) DigitalInterruptNames() []string { - return nil -} - -func (db *dummyBoard) Close(ctx context.Context) error { - db.closeCount++ - return nil -} - -func TestNewTeardown(t *testing.T) { - logger := logging.NewTestLogger(t) - - model := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - var dummyBoard1 dummyBoard - resource.RegisterComponent( - board.API, - model, - resource.Registration[board.Board, resource.NoNativeConfig]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (board.Board, error) { - return &dummyBoard1, nil - }}) - resource.RegisterComponent( - gripper.API, - model, - resource.Registration[gripper.Gripper, resource.NoNativeConfig]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (gripper.Gripper, error) { - return nil, errors.New("whoops") - }}) - - defer func() { - resource.Deregister(board.API, model) - resource.Deregister(gripper.API, model) - }() - - failingConfig := fmt.Sprintf(`{ - "components": [ - { - "model": "%[1]s", - "name": "board1", - "type": "board" - }, - { - "model": "%[1]s", - "name": "gripper1", - "type": "gripper", - "depends_on": ["board1"] - } - ] -} -`, model) - cfg, err := config.FromReader(context.Background(), "", strings.NewReader(failingConfig), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - r := setupLocalRobot(t, ctx, cfg, logger) - test.That(t, r.Close(ctx), test.ShouldBeNil) - test.That(t, dummyBoard1.closeCount, test.ShouldEqual, 1) -} - -func TestMetadataUpdate(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - - resources := r.ResourceNames() - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(resources), test.ShouldEqual, 8) - test.That(t, err, test.ShouldBeNil) - - // 5 declared resources + default sensors - resourceNames := []resource.Name{ - arm.Named("pieceArm"), - audioinput.Named("mic1"), - camera.Named("cameraOver"), - gripper.Named("pieceGripper"), - movementsensor.Named("movement_sensor1"), - movementsensor.Named("movement_sensor2"), - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - } - - resources = r.ResourceNames() - test.That(t, len(resources), test.ShouldEqual, len(resourceNames)) - test.That(t, rtestutils.NewResourceNameSet(resources...), test.ShouldResemble, rtestutils.NewResourceNameSet(resourceNames...)) - - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - resources = r.ResourceNames() - test.That(t, resources, test.ShouldBeEmpty) -} - -func TestSensorsService(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - r := setupLocalRobot(t, context.Background(), cfg, logger) - - svc, err := sensors.FromRobot(r, resource.DefaultServiceName) - test.That(t, err, test.ShouldBeNil) - - sensorNames := []resource.Name{movementsensor.Named("movement_sensor1"), movementsensor.Named("movement_sensor2")} - foundSensors, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, rtestutils.NewResourceNameSet(foundSensors...), test.ShouldResemble, rtestutils.NewResourceNameSet(sensorNames...)) - - readings, err := svc.Readings(context.Background(), []resource.Name{movementsensor.Named("movement_sensor1")}, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(readings), test.ShouldEqual, 1) - test.That(t, readings[0].Name, test.ShouldResemble, movementsensor.Named("movement_sensor1")) - test.That(t, len(readings[0].Readings), test.ShouldBeGreaterThan, 3) - - readings, err = svc.Readings(context.Background(), sensorNames, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(readings), test.ShouldEqual, 2) -} - -func TestStatusService(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - r := setupLocalRobot(t, context.Background(), cfg, logger) - - resourceNames := []resource.Name{arm.Named("pieceArm"), movementsensor.Named("movement_sensor1")} - rArm, err := arm.FromRobot(r, "pieceArm") - test.That(t, err, test.ShouldBeNil) - armStatus, err := arm.CreateStatus(context.Background(), rArm) - test.That(t, err, test.ShouldBeNil) - expected := map[resource.Name]interface{}{ - arm.Named("pieceArm"): armStatus, - movementsensor.Named("movement_sensor1"): map[string]interface{}{}, - } - - statuses, err := r.Status(context.Background(), []resource.Name{movementsensor.Named("movement_sensor1")}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 1) - test.That(t, statuses[0].Name, test.ShouldResemble, movementsensor.Named("movement_sensor1")) - test.That(t, statuses[0].Status, test.ShouldResemble, expected[statuses[0].Name]) - - statuses, err = r.Status(context.Background(), resourceNames) - test.That(t, err, test.ShouldBeNil) - - expectedStatusLength := 2 - test.That(t, len(statuses), test.ShouldEqual, expectedStatusLength) - - for idx := 0; idx < expectedStatusLength; idx++ { - test.That(t, statuses[idx].Status, test.ShouldResemble, expected[statuses[idx].Name]) - } -} - -func TestStatus(t *testing.T) { - buttonAPI := resource.APINamespace("acme").WithComponentType("button") - button1 := resource.NewName(buttonAPI, "button1") - button2 := resource.NewName(buttonAPI, "button2") - - workingAPI := resource.APINamespace("acme").WithComponentType("working") - working1 := resource.NewName(workingAPI, "working1") - - failAPI := resource.APINamespace("acme").WithComponentType("fail") - fail1 := resource.NewName(failAPI, "fail1") - - workingStatus := map[string]interface{}{"position": "up"} - errFailed := errors.New("can't get status") - - resource.RegisterAPI( - workingAPI, - resource.APIRegistration[resource.Resource]{ - Status: func(ctx context.Context, res resource.Resource) (interface{}, error) { return workingStatus, nil }, - }, - ) - - resource.RegisterAPI( - failAPI, - resource.APIRegistration[resource.Resource]{ - Status: func(ctx context.Context, res resource.Resource) (interface{}, error) { return nil, errFailed }, - }, - ) - defer func() { - resource.DeregisterAPI(workingAPI) - resource.DeregisterAPI(failAPI) - }() - - expectedRobotStatus := robot.Status{Name: button1, Status: map[string]interface{}{}} - logger := logging.NewTestLogger(t) - resourceNames := []resource.Name{working1, button1, fail1} - resourceMap := map[resource.Name]resource.Resource{ - working1: rtestutils.NewUnimplementedResource(working1), - button1: rtestutils.NewUnimplementedResource(button1), - fail1: rtestutils.NewUnimplementedResource(fail1), - } - - t.Run("not found", func(t *testing.T) { - r, err := RobotFromResources(context.Background(), resourceMap, logger) - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - - test.That(t, err, test.ShouldBeNil) - - _, err = r.Status(context.Background(), []resource.Name{button2}) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(button2)) - }) - - t.Run("no CreateStatus", func(t *testing.T) { - r, err := RobotFromResources(context.Background(), resourceMap, logger) - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - test.That(t, err, test.ShouldBeNil) - - resp, err := r.Status(context.Background(), []resource.Name{button1}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 1) - test.That(t, resp[0].Name, test.ShouldResemble, expectedRobotStatus.Name) - test.That(t, resp[0].Status, test.ShouldResemble, expectedRobotStatus.Status) - test.That(t, resp[0].LastReconfigured, test.ShouldHappenBetween, - time.Now().Add(-1*time.Hour), time.Now()) - }) - - t.Run("failing resource", func(t *testing.T) { - r, err := RobotFromResources(context.Background(), resourceMap, logger) - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - test.That(t, err, test.ShouldBeNil) - - _, err = r.Status(context.Background(), []resource.Name{fail1}) - test.That(t, err, test.ShouldBeError, errors.Wrapf(errFailed, "failed to get status from %q", fail1)) - }) - - t.Run("many status", func(t *testing.T) { - expected := map[resource.Name]interface{}{ - working1: workingStatus, - button1: map[string]interface{}{}, - } - r, err := RobotFromResources(context.Background(), resourceMap, logger) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - _, err = r.Status(context.Background(), []resource.Name{button2}) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(button2)) - - resp, err := r.Status(context.Background(), []resource.Name{working1}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 1) - status := resp[0] - test.That(t, status.Name, test.ShouldResemble, working1) - test.That(t, status.Status, test.ShouldResemble, workingStatus) - - resp, err = r.Status(context.Background(), []resource.Name{working1, working1, working1}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 1) - status = resp[0] - test.That(t, status.Name, test.ShouldResemble, working1) - test.That(t, status.Status, test.ShouldResemble, workingStatus) - - resp, err = r.Status(context.Background(), []resource.Name{working1, button1}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp), test.ShouldEqual, 2) - test.That(t, resp[0].Status, test.ShouldResemble, expected[resp[0].Name]) - test.That(t, resp[1].Status, test.ShouldResemble, expected[resp[1].Name]) - - _, err = r.Status(context.Background(), resourceNames) - test.That(t, err, test.ShouldBeError, errors.Wrapf(errFailed, "failed to get status from %q", fail1)) - }) - - t.Run("get all status", func(t *testing.T) { - workingResourceMap := map[resource.Name]resource.Resource{ - working1: rtestutils.NewUnimplementedResource(working1), - button1: rtestutils.NewUnimplementedResource(button1), - } - expected := map[resource.Name]interface{}{ - working1: workingStatus, - button1: map[string]interface{}{}, - } - r, err := RobotFromResources(context.Background(), workingResourceMap, logger) - defer func() { - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }() - test.That(t, err, test.ShouldBeNil) - - resp, err := r.Status(context.Background(), []resource.Name{}) - test.That(t, err, test.ShouldBeNil) - // 5 because the 3 default services are always added to a local_robot. We only care - // about the first two (working1 and button1) however. - test.That(t, len(resp), test.ShouldEqual, 4) - - // although the response is length 5, the only thing we actually care about for testing - // is consistency with the expected values in the workingResourceMap. So we eliminate - // the values that aren't in the workingResourceMap. - actual := []robot.Status{} - for _, status := range resp { - if _, ok := workingResourceMap[status.Name]; ok { - actual = append(actual, status) - } - } - test.That(t, len(actual), test.ShouldEqual, 2) - test.That(t, actual[0].Status, test.ShouldResemble, expected[actual[0].Name]) - test.That(t, actual[1].Status, test.ShouldResemble, expected[actual[1].Name]) - }) -} - -func TestStatusRemote(t *testing.T) { - logger := logging.NewTestLogger(t) - // set up remotes - listener1 := testutils.ReserveRandomListener(t) - addr1 := listener1.Addr().String() - - listener2 := testutils.ReserveRandomListener(t) - addr2 := listener2.Addr().String() - - gServer1 := grpc.NewServer() - gServer2 := grpc.NewServer() - resourcesFunc := func() []resource.Name { return []resource.Name{arm.Named("arm1"), arm.Named("arm2")} } - statusCallCount := 0 - - // TODO: RSDK-882 will update this so that this is not necessary - frameSystemConfigFunc := func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{Parts: []*referenceframe.FrameSystemPart{ - { - FrameConfig: referenceframe.NewLinkInFrame(referenceframe.World, nil, "arm1", nil), - ModelFrame: referenceframe.NewSimpleModel("arm1"), - }, - { - FrameConfig: referenceframe.NewLinkInFrame(referenceframe.World, nil, "arm2", nil), - ModelFrame: referenceframe.NewSimpleModel("arm2"), - }, - }}, nil - } - - injectRobot1 := &inject.Robot{ - FrameSystemConfigFunc: frameSystemConfigFunc, - ResourceNamesFunc: resourcesFunc, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - armStatus := &armpb.Status{ - EndPosition: &commonpb.Pose{}, - JointPositions: &armpb.JointPositions{Values: []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}}, - } - - lastReconfigured, err := time.Parse("2006-01-02 15:04:05", "2011-11-11 00:00:00") - test.That(t, err, test.ShouldBeNil) - - injectRobot1.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - statusCallCount++ - statuses := make([]robot.Status, 0, len(resourceNames)) - for _, n := range resourceNames { - statuses = append(statuses, robot.Status{ - Name: n, - LastReconfigured: lastReconfigured, - Status: armStatus, - }) - } - return statuses, nil - } - injectRobot2 := &inject.Robot{ - FrameSystemConfigFunc: frameSystemConfigFunc, - ResourceNamesFunc: resourcesFunc, - ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, - } - injectRobot2.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - statusCallCount++ - statuses := make([]robot.Status, 0, len(resourceNames)) - for _, n := range resourceNames { - statuses = append(statuses, robot.Status{ - Name: n, - LastReconfigured: lastReconfigured, - Status: armStatus, - }) - } - return statuses, nil - } - pb.RegisterRobotServiceServer(gServer1, server.New(injectRobot1)) - pb.RegisterRobotServiceServer(gServer2, server.New(injectRobot2)) - - go gServer1.Serve(listener1) - defer gServer1.Stop() - go gServer2.Serve(listener2) - defer gServer2.Stop() - - remoteConfig := &config.Config{ - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr1, - }, - { - Name: "bar", - Address: addr2, - }, - }, - } - test.That(t, remoteConfig.Ensure(false, logger), test.ShouldBeNil) - ctx := context.Background() - r := setupLocalRobot(t, ctx, remoteConfig, logger) - - test.That( - t, - rtestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rtestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("foo:arm1"), - arm.Named("foo:arm2"), - arm.Named("bar:arm1"), - arm.Named("bar:arm2"), - ), - ) - statuses, err := r.Status( - ctx, []resource.Name{arm.Named("foo:arm1"), arm.Named("foo:arm2"), arm.Named("bar:arm1"), arm.Named("bar:arm2")}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 4) - test.That(t, statusCallCount, test.ShouldEqual, 2) - - for _, status := range statuses { - convMap := &armpb.Status{} - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: &convMap}) - test.That(t, err, test.ShouldBeNil) - err = decoder.Decode(status.Status) - test.That(t, err, test.ShouldBeNil) - test.That(t, convMap, test.ShouldResemble, armStatus) - - // Test that LastReconfigured values are from remotes, and not set based on - // when local resource graph nodes were added for the remote resources. - test.That(t, status.LastReconfigured, test.ShouldEqual, lastReconfigured) - } -} - -func TestGetRemoteResourceAndGrandFather(t *testing.T) { - // set up remotes - options, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - - ctx := context.Background() - logger := logging.NewTestLogger(t) - - remoteRemoteConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - API: arm.API, - Model: fakeModel, - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - }, - { - Name: "arm2", - API: arm.API, - Model: fakeModel, - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - }, - { - Name: "pieceArm", - API: arm.API, - Model: fakeModel, - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - }, - }, - Services: []resource.Config{}, - Remotes: []config.Remote{}, - } - - r0 := setupLocalRobot(t, ctx, remoteRemoteConfig, logger) - - err := r0.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - r0arm1, err := r0.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - r0Arm, ok := r0arm1.(arm.Arm) - test.That(t, ok, test.ShouldBeTrue) - tPos := referenceframe.JointPositionsFromRadians([]float64{math.Pi}) - err = r0Arm.MoveToJointPositions(context.Background(), tPos, nil) - test.That(t, err, test.ShouldBeNil) - p0Arm1, err := r0Arm.JointPositions(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - options, _, addr2 := robottestutils.CreateBaseOptionsAndListener(t) - remoteConfig := &config.Config{ - Remotes: []config.Remote{ - { - Name: "remote", - Address: addr2, - }, - }, - } - - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - cfg.Remotes = append(cfg.Remotes, config.Remote{ - Name: "foo", - Address: addr1, - }) - r1 := setupLocalRobot(t, ctx, cfg, logger) - err = r1.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - r := setupLocalRobot(t, ctx, remoteConfig, logger) - - test.That( - t, - rtestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rtestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("remote:foo:arm1"), arm.Named("remote:foo:arm2"), - arm.Named("remote:pieceArm"), - arm.Named("remote:foo:pieceArm"), - audioinput.Named("remote:mic1"), - camera.Named("remote:cameraOver"), - movementsensor.Named("remote:movement_sensor1"), - movementsensor.Named("remote:movement_sensor2"), - gripper.Named("remote:pieceGripper"), - motion.Named("remote:builtin"), - sensors.Named("remote:builtin"), - motion.Named("remote:foo:builtin"), - sensors.Named("remote:foo:builtin"), - ), - ) - arm1, err := r.ResourceByName(arm.Named("remote:foo:arm1")) - test.That(t, err, test.ShouldBeNil) - rrArm1, ok := arm1.(arm.Arm) - test.That(t, ok, test.ShouldBeTrue) - pos, err := rrArm1.JointPositions(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos.Values, test.ShouldResemble, p0Arm1.Values) - - arm1, err = r.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - rrArm1, ok = arm1.(arm.Arm) - test.That(t, ok, test.ShouldBeTrue) - pos, err = rrArm1.JointPositions(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos.Values, test.ShouldResemble, p0Arm1.Values) - - _, err = r.ResourceByName(arm.Named("remote:foo:pieceArm")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(arm.Named("remote:pieceArm")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(arm.Named("pieceArm")) - test.That(t, err, test.ShouldBeError, "more than one remote resources with name \"pieceArm\" exists") -} - -type someConfig struct { - Thing string -} - -func (someConfig) Validate(path string) ([]string, error) { - return nil, errors.New("fail") -} - -func TestValidationErrorOnReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - badConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "test", - API: base.API, - Model: resource.DefaultModelFamily.WithModel("random"), - ConvertedAttributes: someConfig{}, - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - API: navigation.API, - ConvertedAttributes: someConfig{}, - }, - }, - Remotes: []config.Remote{{ - Name: "remote", - Insecure: true, - Address: "", - }}, - Cloud: &config.Cloud{}, - } - r := setupLocalRobot(t, ctx, badConfig, logger) - - // Test Component Error - name := base.Named("test") - noBase, err := r.ResourceByName(name) - test.That( - t, - err, - test.ShouldBeError, - resource.NewNotAvailableError(name, errors.New("resource config validation error: fail")), - ) - test.That(t, noBase, test.ShouldBeNil) - // Test Service Error - s, err := r.ResourceByName(navigation.Named("fake1")) - test.That(t, s, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "resource \"rdk:service:navigation/fake1\" not available") - // Test Remote Error - rem, ok := r.RemoteByName("remote") - test.That(t, rem, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeFalse) -} - -func TestConfigStartsInvalidReconfiguresValid(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - badConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "test", - API: base.API, - Model: fakeModel, - ConvertedAttributes: someConfig{}, - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - API: datamanager.API, - ConvertedAttributes: someConfig{}, - }, - }, - Remotes: []config.Remote{{ - Name: "remote", - Insecure: true, - Address: "", - }}, - } - test.That(t, badConfig.Ensure(false, logger), test.ShouldBeNil) - r := setupLocalRobot(t, ctx, badConfig, logger) - - options1, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - err := r.StartWeb(context.Background(), options1) - test.That(t, err, test.ShouldBeNil) - - goodConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "test", - API: base.API, - Model: fakeModel, - // Added to force a component reconfigure. - Attributes: rutils.AttributeMap{"version": 1}, - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - API: datamanager.API, - Model: resource.DefaultServiceModel, - // Added to force a service reconfigure. - Attributes: rutils.AttributeMap{"version": 1}, - ConvertedAttributes: &builtin.Config{}, - }, - }, - Remotes: []config.Remote{{ - Name: "remote", - Insecure: true, - Address: addr1, - }}, - } - test.That(t, goodConfig.Ensure(false, logger), test.ShouldBeNil) - - // Test Component Error - name := base.Named("test") - noBase, err := base.FromRobot(r, "test") - test.That( - t, - err, - test.ShouldBeError, - resource.NewNotAvailableError(name, errors.New("resource config validation error: fail")), - ) - test.That(t, noBase, test.ShouldBeNil) - // Test Service Error - s, err := r.ResourceByName(datamanager.Named("fake1")) - test.That(t, s, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "resource \"rdk:service:data_manager/fake1\" not available") - // Test Remote Error - rem, ok := r.RemoteByName("remote") - test.That(t, rem, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeFalse) - - r.Reconfigure(ctx, goodConfig) - // Test Component Valid - noBase, err = base.FromRobot(r, "test") - test.That(t, err, test.ShouldBeNil) - test.That(t, noBase, test.ShouldNotBeNil) - // Test Service Valid - s, err = r.ResourceByName(datamanager.Named("fake1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, s, test.ShouldNotBeNil) - // Test Remote Valid - rem, ok = r.RemoteByName("remote") - test.That(t, ok, test.ShouldBeTrue) - test.That(t, rem, test.ShouldNotBeNil) -} - -func TestConfigStartsValidReconfiguresInvalid(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - armConfig := resource.Config{ - Name: "arm1", - API: arm.API, - Model: fakeModel, - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - } - cfg := config.Config{ - Components: []resource.Config{armConfig}, - } - - robotRemote := setupLocalRobot(t, ctx, &cfg, logger) - options1, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - err := robotRemote.StartWeb(context.Background(), options1) - test.That(t, err, test.ShouldBeNil) - - goodConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "test", - API: base.API, - Model: fakeModel, - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - API: datamanager.API, - Model: resource.DefaultServiceModel, - ConvertedAttributes: &builtin.Config{}, - }, - }, - Remotes: []config.Remote{{ - Name: "remote", - Insecure: true, - Address: addr1, - }}, - } - test.That(t, goodConfig.Ensure(false, logger), test.ShouldBeNil) - r := setupLocalRobot(t, ctx, goodConfig, logger) - - badConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "test", - API: base.API, - Model: fakeModel, - // Added to force a component reconfigure. - Attributes: rutils.AttributeMap{"version": 1}, - ConvertedAttributes: someConfig{}, - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - API: datamanager.API, - // Added to force a service reconfigure. - Attributes: rutils.AttributeMap{"version": 1}, - ConvertedAttributes: someConfig{}, - }, - }, - Remotes: []config.Remote{{ - Name: "remote", - Insecure: true, - Address: "", - }}, - } - test.That(t, badConfig.Ensure(false, logger), test.ShouldBeNil) - // Test Component Valid - noBase, err := base.FromRobot(r, "test") - test.That(t, err, test.ShouldBeNil) - test.That(t, noBase, test.ShouldNotBeNil) - // Test Service Valid - s, err := r.ResourceByName(datamanager.Named("fake1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, s, test.ShouldNotBeNil) - // Test Remote Valid - rem, ok := r.RemoteByName("remote") - test.That(t, ok, test.ShouldBeTrue) - test.That(t, rem, test.ShouldNotBeNil) - - r.Reconfigure(ctx, badConfig) - // Test Component Error - name := base.Named("test") - noBase, err = base.FromRobot(r, "test") - test.That( - t, - err, - test.ShouldBeError, - resource.NewNotAvailableError(name, errors.New("resource config validation error: fail")), - ) - test.That(t, noBase, test.ShouldBeNil) - // Test Service Error - s, err = r.ResourceByName(datamanager.Named("fake1")) - test.That(t, s, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "resource \"rdk:service:data_manager/fake1\" not available") - // Test Remote Error - rem, ok = r.RemoteByName("remote") - test.That(t, rem, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeFalse) -} - -func TestResourceStartsOnReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - badConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "fake0", - API: base.API, - Model: resource.DefaultModelFamily.WithModel("random"), - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - }, - }, - } - test.That(t, badConfig.Ensure(false, logger), test.ShouldBeNil) - - goodConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "fake0", - API: base.API, - Model: fakeModel, - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - API: datamanager.API, - Model: resource.DefaultServiceModel, - ConvertedAttributes: &builtin.Config{}, - }, - }, - } - test.That(t, goodConfig.Ensure(false, logger), test.ShouldBeNil) - r := setupLocalRobot(t, ctx, badConfig, logger) - - noBase, err := r.ResourceByName(base.Named("fake0")) - test.That( - t, - err, - test.ShouldBeError, - resource.NewNotAvailableError( - base.Named("fake0"), - errors.New(`resource build error: unknown resource type: API "rdk:component:base" with model "rdk:builtin:random" not registered`), - ), - ) - test.That(t, noBase, test.ShouldBeNil) - - noSvc, err := r.ResourceByName(datamanager.Named("fake1")) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(datamanager.Named("fake1"))) - test.That(t, noSvc, test.ShouldBeNil) - - r.Reconfigure(ctx, goodConfig) - - yesBase, err := r.ResourceByName(base.Named("fake0")) - test.That(t, err, test.ShouldBeNil) - test.That(t, yesBase, test.ShouldNotBeNil) - - yesSvc, err := r.ResourceByName(datamanager.Named("fake1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, yesSvc, test.ShouldNotBeNil) -} - -func TestConfigProcess(t *testing.T) { - logger, logs := logging.NewObservedTestLogger(t) - r := setupLocalRobot(t, context.Background(), &config.Config{ - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "bash", - Args: []string{"-c", "echo heythere"}, - Log: true, - OneShot: true, - }, - }, - }, logger) - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - test.That(t, logs.FilterField(zap.String("output", "heythere\n")).Len(), test.ShouldEqual, 1) -} - -func TestConfigPackages(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - fakePackageServer, err := putils.NewFakePackageServer(ctx, logger) - test.That(t, err, test.ShouldBeNil) - defer utils.UncheckedErrorFunc(fakePackageServer.Shutdown) - - packageDir := t.TempDir() - - robotConfig := &config.Config{ - Packages: []config.PackageConfig{ - { - Name: "some-name-1", - Package: "package-1", - Version: "v1", - }, - }, - Cloud: &config.Cloud{ - AppAddress: fmt.Sprintf("http://%s", fakePackageServer.Addr().String()), - }, - PackagePath: packageDir, - } - - r := setupLocalRobot(t, ctx, robotConfig, logger) - - _, err = r.PackageManager().PackagePath("some-name-1") - test.That(t, err, test.ShouldEqual, packages.ErrPackageMissing) - - robotConfig2 := &config.Config{ - Packages: []config.PackageConfig{ - { - Name: "some-name-1", - Package: "package-1", - Version: "v1", - Type: "ml_model", - }, - { - Name: "some-name-2", - Package: "package-2", - Version: "v2", - Type: "ml_model", - }, - }, - Cloud: &config.Cloud{ - AppAddress: fmt.Sprintf("http://%s", fakePackageServer.Addr().String()), - }, - PackagePath: packageDir, - } - - fakePackageServer.StorePackage(robotConfig2.Packages...) - r.Reconfigure(ctx, robotConfig2) - - path1, err := r.PackageManager().PackagePath("some-name-1") - test.That(t, err, test.ShouldBeNil) - test.That(t, path1, test.ShouldEqual, path.Join(packageDir, "data", "ml_model", "package-1-v1")) - - path2, err := r.PackageManager().PackagePath("some-name-2") - test.That(t, err, test.ShouldBeNil) - test.That(t, path2, test.ShouldEqual, path.Join(packageDir, "data", "ml_model", "package-2-v2")) -} - -// removeDefaultServices removes default services and returns the removed -// services for testing purposes. -func removeDefaultServices(cfg *config.Config) []resource.Config { - if cfg == nil { - return nil - } - - // Make a set of registered default services. - registeredDefaultSvcs := make(map[resource.Name]bool) - for _, name := range resource.DefaultServices() { - registeredDefaultSvcs[name] = true - } - - var defaultSvcs, nonDefaultSvcs []resource.Config - for _, svc := range cfg.Services { - if registeredDefaultSvcs[svc.ResourceName()] { - defaultSvcs = append(defaultSvcs, svc) - continue - } - nonDefaultSvcs = append(nonDefaultSvcs, svc) - } - - cfg.Services = nonDefaultSvcs - return defaultSvcs -} - -func TestConfigMethod(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Precompile complex module to avoid timeout issues when building takes too long. - complexPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - - r := setupLocalRobot(t, context.Background(), &config.Config{}, logger) - - // Assert that Config method returns the two default services: motion and sensors. - actualCfg := r.Config() - defaultSvcs := removeDefaultServices(actualCfg) - test.That(t, len(defaultSvcs), test.ShouldEqual, 2) - for _, svc := range defaultSvcs { - test.That(t, svc.API.SubtypeName, test.ShouldBeIn, - motion.API.SubtypeName, sensors.API.SubtypeName) - } - test.That(t, actualCfg, test.ShouldResemble, &config.Config{}) - - // Use a remote with components and services to ensure none of its resources - // will be returned by Config. - remoteCfg, err := config.Read(context.Background(), "data/remote_fake.json", logger) - test.That(t, err, test.ShouldBeNil) - remoteRobot := setupLocalRobot(t, ctx, remoteCfg, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = remoteRobot.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // Manually define mybase model, as importing it can cause double registration. - myBaseModel := resource.NewModel("acme", "demo", "mybase") - - cfg := &config.Config{ - Cloud: &config.Cloud{}, - Modules: []config.Module{ - { - Name: "mod", - ExePath: complexPath, - LogLevel: "info", - }, - }, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr, - }, - }, - Components: []resource.Config{ - { - Name: "myBase", - API: base.API, - Model: myBaseModel, - Attributes: rutils.AttributeMap{ - "motorL": "motor1", - "motorR": "motor2", - }, - }, - { - Name: "motor1", - API: motor.API, - Model: fakeModel, - ConvertedAttributes: &fakemotor.Config{}, - ImplicitDependsOn: []string{"builtin:sensors"}, - }, - { - Name: "motor2", - API: motor.API, - Model: fakeModel, - ConvertedAttributes: &fakemotor.Config{}, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "bash", - Args: []string{"-c", "echo heythere"}, - Log: true, - OneShot: true, - }, - }, - Services: []resource.Config{ - { - Name: "fake1", - API: datamanager.API, - Model: resource.DefaultServiceModel, - ConvertedAttributes: &builtin.Config{}, - ImplicitDependsOn: []string{"foo:builtin:data_manager"}, - }, - { - Name: "builtin", - API: navigation.API, - Model: resource.DefaultServiceModel, - }, - }, - Packages: []config.PackageConfig{ - { - Name: "some-name-1", - Package: "package-1", - Version: "v1", - }, - }, - Network: config.NetworkConfig{}, - Auth: config.AuthConfig{}, - Debug: true, - DisablePartialStart: true, - } - - // Create copy of expectedCfg since Reconfigure modifies cfg. - expectedCfg := *cfg - r.Reconfigure(ctx, cfg) - - // Assert that Config method returns expected value. - actualCfg = r.Config() - - // Assert that default motion and sensor services are still present, but data - // manager default service has been replaced by the "fake1" data manager service. - defaultSvcs = removeDefaultServices(actualCfg) - test.That(t, len(defaultSvcs), test.ShouldEqual, 2) - for _, svc := range defaultSvcs { - test.That(t, svc.API.SubtypeName, test.ShouldBeIn, motion.API.SubtypeName, - sensors.API.SubtypeName) - } - - // Manually inspect remaining service resources as ordering of config is - // non-deterministic within slices. - test.That(t, len(actualCfg.Services), test.ShouldEqual, 2) - for _, svc := range actualCfg.Services { - isFake1DM := svc.Equals(expectedCfg.Services[0]) - isBuiltinNav := svc.Equals(expectedCfg.Services[1]) - test.That(t, isFake1DM || isBuiltinNav, test.ShouldBeTrue) - } - actualCfg.Services = nil - expectedCfg.Services = nil - - // Manually inspect component resources as ordering of config is - // non-deterministic within slices - test.That(t, len(actualCfg.Components), test.ShouldEqual, 3) - for _, comp := range actualCfg.Components { - isMyBase := comp.Equals(expectedCfg.Components[0]) - isMotor1 := comp.Equals(expectedCfg.Components[1]) - isMotor2 := comp.Equals(expectedCfg.Components[2]) - test.That(t, isMyBase || isMotor1 || isMotor2, test.ShouldBeTrue) - } - actualCfg.Components = nil - expectedCfg.Components = nil - - // Manually inspect remote resources, modules, and processes as Equals should be used - // (alreadyValidated will have been set to true). - test.That(t, len(actualCfg.Remotes), test.ShouldEqual, 1) - test.That(t, actualCfg.Remotes[0].Equals(expectedCfg.Remotes[0]), test.ShouldBeTrue) - actualCfg.Remotes = nil - expectedCfg.Remotes = nil - test.That(t, len(actualCfg.Processes), test.ShouldEqual, 1) - test.That(t, actualCfg.Processes[0].Equals(expectedCfg.Processes[0]), test.ShouldBeTrue) - actualCfg.Processes = nil - expectedCfg.Processes = nil - test.That(t, len(actualCfg.Modules), test.ShouldEqual, 1) - test.That(t, actualCfg.Modules[0].Equals(expectedCfg.Modules[0]), test.ShouldBeTrue) - actualCfg.Modules = nil - expectedCfg.Modules = nil - - test.That(t, actualCfg, test.ShouldResemble, &expectedCfg) -} - -func TestReconnectRemote(t *testing.T) { - logger := logging.NewTestLogger(t) - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - // start the first robot - ctx := context.Background() - armConfig := resource.Config{ - Name: "arm1", - API: arm.API, - Model: fakeModel, - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - } - cfg := config.Config{ - Components: []resource.Config{armConfig}, - } - - robot := setupLocalRobot(t, ctx, &cfg, logger) - err := robot.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // start the second robot - ctx1 := context.Background() - options1, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - - remoteConf := config.Remote{ - Name: "remote", - Insecure: true, - Address: addr, - } - - cfg1 := config.Config{ - Remotes: []config.Remote{remoteConf}, - } - - robot1 := setupLocalRobot(t, ctx, &cfg1, logger) - - err = robot1.StartWeb(ctx1, options1) - test.That(t, err, test.ShouldBeNil) - - robotClient := robottestutils.NewRobotClient(t, logger, addr1, time.Second) - defer func() { - test.That(t, robotClient.Close(context.Background()), test.ShouldBeNil) - }() - - a1, err := arm.FromRobot(robot1, "arm1") - test.That(t, err, test.ShouldBeNil) - test.That(t, a1, test.ShouldNotBeNil) - - remoteRobot, ok := robot1.RemoteByName("remote") - test.That(t, ok, test.ShouldBeTrue) - test.That(t, remoteRobot, test.ShouldNotBeNil) - remoteRobotClient, ok := remoteRobot.(*client.RobotClient) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, remoteRobotClient, test.ShouldNotBeNil) - - a, err := robotClient.ResourceByName(arm.Named("remote:arm1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, a, test.ShouldNotBeNil) - anArm, ok := a.(arm.Arm) - test.That(t, ok, test.ShouldBeTrue) - _, err = anArm.EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - - // close/disconnect the robot - robot.StopWeb() - test.That(t, <-remoteRobotClient.Changed(), test.ShouldBeTrue) - test.That(t, len(remoteRobotClient.ResourceNames()), test.ShouldEqual, 0) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, len(robotClient.ResourceNames()), test.ShouldEqual, 2) - }) - test.That(t, len(robot1.ResourceNames()), test.ShouldEqual, 2) - _, err = anArm.EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeError) - - // reconnect the first robot - ctx2 := context.Background() - listener, err := net.Listen("tcp", addr) - test.That(t, err, test.ShouldBeNil) - - options.Network.Listener = listener - err = robot.StartWeb(ctx2, options) - test.That(t, err, test.ShouldBeNil) - - // check if the original arm can still be called - test.That(t, <-remoteRobotClient.Changed(), test.ShouldBeTrue) - test.That(t, remoteRobotClient.Connected(), test.ShouldBeTrue) - test.That(t, len(remoteRobotClient.ResourceNames()), test.ShouldEqual, 3) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, len(robotClient.ResourceNames()), test.ShouldEqual, 5) - }) - test.That(t, len(robot1.ResourceNames()), test.ShouldEqual, 5) - _, err = remoteRobotClient.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - - _, err = robotClient.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = anArm.EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) -} - -func TestReconnectRemoteChangeConfig(t *testing.T) { - logger := logging.NewTestLogger(t) - - // start the first robot - ctx := context.Background() - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - armConfig := resource.Config{ - Name: "arm1", - API: arm.API, - Model: fakeModel, - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - } - cfg := config.Config{ - Components: []resource.Config{armConfig}, - } - - robot := setupLocalRobot(t, ctx, &cfg, logger) - err := robot.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // start the second robot - ctx1 := context.Background() - options1, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - remoteConf := config.Remote{ - Name: "remote", - Insecure: true, - Address: addr, - } - - cfg1 := config.Config{ - Remotes: []config.Remote{remoteConf}, - } - - robot1 := setupLocalRobot(t, ctx, &cfg1, logger) - - err = robot1.StartWeb(ctx1, options1) - test.That(t, err, test.ShouldBeNil) - - robotClient := robottestutils.NewRobotClient(t, logger, addr1, time.Second) - defer func() { - test.That(t, robotClient.Close(context.Background()), test.ShouldBeNil) - }() - - a1, err := arm.FromRobot(robot1, "arm1") - test.That(t, err, test.ShouldBeNil) - test.That(t, a1, test.ShouldNotBeNil) - - remoteRobot, ok := robot1.RemoteByName("remote") - test.That(t, ok, test.ShouldBeTrue) - test.That(t, remoteRobot, test.ShouldNotBeNil) - remoteRobotClient, ok := remoteRobot.(*client.RobotClient) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, remoteRobotClient, test.ShouldNotBeNil) - - a, err := robotClient.ResourceByName(arm.Named("remote:arm1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, a, test.ShouldNotBeNil) - anArm, ok := a.(arm.Arm) - test.That(t, ok, test.ShouldBeTrue) - _, err = anArm.EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - - // close/disconnect the robot - test.That(t, robot.Close(context.Background()), test.ShouldBeNil) - test.That(t, <-remoteRobotClient.Changed(), test.ShouldBeTrue) - test.That(t, len(remoteRobotClient.ResourceNames()), test.ShouldEqual, 0) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, len(robotClient.ResourceNames()), test.ShouldEqual, 2) - }) - test.That(t, len(robot1.ResourceNames()), test.ShouldEqual, 2) - _, err = anArm.EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeError) - - // reconnect the first robot - ctx2 := context.Background() - listener, err := net.Listen("tcp", addr) - test.That(t, err, test.ShouldBeNil) - baseConfig := resource.Config{ - Name: "base1", - API: base.API, - Model: fakeModel, - } - cfg = config.Config{ - Components: []resource.Config{baseConfig}, - } - - options = weboptions.New() - options.Network.BindAddress = "" - options.Network.Listener = listener - robot = setupLocalRobot(t, ctx, &cfg, logger) - err = robot.StartWeb(ctx2, options) - test.That(t, err, test.ShouldBeNil) - - // check if the original arm can't be called anymore - test.That(t, <-remoteRobotClient.Changed(), test.ShouldBeTrue) - test.That(t, remoteRobotClient.Connected(), test.ShouldBeTrue) - test.That(t, len(remoteRobotClient.ResourceNames()), test.ShouldEqual, 3) - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, len(robotClient.ResourceNames()), test.ShouldEqual, 5) - }) - test.That(t, len(robot1.ResourceNames()), test.ShouldEqual, 5) - _, err = anArm.EndPosition(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeError) - - // check that base is now instantiated - _, err = remoteRobotClient.ResourceByName(base.Named("base1")) - test.That(t, err, test.ShouldBeNil) - - b, err := robotClient.ResourceByName(base.Named("remote:base1")) - test.That(t, err, test.ShouldBeNil) - aBase, ok := b.(base.Base) - test.That(t, ok, test.ShouldBeTrue) - - err = aBase.Stop(ctx, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(robotClient.ResourceNames()), test.ShouldEqual, 5) -} - -func TestCheckMaxInstanceValid(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg := &config.Config{ - Services: []resource.Config{ - { - Name: "fake1", - Model: resource.DefaultServiceModel, - API: motion.API, - DependsOn: []string{framesystem.InternalServiceName.String()}, - ConvertedAttributes: &motionBuiltin.Config{}, - }, - { - Name: "fake2", - Model: resource.DefaultServiceModel, - API: motion.API, - DependsOn: []string{framesystem.InternalServiceName.String()}, - ConvertedAttributes: &motionBuiltin.Config{}, - }, - }, - Components: []resource.Config{ - { - Name: "fake2", - Model: fake.Model, - API: arm.API, - ConvertedAttributes: &fake.Config{}, - }, - }, - } - r := setupLocalRobot(t, context.Background(), cfg, logger) - res, err := r.ResourceByName(motion.Named("fake1")) - test.That(t, res, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - res, err = r.ResourceByName(motion.Named("fake2")) - test.That(t, res, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - res, err = r.ResourceByName(arm.Named("fake2")) - test.That(t, res, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) -} - -// The max allowed datamanager services is 1 so only one of the datamanager services -// from this config should build. -func TestCheckMaxInstanceInvalid(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg := &config.Config{ - Services: []resource.Config{ - { - Name: "fake1", - Model: resource.DefaultServiceModel, - API: datamanager.API, - ConvertedAttributes: &builtin.Config{}, - DependsOn: []string{internalcloud.InternalServiceName.String()}, - }, - { - Name: "fake2", - Model: resource.DefaultServiceModel, - API: datamanager.API, - ConvertedAttributes: &builtin.Config{}, - DependsOn: []string{internalcloud.InternalServiceName.String()}, - }, - { - Name: "fake3", - Model: resource.DefaultServiceModel, - API: datamanager.API, - ConvertedAttributes: &builtin.Config{}, - DependsOn: []string{internalcloud.InternalServiceName.String()}, - }, - }, - Components: []resource.Config{ - { - Name: "fake2", - Model: fake.Model, - API: arm.API, - ConvertedAttributes: &fake.Config{}, - }, - { - Name: "fake3", - Model: fake.Model, - API: arm.API, - ConvertedAttributes: &fake.Config{}, - }, - }, - } - r := setupLocalRobot(t, context.Background(), cfg, logger) - maxInstance := 0 - for _, name := range r.ResourceNames() { - if name.API == datamanager.API { - maxInstance++ - } - } - test.That(t, maxInstance, test.ShouldEqual, 1) - numInstances := 0 - for _, name := range r.ResourceNames() { - if name.API == arm.API { - numInstances++ - } - } - test.That(t, numInstances, test.ShouldEqual, 2) -} - -func TestCheckMaxInstanceSkipRemote(t *testing.T) { - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - - ctx := context.Background() - logger := logging.NewTestLogger(t) - - remoteConfig := setupLocalRobot(t, ctx, &config.Config{ - Services: []resource.Config{ - { - Name: "fake1", - Model: resource.DefaultServiceModel, - API: datamanager.API, - ConvertedAttributes: &builtin.Config{}, - DependsOn: []string{internalcloud.InternalServiceName.String()}, - }, - }, - }, logger) - - err := remoteConfig.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - otherConfig := &config.Config{ - Services: []resource.Config{ - { - Name: "fake2", - Model: resource.DefaultServiceModel, - API: datamanager.API, - ConvertedAttributes: &builtin.Config{}, - DependsOn: []string{internalcloud.InternalServiceName.String()}, - }, - }, - Remotes: []config.Remote{ - { - Name: "remote", - Address: addr, - AssociatedResourceConfigs: []resource.AssociatedResourceConfig{}, - }, - }, - } - - r := setupLocalRobot(t, ctx, otherConfig, logger) - - maxInstance := 0 - for _, name := range r.ResourceNames() { - if name.API == datamanager.API { - maxInstance++ - } - } - test.That(t, maxInstance, test.ShouldEqual, 2) - - _, err = r.ResourceByName(datamanager.Named("fake1")) - test.That(t, err, test.ShouldBeNil) -} - -func TestDependentResources(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "b", - Model: fakeModel, - API: base.API, - }, - { - Name: "m", - Model: fakeModel, - API: motor.API, - DependsOn: []string{"b"}, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "m1", - Model: fakeModel, - API: motor.API, - DependsOn: []string{"m"}, - ConvertedAttributes: &fakemotor.Config{}, - }, - }, - Services: []resource.Config{ - { - Name: "s", - Model: fakeModel, - API: slam.API, - DependsOn: []string{"b"}, - }, - }, - } - r := setupLocalRobot(t, ctx, cfg, logger) - - // Assert that removing base 'b' removes motors 'm' and 'm1' and slam service 's'. - cfg2 := &config.Config{ - Components: []resource.Config{ - { - Name: "m", - Model: fakeModel, - API: motor.API, - DependsOn: []string{"b"}, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "m1", - Model: fakeModel, - API: motor.API, - DependsOn: []string{"m"}, - ConvertedAttributes: &fakemotor.Config{}, - }, - }, - Services: []resource.Config{ - { - Name: "s", - Model: fakeModel, - API: slam.API, - DependsOn: []string{"b"}, - }, - }, - } - r.Reconfigure(ctx, cfg2) - - res, err := r.ResourceByName(base.Named("b")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(base.Named("b"))) - test.That(t, res, test.ShouldBeNil) - res, err = r.ResourceByName(motor.Named("m")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(motor.Named("m"))) - test.That(t, res, test.ShouldBeNil) - res, err = r.ResourceByName(motor.Named("m1")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(motor.Named("m1"))) - test.That(t, res, test.ShouldBeNil) - res, err = r.ResourceByName(slam.Named("s")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(slam.Named("s"))) - test.That(t, res, test.ShouldBeNil) - - // Assert that adding base 'b' back re-adds 'm' and 'm1' and slam service 's'. - r.Reconfigure(ctx, cfg) - - _, err = r.ResourceByName(base.Named("b")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(motor.Named("m")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(motor.Named("m1")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(slam.Named("s")) - test.That(t, err, test.ShouldBeNil) -} - -func TestOrphanedResources(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - - // Precompile modules to avoid timeout issues when building takes too long. - complexPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - simplePath := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") - testPath := rtestutils.BuildTempModule(t, "module/testmodule") - - // Manually define models, as importing them can cause double registration. - gizmoModel := resource.NewModel("acme", "demo", "mygizmo") - summationModel := resource.NewModel("acme", "demo", "mysum") - gizmoAPI := resource.APINamespace("acme").WithComponentType("gizmo") - summationAPI := resource.APINamespace("acme").WithServiceType("summation") - helperModel := resource.NewModel("rdk", "test", "helper") - - r := setupLocalRobot(t, ctx, &config.Config{}, logger) - - t.Run("manual reconfiguration", func(t *testing.T) { - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: complexPath, - }, - }, - Components: []resource.Config{ - { - Name: "g", - Model: gizmoModel, - API: gizmoAPI, - Attributes: rutils.AttributeMap{ - "arg1": "foo", - }, - }, - }, - Services: []resource.Config{ - { - Name: "s", - Model: summationModel, - API: summationAPI, - }, - }, - } - r.Reconfigure(ctx, cfg) - - // Assert that reconfiguring module 'mod' to a new module that does not - // handle old resources removes modular component 'g' and modular service - // 's'. - cfg2 := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: simplePath, - }, - }, - Components: []resource.Config{ - { - Name: "g", - Model: gizmoModel, - API: gizmoAPI, - Attributes: rutils.AttributeMap{ - "arg1": "foo", - }, - }, - }, - Services: []resource.Config{ - { - Name: "s", - Model: summationModel, - API: summationAPI, - }, - }, - } - r.Reconfigure(ctx, cfg2) - - res, err := r.ResourceByName(gizmoapi.Named("g")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(gizmoapi.Named("g"))) - test.That(t, res, test.ShouldBeNil) - res, err = r.ResourceByName(summationapi.Named("s")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(summationapi.Named("s"))) - test.That(t, res, test.ShouldBeNil) - - // Remove module entirely. - cfg3 := &config.Config{ - Components: []resource.Config{ - { - Name: "g", - Model: gizmoModel, - API: gizmoAPI, - Attributes: rutils.AttributeMap{ - "arg1": "foo", - }, - }, - }, - Services: []resource.Config{ - { - Name: "s", - Model: summationModel, - API: summationAPI, - }, - }, - } - r.Reconfigure(ctx, cfg3) - - // Assert that adding module 'mod' back with original executable path re-adds - // modular component 'g' and modular service 's'. - r.Reconfigure(ctx, cfg) - - _, err = r.ResourceByName(gizmoapi.Named("g")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(summationapi.Named("s")) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("automatic reconfiguration", func(t *testing.T) { - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - }, - }, - Components: []resource.Config{ - { - Name: "h", - Model: helperModel, - API: generic.API, - }, - }, - } - r.Reconfigure(ctx, cfg) - - h, err := r.ResourceByName(generic.Named("h")) - test.That(t, err, test.ShouldBeNil) - - // Assert that removing testmodule binary and killing testmodule orphans - // helper 'h' a couple seconds after third restart attempt. - err = os.Rename(testPath, testPath+".disabled") - test.That(t, err, test.ShouldBeNil) - _, err = h.DoCommand(ctx, map[string]interface{}{"command": "kill_module"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "rpc error") - - // Wait for 3 restart attempts in logs. - testutils.WaitForAssertionWithSleep(t, time.Second, 20, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterFieldKey("restart attempt").Len(), - test.ShouldEqual, 3) - }) - time.Sleep(2 * time.Second) - - _, err = r.ResourceByName(generic.Named("h")) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(generic.Named("h"))) - - // Assert that restoring testmodule, removing testmodule from config and - // adding it back re-adds 'h'. - err = os.Rename(testPath+".disabled", testPath) - test.That(t, err, test.ShouldBeNil) - cfg2 := &config.Config{ - Components: []resource.Config{ - { - Name: "h", - Model: helperModel, - API: generic.API, - }, - }, - } - r.Reconfigure(ctx, cfg2) - r.Reconfigure(ctx, cfg) - - h, err = r.ResourceByName(generic.Named("h")) - test.That(t, err, test.ShouldBeNil) - - // Assert that replacing testmodule binary with disguised simplemodule - // binary and killing testmodule orphans helper 'h' (not reachable), as - // simplemodule binary cannot manage helper 'h'. - tmpPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") - err = os.Rename(tmpPath, testPath) - test.That(t, err, test.ShouldBeNil) - _, err = h.DoCommand(ctx, map[string]interface{}{"command": "kill_module"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "rpc error") - - // Wait for 3 restart attempts in logs. - testutils.WaitForAssertionWithSleep(t, time.Second, 20, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterFieldKey("restart attempt").Len(), - test.ShouldEqual, 3) - }) - time.Sleep(2 * time.Second) - - _, err = r.ResourceByName(generic.Named("h")) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(generic.Named("h"))) - - // Also assert that testmodule's resources were deregistered. - _, ok := resource.LookupRegistration(generic.API, helperModel) - test.That(t, ok, test.ShouldBeFalse) - testMotorModel := resource.NewModel("rdk", "test", "motor") - _, ok = resource.LookupRegistration(motor.API, testMotorModel) - test.That(t, ok, test.ShouldBeFalse) - }) -} - -var ( - doodadModel = resource.DefaultModelFamily.WithModel("mydoodad") - doodadAPI = resource.APINamespaceRDK.WithComponentType("doodad") -) - -// doodad is an RDK-built component that depends on a modular gizmo. -type doodad struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - gizmo gizmoapi.Gizmo -} - -// doThroughGizmo calls the underlying gizmo's DoCommand. -func (d *doodad) doThroughGizmo(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - return d.gizmo.DoCommand(ctx, cmd) -} - -func TestDependentAndOrphanedResources(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Precompile modules to avoid timeout issues when building takes too long. - complexPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - simplePath := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") - - // Manually define gizmo model, as importing it from mygizmo can cause double - // registration. - gizmoModel := resource.NewModel("acme", "demo", "mygizmo") - - // Register a doodad constructor and defer its deregistration. - resource.RegisterComponent(doodadAPI, doodadModel, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - newDoodad := &doodad{ - Named: conf.ResourceName().AsNamed(), - } - for rName, res := range deps { - if rName.API == gizmoapi.API { - gizmo, ok := res.(gizmoapi.Gizmo) - if !ok { - return nil, errors.Errorf("resource %s is not a gizmo", rName.Name) - } - newDoodad.gizmo = gizmo - } - } - if newDoodad.gizmo == nil { - return nil, errors.Errorf("doodad %s must depend on a gizmo", conf.Name) - } - return newDoodad, nil - }, - }) - defer func() { - resource.Deregister(doodadAPI, doodadModel) - }() - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: complexPath, - }, - }, - Components: []resource.Config{ - { - Name: "g", - API: resource.APINamespace("acme").WithComponentType("gizmo"), - Model: gizmoModel, - DependsOn: []string{"m"}, - Attributes: rutils.AttributeMap{ - "arg1": "foo", - }, - }, - { - Name: "m", - Model: fakeModel, - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "d", - API: resource.APINamespaceRDK.WithComponentType("doodad"), - Model: doodadModel, - DependsOn: []string{"g"}, - }, - }, - } - test.That(t, cfg.Ensure(false, logger), test.ShouldBeNil) - r := setupLocalRobot(t, ctx, cfg, logger) - - // Assert that reconfiguring module 'mod' to a new module that does not handle - // 'g' removes modular component 'g' and its dependent 'd' and leaves 'm' as-is. - cfg2 := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: simplePath, - }, - }, - Components: []resource.Config{ - { - Name: "g", - API: resource.APINamespace("acme").WithComponentType("gizmo"), - Model: gizmoModel, - DependsOn: []string{"m"}, - Attributes: rutils.AttributeMap{ - "arg1": "foo", - }, - }, - { - Name: "m", - Model: fakeModel, - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "d", - API: resource.APINamespaceRDK.WithComponentType("doodad"), - Model: doodadModel, - DependsOn: []string{"g"}, - }, - }, - } - r.Reconfigure(ctx, cfg2) - - res, err := r.ResourceByName(gizmoapi.Named("g")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(gizmoapi.Named("g"))) - test.That(t, res, test.ShouldBeNil) - res, err = r.ResourceByName(resource.NewName(doodadAPI, "d")) - test.That(t, err, test.ShouldBeError, - resource.NewNotFoundError(resource.NewName(doodadAPI, "d"))) - test.That(t, res, test.ShouldBeNil) - _, err = r.ResourceByName(motor.Named("m")) - test.That(t, err, test.ShouldBeNil) - - // Remove module entirely. - cfg3 := &config.Config{ - Components: []resource.Config{ - { - Name: "g", - API: resource.APINamespace("acme").WithComponentType("gizmo"), - Model: gizmoModel, - DependsOn: []string{"m"}, - Attributes: rutils.AttributeMap{ - "arg1": "foo", - }, - }, - { - Name: "m", - Model: fakeModel, - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - }, - { - Name: "d", - API: resource.APINamespaceRDK.WithComponentType("doodad"), - Model: doodadModel, - DependsOn: []string{"g"}, - }, - }, - } - r.Reconfigure(ctx, cfg3) - - // Assert that adding module 'mod' back with original executable path re-adds - // modular component 'd' and its dependent 'd', and that 'm' is still present. - r.Reconfigure(ctx, cfg) - - _, err = r.ResourceByName(gizmoapi.Named("g")) - test.That(t, err, test.ShouldBeNil) - d, err := r.ResourceByName(resource.NewName(doodadAPI, "d")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(motor.Named("m")) - test.That(t, err, test.ShouldBeNil) - - // Assert that doodad 'd' can make gRPC calls through underlying 'g'. - doodadD, ok := d.(*doodad) - test.That(t, ok, test.ShouldBeTrue) - cmd := map[string]interface{}{"foo": "bar"} - resp, err := doodadD.doThroughGizmo(ctx, cmd) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldResemble, cmd) -} - -func TestModuleDebugReconfigure(t *testing.T) { - ctx := context.Background() - // We must use an Info level observed test logger to avoid testmodule - // inheriting debug mode from the module manager. - logger, logs := rtestutils.NewInfoObservedTestLogger(t) - - // Precompile module to avoid timeout issues when building takes too long. - testPath := rtestutils.BuildTempModule(t, "module/testmodule") - - // Create robot with testmodule with LogLevel unset and assert that after two - // seconds, "debug mode enabled" debug log is not output by testmodule. - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - }, - }, - } - r := setupLocalRobot(t, ctx, cfg, logger) - - time.Sleep(2 * time.Second) - test.That(t, logs.FilterMessageSnippet("debug mode enabled").Len(), - test.ShouldEqual, 0) - - // Reconfigure testmodule to have a "debug" LogLevel and assert that "debug - // mode enabled" debug log is eventually output by testmodule. - cfg2 := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - LogLevel: "debug", - }, - }, - } - r.Reconfigure(ctx, cfg2) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - test.That(tb, logs.FilterMessageSnippet("debug mode enabled").Len(), - test.ShouldEqual, 1) - }) -} - -func TestResourcelessModuleRemove(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - - // Precompile module to avoid timeout issues when building takes too long. - testPath := rtestutils.BuildTempModule(t, "module/testmodule") - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - }, - }, - } - r := setupLocalRobot(t, ctx, cfg, logger) - - // Reconfigure to an empty config and assert that the testmodule process - // is stopped. - r.Reconfigure(ctx, &config.Config{}) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - test.That(tb, logs.FilterMessageSnippet("Shutting down gracefully").Len(), - test.ShouldEqual, 1) - }) -} - -func TestCrashedModuleReconfigure(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - - testPath := rtestutils.BuildTempModule(t, "module/testmodule") - - // Manually define model, as importing it can cause double registration. - helperModel := resource.NewModel("rdk", "test", "helper") - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - }, - }, - Components: []resource.Config{ - { - Name: "h", - Model: helperModel, - API: generic.API, - }, - }, - } - r := setupLocalRobot(t, ctx, cfg, logger) - - _, err := r.ResourceByName(generic.Named("h")) - test.That(t, err, test.ShouldBeNil) - - t.Run("reconfiguration timeout", func(t *testing.T) { - // Lower timeouts to avoid waiting for 60 seconds for reconfig and module. - defer func() { - test.That(t, os.Unsetenv(rutils.ResourceConfigurationTimeoutEnvVar), - test.ShouldBeNil) - test.That(t, os.Unsetenv(rutils.ModuleStartupTimeoutEnvVar), - test.ShouldBeNil) - }() - t.Setenv(rutils.ResourceConfigurationTimeoutEnvVar, "500ms") - t.Setenv(rutils.ModuleStartupTimeoutEnvVar, "500ms") - - // Reconfigure module to a malformed module (does not start listening). - // Assert that "h" is removed after reconfiguration error. - cfg.Modules[0].ExePath = rutils.ResolveFile("module/testmodule/fakemodule.sh") - r.Reconfigure(ctx, cfg) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - test.That(t, logs.FilterMessage("error reconfiguring module").Len(), test.ShouldEqual, 1) - }) - - _, err = r.ResourceByName(generic.Named("h")) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(generic.Named("h"))) - }) - - // Reconfigure module back to testmodule. Assert that 'h' is eventually - // added back to the resource manager (the module recovers). - cfg.Modules[0].ExePath = testPath - r.Reconfigure(ctx, cfg) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - _, err = r.ResourceByName(generic.Named("h")) - test.That(tb, err, test.ShouldBeNil) - }) -} - -func TestModularResourceReconfigurationCount(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - - testPath := rtestutils.BuildTempModule(t, "module/testmodule") - - // Manually define models, as importing them can cause double registration. - helperModel := resource.NewModel("rdk", "test", "helper") - otherModel := resource.NewModel("rdk", "test", "other") - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - }, - }, - Components: []resource.Config{ - { - Name: "h", - Model: helperModel, - API: generic.API, - }, - }, - Services: []resource.Config{ - { - Name: "o", - Model: otherModel, - API: genericservice.API, - }, - }, - } - r := setupLocalRobot(t, ctx, cfg, logger) - - // Assert that helper and other have not yet `Reconfigure`d (only constructed). - h, err := r.ResourceByName(generic.Named("h")) - test.That(t, err, test.ShouldBeNil) - resp, err := h.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) - o, err := r.ResourceByName(genericservice.Named("o")) - test.That(t, err, test.ShouldBeNil) - resp, err = o.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) - - cfg2 := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - LogLevel: "debug", - }, - }, - Components: []resource.Config{ - { - Name: "h", - Model: helperModel, - API: generic.API, - }, - }, - Services: []resource.Config{ - { - Name: "o", - Model: otherModel, - API: genericservice.API, - }, - }, - } - r.Reconfigure(ctx, cfg2) - - // Assert that helper and other have still not `Reconfigure`d after their - // module did (only constructed in the restarted module). - resp, err = h.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) - resp, err = o.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) - - cfg3 := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - LogLevel: "debug", - }, - }, - Components: []resource.Config{ - { - Name: "h", - Model: helperModel, - API: generic.API, - Attributes: rutils.AttributeMap{ - "foo": "bar", - }, - }, - }, - Services: []resource.Config{ - { - Name: "o", - Model: otherModel, - API: genericservice.API, - Attributes: rutils.AttributeMap{ - "foo": "bar", - }, - }, - }, - } - r.Reconfigure(ctx, cfg3) - - // Assert that helper and other `Reconfigure` once when their attributes are - // changed. - resp, err = h.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 1) - resp, err = o.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 1) - - cfg4 := &config.Config{ - Modules: []config.Module{ - { - Name: "mod", - ExePath: testPath, - }, - }, - Components: []resource.Config{ - { - Name: "h", - Model: helperModel, - API: generic.API, - Attributes: rutils.AttributeMap{ - "bar": "baz", - }, - }, - }, - Services: []resource.Config{ - { - Name: "o", - Model: otherModel, - API: genericservice.API, - Attributes: rutils.AttributeMap{ - "bar": "baz", - }, - }, - }, - } - r.Reconfigure(ctx, cfg4) - - // Assert that if module is reconfigured (`LogLevel` removed), _and_ helper - // and other are reconfigured (attributes changed), helper and other are only - // constructed in new module process and not `Reconfigure`d. - resp, err = h.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) - resp, err = o.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) - - // Assert that helper and other are only constructed after module - // crash/successful restart and not `Reconfigure`d. - _, err = h.DoCommand(ctx, map[string]any{"command": "kill_module"}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "rpc error") - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterMessageSnippet("Module resources successfully re-added after module restart").Len(), test.ShouldEqual, 1) - }) - - resp, err = h.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) - resp, err = o.DoCommand(ctx, map[string]any{"command": "get_num_reconfigurations"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp["num_reconfigurations"], test.ShouldEqual, 0) -} - -func TestImplicitDepsAcrossModules(t *testing.T) { - ctx := context.Background() - logger, _ := logging.NewObservedTestLogger(t) - - // Precompile modules to avoid timeout issues when building takes too long. - complexPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - testPath := rtestutils.BuildTempModule(t, "module/testmodule") - - // Manually define models, as importing them can cause double registration. - myBaseModel := resource.NewModel("acme", "demo", "mybase") - testMotorModel := resource.NewModel("rdk", "test", "motor") - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "complex-module", - ExePath: complexPath, - }, - { - Name: "test-module", - ExePath: testPath, - }, - }, - Components: []resource.Config{ - { - Name: "b", - Model: myBaseModel, - API: base.API, - Attributes: rutils.AttributeMap{ - "motorL": "m1", - "motorR": "m2", - }, - }, - { - Name: "m1", - Model: testMotorModel, - API: motor.API, - }, - { - Name: "m2", - Model: testMotorModel, - API: motor.API, - }, - }, - } - r := setupLocalRobot(t, ctx, cfg, logger) - - _, err := r.ResourceByName(base.Named("b")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(motor.Named("m1")) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(motor.Named("m2")) - test.That(t, err, test.ShouldBeNil) -} - -func TestResourceByNameAcrossRemotes(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Setup a robot1 -> robot2 -> robot3 -> robot4 remote chain. Ensure that if - // robot4 has an encoder "e", all robots in the chain can retrieve it by - // simple name "e" or short name "[remote-prefix]:e". Also ensure that a - // motor "m1" on robot1 can depend on "robot2:robot3:robot4:e" and a motor - // "m2" on robot2 can depend on "e". - - startWeb := func(r robot.LocalRobot) string { - var boundAddress string - for i := 0; i < 10; i++ { - port, err := utils.TryReserveRandomPort() - test.That(t, err, test.ShouldBeNil) - - options := weboptions.New() - boundAddress = fmt.Sprintf("localhost:%v", port) - options.Network.BindAddress = boundAddress - if err := r.StartWeb(ctx, options); err != nil { - r.StopWeb() - if strings.Contains(err.Error(), "address already in use") { - logger.Infow("port in use; restarting on new port", "port", port, "err", err) - continue - } - t.Fatalf("StartWeb error: %v", err) - } - break - } - return boundAddress - } - - cfg4 := &config.Config{ - Components: []resource.Config{ - { - Name: "e", - Model: resource.DefaultModelFamily.WithModel("fake"), - API: encoder.API, - ConvertedAttributes: &fakeencoder.Config{}, - }, - }, - } - robot4 := setupLocalRobot(t, ctx, cfg4, logger) - addr4 := startWeb(robot4) - test.That(t, addr4, test.ShouldNotBeBlank) - - cfg3 := &config.Config{ - Remotes: []config.Remote{ - { - Name: "robot4", - Address: addr4, - }, - }, - } - robot3 := setupLocalRobot(t, ctx, cfg3, logger) - addr3 := startWeb(robot3) - test.That(t, addr3, test.ShouldNotBeBlank) - - cfg2 := &config.Config{ - Remotes: []config.Remote{ - { - Name: "robot3", - Address: addr3, - }, - }, - Components: []resource.Config{ - { - Name: "m2", - Model: resource.DefaultModelFamily.WithModel("fake"), - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - // ensure DependsOn works with simple name (implicit remotes) - DependsOn: []string{"e"}, - }, - }, - } - robot2 := setupLocalRobot(t, ctx, cfg2, logger) - addr2 := startWeb(robot2) - test.That(t, addr2, test.ShouldNotBeBlank) - - cfg1 := &config.Config{ - Remotes: []config.Remote{ - { - Name: "robot2", - Address: addr2, - }, - }, - Components: []resource.Config{ - { - Name: "m1", - Model: resource.DefaultModelFamily.WithModel("fake"), - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - // ensure DependsOn works with short name (explicit remotes) - DependsOn: []string{"robot2:robot3:robot4:e"}, - }, - }, - } - robot1 := setupLocalRobot(t, ctx, cfg1, logger) - - // Ensure that "e" can be retrieved by short and simple names from all - // robots. Also ensure "m1" and "m2" can be retrieved from robot1 and robot2 - // (they built properly). - - _, err := robot4.ResourceByName(encoder.Named("e")) - test.That(t, err, test.ShouldBeNil) - - _, err = robot3.ResourceByName(encoder.Named("e")) - test.That(t, err, test.ShouldBeNil) - _, err = robot3.ResourceByName(encoder.Named("robot4:e")) - test.That(t, err, test.ShouldBeNil) - - _, err = robot2.ResourceByName(encoder.Named("e")) - test.That(t, err, test.ShouldBeNil) - _, err = robot2.ResourceByName(encoder.Named("robot3:robot4:e")) - test.That(t, err, test.ShouldBeNil) - _, err = robot2.ResourceByName(motor.Named("m2")) - test.That(t, err, test.ShouldBeNil) - - _, err = robot1.ResourceByName(encoder.Named("e")) - test.That(t, err, test.ShouldBeNil) - _, err = robot1.ResourceByName(encoder.Named("robot2:robot3:robot4:e")) - test.That(t, err, test.ShouldBeNil) - _, err = robot1.ResourceByName(motor.Named("m1")) - test.That(t, err, test.ShouldBeNil) -} - -func TestCloudMetadata(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - t.Run("no cloud data", func(t *testing.T) { - cfg := &config.Config{} - robot := setupLocalRobot(t, ctx, cfg, logger) - _, err := robot.CloudMetadata(ctx) - test.That(t, err, test.ShouldBeError, errors.New("cloud metadata not available")) - }) - t.Run("with cloud data", func(t *testing.T) { - cfg := &config.Config{ - Cloud: &config.Cloud{ - ID: "the-robot-part", - LocationID: "the-location", - PrimaryOrgID: "the-primary-org", - MachineID: "the-machine", - }, - } - robot := setupLocalRobot(t, ctx, cfg, logger) - md, err := robot.CloudMetadata(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, md, test.ShouldResemble, cloud.Metadata{ - PrimaryOrgID: "the-primary-org", - LocationID: "the-location", - MachineID: "the-machine", - MachinePartID: "the-robot-part", - }) - }) -} diff --git a/robot/impl/resource_manager_modular_test.go b/robot/impl/resource_manager_modular_test.go deleted file mode 100644 index a26b7c65838..00000000000 --- a/robot/impl/resource_manager_modular_test.go +++ /dev/null @@ -1,587 +0,0 @@ -package robotimpl - -import ( - "context" - "sync" - "testing" - - "github.com/jhump/protoreflect/desc" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module/modmanager" - "go.viam.com/rdk/module/modmaninterface" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/services/motion" - motionBuiltin "go.viam.com/rdk/services/motion/builtin" - rtestutils "go.viam.com/rdk/testutils" - "go.viam.com/rdk/utils" -) - -func TestModularResources(t *testing.T) { - ctx := context.Background() - - var ( - compAPI = resource.APINamespace("acme").WithComponentType("anvil") - compModel = resource.ModelNamespace("acme").WithFamily("anvil").WithModel("2000") - compModel2 = resource.ModelNamespace("acme").WithFamily("anvil").WithModel("3000") - - svcAPI = resource.APINamespace("acme").WithServiceType("sign") - svcModel = resource.ModelNamespace("acme").WithFamily("signage").WithModel("handheld") - ) - - setupTest := func(t *testing.T) (*localRobot, *dummyModMan) { - t.Helper() - - logger := logging.NewTestLogger(t) - compAPISvc, err := resource.NewAPIResourceCollection[resource.Resource](compAPI, nil) - test.That(t, err, test.ShouldBeNil) - svcAPISvc, err := resource.NewAPIResourceCollection[resource.Resource](svcAPI, nil) - test.That(t, err, test.ShouldBeNil) - mod := &dummyModMan{ - compAPISvc: compAPISvc, - svcAPISvc: svcAPISvc, - state: make(map[resource.Name]bool), - } - - r := setupLocalRobot(t, context.Background(), &config.Config{}, logger) - actualR := r.(*localRobot) - actualR.manager.moduleManager = mod - - resource.RegisterAPI(compAPI, - resource.APIRegistration[resource.Resource]{ReflectRPCServiceDesc: &desc.ServiceDescriptor{}}) - t.Cleanup(func() { - resource.DeregisterAPI(compAPI) - }) - resource.RegisterComponent(compAPI, compModel, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return mod.AddResource(ctx, conf, modmanager.DepsToNames(deps)) - }, - }) - t.Cleanup(func() { - resource.Deregister(compAPI, compModel) - }) - resource.RegisterComponent(compAPI, compModel2, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return mod.AddResource(ctx, conf, modmanager.DepsToNames(deps)) - }, - }) - t.Cleanup(func() { - resource.Deregister(compAPI, compModel2) - }) - - resource.RegisterAPI(svcAPI, - resource.APIRegistration[resource.Resource]{ReflectRPCServiceDesc: &desc.ServiceDescriptor{}}) - t.Cleanup(func() { - resource.DeregisterAPI(svcAPI) - }) - resource.Register(svcAPI, svcModel, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return mod.AddResource(ctx, conf, modmanager.DepsToNames(deps)) - }, - }) - t.Cleanup(func() { - resource.Deregister(svcAPI, svcModel) - }) - - return actualR, mod - } - - t.Run("process component", func(t *testing.T) { - r, mod := setupTest(t) - - // modular - cfg := resource.Config{Name: "oneton", API: compAPI, Model: compModel, Attributes: utils.AttributeMap{"arg1": "one"}} - _, err := cfg.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // changed attribute - cfg2 := resource.Config{Name: "oneton", API: compAPI, Model: compModel, Attributes: utils.AttributeMap{"arg1": "two"}} - _, err = cfg2.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // non-modular - cfg3 := resource.Config{ - Name: "builtin", - API: motor.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{}, - } - _, err = cfg3.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // changed name - cfg4 := resource.Config{Name: "oneton2", API: compAPI, Model: compModel, Attributes: utils.AttributeMap{"arg1": "two"}} - _, err = cfg4.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // Add a modular component - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg}, - }) - _, err = r.ResourceByName(cfg.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(mod.add), test.ShouldEqual, 1) - test.That(t, mod.add[0], test.ShouldResemble, cfg) - - // Reconfigure a modular component - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg2}, - }) - _, err = r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(mod.add), test.ShouldEqual, 1) - test.That(t, len(mod.reconf), test.ShouldEqual, 1) - test.That(t, mod.reconf[0], test.ShouldResemble, cfg2) - - // Add a non-modular component - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg2, cfg3}, - }) - _, err = r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(cfg3.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(mod.add), test.ShouldEqual, 1) - test.That(t, len(mod.reconf), test.ShouldEqual, 1) - - // Change the name of a modular component - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg4, cfg3}, - }) - _, err = r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(cfg2.ResourceName())) - _, err = r.ResourceByName(cfg4.ResourceName()) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(cfg3.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, mod.add, test.ShouldResemble, []resource.Config{cfg, cfg4}) - test.That(t, mod.remove, test.ShouldResemble, []resource.Name{cfg2.ResourceName()}) - test.That(t, mod.reconf, test.ShouldResemble, []resource.Config{cfg2}) - test.That(t, len(mod.state), test.ShouldEqual, 1) - }) - - t.Run("process service", func(t *testing.T) { - r, mod := setupTest(t) - - // modular - cfg := resource.Config{ - Name: "adder", - API: svcAPI, - Model: svcModel, - Attributes: utils.AttributeMap{"arg1": "one"}, - } - _, err := cfg.Validate("test", resource.APITypeServiceName) - test.That(t, err, test.ShouldBeNil) - - // changed attribute - cfg2 := resource.Config{ - Name: "adder", - API: svcAPI, - Model: svcModel, - Attributes: utils.AttributeMap{"arg1": "two"}, - } - _, err = cfg2.Validate("test", resource.APITypeServiceName) - test.That(t, err, test.ShouldBeNil) - - // non-modular - cfg3 := resource.Config{ - Name: "builtin", - API: motion.API, - Model: resource.DefaultServiceModel, - ConvertedAttributes: &motionBuiltin.Config{}, - DependsOn: []string{framesystem.InternalServiceName.String()}, - } - _, err = cfg3.Validate("test", resource.APITypeServiceName) - test.That(t, err, test.ShouldBeNil) - - test.That(t, err, test.ShouldBeNil) - - // Add a modular service - r.Reconfigure(context.Background(), &config.Config{ - Services: []resource.Config{cfg}, - }) - _, err = r.ResourceByName(cfg.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(mod.add), test.ShouldEqual, 1) - test.That(t, mod.add[0], test.ShouldResemble, cfg) - - // Reconfigure a modular service - r.Reconfigure(context.Background(), &config.Config{ - Services: []resource.Config{cfg2}, - }) - _, err = r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(mod.add), test.ShouldEqual, 1) - test.That(t, len(mod.reconf), test.ShouldEqual, 1) - test.That(t, mod.reconf[0], test.ShouldResemble, cfg2) - - // Add a non-modular service - r.Reconfigure(context.Background(), &config.Config{ - Services: []resource.Config{cfg2, cfg3}, - }) - _, err = r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(cfg3.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(mod.add), test.ShouldEqual, 1) - test.That(t, len(mod.reconf), test.ShouldEqual, 1) - }) - - t.Run("close", func(t *testing.T) { - r, mod := setupTest(t) - - compCfg := resource.Config{Name: "oneton", API: compAPI, Model: compModel, Attributes: utils.AttributeMap{"arg1": "one"}} - _, err := compCfg.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - svcCfg := resource.Config{ - Name: "adder", - API: svcAPI, - Model: svcModel, - Attributes: utils.AttributeMap{"arg1": "one"}, - } - _, err = svcCfg.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{compCfg, svcCfg}, - }) - _, err = r.ResourceByName(compCfg.ResourceName()) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(svcCfg.ResourceName()) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(mod.add), test.ShouldEqual, 2) - - test.That(t, r.manager.Close(ctx), test.ShouldBeNil) - - test.That(t, len(mod.add), test.ShouldEqual, 2) - test.That(t, len(mod.reconf), test.ShouldEqual, 0) - test.That(t, len(mod.remove), test.ShouldEqual, 2) - expected := map[resource.Name]struct{}{ - compCfg.ResourceName(): {}, - svcCfg.ResourceName(): {}, - } - for _, rem := range mod.remove { - test.That(t, expected, test.ShouldContainKey, rem) - delete(expected, rem) - } - test.That(t, expected, test.ShouldBeEmpty) - }) - - t.Run("builtin depends on previously removed but now added modular", func(t *testing.T) { - r, _ := setupTest(t) - - // modular we do not want - cfg := resource.Config{Name: "oneton2", API: compAPI, Model: compModel, Attributes: utils.AttributeMap{"arg1": "one"}} - _, err := cfg.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // non-modular - cfg2 := resource.Config{ - Name: "builtin", - API: motor.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{}, - ImplicitDependsOn: []string{"oneton"}, - } - _, err = cfg2.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // modular we want - cfg3 := resource.Config{Name: "oneton", API: compAPI, Model: compModel, Attributes: utils.AttributeMap{"arg1": "one"}} - _, err = cfg3.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - // what we want is originally available - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg3}, - }) - _, err = r.ResourceByName(cfg3.ResourceName()) - test.That(t, err, test.ShouldBeNil) - - // and then its not but called something else and what wants it cannot get it - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg, cfg2}, - }) - _, err = r.ResourceByName(cfg.ResourceName()) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "pending") - _, err = r.ResourceByName(cfg3.ResourceName()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(cfg3.ResourceName())) - - // we remove what we do not want and add what we do back in, fixing things - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg3, cfg2}, - }) - _, err = r.ResourceByName(cfg3.ResourceName()) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldBeNil) - _, err = r.ResourceByName(cfg.ResourceName()) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(cfg.ResourceName())) - }) - - t.Run("change model", func(t *testing.T) { - r, _ := setupTest(t) - - cfg := resource.Config{Name: "oneton", API: compAPI, Model: compModel, Attributes: utils.AttributeMap{"arg1": "one"}} - _, err := cfg.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg}, - }) - res1, err := r.ResourceByName(cfg.ResourceName()) - test.That(t, err, test.ShouldBeNil) - - cfg2 := resource.Config{Name: "oneton", API: compAPI, Model: compModel2, Attributes: utils.AttributeMap{"arg1": "one"}} - _, err = cfg2.Validate("test", resource.APITypeComponentName) - test.That(t, err, test.ShouldBeNil) - - r.Reconfigure(context.Background(), &config.Config{ - Components: []resource.Config{cfg2}, - }) - res2, err := r.ResourceByName(cfg2.ResourceName()) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldNotEqual, res1) - }) -} - -type dummyRes struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable -} - -type dummyModMan struct { - modmaninterface.ModuleManager - mu sync.Mutex - add []resource.Config - reconf []resource.Config - remove []resource.Name - compAPISvc resource.APIResourceCollection[resource.Resource] - svcAPISvc resource.APIResourceCollection[resource.Resource] - state map[resource.Name]bool -} - -func (m *dummyModMan) AddResource(ctx context.Context, conf resource.Config, deps []string) (resource.Resource, error) { - m.mu.Lock() - defer m.mu.Unlock() - m.add = append(m.add, conf) - m.state[conf.ResourceName()] = true - res := &dummyRes{ - Named: conf.ResourceName().AsNamed(), - } - if conf.API.IsComponent() { - if err := m.compAPISvc.Add(conf.ResourceName(), res); err != nil { - return nil, err - } - } else { - if err := m.svcAPISvc.Add(conf.ResourceName(), res); err != nil { - return nil, err - } - } - return res, nil -} - -func (m *dummyModMan) ReconfigureResource(ctx context.Context, conf resource.Config, deps []string) error { - m.mu.Lock() - defer m.mu.Unlock() - m.reconf = append(m.reconf, conf) - return nil -} - -func (m *dummyModMan) RemoveResource(ctx context.Context, name resource.Name) error { - m.mu.Lock() - defer m.mu.Unlock() - m.remove = append(m.remove, name) - delete(m.state, name) - if name.API.IsComponent() { - if err := m.compAPISvc.Remove(name); err != nil { - return err - } - } else { - if err := m.svcAPISvc.Remove(name); err != nil { - return err - } - } - return nil -} - -func (m *dummyModMan) IsModularResource(name resource.Name) bool { - m.mu.Lock() - defer m.mu.Unlock() - return name.Name != "builtin" -} - -func (m *dummyModMan) Configs() []config.Module { - m.mu.Lock() - defer m.mu.Unlock() - return nil -} - -func (m *dummyModMan) Provides(cfg resource.Config) bool { - m.mu.Lock() - defer m.mu.Unlock() - return cfg.Name != "builtin" -} - -func (m *dummyModMan) ValidateConfig(ctx context.Context, cfg resource.Config) ([]string, error) { - m.mu.Lock() - defer m.mu.Unlock() - return nil, nil -} - -func (m *dummyModMan) ResolveImplicitDependenciesInConfig(ctx context.Context, conf *config.Diff) error { - m.mu.Lock() - defer m.mu.Unlock() - return nil -} - -func (m *dummyModMan) CleanModuleDataDirectory() error { - m.mu.Lock() - defer m.mu.Unlock() - return nil -} - -func (m *dummyModMan) Close(ctx context.Context) error { - if len(m.state) != 0 { - return errors.New("attempt to close with active resources in place") - } - return nil -} - -func TestDynamicModuleLogging(t *testing.T) { - modPath := rtestutils.BuildTempModule(t, "module/testmodule") - - ctx := context.Background() - logger, observer := logging.NewObservedTestLogger(t) - - helperConf := resource.Config{ - Name: "helper", - API: generic.API, - Model: resource.NewModel("rdk", "test", "helper"), - LogConfiguration: resource.LogConfig{ - Level: logging.INFO, - }, - } - cfg := &config.Config{ - Components: []resource.Config{helperConf}, - Modules: []config.Module{{ - Name: "helperModule", - ExePath: modPath, - LogLevel: "info", - Type: "local", - }}, - } - - myRobot := setupLocalRobot(t, ctx, cfg, logger) - - client, err := generic.FromRobot(myRobot, "helper") - test.That(t, err, test.ShouldBeNil) - defer client.Close(ctx) - - //nolint:lll - // Have the module log a line at info. It should appear as: - // 2024-01-08T19:28:11.415-0800 INFO TestModule.rdk:component:generic/helper testmodule/main.go:147 info level log line {"module_log_ts": "2024-01-09T03:28:11.412Z", "foo": "bar"} - infoLogLine := "info level log line" - testCmd := map[string]interface{}{"command": "log", "msg": infoLogLine, "level": "info"} - _, err = client.DoCommand(ctx, testCmd) - test.That(t, err, test.ShouldBeNil) - - // Our log observer should find one occurrence of the log line with `module_log_ts` and `foo` - // arguments. - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, observer.FilterMessageSnippet(infoLogLine).Len(), test.ShouldEqual, 1) - test.That(tb, observer.FilterMessageSnippet(infoLogLine).FilterFieldKey("log_ts").Len(), test.ShouldEqual, 1) - test.That(tb, observer.FilterMessageSnippet(infoLogLine).FilterFieldKey("foo").Len(), test.ShouldEqual, 1) - }) - - // The module is currently configured to log at info. If the module tries to log at debug, - // nothing new should be observed. - debugLogLine := "debug level log line" - testCmd = map[string]interface{}{"command": "log", "msg": debugLogLine, "level": "debug"} - _, err = client.DoCommand(ctx, testCmd) - test.That(t, err, test.ShouldBeNil) - - test.That(t, observer.FilterMessageSnippet(infoLogLine).Len(), test.ShouldEqual, 1) - test.That(t, observer.FilterMessageSnippet(debugLogLine).Len(), test.ShouldEqual, 0) - - // Change the modular component to log at DEBUG instead of INFO. - cfg.Components[0].LogConfiguration.Level = logging.DEBUG - myRobot.Reconfigure(ctx, cfg) - - // Trying to log again at DEBUG should see our log line pattern show up a second time. Now with - // DEBUG in the output string. - testCmd = map[string]interface{}{"command": "log", "msg": debugLogLine, "level": "debug"} - _, err = client.DoCommand(ctx, testCmd) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, observer.FilterMessageSnippet(infoLogLine).Len(), test.ShouldEqual, 1) - test.That(tb, observer.FilterMessageSnippet(debugLogLine).Len(), test.ShouldEqual, 1) - }) -} - -func TestTwoModulesSameName(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - simplePath := rtestutils.BuildTempModule(t, "examples/customresources/demos/simplemodule") - complexPath := rtestutils.BuildTempModule(t, "examples/customresources/demos/complexmodule") - - cfg := &config.Config{ - Modules: []config.Module{ - { - Name: "samename", - ExePath: simplePath, - }, - { - Name: "samename", - ExePath: complexPath, - }, - }, - // This field is false due to zero-value by default, but specify explicitly - // here. When partial start is allowed, we will log an error about the - // duplicate module name, but still start up the first of the two modules. - DisablePartialStart: false, - } - r := setupLocalRobot(t, ctx, cfg, logger) - - rr, ok := r.(*localRobot) - test.That(t, ok, test.ShouldBeTrue) - - // Assert that only the first module with the same name was honored. - moduleCfgs := rr.manager.moduleManager.Configs() - test.That(t, len(moduleCfgs), test.ShouldEqual, 1) - test.That(t, moduleCfgs[0].ExePath, test.ShouldEqual, simplePath) -} diff --git a/robot/impl/resource_manager_test.go b/robot/impl/resource_manager_test.go deleted file mode 100644 index 14c721af720..00000000000 --- a/robot/impl/resource_manager_test.go +++ /dev/null @@ -1,1852 +0,0 @@ -package robotimpl - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "os" - "sync" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/jhump/protoreflect/desc" - "github.com/jhump/protoreflect/grpcreflect" - "go.mongodb.org/mongo-driver/bson/primitive" - armpb "go.viam.com/api/component/arm/v1" - basepb "go.viam.com/api/component/base/v1" - boardpb "go.viam.com/api/component/board/v1" - camerapb "go.viam.com/api/component/camera/v1" - gripperpb "go.viam.com/api/component/gripper/v1" - motionpb "go.viam.com/api/service/motion/v1" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/pexec" - "go.viam.com/utils/rpc" - "go.viam.com/utils/testutils" - "google.golang.org/protobuf/testing/protocmp" - - "go.viam.com/rdk/cloud" - "go.viam.com/rdk/components/arm" - fakearm "go.viam.com/rdk/components/arm/fake" - "go.viam.com/rdk/components/base" - fakebase "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/board" - fakeboard "go.viam.com/rdk/components/board/fake" - "go.viam.com/rdk/components/board/pinwrappers" - "go.viam.com/rdk/components/camera" - fakecamera "go.viam.com/rdk/components/camera/fake" - "go.viam.com/rdk/components/gripper" - fakegripper "go.viam.com/rdk/components/gripper/fake" - "go.viam.com/rdk/components/input" - fakeinput "go.viam.com/rdk/components/input/fake" - "go.viam.com/rdk/components/motor" - fakemotor "go.viam.com/rdk/components/motor/fake" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/components/servo" - fakeservo "go.viam.com/rdk/components/servo/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/module/modmaninterface" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/client" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/robot/packages" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/shell" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/session" - rdktestutils "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/testutils/robottestutils" - rutils "go.viam.com/rdk/utils" - viz "go.viam.com/rdk/vision" -) - -func setupInjectRobot(logger logging.Logger) *inject.Robot { - injectRobot := &inject.Robot{} - armNames := []resource.Name{ - arm.Named("arm1"), - arm.Named("arm2"), - } - baseNames := []resource.Name{ - base.Named("base1"), - base.Named("base2"), - } - boardNames := []resource.Name{ - board.Named("board1"), - board.Named("board2"), - } - cameraNames := []resource.Name{ - camera.Named("camera1"), - camera.Named("camera2"), - } - gripperNames := []resource.Name{ - gripper.Named("gripper1"), - gripper.Named("gripper2"), - } - inputNames := []resource.Name{ - input.Named("inputController1"), - input.Named("inputController2"), - } - motorNames := []resource.Name{ - motor.Named("motor1"), - motor.Named("motor2"), - } - servoNames := []resource.Name{ - servo.Named("servo1"), - servo.Named("servo2"), - } - - injectRobot.RemoteNamesFunc = func() []string { - return []string{"remote1%s", "remote2"} - } - - injectRobot.ResourceNamesFunc = func() []resource.Name { - return rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - cameraNames, - gripperNames, - inputNames, - motorNames, - servoNames, - ) - } - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.LoggerFunc = func() logging.Logger { - return logger - } - - injectRobot.RemoteByNameFunc = func(name string) (robot.Robot, bool) { - if _, ok := utils.NewStringSet(injectRobot.RemoteNames()...)[name]; !ok { - return nil, false - } - return &dummyRobot{}, true - } - - injectRobot.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - for _, rName := range injectRobot.ResourceNames() { - if rName == name { - switch name.API { - case arm.API: - return &fakearm.Arm{Named: name.AsNamed()}, nil - case base.API: - return &fakebase.Base{Named: name.AsNamed()}, nil - case board.API: - fakeBoard, err := fakeboard.NewBoard(context.Background(), resource.Config{ - Name: name.String(), - ConvertedAttributes: &fakeboard.Config{ - AnalogReaders: []board.AnalogReaderConfig{ - {Name: "analog1"}, - {Name: "analog2"}, - }, - DigitalInterrupts: []board.DigitalInterruptConfig{ - {Name: "digital1"}, - {Name: "digital2"}, - }, - }, - }, logger) - if err != nil { - panic(err) - } - return fakeBoard, nil - case camera.API: - conf := resource.NewEmptyConfig(name, resource.DefaultModelFamily.WithModel("fake")) - conf.ConvertedAttributes = &fakecamera.Config{} - return fakecamera.NewCamera(context.Background(), resource.Dependencies{}, conf, logger) - case gripper.API: - return &fakegripper.Gripper{Named: name.AsNamed()}, nil - case input.API: - return &fakeinput.InputController{Named: name.AsNamed()}, nil - case motor.API: - return &fakemotor.Motor{Named: name.AsNamed()}, nil - case servo.API: - return &fakeservo.Servo{Named: name.AsNamed()}, nil - } - if rName.API.IsService() { - return rdktestutils.NewUnimplementedResource(name), nil - } - } - } - return nil, resource.NewNotFoundError(name) - } - - return injectRobot -} - -func TestManagerForRemoteRobot(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := setupInjectRobot(logger) - - manager := managerForDummyRobot(t, injectRobot) - - armNames := []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - baseNames := []resource.Name{base.Named("base1"), base.Named("base2")} - boardNames := []resource.Name{board.Named("board1"), board.Named("board2")} - cameraNames := []resource.Name{camera.Named("camera1"), camera.Named("camera2")} - gripperNames := []resource.Name{gripper.Named("gripper1"), gripper.Named("gripper2")} - inputNames := []resource.Name{input.Named("inputController1"), input.Named("inputController2")} - motorNames := []resource.Name{motor.Named("motor1"), motor.Named("motor2")} - servoNames := []resource.Name{servo.Named("servo1"), servo.Named("servo2")} - - test.That(t, manager.RemoteNames(), test.ShouldBeEmpty) - test.That( - t, - rdktestutils.NewResourceNameSet(manager.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet(rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - cameraNames, - gripperNames, - inputNames, - motorNames, - servoNames, - )...), - ) - - _, err := manager.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(arm.Named("arm_what")) - test.That(t, err, test.ShouldBeError) - _, err = manager.ResourceByName(base.Named("base1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(base.Named("base1_what")) - test.That(t, err, test.ShouldBeError) - _, err = manager.ResourceByName(board.Named("board1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(board.Named("board1_what")) - test.That(t, err, test.ShouldBeError) - _, err = manager.ResourceByName(camera.Named("camera1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(camera.Named("camera1_what")) - test.That(t, err, test.ShouldBeError) - _, err = manager.ResourceByName(gripper.Named("gripper1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(gripper.Named("gripper1_what")) - test.That(t, err, test.ShouldBeError) - _, err = manager.ResourceByName(motor.Named("motor1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(motor.Named("motor1_what")) - test.That(t, err, test.ShouldBeError) - _, err = manager.ResourceByName(servo.Named("servo1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(servo.Named("servo_what")) - test.That(t, err, test.ShouldBeError) -} - -func TestManagerMergeNamesWithRemotes(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := setupInjectRobot(logger) - - manager := managerForDummyRobot(t, injectRobot) - manager.addRemote( - context.Background(), - newDummyRobot(t, setupInjectRobot(logger)), - nil, - config.Remote{Name: "remote1"}, - ) - manager.addRemote( - context.Background(), - newDummyRobot(t, setupInjectRobot(logger)), - nil, - config.Remote{Name: "remote2"}, - ) - - armNames := []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - armNames = append(armNames, rdktestutils.AddRemotes(armNames, "remote1", "remote2")...) - baseNames := []resource.Name{base.Named("base1"), base.Named("base2")} - baseNames = append(baseNames, rdktestutils.AddRemotes(baseNames, "remote1", "remote2")...) - boardNames := []resource.Name{board.Named("board1"), board.Named("board2")} - boardNames = append(boardNames, rdktestutils.AddRemotes(boardNames, "remote1", "remote2")...) - cameraNames := []resource.Name{camera.Named("camera1"), camera.Named("camera2")} - cameraNames = append(cameraNames, rdktestutils.AddRemotes(cameraNames, "remote1", "remote2")...) - gripperNames := []resource.Name{gripper.Named("gripper1"), gripper.Named("gripper2")} - gripperNames = append(gripperNames, rdktestutils.AddRemotes(gripperNames, "remote1", "remote2")...) - inputNames := []resource.Name{input.Named("inputController1"), input.Named("inputController2")} - inputNames = append(inputNames, rdktestutils.AddRemotes(inputNames, "remote1", "remote2")...) - motorNames := []resource.Name{motor.Named("motor1"), motor.Named("motor2")} - motorNames = append(motorNames, rdktestutils.AddRemotes(motorNames, "remote1", "remote2")...) - servoNames := []resource.Name{servo.Named("servo1"), servo.Named("servo2")} - servoNames = append(servoNames, rdktestutils.AddRemotes(servoNames, "remote1", "remote2")...) - - test.That( - t, - utils.NewStringSet(manager.RemoteNames()...), - test.ShouldResemble, - utils.NewStringSet("remote1", "remote2"), - ) - test.That( - t, - rdktestutils.NewResourceNameSet(manager.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet(rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - cameraNames, - gripperNames, - inputNames, - motorNames, - servoNames, - )...), - ) - _, err := manager.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(arm.Named("remote1:arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(arm.Named("remote2:arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(arm.Named("what:arm1")) - test.That(t, err, test.ShouldBeError) - - _, err = manager.ResourceByName(base.Named("base1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(base.Named("remote1:base1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(base.Named("remote2:base1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(base.Named("what:base1")) - test.That(t, err, test.ShouldBeError) - - _, err = manager.ResourceByName(board.Named("board1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(board.Named("remote1:board1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(board.Named("remote2:board1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(board.Named("what:board1")) - test.That(t, err, test.ShouldBeError) - - _, err = manager.ResourceByName(camera.Named("camera1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(camera.Named("remote1:camera1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(camera.Named("remote2:camera1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(camera.Named("what:camera1")) - test.That(t, err, test.ShouldBeError) - - _, err = manager.ResourceByName(gripper.Named("gripper1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(gripper.Named("remote1:gripper1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(gripper.Named("remote2:gripper1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(gripper.Named("what:gripper1")) - test.That(t, err, test.ShouldBeError) - - _, err = manager.ResourceByName(motor.Named("motor1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(motor.Named("remote1:motor1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(motor.Named("remote2:motor1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(motor.Named("what:motor1")) - test.That(t, err, test.ShouldBeError) - - _, err = manager.ResourceByName(servo.Named("servo1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(servo.Named("remote1:servo1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(servo.Named("remote2:servo1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(servo.Named("what:servo1")) - test.That(t, err, test.ShouldBeError) -} - -func TestManagerResourceRemoteName(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := &inject.Robot{} - armNames := []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - injectRobot.ResourceNamesFunc = func() []resource.Name { return armNames } - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - return rdktestutils.NewUnimplementedResource(name), nil - } - injectRobot.LoggerFunc = func() logging.Logger { return logger } - - manager := managerForDummyRobot(t, injectRobot) - - injectRemote := &inject.Robot{} - injectRemote.ResourceNamesFunc = func() []resource.Name { return rdktestutils.AddSuffixes(armNames, "") } - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRemote.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - return rdktestutils.NewUnimplementedResource(name), nil - } - injectRemote.LoggerFunc = func() logging.Logger { return logger } - manager.addRemote( - context.Background(), - newDummyRobot(t, injectRemote), - nil, - config.Remote{Name: "remote1"}, - ) - - manager.updateRemotesResourceNames(context.Background()) - - res := manager.remoteResourceNames(fromRemoteNameToRemoteNodeName("remote1")) - - test.That( - t, - rdktestutils.NewResourceNameSet(res...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet([]resource.Name{arm.Named("remote1:arm1"), arm.Named("remote1:arm2")}...), - ) -} - -func TestManagerWithSameNameInRemoteNoPrefix(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := setupInjectRobot(logger) - - manager := managerForDummyRobot(t, injectRobot) - manager.addRemote( - context.Background(), - newDummyRobot(t, setupInjectRobot(logger)), - nil, - config.Remote{Name: "remote1"}, - ) - manager.addRemote( - context.Background(), - newDummyRobot(t, setupInjectRobot(logger)), - nil, - config.Remote{Name: "remote2"}, - ) - - _, err := manager.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(arm.Named("remote1:arm1")) - test.That(t, err, test.ShouldBeNil) -} - -func TestManagerWithSameNameInBaseAndRemote(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := setupInjectRobot(logger) - - manager := managerForDummyRobot(t, injectRobot) - manager.addRemote( - context.Background(), - newDummyRobot(t, setupInjectRobot(logger)), - nil, - config.Remote{Name: "remote1"}, - ) - - _, err := manager.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - _, err = manager.ResourceByName(arm.Named("remote1:arm1")) - test.That(t, err, test.ShouldBeNil) -} - -func TestManagerAdd(t *testing.T) { - logger := logging.NewTestLogger(t) - manager := newResourceManager(resourceManagerOptions{}, logger) - - injectArm := &inject.Arm{} - cfg := &resource.Config{API: arm.API, Name: "arm1"} - rName := cfg.ResourceName() - manager.resources.AddNode(rName, resource.NewConfiguredGraphNode(*cfg, injectArm, cfg.Model)) - arm1, err := manager.ResourceByName(rName) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1, test.ShouldEqual, injectArm) - - injectBoard := &inject.Board{} - injectBoard.AnalogNamesFunc = func() []string { - return []string{"analog1"} - } - injectBoard.DigitalInterruptNamesFunc = func() []string { - return []string{"digital1"} - } - injectBoard.AnalogByNameFunc = func(name string) (board.Analog, error) { - return &fakeboard.Analog{}, nil - } - injectBoard.DigitalInterruptByNameFunc = func(name string) (board.DigitalInterrupt, error) { - return &pinwrappers.BasicDigitalInterrupt{}, nil - } - - cfg = &resource.Config{ - API: board.API, - Name: "board1", - } - rName = cfg.ResourceName() - manager.resources.AddNode(rName, resource.NewConfiguredGraphNode(*cfg, injectBoard, cfg.Model)) - board1, err := manager.ResourceByName(board.Named("board1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, board1, test.ShouldEqual, injectBoard) - resource1, err := manager.ResourceByName(rName) - test.That(t, err, test.ShouldBeNil) - test.That(t, resource1, test.ShouldEqual, injectBoard) - - injectMotionService := &inject.MotionService{} - injectMotionService.MoveFunc = func( - ctx context.Context, - componentName resource.Name, - grabPose *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *motionpb.Constraints, - extra map[string]interface{}, - ) (bool, error) { - return false, nil - } - objectMResName := motion.Named("motion1") - manager.resources.AddNode(objectMResName, resource.NewConfiguredGraphNode(resource.Config{}, injectMotionService, unknownModel)) - motionService, err := manager.ResourceByName(objectMResName) - test.That(t, err, test.ShouldBeNil) - test.That(t, motionService, test.ShouldEqual, injectMotionService) - - injectVisionService := &inject.VisionService{} - injectVisionService.GetObjectPointCloudsFunc = func( - ctx context.Context, - cameraName string, - extra map[string]interface{}, - ) ([]*viz.Object, error) { - return []*viz.Object{viz.NewEmptyObject()}, nil - } - objectSegResName := vision.Named(resource.DefaultServiceName) - manager.resources.AddNode(objectSegResName, resource.NewConfiguredGraphNode(resource.Config{}, injectVisionService, unknownModel)) - objectSegmentationService, err := manager.ResourceByName(objectSegResName) - test.That(t, err, test.ShouldBeNil) - test.That(t, objectSegmentationService, test.ShouldEqual, injectVisionService) -} - -func TestManagerNewComponent(t *testing.T) { - fakeModel := resource.DefaultModelFamily.WithModel("fake") - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - Model: fakeModel, - API: arm.API, - DependsOn: []string{"board1"}, - }, - { - Name: "arm2", - Model: fakeModel, - API: arm.API, - DependsOn: []string{"board2"}, - }, - { - Name: "arm3", - Model: fakeModel, - API: arm.API, - DependsOn: []string{"board3"}, - }, - { - Name: "base1", - Model: fakeModel, - API: base.API, - DependsOn: []string{"board1"}, - }, - { - Name: "base2", - Model: fakeModel, - API: base.API, - DependsOn: []string{"board2"}, - }, - { - Name: "base3", - Model: fakeModel, - API: base.API, - DependsOn: []string{"board3"}, - }, - { - Name: "board1", - Model: fakeModel, - API: board.API, - ConvertedAttributes: &fakeboard.Config{}, - DependsOn: []string{}, - }, - { - Name: "board2", - Model: fakeModel, - API: board.API, - ConvertedAttributes: &fakeboard.Config{}, - DependsOn: []string{}, - }, - { - Name: "board3", - Model: fakeModel, - API: board.API, - ConvertedAttributes: &fakeboard.Config{}, - DependsOn: []string{}, - }, - { - Name: "camera1", - Model: fakeModel, - API: camera.API, - DependsOn: []string{"board1"}, - }, - { - Name: "camera2", - Model: fakeModel, - API: camera.API, - DependsOn: []string{"board2"}, - }, - { - Name: "camera3", - Model: fakeModel, - API: camera.API, - DependsOn: []string{"board3"}, - }, - { - Name: "gripper1", - Model: fakeModel, - API: gripper.API, - DependsOn: []string{"arm1", "camera1"}, - }, - { - Name: "gripper2", - Model: fakeModel, - API: gripper.API, - DependsOn: []string{"arm2", "camera2"}, - }, - { - Name: "gripper3", - Model: fakeModel, - API: gripper.API, - DependsOn: []string{"arm3", "camera3"}, - }, - { - Name: "inputController1", - Model: fakeModel, - API: input.API, - ConvertedAttributes: &fakeinput.Config{}, - DependsOn: []string{"board1"}, - }, - { - Name: "inputController2", - Model: fakeModel, - API: input.API, - ConvertedAttributes: &fakeinput.Config{}, - DependsOn: []string{"board2"}, - }, - { - Name: "inputController3", - Model: fakeModel, - API: input.API, - ConvertedAttributes: &fakeinput.Config{}, - DependsOn: []string{"board3"}, - }, - { - Name: "motor1", - Model: fakeModel, - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - DependsOn: []string{"board1"}, - }, - { - Name: "motor2", - Model: fakeModel, - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - DependsOn: []string{"board2"}, - }, - { - Name: "motor3", - Model: fakeModel, - API: motor.API, - ConvertedAttributes: &fakemotor.Config{}, - DependsOn: []string{"board3"}, - }, - { - Name: "sensor1", - Model: fakeModel, - API: sensor.API, - DependsOn: []string{"board1"}, - }, - { - Name: "sensor2", - Model: fakeModel, - API: sensor.API, - DependsOn: []string{"board2"}, - }, - { - Name: "sensor3", - Model: fakeModel, - API: sensor.API, - DependsOn: []string{"board3"}, - }, - { - Name: "servo1", - Model: fakeModel, - API: servo.API, - DependsOn: []string{"board1"}, - }, - { - Name: "servo2", - Model: fakeModel, - API: servo.API, - DependsOn: []string{"board2"}, - }, - { - Name: "servo3", - Model: fakeModel, - API: servo.API, - DependsOn: []string{"board3"}, - }, - }, - } - logger := logging.NewTestLogger(t) - robotForRemote := &localRobot{ - manager: newResourceManager(resourceManagerOptions{}, logger), - } - diff, err := config.DiffConfigs(config.Config{}, *cfg, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, robotForRemote.manager.updateResources(context.Background(), diff), test.ShouldBeNil) - test.That(t, robotForRemote.manager.resources.ResolveDependencies(logger), test.ShouldBeNil) - - diff = &config.Diff{ - Added: &config.Config{}, - Modified: &config.ModifiedConfigDiff{ - Components: []resource.Config{}, - }, - } - - diff.Modified.Components = append(diff.Modified.Components, resource.Config{ - Name: "board3", - Model: fakeModel, - API: board.API, - ConvertedAttributes: &fakeboard.Config{}, - DependsOn: []string{"arm3"}, - }) - test.That(t, robotForRemote.manager.updateResources(context.Background(), diff), test.ShouldBeNil) - err = robotForRemote.manager.resources.ResolveDependencies(logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "circular dependency") - test.That(t, err.Error(), test.ShouldContainSubstring, "arm3") - test.That(t, err.Error(), test.ShouldContainSubstring, "board3") -} - -func managerForTest(ctx context.Context, t *testing.T, l logging.Logger) *resourceManager { - t.Helper() - injectRobot := setupInjectRobot(l) - manager := managerForDummyRobot(t, injectRobot) - - manager.addRemote( - context.Background(), - newDummyRobot(t, setupInjectRobot(l)), - nil, - config.Remote{Name: "remote1"}, - ) - manager.addRemote( - context.Background(), - newDummyRobot(t, setupInjectRobot(l)), - nil, - config.Remote{Name: "remote2"}, - ) - _, err := manager.processManager.AddProcess(ctx, &fakeProcess{id: "1"}, false) - test.That(t, err, test.ShouldBeNil) - _, err = manager.processManager.AddProcess(ctx, &fakeProcess{id: "2"}, false) - test.That(t, err, test.ShouldBeNil) - return manager -} - -func TestManagerMarkRemoved(t *testing.T) { - logger := logging.NewTestLogger(t) - - ctx, cancel := context.WithCancel(context.Background()) - manager := managerForTest(ctx, t, logger) - test.That(t, manager, test.ShouldNotBeNil) - - checkEmpty := func( - procMan pexec.ProcessManager, - resourcesToCloseBeforeComplete []resource.Resource, - names map[resource.Name]struct{}, - ) { - t.Helper() - test.That(t, names, test.ShouldBeEmpty) - test.That(t, resourcesToCloseBeforeComplete, test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(procMan.ProcessIDs()...), test.ShouldBeEmpty) - } - - processesToRemove, resourcesToCloseBeforeComplete, markedResourceNames := manager.markRemoved(ctx, &config.Config{}, logger) - checkEmpty(processesToRemove, resourcesToCloseBeforeComplete, markedResourceNames) - - processesToRemove, resourcesToCloseBeforeComplete, markedResourceNames = manager.markRemoved(ctx, &config.Config{ - Remotes: []config.Remote{ - { - Name: "what", - }, - }, - Components: []resource.Config{ - { - Name: "what1", - API: arm.API, - }, - { - Name: "what5", - API: base.API, - }, - { - Name: "what3", - API: board.API, - }, - { - Name: "what4", - API: camera.API, - }, - { - Name: "what5", - API: gripper.API, - }, - { - Name: "what6", - API: motor.API, - }, - { - Name: "what7", - API: sensor.API, - }, - { - Name: "what8", - API: servo.API, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "what", - Name: "echo", - }, - }, - }, logger) - checkEmpty(processesToRemove, resourcesToCloseBeforeComplete, markedResourceNames) - - processesToRemove, resourcesToCloseBeforeComplete, markedResourceNames = manager.markRemoved(ctx, &config.Config{ - Components: []resource.Config{ - { - Name: "what1", - }, - }, - }, logger) - checkEmpty(processesToRemove, resourcesToCloseBeforeComplete, markedResourceNames) - - test.That(t, manager.Close(ctx), test.ShouldBeNil) - cancel() - - ctx, cancel = context.WithCancel(context.Background()) - manager = managerForTest(ctx, t, logger) - test.That(t, manager, test.ShouldNotBeNil) - - processesToRemove, _, markedResourceNames = manager.markRemoved(ctx, &config.Config{ - Components: []resource.Config{ - { - Name: "arm2", - API: arm.API, - }, - { - Name: "base2", - API: base.API, - }, - { - Name: "board2", - API: board.API, - }, - { - Name: "camera2", - API: camera.API, - }, - { - Name: "gripper2", - API: gripper.API, - }, - { - Name: "inputController2", - API: input.API, - }, - { - Name: "motor2", - API: motor.API, - }, - { - Name: "sensor2", - API: sensor.API, - }, - - { - Name: "servo2", - API: servo.API, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "2", - Name: "echo", // does not matter - }, - }, - }, logger) - - armNames := []resource.Name{arm.Named("arm2")} - baseNames := []resource.Name{base.Named("base2")} - boardNames := []resource.Name{board.Named("board2")} - cameraNames := []resource.Name{camera.Named("camera2")} - gripperNames := []resource.Name{gripper.Named("gripper2")} - inputNames := []resource.Name{input.Named("inputController2")} - motorNames := []resource.Name{motor.Named("motor2")} - servoNames := []resource.Name{servo.Named("servo2")} - - test.That( - t, - markedResourceNames, - test.ShouldResemble, - rdktestutils.NewResourceNameSet(rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - cameraNames, - gripperNames, - inputNames, - motorNames, - servoNames, - )...), - ) - test.That( - t, - utils.NewStringSet(processesToRemove.ProcessIDs()...), - test.ShouldResemble, - utils.NewStringSet("2"), - ) - - test.That(t, manager.Close(ctx), test.ShouldBeNil) - cancel() - - ctx, cancel = context.WithCancel(context.Background()) - manager = managerForTest(ctx, t, logger) - test.That(t, manager, test.ShouldNotBeNil) - - processesToRemove, _, markedResourceNames = manager.markRemoved(ctx, &config.Config{ - Remotes: []config.Remote{ - { - Name: "remote2", - }, - }, - Components: []resource.Config{ - { - Name: "arm2", - API: arm.API, - }, - { - Name: "base2", - API: base.API, - }, - { - Name: "board2", - API: board.API, - }, - { - Name: "camera2", - API: camera.API, - }, - { - Name: "gripper2", - API: gripper.API, - }, - { - Name: "inputController2", - API: input.API, - }, - { - Name: "motor2", - API: motor.API, - }, - { - Name: "sensor2", - API: sensor.API, - }, - { - Name: "servo2", - API: servo.API, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "2", - Name: "echo", // does not matter - }, - }, - }, logger) - - armNames = []resource.Name{arm.Named("arm2"), arm.Named("remote2:arm1"), arm.Named("remote2:arm2")} - baseNames = []resource.Name{ - base.Named("base2"), - base.Named("remote2:base1"), - base.Named("remote2:base2"), - } - boardNames = []resource.Name{ - board.Named("board2"), - board.Named("remote2:board1"), - board.Named("remote2:board2"), - } - cameraNames = []resource.Name{ - camera.Named("camera2"), - camera.Named("remote2:camera1"), - camera.Named("remote2:camera2"), - } - gripperNames = []resource.Name{ - gripper.Named("gripper2"), - gripper.Named("remote2:gripper1"), - gripper.Named("remote2:gripper2"), - } - inputNames = []resource.Name{ - input.Named("inputController2"), - input.Named("remote2:inputController1"), - input.Named("remote2:inputController2"), - } - motorNames = []resource.Name{ - motor.Named("motor2"), - motor.Named("remote2:motor1"), - motor.Named("remote2:motor2"), - } - servoNames = []resource.Name{ - servo.Named("servo2"), - servo.Named("remote2:servo1"), - servo.Named("remote2:servo2"), - } - - test.That( - t, - markedResourceNames, - test.ShouldResemble, - rdktestutils.NewResourceNameSet(rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - cameraNames, - gripperNames, - inputNames, - motorNames, - servoNames, - []resource.Name{fromRemoteNameToRemoteNodeName("remote2")}, - )...), - ) - test.That( - t, - utils.NewStringSet(processesToRemove.ProcessIDs()...), - test.ShouldResemble, - utils.NewStringSet("2"), - ) - - test.That(t, manager.Close(ctx), test.ShouldBeNil) - cancel() - - ctx, cancel = context.WithCancel(context.Background()) - manager = managerForTest(ctx, t, logger) - test.That(t, manager, test.ShouldNotBeNil) - - processesToRemove, _, markedResourceNames = manager.markRemoved(ctx, &config.Config{ - Remotes: []config.Remote{ - { - Name: "remote1", - }, - { - Name: "remote2", - }, - { - Name: "remote3", - }, - }, - Components: []resource.Config{ - { - Name: "arm1", - API: arm.API, - }, - { - Name: "arm2", - API: arm.API, - }, - { - Name: "arm3", - API: arm.API, - }, - { - Name: "base1", - API: base.API, - }, - { - Name: "base2", - API: base.API, - }, - { - Name: "base3", - API: base.API, - }, - { - Name: "board1", - API: board.API, - }, - { - Name: "board2", - API: board.API, - }, - { - Name: "board3", - API: board.API, - }, - { - Name: "camera1", - API: camera.API, - }, - { - Name: "camera2", - API: camera.API, - }, - { - Name: "camera3", - API: camera.API, - }, - { - Name: "gripper1", - API: gripper.API, - }, - { - Name: "gripper2", - API: gripper.API, - }, - { - Name: "gripper3", - API: gripper.API, - }, - { - Name: "inputController1", - API: input.API, - }, - { - Name: "inputController2", - API: input.API, - }, - { - Name: "inputController3", - API: input.API, - }, - { - Name: "motor1", - API: motor.API, - }, - { - Name: "motor2", - API: motor.API, - }, - { - Name: "motor3", - API: motor.API, - }, - { - Name: "sensor1", - API: sensor.API, - }, - { - Name: "sensor2", - API: sensor.API, - }, - { - Name: "sensor3", - API: sensor.API, - }, - { - Name: "servo1", - API: servo.API, - }, - { - Name: "servo2", - API: servo.API, - }, - { - Name: "servo3", - API: servo.API, - }, - }, - Processes: []pexec.ProcessConfig{ - { - ID: "1", - Name: "echo", // does not matter - }, - { - ID: "2", - Name: "echo", // does not matter - }, - { - ID: "3", - Name: "echo", // does not matter - }, - }, - }, logger) - - armNames = []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - armNames = append(armNames, rdktestutils.AddRemotes(armNames, "remote1", "remote2")...) - baseNames = []resource.Name{base.Named("base1"), base.Named("base2")} - baseNames = append(baseNames, rdktestutils.AddRemotes(baseNames, "remote1", "remote2")...) - boardNames = []resource.Name{board.Named("board1"), board.Named("board2")} - boardNames = append(boardNames, rdktestutils.AddRemotes(boardNames, "remote1", "remote2")...) - cameraNames = []resource.Name{camera.Named("camera1"), camera.Named("camera2")} - cameraNames = append(cameraNames, rdktestutils.AddRemotes(cameraNames, "remote1", "remote2")...) - gripperNames = []resource.Name{gripper.Named("gripper1"), gripper.Named("gripper2")} - gripperNames = append(gripperNames, rdktestutils.AddRemotes(gripperNames, "remote1", "remote2")...) - inputNames = []resource.Name{input.Named("inputController1"), input.Named("inputController2")} - inputNames = append(inputNames, rdktestutils.AddRemotes(inputNames, "remote1", "remote2")...) - motorNames = []resource.Name{motor.Named("motor1"), motor.Named("motor2")} - motorNames = append(motorNames, rdktestutils.AddRemotes(motorNames, "remote1", "remote2")...) - servoNames = []resource.Name{servo.Named("servo1"), servo.Named("servo2")} - servoNames = append(servoNames, rdktestutils.AddRemotes(servoNames, "remote1", "remote2")...) - - test.That( - t, - markedResourceNames, - test.ShouldResemble, - rdktestutils.NewResourceNameSet(rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - cameraNames, - gripperNames, - inputNames, - motorNames, - servoNames, - []resource.Name{ - fromRemoteNameToRemoteNodeName("remote1"), - fromRemoteNameToRemoteNodeName("remote2"), - }, - )...), - ) - test.That( - t, - utils.NewStringSet(processesToRemove.ProcessIDs()...), - test.ShouldResemble, - utils.NewStringSet("1", "2"), - ) - test.That(t, manager.Close(ctx), test.ShouldBeNil) - cancel() -} - -func TestConfigRemoteAllowInsecureCreds(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - - altName := primitive.NewObjectID().Hex() - cert, certFile, keyFile, certPool, err := testutils.GenerateSelfSignedCertificate("somename", altName) - test.That(t, err, test.ShouldBeNil) - t.Cleanup(func() { - os.Remove(certFile) - os.Remove(keyFile) - }) - - leaf, err := x509.ParseCertificate(cert.Certificate[0]) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - options.Network.TLSConfig = &tls.Config{ - RootCAs: certPool, - ClientCAs: certPool, - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS12, - ClientAuth: tls.VerifyClientCertIfGiven, - } - options.Auth.TLSAuthEntities = leaf.DNSNames - options.Managed = true - options.FQDN = altName - locationSecret := "locsosecret" - - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: rutils.CredentialsTypeRobotLocationSecret, - Config: rutils.AttributeMap{ - "secret": locationSecret, - }, - }, - } - - options.BakedAuthEntity = "blah" - options.BakedAuthCreds = rpc.Credentials{Type: "blah"} - - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - remoteTLSConfig := options.Network.TLSConfig.Clone() - remoteTLSConfig.Certificates = nil - remoteTLSConfig.ServerName = "somename" - remote := config.Remote{ - Name: "foo", - Address: addr, - Auth: config.RemoteAuth{ - Managed: true, - }, - } - manager := newResourceManager(resourceManagerOptions{ - tlsConfig: remoteTLSConfig, - }, logger) - - gNode := resource.NewUninitializedNode() - gNode.InitializeLogger(logger, "remote", logger.GetLevel()) - _, err = manager.processRemote(context.Background(), remote, gNode) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "authentication required") - - remote.Auth.Entity = "wrong" - _, err = manager.processRemote(context.Background(), remote, gNode) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "authentication required") - - remote.Auth.Entity = options.FQDN - _, err = manager.processRemote(context.Background(), remote, gNode) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "authentication required") -} - -func TestConfigUntrustedEnv(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - manager := newResourceManager(resourceManagerOptions{ - untrustedEnv: true, - }, logger) - test.That(t, manager.processManager, test.ShouldEqual, pexec.NoopProcessManager) - - t.Run("disable processes", func(t *testing.T) { - err := manager.updateResources(ctx, &config.Diff{ - Added: &config.Config{ - Processes: []pexec.ProcessConfig{{ID: "id1", Name: "echo"}}, - }, - Modified: &config.ModifiedConfigDiff{ - Processes: []pexec.ProcessConfig{{ID: "id2", Name: "echo"}}, - }, - }) - test.That(t, errors.Is(err, errProcessesDisabled), test.ShouldBeTrue) - - processesToClose, _, _ := manager.markRemoved(ctx, &config.Config{ - Processes: []pexec.ProcessConfig{{ID: "id1", Name: "echo"}}, - }, logger) - test.That(t, processesToClose.ProcessIDs(), test.ShouldBeEmpty) - }) - - t.Run("disable shell service", func(t *testing.T) { - err := manager.updateResources(ctx, &config.Diff{ - Added: &config.Config{ - Services: []resource.Config{{ - Name: "shell-service", - API: shell.API, - }}, - }, - Modified: &config.ModifiedConfigDiff{ - Services: []resource.Config{{ - Name: "shell-service", - API: shell.API, - }}, - }, - }) - test.That(t, errors.Is(err, errShellServiceDisabled), test.ShouldBeTrue) - - _, resourcesToCloseBeforeComplete, markedResourceNames := manager.markRemoved(ctx, &config.Config{ - Services: []resource.Config{{ - Name: "shell-service", - API: shell.API, - }}, - }, logger) - test.That(t, resourcesToCloseBeforeComplete, test.ShouldBeEmpty) - test.That(t, markedResourceNames, test.ShouldBeEmpty) - }) -} - -type fakeProcess struct { - id string -} - -func (fp *fakeProcess) ID() string { - return fp.id -} - -func (fp *fakeProcess) Start(ctx context.Context) error { - return nil -} - -func (fp *fakeProcess) Stop() error { - return nil -} - -func (fp *fakeProcess) Status() error { - return nil -} - -func TestManagerResourceRPCAPIs(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := &inject.Robot{} - injectRobot.LoggerFunc = func() logging.Logger { - return logger - } - injectRobot.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{ - arm.Named("arm1"), - arm.Named("arm2"), - base.Named("base1"), - base.Named("base2"), - } - } - injectRobot.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - for _, rName := range injectRobot.ResourceNames() { - if rName == name { - switch name.API { - case arm.API: - return &fakearm.Arm{Named: name.AsNamed()}, nil - case base.API: - return &fakebase.Base{Named: name.AsNamed()}, nil - } - } - } - return nil, resource.NewNotFoundError(name) - } - - manager := managerForDummyRobot(t, injectRobot) - - api1 := resource.APINamespace("acme").WithComponentType("huwat") - api2 := resource.APINamespace("acme").WithComponentType("wat") - - resName1 := resource.NewName(api1, "thing1") - resName2 := resource.NewName(api2, "thing2") - - injectRobotRemote1 := &inject.Robot{} - injectRobotRemote1.LoggerFunc = func() logging.Logger { - return logger - } - injectRobotRemote1.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{ - resName1, - resName2, - } - } - injectRobotRemote1.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - for _, rName := range injectRobotRemote1.ResourceNames() { - if rName == name { - return grpc.NewForeignResource(rName, nil), nil - } - } - return nil, resource.NewNotFoundError(name) - } - - armDesc, err := grpcreflect.LoadServiceDescriptor(&armpb.ArmService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - baseDesc, err := grpcreflect.LoadServiceDescriptor(&basepb.BaseService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - boardDesc, err := grpcreflect.LoadServiceDescriptor(&boardpb.BoardService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - cameraDesc, err := grpcreflect.LoadServiceDescriptor(&camerapb.CameraService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - injectRobotRemote1.ResourceRPCAPIsFunc = func() []resource.RPCAPI { - return []resource.RPCAPI{ - { - API: api1, - Desc: boardDesc, - }, - { - API: api2, - Desc: cameraDesc, - }, - } - } - - manager.addRemote( - context.Background(), - newDummyRobot(t, injectRobotRemote1), - nil, - config.Remote{Name: "remote1"}, - ) - - injectRobotRemote2 := &inject.Robot{} - injectRobotRemote2.LoggerFunc = func() logging.Logger { - return logger - } - injectRobotRemote2.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{ - resName1, - resName2, - } - } - injectRobotRemote2.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - for _, rName := range injectRobotRemote2.ResourceNames() { - if rName == name { - return grpc.NewForeignResource(rName, nil), nil - } - } - return nil, resource.NewNotFoundError(name) - } - - gripperDesc, err := grpcreflect.LoadServiceDescriptor(&gripperpb.GripperService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - injectRobotRemote2.ResourceRPCAPIsFunc = func() []resource.RPCAPI { - return []resource.RPCAPI{ - { - API: api1, - Desc: boardDesc, - }, - { - API: api2, - Desc: gripperDesc, - }, - } - } - - manager.addRemote( - context.Background(), - newDummyRobot(t, injectRobotRemote2), - nil, - config.Remote{Name: "remote2"}, - ) - - apis := manager.ResourceRPCAPIs() - test.That(t, apis, test.ShouldHaveLength, 4) - - apisM := make(map[resource.API]*desc.ServiceDescriptor, len(apis)) - for _, api := range apis { - apisM[api.API] = api.Desc - } - - test.That(t, apisM, test.ShouldContainKey, arm.API) - test.That(t, cmp.Equal(apisM[arm.API].AsProto(), armDesc.AsProto(), protocmp.Transform()), test.ShouldBeTrue) - - test.That(t, apisM, test.ShouldContainKey, base.API) - test.That(t, cmp.Equal(apisM[base.API].AsProto(), baseDesc.AsProto(), protocmp.Transform()), test.ShouldBeTrue) - - test.That(t, apisM, test.ShouldContainKey, api1) - test.That(t, cmp.Equal(apisM[api1].AsProto(), boardDesc.AsProto(), protocmp.Transform()), test.ShouldBeTrue) - - test.That(t, apisM, test.ShouldContainKey, api2) - // one of these will be true due to a clash - test.That(t, - cmp.Equal( - apisM[api2].AsProto(), cameraDesc.AsProto(), protocmp.Transform()) || - cmp.Equal(apisM[api2].AsProto(), gripperDesc.AsProto(), protocmp.Transform()), - test.ShouldBeTrue) -} - -func TestManagerEmptyResourceDesc(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := &inject.Robot{} - injectRobot.LoggerFunc = func() logging.Logger { - return logger - } - api := resource.APINamespaceRDK.WithComponentType("mockDesc") - resource.RegisterAPI( - api, - resource.APIRegistration[resource.Resource]{}, - ) - defer func() { - resource.DeregisterAPI(api) - }() - - injectRobot.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{resource.NewName(api, "mock1")} - } - injectRobot.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - return rdktestutils.NewUnimplementedResource(name), nil - } - - manager := managerForDummyRobot(t, injectRobot) - - apis := manager.ResourceRPCAPIs() - test.That(t, apis, test.ShouldHaveLength, 0) -} - -func TestReconfigure(t *testing.T) { - const subtypeName = "testSubType" - - api := resource.APINamespaceRDK.WithServiceType(subtypeName) - - logger := logging.NewTestLogger(t) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - r := setupLocalRobot(t, ctx, cfg, logger) - - resource.RegisterAPI(api, resource.APIRegistration[resource.Resource]{}) - defer func() { - resource.DeregisterAPI(api) - }() - - resource.Register(api, resource.DefaultServiceModel, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return &mock{ - Named: conf.ResourceName().AsNamed(), - }, nil - }, - }) - defer func() { - resource.Deregister(api, resource.DefaultServiceModel) - }() - - manager := managerForDummyRobot(t, r) - - svc1 := resource.Config{ - Name: "somesvc", - Model: resource.DefaultServiceModel, - API: api, - } - - local, ok := r.(*localRobot) - test.That(t, ok, test.ShouldBeTrue) - newService, newlyBuilt, err := manager.processResource(ctx, svc1, resource.NewUninitializedNode(), local) - test.That(t, err, test.ShouldBeNil) - test.That(t, newlyBuilt, test.ShouldBeTrue) - svcNode := resource.NewConfiguredGraphNode(svc1, newService, svc1.Model) - manager.resources.AddNode(svc1.ResourceName(), svcNode) - newService, newlyBuilt, err = manager.processResource(ctx, svc1, svcNode, local) - test.That(t, err, test.ShouldBeNil) - test.That(t, newlyBuilt, test.ShouldBeFalse) - - mockRe, ok := newService.(*mock) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, mockRe, test.ShouldNotBeNil) - test.That(t, mockRe.reconfigCount, test.ShouldEqual, 1) - - defer func() { - test.That(t, local.Close(ctx), test.ShouldBeNil) - }() -} - -func TestResourceCreationPanic(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx := context.Background() - - r := setupLocalRobot(t, ctx, &config.Config{}, logger) - manager := managerForDummyRobot(t, r) - - t.Run("component", func(t *testing.T) { - subtypeName := "testComponentAPI" - api := resource.APINamespaceRDK.WithComponentType(subtypeName) - model := resource.DefaultModelFamily.WithModel("test") - - resource.RegisterComponent(api, model, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, deps resource.Dependencies, c resource.Config, logger logging.Logger, - ) (resource.Resource, error) { - panic("hello") - }, - }) - defer func() { - resource.Deregister(api, model) - }() - - svc1 := resource.Config{ - Name: "test", - Model: model, - API: api, - } - - local, ok := r.(*localRobot) - test.That(t, ok, test.ShouldBeTrue) - _, _, err := manager.processResource(ctx, svc1, resource.NewUninitializedNode(), local) - test.That(t, err.Error(), test.ShouldContainSubstring, "hello") - }) - - t.Run("service", func(t *testing.T) { - subtypeName := "testServiceAPI" - api := resource.APINamespaceRDK.WithServiceType(subtypeName) - - resource.Register(api, resource.DefaultServiceModel, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - c resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - panic("hello") - }, - }) - defer func() { - resource.Deregister(api, resource.DefaultServiceModel) - }() - - resource.RegisterAPI(api, resource.APIRegistration[resource.Resource]{}) - defer func() { - resource.DeregisterAPI(api) - }() - - svc1 := resource.Config{ - Name: "", - Model: resource.DefaultServiceModel, - API: api, - } - - local, ok := r.(*localRobot) - test.That(t, ok, test.ShouldBeTrue) - _, _, err := manager.processResource(ctx, svc1, resource.NewUninitializedNode(), local) - test.That(t, err.Error(), test.ShouldContainSubstring, "hello") - }) -} - -type mock struct { - resource.Named - resource.TriviallyCloseable - reconfigCount int -} - -func (m *mock) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - m.reconfigCount++ - return nil -} - -// A dummyRobot implements wraps an robot.Robot. It's only use for testing purposes. -type dummyRobot struct { - resource.Named - mu sync.Mutex - robot robot.Robot - manager *resourceManager - modmanager modmaninterface.ModuleManager -} - -// newDummyRobot returns a new dummy robot wrapping a given robot.Robot -// and its configuration. -func newDummyRobot(t *testing.T, robot robot.Robot) *dummyRobot { - t.Helper() - - remoteManager := managerForDummyRobot(t, robot) - remote := &dummyRobot{ - Named: resource.NewName(client.RemoteAPI, "something").AsNamed(), - robot: robot, - manager: remoteManager, - } - return remote -} - -func (rr *dummyRobot) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - return errors.New("unsupported") -} - -// DiscoverComponents takes a list of discovery queries and returns corresponding -// component configurations. -func (rr *dummyRobot) DiscoverComponents(ctx context.Context, qs []resource.DiscoveryQuery) ([]resource.Discovery, error) { - return rr.robot.DiscoverComponents(ctx, qs) -} - -func (rr *dummyRobot) RemoteNames() []string { - return nil -} - -func (rr *dummyRobot) ResourceNames() []resource.Name { - rr.mu.Lock() - defer rr.mu.Unlock() - names := rr.manager.ResourceNames() - newNames := make([]resource.Name, 0, len(names)) - newNames = append(newNames, names...) - return newNames -} - -func (rr *dummyRobot) ResourceRPCAPIs() []resource.RPCAPI { - return rr.robot.ResourceRPCAPIs() -} - -func (rr *dummyRobot) RemoteByName(name string) (robot.Robot, bool) { - return nil, false -} - -func (rr *dummyRobot) ResourceByName(name resource.Name) (resource.Resource, error) { - rr.mu.Lock() - defer rr.mu.Unlock() - return rr.manager.ResourceByName(name) -} - -// FrameSystemConfig returns a remote robot's FrameSystem Config. -func (rr *dummyRobot) FrameSystemConfig(ctx context.Context) (*framesystem.Config, error) { - panic("change to return nil") -} - -func (rr *dummyRobot) TransformPose( - ctx context.Context, - pose *referenceframe.PoseInFrame, - dst string, - additionalTransforms []*referenceframe.LinkInFrame, -) (*referenceframe.PoseInFrame, error) { - panic("change to return nil") -} - -func (rr *dummyRobot) TransformPointCloud(ctx context.Context, srcpc pointcloud.PointCloud, srcName, dstName string, -) (pointcloud.PointCloud, error) { - panic("change to return nil") -} - -func (rr *dummyRobot) Status(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - panic("change to return nil") -} - -func (rr *dummyRobot) ProcessManager() pexec.ProcessManager { - panic("change to return nil") -} - -func (rr *dummyRobot) OperationManager() *operation.Manager { - panic("change to return nil") -} - -func (rr *dummyRobot) ModuleManager() modmaninterface.ModuleManager { - return rr.modmanager -} - -func (rr *dummyRobot) SessionManager() session.Manager { - panic("change to return nil") -} - -func (rr *dummyRobot) PackageManager() packages.Manager { - panic("change to return nil") -} - -func (rr *dummyRobot) Logger() logging.Logger { - return rr.robot.Logger() -} - -func (rr *dummyRobot) CloudMetadata(ctx context.Context) (cloud.Metadata, error) { - return rr.robot.CloudMetadata(ctx) -} - -func (rr *dummyRobot) Close(ctx context.Context) error { - return rr.robot.Close(ctx) -} - -func (rr *dummyRobot) StopAll(ctx context.Context, extra map[resource.Name]map[string]interface{}) error { - return rr.robot.StopAll(ctx, extra) -} - -func (rr *dummyRobot) RestartModule(ctx context.Context, req robot.RestartModuleRequest) error { - return rr.robot.RestartModule(ctx, req) -} - -// managerForDummyRobot integrates all parts from a given robot except for its remotes. -// It also close itself when the test and all subtests complete. -func managerForDummyRobot(t *testing.T, robot robot.Robot) *resourceManager { - t.Helper() - - manager := newResourceManager(resourceManagerOptions{}, robot.Logger().Sublogger("manager")) - t.Cleanup(func() { - test.That(t, manager.Close(context.Background()), test.ShouldBeNil) - }) - - // start a dummy module manager so calls to moduleManager.Provides() do not - // panic. - manager.startModuleManager(context.Background(), "", nil, false, "", "", robot.Logger()) - - for _, name := range robot.ResourceNames() { - res, err := robot.ResourceByName(name) - if err != nil { - robot.Logger().Debugw("error getting resource", "resource", name, "error", err) - continue - } - gNode := resource.NewConfiguredGraphNode(resource.Config{}, res, unknownModel) - test.That(t, manager.resources.AddNode(name, gNode), test.ShouldBeNil) - } - return manager -} diff --git a/robot/impl/robot_framesystem_test.go b/robot/impl/robot_framesystem_test.go deleted file mode 100644 index 2020e74a50a..00000000000 --- a/robot/impl/robot_framesystem_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package robotimpl - -import ( - "context" - "math" - "testing" - "time" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - _ "go.viam.com/rdk/services/datamanager/builtin" - "go.viam.com/rdk/services/motion" - _ "go.viam.com/rdk/services/motion/builtin" - "go.viam.com/rdk/services/sensors" - _ "go.viam.com/rdk/services/sensors/builtin" - "go.viam.com/rdk/spatialmath" - rdktestutils "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/robottestutils" - rutils "go.viam.com/rdk/utils" -) - -func TestFrameSystemConfigWithRemote(t *testing.T) { - logger := logging.NewTestLogger(t) - // make the remote robots - remoteConfig, err := config.Read(context.Background(), rutils.ResolveFile("robot/impl/data/fake.json"), logger.Sublogger("remote")) - test.That(t, err, test.ShouldBeNil) - ctx := context.Background() - remoteRobot := setupLocalRobot(t, ctx, remoteConfig, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = remoteRobot.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - o1 := &spatialmath.R4AA{math.Pi / 2., 0, 0, 1} - o1Cfg, err := spatialmath.NewOrientationConfig(o1) - test.That(t, err, test.ShouldBeNil) - - o2 := &spatialmath.R4AA{math.Pi / 2., 1, 0, 0} - o2Cfg, err := spatialmath.NewOrientationConfig(o2) - test.That(t, err, test.ShouldBeNil) - - // make the local robot - localConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "foo", - API: base.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - }, - }, - { - Name: "myParentIsRemote", - API: gripper.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - Frame: &referenceframe.LinkConfig{ - Parent: "bar:pieceArm", - }, - }, - }, - Remotes: []config.Remote{ - { - Name: "bar", - Address: addr, - Frame: &referenceframe.LinkConfig{ - Parent: "foo", - Translation: r3.Vector{100, 200, 300}, - Orientation: o1Cfg, - }, - }, - { - Name: "squee", - Address: addr, - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - Translation: r3.Vector{500, 600, 700}, - Orientation: o2Cfg, - }, - }, - { - Name: "dontAddMe", // no frame info, should be skipped - Address: addr, - }, - }, - } - - testPose := spatialmath.NewPose( - r3.Vector{X: 1., Y: 2., Z: 3.}, - &spatialmath.R4AA{Theta: math.Pi / 2, RX: 0., RY: 1., RZ: 0.}, - ) - - transforms := []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame("bar:pieceArm", testPose, "frame1", nil), - referenceframe.NewLinkInFrame("bar:pieceGripper", testPose, "frame2", nil), - referenceframe.NewLinkInFrame("frame2", testPose, "frame2a", nil), - referenceframe.NewLinkInFrame("frame2", testPose, "frame2c", nil), - referenceframe.NewLinkInFrame(referenceframe.World, testPose, "frame3", nil), - } - r2 := setupLocalRobot(t, ctx, localConfig, logger.Sublogger("local")) - - test.That(t, err, test.ShouldBeNil) - fsCfg, err := r2.FrameSystemConfig(context.Background()) - test.That(t, err, test.ShouldBeNil) - fsCfg.AdditionalTransforms = transforms - fs, err := referenceframe.NewFrameSystem("test", fsCfg.Parts, fsCfg.AdditionalTransforms) - - test.That(t, err, test.ShouldBeNil) - test.That(t, fs.FrameNames(), test.ShouldHaveLength, 34) - // run the frame system service - allParts, err := r2.FrameSystemConfig(context.Background()) - test.That(t, err, test.ShouldBeNil) - t.Logf("frame system:\n%v", allParts) - - test.That(t, remoteRobot.Close(context.Background()), test.ShouldBeNil) - - rr, ok := r2.(*localRobot) - test.That(t, ok, test.ShouldBeTrue) - - rr.triggerConfig <- struct{}{} - - finalSet := rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - base.Named("foo"), - gripper.Named("myParentIsRemote"), - ) - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r2.ResourceNames()...), test.ShouldResemble, finalSet) - }) - - fsCfg, err = r2.FrameSystemConfig(context.Background()) - test.That(t, err, test.ShouldBeNil) - - // expected error as remote parent frame is missing - _, err = referenceframe.NewFrameSystem("test", fsCfg.Parts, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "references non-existent parent") - - // reconfigure to no longer have remote parent frame - localConfig = &config.Config{ - Components: []resource.Config{ - { - Name: "foo", - API: base.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - }, - }, - { - Name: "myParentIsRemote", - API: gripper.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - }, - }, - }, - Remotes: []config.Remote{ - { - Name: "bar", - Address: addr, - Frame: &referenceframe.LinkConfig{ - Parent: "foo", - Translation: r3.Vector{100, 200, 300}, - Orientation: o1Cfg, - }, - }, - { - Name: "squee", - Address: addr, - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - Translation: r3.Vector{500, 600, 700}, - Orientation: o2Cfg, - }, - }, - { - Name: "dontAddMe", // no frame info, should be skipped - Address: addr, - }, - }, - } - r2.Reconfigure(context.Background(), localConfig) - - fsCfg, err = r2.FrameSystemConfig(context.Background()) - test.That(t, err, test.ShouldBeNil) - fs, err = referenceframe.NewFrameSystem("test", fsCfg.Parts, nil) - test.That(t, err, test.ShouldBeNil) - t.Logf("frame system:\n%v", fsCfg) - test.That(t, fs.FrameNames(), test.ShouldHaveLength, 4) -} - -func TestServiceWithUnavailableRemote(t *testing.T) { - logger := logging.NewTestLogger(t) - o1 := &spatialmath.R4AA{math.Pi / 2., 0, 0, 1} - o1Cfg, err := spatialmath.NewOrientationConfig(o1) - test.That(t, err, test.ShouldBeNil) - - // make the local robot - localConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "foo", - API: base.API, - Model: resource.DefaultModelFamily.WithModel("fake"), - Frame: &referenceframe.LinkConfig{ - Parent: referenceframe.World, - }, - }, - }, - Remotes: []config.Remote{ - { - Name: "bar", - Address: "addr", - Frame: &referenceframe.LinkConfig{ - Parent: "foo", - Translation: r3.Vector{100, 200, 300}, - Orientation: o1Cfg, - }, - }, - }, - } - - r := setupLocalRobot(t, context.Background(), localConfig, logger) - - // make sure calling into remotes don't error - fsCfg, err := r.FrameSystemConfig(context.Background()) - test.That(t, err, test.ShouldBeNil) - - fs, err := referenceframe.NewFrameSystem("test", fsCfg.Parts, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, fs.FrameNames(), test.ShouldHaveLength, 2) -} diff --git a/robot/impl/robot_options.go b/robot/impl/robot_options.go index 266ce9afab6..da48d6e0a3f 100644 --- a/robot/impl/robot_options.go +++ b/robot/impl/robot_options.go @@ -1,13 +1,8 @@ package robotimpl -import ( - "go.viam.com/rdk/robot/web" -) - // options configures a Robot. type options struct { // webOptions are used to initially configure the web service. - webOptions []web.Option // viamHomeDir is used to configure the Viam home directory. viamHomeDir string @@ -39,14 +34,6 @@ func newFuncOption(f func(*options)) *funcOption { } } -// WithWebOptions returns a Option which sets the streamConfig -// used to enable audio/video streaming over WebRTC. -func WithWebOptions(opts ...web.Option) Option { - return newFuncOption(func(o *options) { - o.webOptions = opts - }) -} - // WithRevealSensitiveConfigDiffs returns an Option which causes config // diffs - which may contain sensitive information - to be displayed // in logs. diff --git a/robot/impl/robot_reconfigure_test.go b/robot/impl/robot_reconfigure_test.go deleted file mode 100644 index 505f0ce781d..00000000000 --- a/robot/impl/robot_reconfigure_test.go +++ /dev/null @@ -1,3681 +0,0 @@ -package robotimpl - -import ( - "bytes" - "context" - "crypto/rand" - "fmt" - "net" - "os" - "path/filepath" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/a8m/envsubst" - "github.com/google/uuid" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/pexec" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/arm/fake" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - _ "go.viam.com/rdk/services/datamanager/builtin" - "go.viam.com/rdk/services/motion" - _ "go.viam.com/rdk/services/motion/builtin" - "go.viam.com/rdk/services/sensors" - _ "go.viam.com/rdk/services/sensors/builtin" - rdktestutils "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/robottestutils" - rutils "go.viam.com/rdk/utils" -) - -var ( - // these settings to be toggled in test cases specifically - // testing for a reconfigurability mismatch. - reconfigurableTrue = true - testReconfiguringMismatch = false -) - -func TestRobotReconfigure(t *testing.T) { - test.That(t, len(resource.DefaultServices()), test.ShouldEqual, 2) - ConfigFromFile := func(t *testing.T, filePath string) *config.Config { - t.Helper() - logger := logging.NewTestLogger(t) - buf, err := envsubst.ReadFile(filePath) - test.That(t, err, test.ShouldBeNil) - conf, err := config.FromReader(context.Background(), filePath, bytes.NewReader(buf), logger) - test.That(t, err, test.ShouldBeNil) - return conf - } - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - mockNamed := func(name string) resource.Name { - return resource.NewName(mockAPI, name) - } - modelName1 := utils.RandomAlphaString(5) - modelName2 := utils.RandomAlphaString(5) - test.That(t, os.Setenv("TEST_MODEL_NAME_1", modelName1), test.ShouldBeNil) - test.That(t, os.Setenv("TEST_MODEL_NAME_2", modelName2), test.ShouldBeNil) - - resource.RegisterComponent(mockAPI, resource.DefaultModelFamily.WithModel(modelName1), - resource.Registration[resource.Resource, *mockFakeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - // test if implicit depencies are properly propagated - for _, dep := range conf.ConvertedAttributes.(*mockFakeConfig).InferredDep { - if _, ok := deps[mockNamed(dep)]; !ok { - return nil, errors.Errorf("inferred dependency %q cannot be found", mockNamed(dep)) - } - } - if conf.ConvertedAttributes.(*mockFakeConfig).ShouldFail { - return nil, errors.Errorf("cannot build %q for some obscure reason", conf.Name) - } - return &mockFake{Named: conf.ResourceName().AsNamed()}, nil - }, - }) - - resetComponentFailureState := func() { - reconfigurableTrue = true - testReconfiguringMismatch = false - } - resource.RegisterComponent(mockAPI, resource.DefaultModelFamily.WithModel(modelName2), - resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - if reconfigurableTrue && testReconfiguringMismatch { - reconfigurableTrue = false - return &mockFake{Named: conf.ResourceName().AsNamed()}, nil - } - return &mockFake2{Named: conf.ResourceName().AsNamed()}, nil - }, - }) - - defer func() { - resource.Deregister(mockAPI, resource.DefaultModelFamily.WithModel(modelName1)) - resource.Deregister(mockAPI, resource.DefaultModelFamily.WithModel(modelName2)) - }() - - t.Run("no diff", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf1 := ConfigFromFile(t, "data/diff_config_1.json") - - ctx := context.Background() - robot := setupLocalRobot(t, ctx, conf1, logger) - - resources := robot.ResourceNames() - test.That(t, len(resources), test.ShouldEqual, 7) - - armNames := []resource.Name{arm.Named("arm1")} - baseNames := []resource.Name{base.Named("base1")} - boardNames := []resource.Name{board.Named("board1")} - mockNames := []resource.Name{mockNamed("mock1"), mockNamed("mock2")} - - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - names := rdktestutils.NewResourceNameSet(robot.ResourceNames()...) - names2 := rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - mockNames, - resource.DefaultServices(), - ) - _ = names2 - _ = names - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - mockNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - robot.Reconfigure(ctx, conf1) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - mockNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err := arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldBeNil) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - _, err = robot.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - - _, err = robot.ResourceByName(board.Named("board1")) - test.That(t, err, test.ShouldBeNil) - - mock1, err := robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock2, err := robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock2.(*mockFake2).reconfCount, test.ShouldEqual, 0) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - }) - - t.Run("reconfiguring unreconfigurable", func(t *testing.T) { - resetComponentFailureState() - testReconfiguringMismatch = true - // processing modify will fail - logger := logging.NewTestLogger(t) - conf1 := ConfigFromFile(t, "data/diff_config_1.json") - conf3 := ConfigFromFile(t, "data/diff_config_4_bad.json") - robot := setupLocalRobot(t, context.Background(), conf1, logger) - - armNames := []resource.Name{arm.Named("arm1")} - baseNames := []resource.Name{base.Named("base1")} - boardNames := []resource.Name{board.Named("board1")} - mockNames := []resource.Name{mockNamed("mock1"), mockNamed("mock2")} - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - mockNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - arm1, err := arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldBeNil) - - base1, err := base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldBeNil) - - board1, err := board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - resource1, err := robot.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - - mock1, err := robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 0) - - reconfigurableTrue = false - robot.Reconfigure(context.Background(), conf3) - - _, err = robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - - reconfigurableTrue = true - - rr, ok := robot.(*localRobot) - test.That(t, ok, test.ShouldBeTrue) - - rr.triggerConfig <- struct{}{} - - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 20, func(tb testing.TB) { - _, err = robot.ResourceByName(mockNamed("mock2")) - test.That(tb, err, test.ShouldBeNil) - }) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - mockNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 0) - - newArm1, err := arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldBeNil) - test.That(t, newArm1, test.ShouldEqual, arm1) - - newBase1, err := base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldBeNil) - test.That(t, newBase1, test.ShouldEqual, base1) - - newBoard1, err := board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - test.That(t, newBoard1, test.ShouldEqual, board1) - - newResource1, err := robot.ResourceByName(arm.Named("arm1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, newResource1, test.ShouldEqual, resource1) - - newMock1, err := robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, newMock1, test.ShouldEqual, mock1) - - _, ok = robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - - testReconfiguringMismatch = false - }) - - t.Run("additive deps diff", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf1 := ConfigFromFile(t, "data/diff_config_deps1.json") - conf2 := ConfigFromFile(t, "data/diff_config_deps10.json") - robot := setupLocalRobot(t, context.Background(), conf1, logger) - - armNames := []resource.Name{arm.Named("arm1")} - baseNames := []resource.Name{base.Named("base1")} - boardNames := []resource.Name{board.Named("board1")} - mockNames := []resource.Name{ - mockNamed("mock1"), mockNamed("mock2"), - mockNamed("mock3"), - } - - robot.Reconfigure(context.Background(), conf1) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(motor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - resource.DefaultServices(), - mockNames, - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - armNames = []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - baseNames = []resource.Name{base.Named("base1"), base.Named("base2")} - motorNames := []resource.Name{motor.Named("m1"), motor.Named("m2"), motor.Named("m3"), motor.Named("m4")} - robot.Reconfigure(context.Background(), conf2) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - motorNames, - mockNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err := arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldBeNil) - - _, err = arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldBeNil) - - b, err := board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - pin, err := b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - pwmF, err := pin.PWMFreq(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pwmF, test.ShouldEqual, 1000) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - - rdktestutils.VerifyTopologicallySortedLevels( - t, - robot.(*localRobot).manager.resources, - [][]resource.Name{ - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - []resource.Name{mockNamed("mock1")}), - rdktestutils.ConcatResourceNames( - armNames, - []resource.Name{mockNamed("mock2"), mockNamed("mock3")}), - baseNames, - boardNames, - }, - robot.(*localRobot).manager.internalResourceNames()..., - ) - }) - - t.Run("modificative deps diff", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf3 := ConfigFromFile(t, "data/diff_config_deps3.json") - conf2 := ConfigFromFile(t, "data/diff_config_deps2.json") - robot := setupLocalRobot(t, context.Background(), conf3, logger) - - armNames := []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - baseNames := []resource.Name{base.Named("base1"), base.Named("base2")} - motorNames := []resource.Name{motor.Named("m1"), motor.Named("m2"), motor.Named("m3"), motor.Named("m4")} - boardNames := []resource.Name{board.Named("board1")} - - robot.Reconfigure(context.Background(), conf3) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - motorNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - b, err := board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - pin, err := b.GPIOPinByName("5") - test.That(t, err, test.ShouldBeNil) - pwmF, err := pin.PWMFreq(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pwmF, test.ShouldEqual, 4000) - _, err = b.DigitalInterruptByName("encoder") - test.That(t, err, test.ShouldNotBeNil) - - robot.Reconfigure(context.Background(), conf2) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - motorNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err = arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldBeNil) - - _, err = arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldBeNil) - - b, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - _, err = b.GPIOPinByName("5") - test.That(t, err, test.ShouldBeNil) - pwmF, err = pin.PWMFreq(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pwmF, test.ShouldEqual, 4000) - pin, err = b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - pwmF, err = pin.PWMFreq(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pwmF, test.ShouldEqual, 1000) // TODO double check this is the expected result - _, err = b.DigitalInterruptByName("encoder") - test.That(t, err, test.ShouldBeNil) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - - rdktestutils.VerifyTopologicallySortedLevels( - t, - robot.(*localRobot).manager.resources, - [][]resource.Name{ - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices()), - armNames, - baseNames, - boardNames, - }, - robot.(*localRobot).manager.internalResourceNames()..., - ) - }) - - t.Run("deletion deps diff", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf2 := ConfigFromFile(t, "data/diff_config_deps2.json") - conf4 := ConfigFromFile(t, "data/diff_config_deps4.json") - robot := setupLocalRobot(t, context.Background(), conf2, logger) - - armNames := []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - baseNames := []resource.Name{base.Named("base1"), base.Named("base2")} - motorNames := []resource.Name{motor.Named("m1"), motor.Named("m2"), motor.Named("m3"), motor.Named("m4")} - boardNames := []resource.Name{board.Named("board1")} - - robot.Reconfigure(context.Background(), conf2) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - motorNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - arm2, err := arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldBeNil) - - test.That(t, arm2.(*fake.Arm).CloseCount, test.ShouldEqual, 0) - robot.Reconfigure(context.Background(), conf4) - test.That(t, arm2.(*fake.Arm).CloseCount, test.ShouldEqual, 1) - - boardNames = []resource.Name{board.Named("board1"), board.Named("board2")} - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err = arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - sorted := robot.(*localRobot).manager.resources.TopologicalSort() - sorted = rdktestutils.SubtractNames(sorted, robot.(*localRobot).manager.internalResourceNames()...) - test.That(t, rdktestutils.NewResourceNameSet(sorted...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - []resource.Name{ - mockNamed("mock6"), - }, - )...)) - }) - - t.Run("mixed deps diff", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf2 := ConfigFromFile(t, "data/diff_config_deps2.json") - conf6 := ConfigFromFile(t, "data/diff_config_deps6.json") - robot := setupLocalRobot(t, context.Background(), conf2, logger) - - armNames := []resource.Name{arm.Named("arm1"), arm.Named("arm2")} - baseNames := []resource.Name{base.Named("base1"), base.Named("base2")} - motorNames := []resource.Name{motor.Named("m1"), motor.Named("m2"), motor.Named("m3"), motor.Named("m4")} - boardNames := []resource.Name{board.Named("board1")} - - robot.Reconfigure(context.Background(), conf2) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - motorNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - b, err := board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - pin, err := b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - pwmF, err := pin.PWMFreq(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pwmF, test.ShouldEqual, 1000) - _, err = b.DigitalInterruptByName("encoder") - test.That(t, err, test.ShouldBeNil) - - armNames = []resource.Name{arm.Named("arm1"), arm.Named("arm3")} - baseNames = []resource.Name{base.Named("base1"), base.Named("base2")} - motorNames = []resource.Name{motor.Named("m1"), motor.Named("m2"), motor.Named("m4"), motor.Named("m5")} - boardNames = []resource.Name{ - board.Named("board1"), - board.Named("board2"), board.Named("board3"), - } - - motor2, err := motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldBeNil) - - robot.Reconfigure(context.Background(), conf6) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - motorNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err = arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldBeNil) - - _, err = arm.FromRobot(robot, "arm3") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldBeNil) - - nextMotor2, err := motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldBeNil) - // m2 lost its dependency on arm2 after looking conf6 - // but only relies on base1 so it should never have been - // removed but only reconfigured. - test.That(t, nextMotor2, test.ShouldPointTo, motor2) - - _, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m5") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldBeNil) - - b, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - pin, err = b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - pwmF, err = pin.PWMFreq(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pwmF, test.ShouldEqual, 0) - _, err = b.DigitalInterruptByName("encoder") - test.That(t, err, test.ShouldNotBeNil) - _, err = b.DigitalInterruptByName("encoderC") - test.That(t, err, test.ShouldBeNil) - - _, err = board.FromRobot(robot, "board3") - test.That(t, err, test.ShouldBeNil) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - - rdktestutils.VerifyTopologicallySortedLevels( - t, - robot.(*localRobot).manager.resources, - [][]resource.Name{ - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - []resource.Name{arm.Named("arm1")}, - ), - { - arm.Named("arm3"), - base.Named("base1"), - board.Named("board3"), - }, - { - base.Named("base2"), - board.Named("board2"), - }, - {board.Named("board1")}, - }, - robot.(*localRobot).manager.internalResourceNames()..., - ) - }) - - t.Run("from empty conf with deps", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - cempty := ConfigFromFile(t, "data/diff_config_empty.json") - conf6 := ConfigFromFile(t, "data/diff_config_deps6.json") - ctx := context.Background() - robot := setupLocalRobot(t, ctx, cempty, logger) - - resources := robot.ResourceNames() - test.That(t, len(resources), test.ShouldEqual, 2) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(arm.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(base.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(board.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That( - t, - rdktestutils.NewResourceNameSet(robot.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet(resource.DefaultServices()...), - ) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldBeEmpty) - - armNames := []resource.Name{arm.Named("arm1"), arm.Named("arm3")} - baseNames := []resource.Name{base.Named("base1"), base.Named("base2")} - motorNames := []resource.Name{motor.Named("m1"), motor.Named("m2"), motor.Named("m4"), motor.Named("m5")} - boardNames := []resource.Name{ - board.Named("board1"), - board.Named("board2"), board.Named("board3"), - } - robot.Reconfigure(context.Background(), conf6) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That(t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(baseNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - baseNames, - boardNames, - motorNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err := arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldBeNil) - - _, err = arm.FromRobot(robot, "arm3") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m5") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldBeNil) - - b, err := board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - pin, err := b.GPIOPinByName("1") - test.That(t, err, test.ShouldBeNil) - pwmF, err := pin.PWMFreq(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pwmF, test.ShouldEqual, 0) - _, err = b.DigitalInterruptByName("encoder") - test.That(t, err, test.ShouldNotBeNil) - _, err = b.DigitalInterruptByName("encoderC") - test.That(t, err, test.ShouldBeNil) - - _, err = board.FromRobot(robot, "board3") - test.That(t, err, test.ShouldBeNil) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - - rdktestutils.VerifyTopologicallySortedLevels( - t, - robot.(*localRobot).manager.resources, - [][]resource.Name{ - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - []resource.Name{arm.Named("arm1")}, - ), - { - arm.Named("arm3"), - base.Named("base1"), - board.Named("board3"), - }, - { - base.Named("base2"), - board.Named("board2"), - }, - {board.Named("board1")}, - }, - robot.(*localRobot).manager.internalResourceNames()..., - ) - }) - - t.Run("incremental deps config", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf4 := ConfigFromFile(t, "data/diff_config_deps4.json") - conf7 := ConfigFromFile(t, "data/diff_config_deps7.json") - robot := setupLocalRobot(t, context.Background(), conf4, logger) - - boardNames := []resource.Name{board.Named("board1"), board.Named("board2")} - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err := arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - motorNames := []resource.Name{motor.Named("m1")} - mockNames := []resource.Name{ - mockNamed("mock1"), mockNamed("mock2"), - mockNamed("mock3"), mockNamed("mock4"), mockNamed("mock5"), - mockNamed("mock6"), - } - encoderNames := []resource.Name{encoder.Named("e1")} - - robot.Reconfigure(context.Background(), conf7) - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That( - t, - utils.NewStringSet(encoder.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(encoderNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - motorNames, - mockNames, - encoderNames, - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err = arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - mock1, err := robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock2, err := robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock2.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock3, err := robot.ResourceByName(mockNamed("mock3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock3.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock4, err := robot.ResourceByName(mockNamed("mock4")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock4.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock5, err := robot.ResourceByName(mockNamed("mock5")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock5.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock6, err := robot.ResourceByName(mockNamed("mock6")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock6.(*mockFake).reconfCount, test.ShouldEqual, 0) - - _, ok = robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - sorted := robot.(*localRobot).manager.resources.TopologicalSort() - sorted = rdktestutils.SubtractNames(sorted, robot.(*localRobot).manager.internalResourceNames()...) - test.That(t, rdktestutils.NewResourceNameSet(sorted...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - boardNames, - mockNames, - encoderNames, - )...)) - }) - - t.Run("parent attribute change deps config", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf7 := ConfigFromFile(t, "data/diff_config_deps7.json") - conf8 := ConfigFromFile(t, "data/diff_config_deps8.json") - robot := setupLocalRobot(t, context.Background(), conf7, logger) - - boardNames := []resource.Name{board.Named("board1"), board.Named("board2")} - motorNames := []resource.Name{motor.Named("m1")} - encoderNames := []resource.Name{encoder.Named("e1")} - mockNames := []resource.Name{ - mockNamed("mock1"), mockNamed("mock2"), mockNamed("mock6"), - mockNamed("mock3"), mockNamed("mock4"), mockNamed("mock5"), - } - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That( - t, - utils.NewStringSet(encoder.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(encoderNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - encoderNames, - resource.DefaultServices(), - motorNames, - mockNames, - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err := arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - m, err := motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - c, err := m.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, c, test.ShouldEqual, 0) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - mock1, err := robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock2, err := robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock2.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock3, err := robot.ResourceByName(mockNamed("mock3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock3.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock4, err := robot.ResourceByName(mockNamed("mock4")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock4.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock5, err := robot.ResourceByName(mockNamed("mock5")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock5.(*mockFake).reconfCount, test.ShouldEqual, 0) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - sorted := robot.(*localRobot).manager.resources.TopologicalSort() - sorted = rdktestutils.SubtractNames(sorted, robot.(*localRobot).manager.internalResourceNames()...) - test.That(t, rdktestutils.NewResourceNameSet(sorted...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - boardNames, - mockNames, - encoderNames, - )...)) - robot.Reconfigure(context.Background(), conf8) - mockNames = []resource.Name{ - mockNamed("mock1"), mockNamed("mock2"), - mockNamed("mock3"), mockNamed("mock4"), mockNamed("mock5"), - } - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(base.NamesFromRobot(robot)...), - test.ShouldBeEmpty, - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That( - t, - utils.NewStringSet(encoder.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(encoderNames...)...), - ) - test.That(t, utils.NewStringSet(camera.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(gripper.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(sensor.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, utils.NewStringSet(servo.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - motorNames, - mockNames, - encoderNames, - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err = arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = arm.FromRobot(robot, "arm2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - m, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - c, err = m.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - t.Log("the underlying pins changed but not the encoder names, so we keep the value") - test.That(t, c, test.ShouldEqual, 0) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - mock1, err = robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 1) - - mock2, err = robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock2.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock3, err = robot.ResourceByName(mockNamed("mock3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock3.(*mockFake).reconfCount, test.ShouldEqual, 1) - - mock4, err = robot.ResourceByName(mockNamed("mock4")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock4.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock5, err = robot.ResourceByName(mockNamed("mock5")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock5.(*mockFake).reconfCount, test.ShouldEqual, 1) - - _, ok = robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - }) - - // test starts with a working config, then reconfigures into a config where dependencies - // fail to reconfigure, and then to a working config again. - t.Run("child component fails dep", func(t *testing.T) { - resetComponentFailureState() - testReconfiguringMismatch = true - reconfigurableTrue = true - logger := logging.NewTestLogger(t) - conf7 := ConfigFromFile(t, "data/diff_config_deps7.json") - conf9 := ConfigFromFile(t, "data/diff_config_deps9_bad.json") - robot := setupLocalRobot(t, context.Background(), conf7, logger) - - boardNames := []resource.Name{board.Named("board1"), board.Named("board2")} - motorNames := []resource.Name{motor.Named("m1")} - encoderNames := []resource.Name{encoder.Named("e1")} - mockNames := []resource.Name{ - mockNamed("mock1"), mockNamed("mock2"), - mockNamed("mock3"), mockNamed("mock4"), mockNamed("mock5"), - mockNamed("mock6"), - } - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That( - t, - utils.NewStringSet(encoder.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(encoderNames...)...), - ) - - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - motorNames, - mockNames, - encoderNames, - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err := board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - m, err := motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - c, err := m.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, c, test.ShouldEqual, 0) - - _, err = motor.FromRobot(robot, "m2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m3") - test.That(t, err, test.ShouldNotBeNil) - - _, err = motor.FromRobot(robot, "m4") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base1") - test.That(t, err, test.ShouldNotBeNil) - - _, err = base.FromRobot(robot, "base2") - test.That(t, err, test.ShouldNotBeNil) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - mock1, err := robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock2, err := robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock2.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock3, err := robot.ResourceByName(mockNamed("mock3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock3.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock4, err := robot.ResourceByName(mockNamed("mock4")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock4.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock5, err := robot.ResourceByName(mockNamed("mock5")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock5.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock6, err := robot.ResourceByName(mockNamed("mock6")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock6.(*mockFake).reconfCount, test.ShouldEqual, 0) - - _, ok := robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - sorted := robot.(*localRobot).manager.resources.TopologicalSort() - sorted = rdktestutils.SubtractNames(sorted, robot.(*localRobot).manager.internalResourceNames()...) - test.That(t, rdktestutils.NewResourceNameSet(sorted...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - boardNames, - mockNames, - encoderNames, - )...)) - - reconfigurableTrue = false - robot.Reconfigure(context.Background(), conf9) - - mockNames = []resource.Name{ - mockNamed("mock2"), - mockNamed("mock3"), - } - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(encoder.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(encoderNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - motorNames, - mockNames, - encoderNames, - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - m, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - c, err = m.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, c, test.ShouldEqual, 0) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - _, err = robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldNotBeNil) - - mock2, err = robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock2.(*mockFake).reconfCount, test.ShouldEqual, 1) - - mock3, err = robot.ResourceByName(mockNamed("mock3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock3.(*mockFake).reconfCount, test.ShouldEqual, 1) - - _, err = robot.ResourceByName(mockNamed("mock4")) - test.That(t, err, test.ShouldNotBeNil) - - _, err = robot.ResourceByName(mockNamed("mock5")) - test.That(t, err, test.ShouldNotBeNil) - - // `mock6` is configured to be in a "failing" state. - _, err = robot.ResourceByName(mockNamed("mock6")) - test.That(t, err, test.ShouldNotBeNil) - - // `armFake` depends on `mock6` and is therefore also in an error state. - _, err = robot.ResourceByName(arm.Named("armFake")) - test.That(t, err, test.ShouldNotBeNil) - - _, ok = robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - sorted = robot.(*localRobot).manager.resources.TopologicalSort() - sorted = rdktestutils.SubtractNames(sorted, robot.(*localRobot).manager.internalResourceNames()...) - test.That(t, rdktestutils.NewResourceNameSet(sorted...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - boardNames, - mockNames, - encoderNames, - []resource.Name{ - arm.Named("armFake"), - mockNamed("mock1"), - mockNamed("mock4"), - mockNamed("mock5"), - mockNamed("mock6"), - }, - )...)) - - // This configuration will put `mock6` into a good state after two calls to "reconfigure". - conf9good := ConfigFromFile(t, "data/diff_config_deps9_good.json") - robot.Reconfigure(context.Background(), conf9good) - - mockNames = []resource.Name{ - mockNamed("mock2"), mockNamed("mock1"), mockNamed("mock3"), - mockNamed("mock4"), mockNamed("mock5"), - } - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - test.That( - t, - utils.NewStringSet(motor.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(motorNames...)...), - ) - test.That( - t, - utils.NewStringSet(board.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(boardNames...)...), - ) - test.That( - t, - utils.NewStringSet(encoder.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(encoderNames...)...), - ) - - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - boardNames, - resource.DefaultServices(), - motorNames, - mockNames, - encoderNames, - )...)) - test.That(t, utils.NewStringSet(robot.ProcessManager().ProcessIDs()...), test.ShouldResemble, utils.NewStringSet("1", "2")) - - _, err = board.FromRobot(robot, "board1") - test.That(t, err, test.ShouldBeNil) - - m, err = motor.FromRobot(robot, "m1") - test.That(t, err, test.ShouldBeNil) - c, err = m.Position(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, c, test.ShouldEqual, 0) - - _, err = board.FromRobot(robot, "board2") - test.That(t, err, test.ShouldBeNil) - - // resources which failed previous reconfiguration attempts because of missing dependencies will be rebuilt, - // so reconfCount should be 0. resources which failed previous reconfiguration attempts because of an error - // during reconfiguration would not have its reconfCount reset, so reconfCount for mock4 should be 1. - mock1, err = robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock1.(*mockFake).reconfCount, test.ShouldEqual, 0) - - mock2, err = robot.ResourceByName(mockNamed("mock2")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock2.(*mockFake).reconfCount, test.ShouldEqual, 1) - - mock3, err = robot.ResourceByName(mockNamed("mock3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock3.(*mockFake).reconfCount, test.ShouldEqual, 1) - - mock4, err = robot.ResourceByName(mockNamed("mock4")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock4.(*mockFake).reconfCount, test.ShouldEqual, 1) - - mock5, err = robot.ResourceByName(mockNamed("mock5")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock5.(*mockFake).reconfCount, test.ShouldEqual, 0) - - // `mock6` is configured to be in a "failing" state. - _, err = robot.ResourceByName(mockNamed("mock6")) - test.That(t, err, test.ShouldNotBeNil) - - // `armFake` depends on `mock6` and is therefore also in an error state. - _, err = robot.ResourceByName(arm.Named("armFake")) - test.That(t, err, test.ShouldNotBeNil) - - _, ok = robot.ProcessManager().ProcessByID("1") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("2") - test.That(t, ok, test.ShouldBeTrue) - - reconfigurableTrue = true - - rr, ok := robot.(*localRobot) - test.That(t, ok, test.ShouldBeTrue) - - // The newly set configuration fixes the `mock6` component. A (second) reconfig should pick - // that up and consequently bubble up the working `mock6` change to anything that depended - // on `mock6`, notably `armFake`. - rr.triggerConfig <- struct{}{} - - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 30, func(tb testing.TB) { - armFake, err := robot.ResourceByName(arm.Named("armFake")) - test.That(tb, err, test.ShouldBeNil) - test.That(tb, armFake, test.ShouldNotBeNil) - }) - - // Seeing `armFake` in a working state implies that `mock6` must also be in a working state - // with its `reconfCount` bumped. - mock6, err = robot.ResourceByName(mockNamed("mock6")) - test.That(t, err, test.ShouldBeNil) - test.That(t, mock6.(*mockFake).reconfCount, test.ShouldEqual, 1) - - sorted = robot.(*localRobot).manager.resources.TopologicalSort() - sorted = rdktestutils.SubtractNames(sorted, robot.(*localRobot).manager.internalResourceNames()...) - test.That(t, rdktestutils.NewResourceNameSet(sorted...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - motorNames, - resource.DefaultServices(), - boardNames, - mockNames, - encoderNames, - []resource.Name{ - arm.Named("armFake"), - mockNamed("mock6"), - }, - )...)) - }) - t.Run("complex diff", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - conf1 := ConfigFromFile(t, "data/diff_config_deps11.json") - conf2 := ConfigFromFile(t, "data/diff_config_deps12.json") - robot := setupLocalRobot(t, context.Background(), conf1, logger) - - armNames := []resource.Name{arm.Named("mock7")} - mockNames := []resource.Name{ - mockNamed("mock3"), mockNamed("mock4"), - mockNamed("mock6"), mockNamed("mock5"), - } - - robot.Reconfigure(context.Background(), conf1) - test.That( - t, - utils.NewStringSet(arm.NamesFromRobot(robot)...), - test.ShouldResemble, - utils.NewStringSet(rdktestutils.ExtractNames(armNames...)...), - ) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - armNames, - resource.DefaultServices(), - mockNames, - )...)) - _, err := robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldNotBeNil) - _, err = arm.FromRobot(robot, "mock7") - test.That(t, err, test.ShouldBeNil) - - robot.Reconfigure(context.Background(), conf2) - mockNames = []resource.Name{ - mockNamed("mock1"), - mockNamed("mock3"), mockNamed("mock2"), mockNamed("mock5"), - } - test.That(t, utils.NewStringSet(robot.RemoteNames()...), test.ShouldBeEmpty) - - test.That(t, utils.NewStringSet(arm.NamesFromRobot(robot)...), test.ShouldBeEmpty) - test.That(t, rdktestutils.NewResourceNameSet(robot.ResourceNames()...), test.ShouldResemble, rdktestutils.NewResourceNameSet( - rdktestutils.ConcatResourceNames( - mockNames, - resource.DefaultServices(), - )...)) - - _, err = arm.FromRobot(robot, "arm1") - test.That(t, err, test.ShouldNotBeNil) - _, err = robot.ResourceByName(mockNamed("mock1")) - test.That(t, err, test.ShouldBeNil) - }) - t.Run("test processes", func(t *testing.T) { - resetComponentFailureState() - logger := logging.NewTestLogger(t) - tempDir := t.TempDir() - robot := setupLocalRobot(t, context.Background(), &config.Config{}, logger) - - // create a unexecutable file - noExecF, err := os.CreateTemp(tempDir, "noexec*.sh") - test.That(t, err, test.ShouldBeNil) - err = noExecF.Close() - test.That(t, err, test.ShouldBeNil) - // create a origin file - originF, err := os.CreateTemp(tempDir, "origin*") - test.That(t, err, test.ShouldBeNil) - token := make([]byte, 128) - _, err = rand.Read(token) - test.That(t, err, test.ShouldBeNil) - _, err = originF.Write(token) - test.That(t, err, test.ShouldBeNil) - err = originF.Sync() - test.That(t, err, test.ShouldBeNil) - // create a target file - targetF, err := os.CreateTemp(tempDir, "target*") - test.That(t, err, test.ShouldBeNil) - - // create a second target file - target2F, err := os.CreateTemp(tempDir, "target*") - test.That(t, err, test.ShouldBeNil) - - // config1 - config1 := &config.Config{ - Processes: []pexec.ProcessConfig{ - { - ID: "shouldfail", // this process won't be executed - Name: "false", - OneShot: true, - }, - { - ID: "noexec", // file exist but exec bit not set - Name: noExecF.Name(), - OneShot: true, - }, - { - ID: "shouldsuceed", // this keep succeeding - Name: "true", - }, - { - ID: "noexist", // file doesn't exists - Name: fmt.Sprintf("%s/%s", tempDir, "noexistfile"), - OneShot: true, - Log: true, - }, - { - ID: "filehandle", // this keep succeeding and will be changed - Name: "true", - }, - { - ID: "touch", // touch a file - Name: "sh", - CWD: tempDir, - Args: []string{ - "-c", - "sleep 0.4;touch afile", - }, - OneShot: true, - }, - }, - } - robot.Reconfigure(context.Background(), config1) - _, ok := robot.ProcessManager().ProcessByID("shouldfail") - test.That(t, ok, test.ShouldBeFalse) - _, ok = robot.ProcessManager().ProcessByID("shouldsuceed") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("noexist") - test.That(t, ok, test.ShouldBeFalse) - _, ok = robot.ProcessManager().ProcessByID("noexec") - test.That(t, ok, test.ShouldBeFalse) - _, ok = robot.ProcessManager().ProcessByID("filehandle") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("touch") - test.That(t, ok, test.ShouldBeTrue) - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 50, func(tb testing.TB) { - _, err = os.Stat(filepath.Join(tempDir, "afile")) - test.That(tb, err, test.ShouldBeNil) - }) - config2 := &config.Config{ - Processes: []pexec.ProcessConfig{ - { - ID: "shouldfail", // now it succeeds - Name: "true", - OneShot: true, - }, - { - ID: "shouldsuceed", // now it fails - Name: "false", - OneShot: true, - }, - { - ID: "filehandle", // this transfer originF to targetF after 2s - Name: "sh", - Args: []string{ - "-c", - fmt.Sprintf("sleep 2; cat %s >> %s", originF.Name(), targetF.Name()), - }, - OneShot: true, - }, - { - ID: "filehandle2", // this transfer originF to target2F after 0.4s - Name: "sh", - Args: []string{ - "-c", - fmt.Sprintf("sleep 0.4;cat %s >> %s", originF.Name(), target2F.Name()), - }, - }, - { - ID: "remove", // remove the file - Name: "sh", - CWD: tempDir, - Args: []string{ - "-c", - "sleep 0.2;rm afile", - }, - OneShot: true, - Log: true, - }, - }, - } - robot.Reconfigure(context.Background(), config2) - _, ok = robot.ProcessManager().ProcessByID("shouldfail") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("shouldsuceed") - test.That(t, ok, test.ShouldBeFalse) - _, ok = robot.ProcessManager().ProcessByID("noexist") - test.That(t, ok, test.ShouldBeFalse) - _, ok = robot.ProcessManager().ProcessByID("noexec") - test.That(t, ok, test.ShouldBeFalse) - _, ok = robot.ProcessManager().ProcessByID("filehandle") - test.That(t, ok, test.ShouldBeTrue) - _, ok = robot.ProcessManager().ProcessByID("touch") - test.That(t, ok, test.ShouldBeFalse) - _, ok = robot.ProcessManager().ProcessByID("remove") - test.That(t, ok, test.ShouldBeTrue) - r := make([]byte, 128) - n, err := targetF.Read(r) - test.That(t, err, test.ShouldBeNil) - test.That(t, n, test.ShouldEqual, 128) - time.Sleep(3 * time.Second) - _, err = targetF.Seek(0, 0) - test.That(t, err, test.ShouldBeNil) - n, err = targetF.Read(r) - test.That(t, err, test.ShouldBeNil) - test.That(t, n, test.ShouldEqual, 128) - test.That(t, r, test.ShouldResemble, token) - time.Sleep(3 * time.Second) - _, err = targetF.Read(r) - test.That(t, err, test.ShouldNotBeNil) - err = originF.Close() - test.That(t, err, test.ShouldBeNil) - err = targetF.Close() - test.That(t, err, test.ShouldBeNil) - stat, err := target2F.Stat() - test.That(t, err, test.ShouldBeNil) - test.That(t, stat.Size(), test.ShouldBeGreaterThan, 128) - err = target2F.Close() - test.That(t, err, test.ShouldBeNil) - _, err = os.Stat(filepath.Join(tempDir, "afile")) - test.That(t, err, test.ShouldNotBeNil) - }) -} - -// this serves as a test for updateWeakDependents as the sensors service defines a weak -// dependency. -func TestSensorsServiceReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - - emptyCfg, err := config.Read(context.Background(), "data/diff_config_empty.json", logger) - test.That(t, err, test.ShouldBeNil) - cfg, err := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, err, test.ShouldBeNil) - - sensorNames := []resource.Name{movementsensor.Named("movement_sensor1"), movementsensor.Named("movement_sensor2")} - - t.Run("empty to two sensors", func(t *testing.T) { - robot := setupLocalRobot(t, context.Background(), emptyCfg, logger) - - svc, err := sensors.FromRobot(robot, resource.DefaultServiceName) - test.That(t, err, test.ShouldBeNil) - - foundSensors, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, foundSensors, test.ShouldBeEmpty) - - robot.Reconfigure(context.Background(), cfg) - - foundSensors, err = svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, rdktestutils.NewResourceNameSet(foundSensors...), test.ShouldResemble, rdktestutils.NewResourceNameSet(sensorNames...)) - }) - - t.Run("two sensors to empty", func(t *testing.T) { - robot := setupLocalRobot(t, context.Background(), cfg, logger) - - svc, err := sensors.FromRobot(robot, resource.DefaultServiceName) - test.That(t, err, test.ShouldBeNil) - - foundSensors, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, rdktestutils.NewResourceNameSet(foundSensors...), test.ShouldResemble, rdktestutils.NewResourceNameSet(sensorNames...)) - - robot.Reconfigure(context.Background(), emptyCfg) - - foundSensors, err = svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, foundSensors, test.ShouldBeEmpty) - }) - - t.Run("two sensors to two sensors", func(t *testing.T) { - robot := setupLocalRobot(t, context.Background(), cfg, logger) - - svc, err := sensors.FromRobot(robot, resource.DefaultServiceName) - test.That(t, err, test.ShouldBeNil) - - foundSensors, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, rdktestutils.NewResourceNameSet(foundSensors...), test.ShouldResemble, rdktestutils.NewResourceNameSet(sensorNames...)) - - robot.Reconfigure(context.Background(), cfg) - - foundSensors, err = svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, rdktestutils.NewResourceNameSet(foundSensors...), test.ShouldResemble, rdktestutils.NewResourceNameSet(sensorNames...)) - }) -} - -type someTypeWithWeakAndStrongDeps struct { - resource.Named - resource.TriviallyCloseable - resources resource.Dependencies -} - -func (s *someTypeWithWeakAndStrongDeps) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - s.resources = deps - ourConf, err := resource.NativeConfig[*someTypeWithWeakAndStrongDepsConfig](conf) - if err != nil { - return err - } - for _, dep := range ourConf.deps { - if _, err := deps.Lookup(dep); err != nil { - return err - } - } - for _, dep := range ourConf.weakDeps { - if _, err := deps.Lookup(dep); err != nil { - return err - } - } - return nil -} - -type someTypeWithWeakAndStrongDepsConfig struct { - deps []resource.Name - weakDeps []resource.Name -} - -func (s *someTypeWithWeakAndStrongDepsConfig) Validate(_ string) ([]string, error) { - depNames := make([]string, 0, len(s.deps)) - for _, dep := range s.deps { - depNames = append(depNames, dep.String()) - } - return depNames, nil -} - -func TestUpdateWeakDependents(t *testing.T) { - logger := logging.NewTestLogger(t) - - var emptyCfg config.Config - test.That(t, emptyCfg.Ensure(false, logger), test.ShouldBeNil) - - robot := setupLocalRobot(t, context.Background(), &emptyCfg, logger) - - // Register a `Resource` that generates weak dependencies. Specifically instance of - // this resource will depend on every `component` resource. See the definition of - // `internal.ComponentDependencyWildcardMatcher`. - weakAPI := resource.NewAPI(uuid.NewString(), "component", "weaktype") - weakModel := resource.NewModel(uuid.NewString(), "soweak", "weak1000") - weak1Name := resource.NewName(weakAPI, "weak1") - resource.Register( - weakAPI, - weakModel, - resource.Registration[*someTypeWithWeakAndStrongDeps, *someTypeWithWeakAndStrongDepsConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (*someTypeWithWeakAndStrongDeps, error) { - return &someTypeWithWeakAndStrongDeps{ - Named: conf.ResourceName().AsNamed(), - resources: deps, - }, nil - }, - WeakDependencies: []resource.Matcher{resource.TypeMatcher{Type: resource.APITypeComponentName}}, - }) - defer func() { - resource.Deregister(weakAPI, weakModel) - }() - - // Create a configuration with a single component that has an explicit, unresolved - // dependency. Reconfiguring will succeed, but getting a handle on the `weak1Name` resource fails - // with `unresolved dependencies`. - base1Name := base.Named("base1") - weakCfg1 := config.Config{ - Components: []resource.Config{ - { - Name: weak1Name.Name, - API: weakAPI, - Model: weakModel, - DependsOn: []string{base1Name.Name}, - }, - }, - } - test.That(t, weakCfg1.Ensure(false, logger), test.ShouldBeNil) - robot.Reconfigure(context.Background(), &weakCfg1) - - _, err := robot.ResourceByName(weak1Name) - test.That(t, err, test.ShouldNotBeNil) - // Assert that the explicit dependency was observed. - test.That(t, err.Error(), test.ShouldContainSubstring, "unresolved dependencies") - test.That(t, err.Error(), test.ShouldContainSubstring, "base1") - - // Reconfigure without the explicit dependency. While also adding a second component that would - // have satisfied the dependency from the prior `weakCfg1`. Due to the weak dependency wildcard - // matcher, this `base1` component will be parsed as a weak dependency of `weak1`. - weakCfg2 := config.Config{ - Components: []resource.Config{ - { - Name: weak1Name.Name, - API: weakAPI, - Model: weakModel, - }, - { - Name: base1Name.Name, - API: base.API, - Model: fake.Model, - }, - }, - } - test.That(t, weakCfg2.Ensure(false, logger), test.ShouldBeNil) - robot.Reconfigure(context.Background(), &weakCfg2) - - res, err := robot.ResourceByName(weak1Name) - // The resource was found and all dependencies were properly resolved. - test.That(t, err, test.ShouldBeNil) - weak1, err := resource.AsType[*someTypeWithWeakAndStrongDeps](res) - test.That(t, err, test.ShouldBeNil) - // Assert that the weak dependency was tracked. - test.That(t, weak1.resources, test.ShouldHaveLength, 1) - test.That(t, weak1.resources, test.ShouldContainKey, base1Name) - - // Reconfigure again with a new third `arm` component. - arm1Name := arm.Named("arm1") - weakCfg3 := config.Config{ - Components: []resource.Config{ - { - Name: weak1Name.Name, - API: weakAPI, - Model: weakModel, - }, - { - Name: base1Name.Name, - API: base.API, - Model: fake.Model, - }, - { - Name: arm1Name.Name, - API: arm.API, - Model: fake.Model, - ConvertedAttributes: &fake.Config{}, - }, - }, - } - test.That(t, weakCfg3.Ensure(false, logger), test.ShouldBeNil) - robot.Reconfigure(context.Background(), &weakCfg3) - - res, err = robot.ResourceByName(weak1Name) - test.That(t, err, test.ShouldBeNil) - weak1, err = resource.AsType[*someTypeWithWeakAndStrongDeps](res) - test.That(t, err, test.ShouldBeNil) - // With two other components, `weak1` now has two (weak) dependencies. - test.That(t, weak1.resources, test.ShouldHaveLength, 2) - test.That(t, weak1.resources, test.ShouldContainKey, base1Name) - test.That(t, weak1.resources, test.ShouldContainKey, arm1Name) - - base2Name := base.Named("base2") - weakCfg5 := config.Config{ - Components: []resource.Config{ - { - Name: weak1Name.Name, - API: weakAPI, - Model: weakModel, - // We need the following `robot.Reconfigure` to call `Reconfigure` on this `weak1` - // component. We change the `Attributes` field from the previous (nil) value to - // accomplish that. - Attributes: rutils.AttributeMap{"version": 1}, - ConvertedAttributes: &someTypeWithWeakAndStrongDepsConfig{ - deps: []resource.Name{generic.Named("foo")}, - }, - }, - { - Name: base1Name.Name, - API: base.API, - Model: fake.Model, - }, - { - Name: base2Name.Name, - API: base.API, - Model: fake.Model, - }, - }, - } - test.That(t, weakCfg5.Ensure(false, logger), test.ShouldBeNil) - robot.Reconfigure(context.Background(), &weakCfg5) - - _, err = robot.ResourceByName(weak1Name) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not initialized") - - weakCfg6 := config.Config{ - Components: []resource.Config{ - { - Name: weak1Name.Name, - API: weakAPI, - Model: weakModel, - Attributes: rutils.AttributeMap{"version": 2}, - ConvertedAttributes: &someTypeWithWeakAndStrongDepsConfig{ - weakDeps: []resource.Name{base1Name}, - }, - }, - { - Name: base1Name.Name, - API: base.API, - Model: fake.Model, - }, - { - Name: base2Name.Name, - API: base.API, - Model: fake.Model, - }, - }, - } - test.That(t, weakCfg6.Ensure(false, logger), test.ShouldBeNil) - robot.Reconfigure(context.Background(), &weakCfg6) - res, err = robot.ResourceByName(weak1Name) - test.That(t, err, test.ShouldBeNil) - weak1, err = resource.AsType[*someTypeWithWeakAndStrongDeps](res) - test.That(t, err, test.ShouldBeNil) - test.That(t, weak1.resources, test.ShouldHaveLength, 2) - test.That(t, weak1.resources, test.ShouldContainKey, base1Name) - test.That(t, weak1.resources, test.ShouldContainKey, base2Name) - - weakCfg7 := config.Config{ - Components: []resource.Config{ - { - Name: weak1Name.Name, - API: weakAPI, - Model: weakModel, - Attributes: rutils.AttributeMap{"version": 3}, - ConvertedAttributes: &someTypeWithWeakAndStrongDepsConfig{ - deps: []resource.Name{base2Name}, - weakDeps: []resource.Name{base1Name}, - }, - }, - { - Name: base1Name.Name, - API: base.API, - Model: fake.Model, - }, - { - Name: base2Name.Name, - API: base.API, - Model: fake.Model, - }, - }, - } - test.That(t, weakCfg7.Ensure(false, logger), test.ShouldBeNil) - robot.Reconfigure(context.Background(), &weakCfg7) - - res, err = robot.ResourceByName(weak1Name) - test.That(t, err, test.ShouldBeNil) - weak1, err = resource.AsType[*someTypeWithWeakAndStrongDeps](res) - test.That(t, err, test.ShouldBeNil) - test.That(t, weak1.resources, test.ShouldHaveLength, 2) - test.That(t, weak1.resources, test.ShouldContainKey, base1Name) - test.That(t, weak1.resources, test.ShouldContainKey, base2Name) -} - -func TestDefaultServiceReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - - motionName := "motion" - cfg1 := &config.Config{ - Services: []resource.Config{ - { - Name: motionName, - API: motion.API, - Model: resource.DefaultServiceModel, - }, - }, - } - robot := setupLocalRobot(t, context.Background(), cfg1, logger) - - test.That( - t, - rdktestutils.NewResourceNameSet(robot.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(motionName), - sensors.Named(resource.DefaultServiceName), - ), - ) - sName := "sensors" - cfg2 := &config.Config{ - Services: []resource.Config{ - { - Name: sName, - API: sensors.API, - Model: resource.DefaultServiceModel, - }, - }, - } - robot.Reconfigure(context.Background(), cfg2) - test.That( - t, - rdktestutils.NewResourceNameSet(robot.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(sName), - ), - ) -} - -func TestStatusServiceUpdate(t *testing.T) { - logger := logging.NewTestLogger(t) - - emptyCfg, err := config.Read(context.Background(), "data/diff_config_empty.json", logger) - test.That(t, err, test.ShouldBeNil) - cfg, cfgErr := config.Read(context.Background(), "data/fake.json", logger) - test.That(t, cfgErr, test.ShouldBeNil) - - resourceNames := []resource.Name{ - movementsensor.Named("movement_sensor1"), - movementsensor.Named("movement_sensor2"), - } - expected := map[resource.Name]interface{}{ - movementsensor.Named("movement_sensor1"): map[string]interface{}{}, - movementsensor.Named("movement_sensor2"): map[string]interface{}{}, - } - - t.Run("empty to not empty", func(t *testing.T) { - robot := setupLocalRobot(t, context.Background(), emptyCfg, logger) - - _, err := robot.Status(context.Background(), resourceNames) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - robot.Reconfigure(context.Background(), cfg) - - statuses, err := robot.Status(context.Background(), resourceNames) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 2) - test.That(t, statuses[0].Status, test.ShouldResemble, expected[statuses[0].Name]) - test.That(t, statuses[1].Status, test.ShouldResemble, expected[statuses[1].Name]) - }) - - t.Run("not empty to empty", func(t *testing.T) { - robot := setupLocalRobot(t, context.Background(), cfg, logger) - - statuses, err := robot.Status(context.Background(), resourceNames) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 2) - test.That(t, statuses[0].Status, test.ShouldResemble, expected[statuses[0].Name]) - test.That(t, statuses[1].Status, test.ShouldResemble, expected[statuses[1].Name]) - - robot.Reconfigure(context.Background(), emptyCfg) - - _, err = robot.Status(context.Background(), resourceNames) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - }) - - t.Run("no change", func(t *testing.T) { - robot := setupLocalRobot(t, context.Background(), cfg, logger) - - statuses, err := robot.Status(context.Background(), resourceNames) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 2) - test.That(t, statuses[0].Status, test.ShouldResemble, expected[statuses[0].Name]) - test.That(t, statuses[1].Status, test.ShouldResemble, expected[statuses[1].Name]) - - robot.Reconfigure(context.Background(), cfg) - - statuses, err = robot.Status(context.Background(), resourceNames) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(statuses), test.ShouldEqual, 2) - test.That(t, statuses[0].Status, test.ShouldResemble, expected[statuses[0].Name]) - test.That(t, statuses[1].Status, test.ShouldResemble, expected[statuses[1].Name]) - }) -} - -func TestRemoteRobotsGold(t *testing.T) { - // This tests that a main part is able to start up with an offline remote robot, connect to it and - // depend on the remote robot's resources when it comes online. And react appropriately when the remote robot goes offline again. - - // If a new robot object/process comes online at the same address+port, the main robot should still be able - // to use the new remote robot's resources. - - // To do so, the test initially sets up two remote robots, Remote 1 and 2, and then a third remote, Remote 3, - // in the following scenario: - // 1) Remote 1's server is started. - // 2) The main robot is then set up with resources that depend on resources on both Remote 1 and 2. Since - // Remote 2 is not up, their resources are not available to the main robot. - // 3) After initial configuration, Remote 2's server starts up and the main robot should then connect - // and pick up the new available resources. - // 4) Remote 2 goes down, and the main robot should remove any resources or resources that depend on - // resources from Remote 2. - // 5) Remote 3 comes online at the same address as Remote 2, and the main robot should treat it the same as - // if Remote 2 came online again and re-add all the removed resources. - logger := logging.NewTestLogger(t) - remoteConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "remoteArm", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - }, - }, - } - - ctx := context.Background() - - // set up and start remote1's web service - remote1 := setupLocalRobot(t, ctx, remoteConfig, logger.Sublogger("remote1")) - options, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - err := remote1.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // set up but do not start remote2's web service - remote2 := setupLocalRobot(t, ctx, remoteConfig, logger.Sublogger("remote2")) - options, listener2, addr2 := robottestutils.CreateBaseOptionsAndListener(t) - - localConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - DependsOn: []string{"foo:remoteArm"}, - }, - { - Name: "arm2", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - DependsOn: []string{"bar:remoteArm"}, - }, - }, - Services: []resource.Config{}, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr1, - }, - { - Name: "bar", - Address: addr2, - }, - }, - } - r := setupLocalRobot(t, ctx, localConfig, logger.Sublogger("main")) - - // assert all of remote1's resources exist on main but none of remote2's - test.That( - t, - rdktestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("arm1"), - arm.Named("foo:remoteArm"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - ), - ) - - // start remote2's web service - err = remote2.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - mainPartAndFooAndBarResources := rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("arm1"), - arm.Named("arm2"), - arm.Named("foo:remoteArm"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - arm.Named("bar:remoteArm"), - motion.Named("bar:builtin"), - sensors.Named("bar:builtin"), - ) - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, mainPartAndFooAndBarResources) - }) - test.That(t, remote2.Close(context.Background()), test.ShouldBeNil) - - // wait for local_robot to detect that the remote is now offline - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That( - tb, - rdktestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("arm1"), - arm.Named("foo:remoteArm"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - ), - ) - }) - - remote3 := setupLocalRobot(t, ctx, remoteConfig, logger.Sublogger("remote3")) - - // Note: There's a slight chance this test can fail if someone else - // claims the port we just released by closing the server. - listener2, err = net.Listen("tcp", listener2.Addr().String()) - test.That(t, err, test.ShouldBeNil) - options.Network.Listener = listener2 - err = remote3.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, mainPartAndFooAndBarResources) - }) -} - -func TestRemoteRobotsUpdate(t *testing.T) { - // The test tests that the robot is able to update when multiple remote robot - // updates happen at the same time. - logger := logging.NewTestLogger(t) - remoteConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - }, - }, - } - ctx := context.Background() - remote := setupLocalRobot(t, ctx, remoteConfig, logger.Sublogger("remote")) - - options, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - err := remote.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - localConfig := &config.Config{ - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr1, - }, - { - Name: "bar", - Address: addr1, - }, - { - Name: "hello", - Address: addr1, - }, - { - Name: "world", - Address: addr1, - }, - }, - } - r := setupLocalRobot(t, ctx, localConfig, logger.Sublogger("local")) - - expectedSet := rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("foo:arm1"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - arm.Named("bar:arm1"), - motion.Named("bar:builtin"), - sensors.Named("bar:builtin"), - arm.Named("hello:arm1"), - motion.Named("hello:builtin"), - sensors.Named("hello:builtin"), - arm.Named("world:arm1"), - motion.Named("world:builtin"), - sensors.Named("world:builtin"), - ) - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, expectedSet) - }) - test.That(t, remote.Close(context.Background()), test.ShouldBeNil) - - // wait for local_robot to detect that the remote is now offline - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That( - tb, - rdktestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - ), - ) - }) -} - -func TestInferRemoteRobotDependencyConnectAtStartup(t *testing.T) { - // The test tests that the robot is able to infer remote dependencies - // if remote name is not part of the specified dependency - // and the remote is online at start up. - logger := logging.NewTestLogger(t) - - fooCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "pieceArm", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - }, - }, - } - ctx := context.Background() - foo := setupLocalRobot(t, ctx, fooCfg, logger.Sublogger("foo")) - - options, listener1, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - err := foo.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - localConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - DependsOn: []string{"pieceArm"}, - }, - }, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr1, - }, - }, - } - r := setupLocalRobot(t, ctx, localConfig, logger.Sublogger("local")) - expectedSet := rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("arm1"), - arm.Named("foo:pieceArm"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - ) - test.That( - t, - rdktestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - expectedSet, - ) - test.That(t, foo.Close(context.Background()), test.ShouldBeNil) - - // wait for local_robot to detect that the remote is now offline - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That( - tb, - rdktestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - ), - ) - }) - - foo2 := setupLocalRobot(t, ctx, fooCfg, logger.Sublogger("foo2")) - - // Note: There's a slight chance this test can fail if someone else - // claims the port we just released by closing the server. - listener1, err = net.Listen("tcp", listener1.Addr().String()) - test.That(t, err, test.ShouldBeNil) - options.Network.Listener = listener1 - err = foo2.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, expectedSet) - }) -} - -func TestInferRemoteRobotDependencyConnectAfterStartup(t *testing.T) { - // The test tests that the robot is able to infer remote dependencies - // if remote name is not part of the specified dependency - // and the remote is offline at start up. - logger := logging.NewTestLogger(t) - - fooCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "pieceArm", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - }, - }, - } - - ctx := context.Background() - - foo := setupLocalRobot(t, ctx, fooCfg, logger.Sublogger("foo")) - - options, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - - localConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - DependsOn: []string{"pieceArm"}, - }, - }, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr1, - }, - }, - } - r := setupLocalRobot(t, ctx, localConfig, logger.Sublogger("local")) - test.That( - t, - rdktestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - ), - ) - err := foo.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - expectedSet := rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("arm1"), - arm.Named("foo:pieceArm"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - ) - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, expectedSet) - }) - test.That(t, foo.Close(context.Background()), test.ShouldBeNil) - - // wait for local_robot to detect that the remote is now offline - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That( - tb, - rdktestutils.NewResourceNameSet(r.ResourceNames()...), - test.ShouldResemble, - rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - ), - ) - }) -} - -func TestInferRemoteRobotDependencyAmbiguous(t *testing.T) { - // The test tests that the robot will not build a resource if the dependency - // is ambiguous. In this case, "pieceArm" can refer to both "foo:pieceArm" - // and "bar:pieceArm". - logger := logging.NewTestLogger(t) - - remoteCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "pieceArm", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - }, - }, - } - - ctx := context.Background() - - foo := setupLocalRobot(t, ctx, remoteCfg, logger.Sublogger("foo")) - bar := setupLocalRobot(t, ctx, remoteCfg, logger.Sublogger("bar")) - - options1, _, addr1 := robottestutils.CreateBaseOptionsAndListener(t) - err := foo.StartWeb(ctx, options1) - test.That(t, err, test.ShouldBeNil) - - options2, _, addr2 := robottestutils.CreateBaseOptionsAndListener(t) - err = bar.StartWeb(ctx, options2) - test.That(t, err, test.ShouldBeNil) - - localConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - DependsOn: []string{"pieceArm"}, - }, - }, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr1, - }, - { - Name: "bar", - Address: addr2, - }, - }, - } - r := setupLocalRobot(t, ctx, localConfig, logger.Sublogger("local")) - - expectedSet := rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("foo:pieceArm"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - arm.Named("bar:pieceArm"), - motion.Named("bar:builtin"), - sensors.Named("bar:builtin"), - ) - - test.That(t, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, expectedSet) - - // we expect the robot to correctly detect the ambiguous dependency and not build the resource - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 150, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, expectedSet) - }) - - // now reconfig with a fully qualified name - reConfig := &config.Config{ - Components: []resource.Config{ - { - Name: "arm1", - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - ModelFilePath: "../../components/arm/fake/fake_model.json", - }, - API: arm.API, - DependsOn: []string{"foo:pieceArm"}, - }, - }, - Remotes: []config.Remote{ - { - Name: "foo", - Address: addr1, - }, - { - Name: "bar", - Address: addr2, - }, - }, - } - r.Reconfigure(ctx, reConfig) - - finalSet := rdktestutils.NewResourceNameSet( - motion.Named(resource.DefaultServiceName), - sensors.Named(resource.DefaultServiceName), - arm.Named("foo:pieceArm"), - motion.Named("foo:builtin"), - sensors.Named("foo:builtin"), - arm.Named("bar:pieceArm"), - motion.Named("bar:builtin"), - sensors.Named("bar:builtin"), - arm.Named("arm1"), - ) - - testutils.WaitForAssertionWithSleep(t, time.Millisecond*100, 300, func(tb testing.TB) { - test.That(tb, rdktestutils.NewResourceNameSet(r.ResourceNames()...), test.ShouldResemble, finalSet) - }) -} - -func TestReconfigureModelRebuild(t *testing.T) { - logger := logging.NewTestLogger(t) - - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - mockNamed := func(name string) resource.Name { - return resource.NewName(mockAPI, name) - } - modelName1 := utils.RandomAlphaString(5) - model1 := resource.DefaultModelFamily.WithModel(modelName1) - - resource.RegisterComponent(mockAPI, model1, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return &mockFake{Named: conf.ResourceName().AsNamed(), shouldRebuild: true}, nil - }, - }) - defer func() { - resource.Deregister(mockAPI, model1) - }() - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - }, - }, - } - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - - name1 := mockNamed("one") - res1, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 0) - - r.Reconfigure(ctx, cfg) - res2, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldEqual, res1) - test.That(t, res2.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res2.(*mockFake).closeCount, test.ShouldEqual, 0) - - newCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - // Change the `Attributes` to force this component to be reconfigured. - Attributes: rutils.AttributeMap{"version": 1}, - ConvertedAttributes: resource.NoNativeConfig{}, - }, - }, - } - - r.Reconfigure(ctx, newCfg) - res3, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res3, test.ShouldNotEqual, res1) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 1) - test.That(t, res3.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res3.(*mockFake).closeCount, test.ShouldEqual, 0) -} - -func TestReconfigureModelSwitch(t *testing.T) { - logger := logging.NewTestLogger(t) - - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - mockNamed := func(name string) resource.Name { - return resource.NewName(mockAPI, name) - } - modelName1 := utils.RandomAlphaString(5) - modelName2 := utils.RandomAlphaString(5) - model1 := resource.DefaultModelFamily.WithModel(modelName1) - model2 := resource.DefaultModelFamily.WithModel(modelName2) - - resource.RegisterComponent(mockAPI, model1, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return &mockFake{Named: conf.ResourceName().AsNamed()}, nil - }, - }) - resource.RegisterComponent(mockAPI, model2, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return &mockFake2{Named: conf.ResourceName().AsNamed()}, nil - }, - }) - - defer func() { - resource.Deregister(mockAPI, model1) - resource.Deregister(mockAPI, model2) - }() - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - }, - }, - } - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - - name1 := mockNamed("one") - res1, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 0) - - r.Reconfigure(ctx, cfg) - res2, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldEqual, res1) - test.That(t, res2.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res2.(*mockFake).closeCount, test.ShouldEqual, 0) - - newCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model2, - API: mockAPI, - ConvertedAttributes: resource.NoNativeConfig{}, - }, - }, - } - - r.Reconfigure(ctx, newCfg) - res3, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res3, test.ShouldNotEqual, res1) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 1) - test.That(t, res3.(*mockFake2).reconfCount, test.ShouldEqual, 0) - test.That(t, res3.(*mockFake2).closeCount, test.ShouldEqual, 0) -} - -func TestReconfigureModelSwitchErr(t *testing.T) { - logger := logging.NewTestLogger(t) - - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - mockNamed := func(name string) resource.Name { - return resource.NewName(mockAPI, name) - } - modelName1 := utils.RandomAlphaString(5) - model1 := resource.DefaultModelFamily.WithModel(modelName1) - - newCount := 0 - resource.RegisterComponent(mockAPI, model1, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - newCount++ - return &mockFake{Named: conf.ResourceName().AsNamed()}, nil - }, - }) - - defer func() { - resource.Deregister(mockAPI, model1) - }() - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - }, - }, - } - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - test.That(t, newCount, test.ShouldEqual, 1) - - name1 := mockNamed("one") - res1, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 0) - - modelName2 := utils.RandomAlphaString(5) - model2 := resource.DefaultModelFamily.WithModel(modelName2) - - newCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model2, - API: mockAPI, - }, - }, - } - r.Reconfigure(ctx, newCfg) - test.That(t, newCount, test.ShouldEqual, 1) - - _, err = r.ResourceByName(name1) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 1) - - r.Reconfigure(ctx, cfg) - test.That(t, newCount, test.ShouldEqual, 2) - - res2, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldNotEqual, res1) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 1) - test.That(t, res2.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res2.(*mockFake).closeCount, test.ShouldEqual, 0) -} - -func TestReconfigureRename(t *testing.T) { - logger := logging.NewTestLogger(t) - - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - mockNamed := func(name string) resource.Name { - return resource.NewName(mockAPI, name) - } - modelName1 := utils.RandomAlphaString(5) - model1 := resource.DefaultModelFamily.WithModel(modelName1) - - var logicalClock atomic.Int64 - - resource.RegisterComponent(mockAPI, model1, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return &mockFake{ - Named: conf.ResourceName().AsNamed(), - logicalClock: &logicalClock, - createdAt: int(logicalClock.Add(1)), - }, nil - }, - }) - defer func() { - resource.Deregister(mockAPI, model1) - }() - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - }, - }, - } - - ctx := context.Background() - - r := setupLocalRobot(t, ctx, cfg, logger) - - name1 := mockNamed("one") - name2 := mockNamed("two") - res1, err := r.ResourceByName(name1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).createdAt, test.ShouldEqual, 1) - - newCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "two", - Model: model1, - API: mockAPI, - ConvertedAttributes: resource.NoNativeConfig{}, - }, - }, - } - - r.Reconfigure(ctx, newCfg) - res2, err := r.ResourceByName(name2) - test.That(t, err, test.ShouldBeNil) - test.That(t, res2, test.ShouldNotEqual, res1) - test.That(t, res1.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res1.(*mockFake).closeCount, test.ShouldEqual, 1) - test.That(t, res1.(*mockFake).closedAt, test.ShouldEqual, 2) - test.That(t, res2.(*mockFake).createdAt, test.ShouldEqual, 3) - test.That(t, res2.(*mockFake).reconfCount, test.ShouldEqual, 0) - test.That(t, res2.(*mockFake).closeCount, test.ShouldEqual, 0) -} - -// tests that the resource configuration timeout is passed into each resource constructor. -func TestResourceConstructTimeout(t *testing.T) { - logger := logging.NewTestLogger(t) - - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - modelName1 := utils.RandomAlphaString(5) - model1 := resource.DefaultModelFamily.WithModel(modelName1) - - var timeout time.Duration - - resource.RegisterComponent(mockAPI, model1, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - deadline, ok := ctx.Deadline() - test.That(t, ok, test.ShouldBeTrue) - test.That(t, time.Now().Add(timeout), test.ShouldHappenOnOrAfter, deadline) - return &mockFake{Named: conf.ResourceName().AsNamed()}, nil - }, - }) - defer func() { - resource.Deregister(mockAPI, model1) - }() - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - }, - { - Name: "two", - Model: model1, - API: mockAPI, - }, - }, - } - t.Run("new", func(t *testing.T) { - timeout = 50 * time.Millisecond - test.That(t, os.Setenv(rutils.ResourceConfigurationTimeoutEnvVar, timeout.String()), - test.ShouldBeNil) - defer func() { - test.That(t, os.Unsetenv(rutils.ResourceConfigurationTimeoutEnvVar), - test.ShouldBeNil) - }() - - r := setupLocalRobot(t, context.Background(), cfg, logger) - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }) - t.Run("reconfigure", func(t *testing.T) { - timeout = rutils.DefaultResourceConfigurationTimeout - r := setupLocalRobot(t, context.Background(), cfg, logger) - - timeout = 200 * time.Millisecond - test.That(t, os.Setenv(rutils.ResourceConfigurationTimeoutEnvVar, timeout.String()), - test.ShouldBeNil) - defer func() { - test.That(t, os.Unsetenv(rutils.ResourceConfigurationTimeoutEnvVar), - test.ShouldBeNil) - }() - - newCfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - }, - { - Name: "two", - Model: model1, - API: mockAPI, - }, - { - Name: "three", - Model: model1, - API: mockAPI, - }, - }, - } - - r.Reconfigure(context.Background(), newCfg) - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - }) -} - -// tests that on context cancellation, the resource re/configuration loop never gets inside the resource constructor. -func TestResourceConstructCtxCancel(t *testing.T) { - logger := logging.NewTestLogger(t) - - contructCount := 0 - var wg sync.WaitGroup - - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - modelName1 := utils.RandomAlphaString(5) - model1 := resource.DefaultModelFamily.WithModel(modelName1) - - type cancelFunc struct { - c context.CancelFunc - } - var cFunc cancelFunc - - resource.RegisterComponent(mockAPI, model1, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - contructCount++ - wg.Add(1) - defer wg.Done() - cFunc.c() - <-ctx.Done() - return &mockFake{Named: conf.ResourceName().AsNamed()}, nil - }, - }) - defer func() { - resource.Deregister(mockAPI, model1) - }() - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "one", - Model: model1, - API: mockAPI, - }, - { - Name: "two", - Model: model1, - API: mockAPI, - }, - }, - } - t.Run("new", func(t *testing.T) { - contructCount = 0 - ctxWithCancel, cancel := context.WithCancel(context.Background()) - cFunc.c = cancel - r := setupLocalRobot(t, ctxWithCancel, cfg, logger) - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - - wg.Wait() - test.That(t, contructCount, test.ShouldEqual, 1) - }) - t.Run("reconfigure", func(t *testing.T) { - contructCount = 0 - r := setupLocalRobot(t, context.Background(), &config.Config{}, logger) - test.That(t, contructCount, test.ShouldEqual, 0) - - ctxWithCancel, cancel := context.WithCancel(context.Background()) - cFunc.c = cancel - r.Reconfigure(ctxWithCancel, cfg) - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - - wg.Wait() - test.That(t, contructCount, test.ShouldEqual, 1) - }) -} - -func TestResourceCloseNoHang(t *testing.T) { - logger := logging.NewTestLogger(t) - - mockAPI := resource.APINamespaceRDK.WithComponentType("mock") - modelName1 := utils.RandomAlphaString(5) - model1 := resource.DefaultModelFamily.WithModel(modelName1) - - mf := &mockFake{Named: resource.NewName(mockAPI, "mock").AsNamed()} - resource.RegisterComponent(mockAPI, model1, resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return mf, nil - }, - }) - defer func() { - resource.Deregister(mockAPI, model1) - }() - - cfg := &config.Config{ - Components: []resource.Config{ - { - Name: "mock", - Model: model1, - API: mockAPI, - }, - }, - } - r := setupLocalRobot(t, context.Background(), cfg, logger) - - test.That(t, r.Close(context.Background()), test.ShouldBeNil) - test.That(t, mf.closeCtxDeadline, test.ShouldNotBeNil) - test.That(t, time.Now().Add(resourceCloseTimeout), test.ShouldHappenOnOrAfter, mf.closeCtxDeadline) -} - -type mockFake struct { - resource.Named - createdAt int - reconfCount int - reconfiguredAt int64 - failCount int - shouldRebuild bool - closedAt int64 - closeCount int - closeCtxDeadline time.Time - logicalClock *atomic.Int64 -} - -type mockFakeConfig struct { - InferredDep []string `json:"inferred_dep"` - ShouldFail bool `json:"should_fail"` - ShouldFailReconfigure int `json:"should_fail_reconfigure"` - Blah int `json:"blah"` -} - -func (m *mockFake) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - if m.logicalClock != nil { - m.reconfiguredAt = m.logicalClock.Add(1) - } - if m.shouldRebuild { - return resource.NewMustRebuildError(conf.ResourceName()) - } - if c, err := resource.NativeConfig[*mockFakeConfig](conf); err == nil && m.failCount == 0 && c.ShouldFailReconfigure != 0 { - m.failCount = c.ShouldFailReconfigure - } - if m.failCount != 0 { - m.failCount-- - return errors.Errorf("failed to reconfigure (left %d)", m.failCount) - } - m.reconfCount++ - return nil -} - -func (m *mockFake) Close(ctx context.Context) error { - if m.logicalClock != nil { - m.closedAt = m.logicalClock.Add(1) - } - m.closeCount++ - if dl, exists := ctx.Deadline(); exists { - m.closeCtxDeadline = dl - } - return nil -} - -func (m *mockFakeConfig) Validate(path string) ([]string, error) { - depOut := []string{} - depOut = append(depOut, m.InferredDep...) - return depOut, nil -} - -type mockFake2 struct { - resource.Named - reconfCount int - closeCount int -} - -func (m *mockFake2) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - m.reconfCount++ - return errors.New("oh no") -} - -func (m *mockFake2) Close(ctx context.Context) error { - m.closeCount++ - return nil -} diff --git a/robot/impl/verify_main_test.go b/robot/impl/verify_main_test.go deleted file mode 100644 index 6407a4b588b..00000000000 --- a/robot/impl/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package robotimpl - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/robot/packages/cloud_package_manager_test.go b/robot/packages/cloud_package_manager_test.go deleted file mode 100644 index a9da8a09318..00000000000 --- a/robot/packages/cloud_package_manager_test.go +++ /dev/null @@ -1,581 +0,0 @@ -package packages - -import ( - "archive/tar" - "compress/gzip" - "context" - "errors" - "os" - "path" - "path/filepath" - "slices" - "testing" - "time" - - pb "go.viam.com/api/app/packages/v1" - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - putils "go.viam.com/rdk/robot/packages/testutils" -) - -func newPackageManager(t *testing.T, - client pb.PackageServiceClient, - fakeServer *putils.FakePackagesClientAndGCSServer, logger logging.Logger, packageDir string, -) (string, ManagerSyncer) { - fakeServer.Clear() - - if packageDir == "" { - packageDir = t.TempDir() - } - logger.Info(packageDir) - - testCloudConfig := &config.Cloud{ - ID: "some-id", - Secret: "some-secret", - } - - pm, err := NewCloudManager(testCloudConfig, client, packageDir, logger) - test.That(t, err, test.ShouldBeNil) - - return packageDir, pm -} - -func TestCloud(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - fakeServer, err := putils.NewFakePackageServer(ctx, logger) - test.That(t, err, test.ShouldBeNil) - defer utils.UncheckedErrorFunc(fakeServer.Shutdown) - - client, conn, err := fakeServer.Client(ctx) - test.That(t, err, test.ShouldBeNil) - defer utils.UncheckedErrorFunc(conn.Close) - - t.Run("missing package on server", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - input := []config.PackageConfig{{Name: "some-name", Package: "org1/test-model", Version: "v1", Type: "ml_model"}} - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failed loading package url") - - // validate dir should be empty - validatePackageDir(t, packageDir, []config.PackageConfig{}) - }) - - t.Run("valid packages on server", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - input := []config.PackageConfig{ - {Name: "some-name", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - {Name: "some-name-2", Package: "org1/test-model", Version: "v2", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - // validate dir should be empty - validatePackageDir(t, packageDir, input) - }) - - t.Run("sync continues on error", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - input := []config.PackageConfig{ - {Name: "some-name", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - {Name: "some-name-2", Package: "org1/test-model", Version: "v2", Type: "ml_model"}, - } - fakeServer.StorePackage(input[1]) // only store second - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failed loading package url for org1/test-model:v1") - - // validate dir should be empty - validatePackageDir(t, packageDir, []config.PackageConfig{input[1]}) - }) - - t.Run("sync re-downloads on error", func(t *testing.T) { - pkg := config.PackageConfig{Name: "some-name", Package: "org1/test-model", Version: "v1", Type: "module"} - - // create a package manager and Sync to download the package - _, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - pkgDir := pkg.LocalDataDirectory(pm.(*cloudManager).packagesDir) - module := config.Module{ExePath: pkgDir + "/some-text.txt"} - fakeServer.StorePackage(pkg) - err = pm.Sync(ctx, []config.PackageConfig{pkg}, []config.Module{module}) - test.That(t, err, test.ShouldBeNil) - - // grab ModTime for comparison - info, err := os.Stat(module.ExePath) - test.That(t, err, test.ShouldBeNil) - modTime := info.ModTime() - - // close previous package manager, make sure new PM *doesn't* re-download with intact ExePath - pm.Close(ctx) - _, pm = newPackageManager(t, client, fakeServer, logger, pm.(*cloudManager).packagesDir) - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - fakeServer.StorePackage(pkg) - // sleep to make super sure modification time increments - time.Sleep(10 * time.Millisecond) - err = pm.Sync(ctx, []config.PackageConfig{pkg}, []config.Module{module}) - test.That(t, err, test.ShouldBeNil) - info, err = os.Stat(module.ExePath) - test.That(t, err, test.ShouldBeNil) - test.That(t, info.ModTime(), test.ShouldEqual, modTime) - - // close previous package manager, then corrupt the module entrypoint file - pm.Close(ctx) - info, err = os.Stat(module.ExePath) - test.That(t, err, test.ShouldBeNil) - test.That(t, info.Size(), test.ShouldNotBeZeroValue) - err = os.Remove(module.ExePath) - test.That(t, err, test.ShouldBeNil) - - // create fresh packageManager to simulate a reboot, i.e. so the system doesn't think the module is already managed. - _, pm = newPackageManager(t, client, fakeServer, logger, pm.(*cloudManager).packagesDir) - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - fakeServer.StorePackage(pkg) - err = pm.Sync(ctx, []config.PackageConfig{pkg}, []config.Module{module}) - test.That(t, err, test.ShouldBeNil) - - // test that file exists, is non-empty, and modTime is different - info, err = os.Stat(module.ExePath) - test.That(t, err, test.ShouldBeNil) - test.That(t, info.Size(), test.ShouldNotBeZeroValue) - test.That(t, info.ModTime(), test.ShouldNotEqual, modTime) - }) - - t.Run("sync and clean should remove file", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - input := []config.PackageConfig{ - {Name: "some-name-1", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - {Name: "some-name-2", Package: "org1/test-model", Version: "v2", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - // first sync - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - // validate dir should be empty - validatePackageDir(t, packageDir, input) - - // second sync - err = pm.Sync(ctx, []config.PackageConfig{input[1]}, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - validatePackageDir(t, packageDir, input) - - // clean dir - err = pm.Cleanup(ctx) - test.That(t, err, test.ShouldBeNil) - - // validate dir should be empty - validatePackageDir(t, packageDir, []config.PackageConfig{input[1]}) - }) - - t.Run("second sync should not call http server", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - input := []config.PackageConfig{ - {Name: "some-name", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - {Name: "some-name-2", Package: "org1/test-model", Version: "v2", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - getCount, downloadCount := fakeServer.RequestCounts() - test.That(t, getCount, test.ShouldEqual, 2) - test.That(t, downloadCount, test.ShouldEqual, 2) - - // validate dir should be empty - validatePackageDir(t, packageDir, input) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - // validate dir should be empty - validatePackageDir(t, packageDir, input) - - getCount, downloadCount = fakeServer.RequestCounts() - test.That(t, getCount, test.ShouldEqual, 2) - test.That(t, downloadCount, test.ShouldEqual, 2) - }) - - t.Run("upgrade version", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - input := []config.PackageConfig{ - {Name: "some-name-1", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - validatePackageDir(t, packageDir, input) - - input[0].Version = "v2" - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - err = pm.Cleanup(ctx) - test.That(t, err, test.ShouldBeNil) - - validatePackageDir(t, packageDir, input) - }) - - t.Run("invalid checksum", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - fakeServer.SetInvalidChecksum(true) - - input := []config.PackageConfig{ - {Name: "some-name-1", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failed to decode") - - err = pm.Cleanup(ctx) - test.That(t, err, test.ShouldBeNil) - - validatePackageDir(t, packageDir, []config.PackageConfig{}) - }) - - t.Run("leading zeroes checksum", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - fakeServer.SetChecksumWithLeadingZeroes(true) - - input := []config.PackageConfig{ - {Name: "some-name", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - {Name: "some-name-2", Package: "org1/test-model", Version: "v2", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - // validate dir should be empty - validatePackageDir(t, packageDir, input) - }) - - t.Run("invalid gcs download", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - fakeServer.SetInvalidHTTPRes(true) - - input := []config.PackageConfig{ - {Name: "some-name-1", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid status code 500") - - err = pm.Cleanup(ctx) - test.That(t, err, test.ShouldBeNil) - - validatePackageDir(t, packageDir, []config.PackageConfig{}) - }) - - t.Run("invalid tar", func(t *testing.T) { - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - fakeServer.SetInvalidTar(true) - - input := []config.PackageConfig{ - {Name: "some-name-1", Package: "org1/test-model", Version: "v1", Type: "ml_model"}, - } - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "unexpected EOF") - - err = pm.Cleanup(ctx) - test.That(t, err, test.ShouldBeNil) - - validatePackageDir(t, packageDir, []config.PackageConfig{}) - }) -} - -func validatePackageDir(t *testing.T, dir string, input []config.PackageConfig) { - // t.Helper() - - // create maps to make lookups easier. - bySanitizedName := make(map[string]*config.PackageConfig) - byLogicalName := make(map[string]*config.PackageConfig) - byType := make(map[string][]string) - for _, pI := range input { - p := pI - bySanitizedName[p.SanitizedName()] = &p - byLogicalName[p.Name] = &p - pType := string(p.Type) - byType[pType] = append(byType[pType], p.SanitizedName()) - } - - // check all known packages exist and are linked to the correct package dir. - for _, p := range input { - logicalPath := filepath.Join(dir, p.Name) - dataPath := filepath.Join(dir, "data", string(p.Type), p.SanitizedName()) - - info, err := os.Stat(logicalPath) - test.That(t, err, test.ShouldBeNil) - - if !isSymLink(t, logicalPath) { - t.Fatalf("found non symlink file in package dir %s at %s", info.Name(), logicalPath) - } - - linkTarget, err := os.Readlink(logicalPath) - test.That(t, err, test.ShouldBeNil) - - test.That(t, linkTarget, test.ShouldEqual, dataPath) - - info, err = os.Stat(dataPath) - test.That(t, err, test.ShouldBeNil) - test.That(t, info.IsDir(), test.ShouldBeTrue) - } - - // find any dangling files in the package dir or data sub dir - // packageDir will contain either symlinks to the packages or the data directory. - files, err := os.ReadDir(dir) - test.That(t, err, test.ShouldBeNil) - - for _, f := range files { - if isSymLink(t, filepath.Join(dir, f.Name())) { - if _, ok := byLogicalName[f.Name()]; !ok { - t.Fatalf("found unknown symlink in package dir %s", f.Name()) - } - continue - } - - // skip over any directories including the data - if f.IsDir() && f.Name() == "data" { - continue - } - - t.Fatalf("found unknown file in package dir %s", f.Name()) - } - - typeFolders, err := os.ReadDir(filepath.Join(dir, "data")) - test.That(t, err, test.ShouldBeNil) - - for _, typeFile := range typeFolders { - expectedPackages, ok := byType[typeFile.Name()] - if !ok { - t.Errorf("found unknown file in package data dir %s", typeFile.Name()) - } - foundFiles, err := os.ReadDir(filepath.Join(dir, "data", typeFile.Name())) - test.That(t, err, test.ShouldBeNil) - for _, packageFile := range foundFiles { - if !slices.Contains(expectedPackages, packageFile.Name()) { - t.Errorf("found unknown file in package %s dir %s", typeFile.Name(), packageFile.Name()) - } - } - } -} - -func TestPackageRefs(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - fakeServer, err := putils.NewFakePackageServer(ctx, logger) - test.That(t, err, test.ShouldBeNil) - defer utils.UncheckedErrorFunc(fakeServer.Shutdown) - - client, conn, err := fakeServer.Client(ctx) - test.That(t, err, test.ShouldBeNil) - defer utils.UncheckedErrorFunc(conn.Close) - - packageDir, pm := newPackageManager(t, client, fakeServer, logger, "") - defer utils.UncheckedErrorFunc(func() error { return pm.Close(context.Background()) }) - - input := []config.PackageConfig{{Name: "some-name", Package: "org1/test-model", Version: "v1", Type: "ml_model"}} - fakeServer.StorePackage(input...) - - err = pm.Sync(ctx, input, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - - t.Run("PackagePath", func(t *testing.T) { - t.Run("valid package", func(t *testing.T) { - pPath, err := pm.PackagePath("some-name") - test.That(t, err, test.ShouldBeNil) - test.That(t, pPath, test.ShouldEqual, input[0].LocalDataDirectory(packageDir)) - putils.ValidateContentsOfPPackage(t, pPath) - }) - - t.Run("missing package", func(t *testing.T) { - _, err = pm.PackagePath("not-valid") - test.That(t, err, test.ShouldEqual, ErrPackageMissing) - }) - - t.Run("missing package for empty", func(t *testing.T) { - _, err = pm.PackagePath("") - test.That(t, err, test.ShouldEqual, ErrPackageMissing) - }) - }) -} - -func isSymLink(t *testing.T, file string) bool { - fileInfo, err := os.Lstat(file) - test.That(t, err, test.ShouldBeNil) - - return fileInfo.Mode()&os.ModeSymlink != 0 -} - -func TestSafeJoin(t *testing.T) { - parentDir := "/some/parent" - - validate := func(in, expectedOut string, expectedErr error) { - t.Helper() - - out, err := safeJoin(parentDir, in) - if expectedErr == nil { - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldEqual, expectedOut) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - } - } - - validate("sub/dir", "/some/parent/sub/dir", nil) - validate("/other/parent", "/some/parent/other/parent", nil) - validate("../../../root", "", errors.New("unsafe path join")) -} - -func TestSafeLink(t *testing.T) { - parentDir := "/some/parent" - - validate := func(in, expectedOut string, expectedErr error) { - t.Helper() - out, err := safeLink(parentDir, in) - if expectedErr == nil { - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldEqual, expectedOut) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, expectedErr.Error()) - } - } - - validate("sub/dir", "sub/dir", nil) - validate("sub/../dir", "sub/../dir", nil) - validate("sub/../../dir", "", errors.New("unsafe path join")) - validate("/root", "", errors.New("unsafe path link")) -} - -func TestMissingDirEntry(t *testing.T) { - file, err := os.CreateTemp("", "missing-dir-entry") - test.That(t, err, test.ShouldBeNil) - head := tar.Header{ - Name: "subdir/file.md", - Size: 1, - Mode: 0o600, - Uid: 1000, - Gid: 1000, - } - gzipWriter := gzip.NewWriter(file) - writer := tar.NewWriter(gzipWriter) - writer.WriteHeader(&head) - writer.Write([]byte("x")) - writer.Close() - gzipWriter.Close() - file.Close() - defer os.Remove(file.Name()) - dest, err := os.MkdirTemp("", "missing-dir-entry") - defer os.RemoveAll(dest) - test.That(t, err, test.ShouldBeNil) - // The inner MkdirAll in unpackFile will fail with 'permission denied' if we - // create the subdirectory with the wrong permissions. - err = unpackFile(context.Background(), file.Name(), dest) - test.That(t, err, test.ShouldBeNil) -} - -func TestTrimLeadingZeroes(t *testing.T) { - testCases := []struct { - name string - input []byte - expected []byte - }{ - { - name: "Empty slice", - input: []byte{}, - expected: []byte{}, - }, - { - name: "Single zero byte", - input: []byte{0x00}, - expected: []byte{0x00}, - }, - { - name: "Leading zeroes trimmed", - input: []byte{0x00, 0x00, 0x03, 0x04, 0x05}, - expected: []byte{0x03, 0x04, 0x05}, - }, - { - name: "All zero bytes", - input: []byte{0x00, 0x00, 0x00}, - expected: []byte{0x00}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - test.That(t, trimLeadingZeroes(tc.input), test.ShouldResemble, tc.expected) - }) - } -} - -func TestCheckNonemptyPaths(t *testing.T) { - dataDir, err := os.MkdirTemp("", "nonempty-paths") - defer os.RemoveAll(dataDir) - test.That(t, err, test.ShouldBeNil) - logger := logging.NewTestLogger(t) - - // path missing - test.That(t, checkNonemptyPaths("packageName", logger, []string{dataDir + "/hello"}), test.ShouldBeFalse) - - // file empty - fullPath := path.Join(dataDir, "hello") - _, err = os.Create(fullPath) - test.That(t, err, test.ShouldBeNil) - test.That(t, checkNonemptyPaths("packageName", logger, []string{dataDir + "/hello"}), test.ShouldBeFalse) - - // file exists and is non-empty - err = os.WriteFile(fullPath, []byte("hello"), 0) - test.That(t, err, test.ShouldBeNil) - test.That(t, checkNonemptyPaths("packageName", logger, []string{dataDir + "/hello"}), test.ShouldBeTrue) - - // file is a symlink - err = os.Symlink(fullPath, path.Join(dataDir, "sym-hello")) - test.That(t, err, test.ShouldBeNil) - test.That(t, checkNonemptyPaths("packageName", logger, []string{dataDir + "/sym-hello"}), test.ShouldBeTrue) -} diff --git a/robot/packages/deferred_package_manager_test.go b/robot/packages/deferred_package_manager_test.go deleted file mode 100644 index 9c1237298eb..00000000000 --- a/robot/packages/deferred_package_manager_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package packages - -import ( - "context" - "os" - "testing" - - "github.com/pkg/errors" - pb "go.viam.com/api/app/packages/v1" - "go.viam.com/test" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - putils "go.viam.com/rdk/robot/packages/testutils" -) - -func TestDeferredPackageManager(t *testing.T) { - type mockChanVal struct { - client pb.PackageServiceClient - err error - } - - // bag of random test infra - type testBag struct { - pm *deferredPackageManager - ctx context.Context - // mockChan is used to populate the return value of establishConnection() - mockChan chan mockChanVal - // fake gcs stuff - client pb.PackageServiceClient - fakeServer *putils.FakePackagesClientAndGCSServer - packagesDir string - // cleanup fake gcs - teardown func() - } - - setup := func(t *testing.T) testBag { - t.Helper() - ctx := context.Background() - logger := logging.NewTestLogger(t) - - fakeServer, err := putils.NewFakePackageServer(ctx, logger) - test.That(t, err, test.ShouldBeNil) - - client, conn, err := fakeServer.Client(ctx) - test.That(t, err, test.ShouldBeNil) - - teardown := func() { - conn.Close() - fakeServer.Shutdown() - } - cloudConfig := &config.Cloud{ - ID: "some-id", - Secret: "some-secret", - } - packagesDir := t.TempDir() - mockChan := make(chan mockChanVal, 1) - - pm := NewDeferredPackageManager( - ctx, - func(c context.Context) (pb.PackageServiceClient, error) { - v := <-mockChan - return v.client, v.err - }, - cloudConfig, - packagesDir, - logger, - ).(*deferredPackageManager) - - return testBag{ - pm, - ctx, - mockChan, - client, - fakeServer, - packagesDir, - teardown, - } - } - - pkgA := config.PackageConfig{ - Name: "some-name", - Package: "org1/test-model", - Version: "v1", - Type: "ml_model", - } - - pkgB := config.PackageConfig{ - Name: "some-name-2", - Package: "org1/test-model-2", - Version: "v2", - Type: "module", - } - - t.Run("getManagerForSync async", func(t *testing.T) { - bag := setup(t) - defer bag.teardown() - - // Assert that the cloud manager is nil initially - mgr, err := bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{}) - test.That(t, err, test.ShouldBeNil) - _, isNoop := mgr.(*noopManager) - test.That(t, isNoop, test.ShouldBeTrue) - // send a msg on the chan indicating a connection - bag.mockChan <- mockChanVal{client: bag.client, err: nil} - // this will wait until we start the new cloud manager - mgr, err = bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{}) - test.That(t, err, test.ShouldBeNil) - _, isCloud := mgr.(*cloudManager) - test.That(t, isCloud, test.ShouldBeTrue) - // test that we have cached that cloud_manager (in the async case) - test.That(t, bag.pm.cloudManager, test.ShouldNotBeNil) - _, err = bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{}) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("getManagerForSync async will keep trying", func(t *testing.T) { - bag := setup(t) - defer bag.teardown() - - // we know that it is running establishConnection because it is pulling - // from the 1-capacity channel - // the err will still be nil because it is returning the noop manager and starting - // the goroutine - bag.mockChan <- mockChanVal{client: nil, err: errors.New("foo")} - _, err := bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{}) - test.That(t, err, test.ShouldBeNil) - bag.mockChan <- mockChanVal{client: nil, err: errors.New("foo")} - _, err = bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{}) - test.That(t, err, test.ShouldBeNil) - bag.mockChan <- mockChanVal{client: nil, err: errors.New("foo")} - _, err = bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{}) - test.That(t, err, test.ShouldBeNil) - bag.mockChan <- mockChanVal{client: nil, err: errors.New("foo")} - _, err = bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{}) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("getManagerForSync sync", func(t *testing.T) { - bag := setup(t) - defer bag.teardown() - - bag.mockChan <- mockChanVal{client: bag.client, err: nil} - // Assert that missing pkgs cause sync loading - mgr, err := bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{pkgA}) - test.That(t, err, test.ShouldBeNil) - - _, isCloud := mgr.(*cloudManager) - test.That(t, isCloud, test.ShouldBeTrue) - - // test that we have cached that cloud_manager (in the sync case) - test.That(t, bag.pm.cloudManager, test.ShouldNotBeNil) - _, err = bag.pm.getManagerForSync(bag.ctx, []config.PackageConfig{pkgA}) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("isMissingPackages", func(t *testing.T) { - bag := setup(t) - defer bag.teardown() - - // Create a package config - packages := []config.PackageConfig{ - pkgA, - } - // Assert that the package is missing initially - test.That(t, bag.pm.isMissingPackages(packages), test.ShouldBeTrue) - // Create a directory for the package - err := os.MkdirAll(packages[0].LocalDataDirectory(bag.packagesDir), os.ModePerm) - test.That(t, err, test.ShouldBeNil) - // Assert that the package is not missing after creating the directory - test.That(t, bag.pm.isMissingPackages(packages), test.ShouldBeFalse) - }) - - t.Run("Sync + cleanup", func(t *testing.T) { - bag := setup(t) - defer bag.teardown() - - err := bag.pm.Sync(bag.ctx, []config.PackageConfig{}, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - _, isNoop := bag.pm.lastSyncedManager.(*noopManager) - test.That(t, isNoop, test.ShouldBeTrue) - - // send a msg on the chan indicating a connection - bag.mockChan <- mockChanVal{client: bag.client, err: nil} - bag.fakeServer.StorePackage(pkgA) - bag.fakeServer.StorePackage(pkgB) - // this will wait until we start the new cloud manager - err = bag.pm.Sync(bag.ctx, []config.PackageConfig{pkgA}, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - _, isCloud := bag.pm.lastSyncedManager.(*cloudManager) - test.That(t, isCloud, test.ShouldBeTrue) - // Assert that the package exists in the file system - _, err = os.Stat(pkgA.LocalDataDirectory(bag.packagesDir)) - test.That(t, err, test.ShouldBeNil) - - // cleanup wont clean up the package yet so it should still exist - err = bag.pm.Cleanup(bag.ctx) - test.That(t, err, test.ShouldBeNil) - _, err = os.Stat(pkgA.LocalDataDirectory(bag.packagesDir)) - test.That(t, err, test.ShouldBeNil) - - // sync over to pkgB and cleanup - err = bag.pm.Sync(bag.ctx, []config.PackageConfig{pkgB}, []config.Module{}) - test.That(t, err, test.ShouldBeNil) - err = bag.pm.Cleanup(bag.ctx) - test.That(t, err, test.ShouldBeNil) - - // pkgA should be cleaned up and pkgB should exist - _, err = os.Stat(pkgA.LocalDataDirectory(bag.packagesDir)) - test.That(t, err, test.ShouldNotBeNil) - _, err = os.Stat(pkgB.LocalDataDirectory(bag.packagesDir)) - test.That(t, err, test.ShouldBeNil) - }) -} diff --git a/robot/packages/verify_main_test.go b/robot/packages/verify_main_test.go deleted file mode 100644 index 481cf16079e..00000000000 --- a/robot/packages/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package packages - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/robot/robot_test.go b/robot/robot_test.go deleted file mode 100644 index 0b196cbd42a..00000000000 --- a/robot/robot_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package robot_test - -import ( - "testing" - - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/config" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - button1 = resource.NewName(resource.APINamespaceRDK.WithComponentType("button"), "arm1") - - armNames = []resource.Name{arm.Named("arm1"), arm.Named("arm2"), arm.Named("remote:arm1")} - buttonNames = []resource.Name{button1} - sensorNames = []resource.Name{sensor.Named("sensor1")} -) - -var hereRes = testutils.NewUnimplementedResource(generic.Named("here")) - -func setupInjectRobot() *inject.Robot { - arm3 := inject.NewArm("arm3") - r := &inject.Robot{} - r.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - if name.Name == "arm2" { - return nil, resource.NewNotFoundError(name) - } - if name.Name == "arm3" { - return arm3, nil - } - - return hereRes, nil - } - r.ResourceNamesFunc = func() []resource.Name { - return testutils.ConcatResourceNames( - armNames, - buttonNames, - sensorNames, - ) - } - - return r -} - -func TestAllResourcesByName(t *testing.T) { - r := setupInjectRobot() - - resources := robot.AllResourcesByName(r, "arm1") - test.That(t, resources, test.ShouldResemble, []resource.Resource{hereRes, hereRes}) - - resources = robot.AllResourcesByName(r, "remote:arm1") - test.That(t, resources, test.ShouldResemble, []resource.Resource{hereRes}) - - test.That(t, func() { robot.AllResourcesByName(r, "arm2") }, test.ShouldPanic) - - resources = robot.AllResourcesByName(r, "sensor1") - test.That(t, resources, test.ShouldResemble, []resource.Resource{hereRes}) - - resources = robot.AllResourcesByName(r, "blah") - test.That(t, resources, test.ShouldBeEmpty) -} - -func TestNamesFromRobot(t *testing.T) { - r := setupInjectRobot() - - names := robot.NamesByAPI(r, gantry.API) - test.That(t, names, test.ShouldBeEmpty) - - names = robot.NamesByAPI(r, sensor.API) - test.That(t, utils.NewStringSet(names...), test.ShouldResemble, utils.NewStringSet(testutils.ExtractNames(sensorNames...)...)) - - names = robot.NamesByAPI(r, arm.API) - test.That(t, utils.NewStringSet(names...), test.ShouldResemble, utils.NewStringSet(testutils.ExtractNames(armNames...)...)) -} - -func TestResourceFromRobot(t *testing.T) { - r := setupInjectRobot() - - res, err := robot.ResourceFromRobot[arm.Arm](r, arm.Named("arm3")) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldNotBeNil) - - res, err = robot.ResourceFromRobot[arm.Arm](r, arm.Named("arm5")) - test.That(t, err, test.ShouldBeError, - resource.TypeError[arm.Arm](testutils.NewUnimplementedResource(generic.Named("foo")))) - test.That(t, res, test.ShouldBeNil) - - res, err = robot.ResourceFromRobot[arm.Arm](r, arm.Named("arm2")) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(arm.Named("arm2"))) - test.That(t, res, test.ShouldBeNil) -} - -func TestMatchesModule(t *testing.T) { - idRequest := robot.RestartModuleRequest{ModuleID: "matching-id"} - test.That(t, idRequest.MatchesModule(config.Module{ModuleID: "matching-id"}), test.ShouldBeTrue) - test.That(t, idRequest.MatchesModule(config.Module{Name: "matching-id"}), test.ShouldBeFalse) - test.That(t, idRequest.MatchesModule(config.Module{ModuleID: "other"}), test.ShouldBeFalse) - - nameRequest := robot.RestartModuleRequest{ModuleName: "matching-name"} - test.That(t, nameRequest.MatchesModule(config.Module{Name: "matching-name"}), test.ShouldBeTrue) - test.That(t, nameRequest.MatchesModule(config.Module{ModuleID: "matching-name"}), test.ShouldBeFalse) - test.That(t, nameRequest.MatchesModule(config.Module{Name: "other"}), test.ShouldBeFalse) -} diff --git a/robot/server/server_test.go b/robot/server/server_test.go deleted file mode 100644 index 7b8c591a509..00000000000 --- a/robot/server/server_test.go +++ /dev/null @@ -1,723 +0,0 @@ -package server_test - -import ( - "context" - "errors" - "fmt" - "math" - "net" - "sync" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - "github.com/jhump/protoreflect/grpcreflect" - commonpb "go.viam.com/api/common/v1" - armpb "go.viam.com/api/component/arm/v1" - pb "go.viam.com/api/robot/v1" - "go.viam.com/test" - vprotoutils "go.viam.com/utils/protoutils" - "google.golang.org/grpc" - "google.golang.org/grpc/peer" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/cloud" - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/robot/server" - "go.viam.com/rdk/session" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var emptyResources = &pb.ResourceNamesResponse{ - Resources: []*commonpb.ResourceName{}, -} - -var serverNewResource = arm.Named("") - -var serverOneResourceResponse = []*commonpb.ResourceName{ - { - Namespace: string(serverNewResource.API.Type.Namespace), - Type: serverNewResource.API.Type.Name, - Subtype: serverNewResource.API.SubtypeName, - Name: serverNewResource.Name, - }, -} - -func TestServer(t *testing.T) { - t.Run("Metadata", func(t *testing.T) { - injectRobot := &inject.Robot{} - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return []resource.Name{} } - server := server.New(injectRobot) - - resourceResp, err := server.ResourceNames(context.Background(), &pb.ResourceNamesRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resourceResp, test.ShouldResemble, emptyResources) - - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return []resource.Name{serverNewResource} } - - resourceResp, err = server.ResourceNames(context.Background(), &pb.ResourceNamesRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resourceResp.Resources, test.ShouldResemble, serverOneResourceResponse) - }) - - t.Run("GetCloudMetadata", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - req := pb.GetCloudMetadataRequest{} - injectRobot.CloudMetadataFunc = func(ctx context.Context) (cloud.Metadata, error) { - return cloud.Metadata{ - PrimaryOrgID: "the-primary-org", - LocationID: "the-location", - MachineID: "the-machine", - MachinePartID: "the-robot-part", - }, nil - } - resp, err := server.GetCloudMetadata(context.Background(), &req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.GetLocationId(), test.ShouldEqual, "the-location") - test.That(t, resp.GetPrimaryOrgId(), test.ShouldEqual, "the-primary-org") - test.That(t, resp.GetMachineId(), test.ShouldEqual, "the-machine") - test.That(t, resp.GetMachinePartId(), test.ShouldEqual, "the-robot-part") - }) - - t.Run("Discovery", func(t *testing.T) { - injectRobot := &inject.Robot{} - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return []resource.Name{} } - server := server.New(injectRobot) - - q := resource.DiscoveryQuery{arm.Named("arm").API, resource.DefaultModelFamily.WithModel("some-arm")} - disc := resource.Discovery{Query: q, Results: struct{}{}} - discoveries := []resource.Discovery{disc} - injectRobot.DiscoverComponentsFunc = func(ctx context.Context, keys []resource.DiscoveryQuery) ([]resource.Discovery, error) { - return discoveries, nil - } - - t.Run("full api and model", func(t *testing.T) { - req := &pb.DiscoverComponentsRequest{ - Queries: []*pb.DiscoveryQuery{{Subtype: q.API.String(), Model: q.Model.String()}}, - } - - resp, err := server.DiscoverComponents(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.Discovery), test.ShouldEqual, 1) - - observed := resp.Discovery[0].Results.AsMap() - expected := map[string]interface{}{} - expectedQ := &pb.DiscoveryQuery{Subtype: "rdk:component:arm", Model: "rdk:builtin:some-arm"} - test.That(t, resp.Discovery[0].Query, test.ShouldResemble, expectedQ) - test.That(t, observed, test.ShouldResemble, expected) - }) - t.Run("short api and model", func(t *testing.T) { - req := &pb.DiscoverComponentsRequest{ - Queries: []*pb.DiscoveryQuery{{Subtype: "arm", Model: "some-arm"}}, - } - - resp, err := server.DiscoverComponents(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.Discovery), test.ShouldEqual, 1) - - observed := resp.Discovery[0].Results.AsMap() - expected := map[string]interface{}{} - expectedQ := &pb.DiscoveryQuery{Subtype: "arm", Model: "some-arm"} - test.That(t, resp.Discovery[0].Query, test.ShouldResemble, expectedQ) - test.That(t, observed, test.ShouldResemble, expected) - }) - }) - - t.Run("ResourceRPCSubtypes", func(t *testing.T) { - injectRobot := &inject.Robot{} - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return nil } - server := server.New(injectRobot) - - typesResp, err := server.ResourceRPCSubtypes(context.Background(), &pb.ResourceRPCSubtypesRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, typesResp, test.ShouldResemble, &pb.ResourceRPCSubtypesResponse{ - ResourceRpcSubtypes: []*pb.ResourceRPCSubtype{}, - }) - - desc1, err := grpcreflect.LoadServiceDescriptor(&pb.RobotService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - desc2, err := grpcreflect.LoadServiceDescriptor(&armpb.ArmService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - otherAPI := resource.NewAPI("acme", "component", "wat") - respWith := []resource.RPCAPI{ - { - API: serverNewResource.API, - Desc: desc1, - }, - { - API: resource.NewAPI("acme", "component", "wat"), - Desc: desc2, - }, - } - - expectedResp := []*pb.ResourceRPCSubtype{ - { - Subtype: &commonpb.ResourceName{ - Namespace: string(serverNewResource.API.Type.Namespace), - Type: serverNewResource.API.Type.Name, - Subtype: serverNewResource.API.SubtypeName, - }, - ProtoService: desc1.GetFullyQualifiedName(), - }, - { - Subtype: &commonpb.ResourceName{ - Namespace: string(otherAPI.Type.Namespace), - Type: otherAPI.Type.Name, - Subtype: otherAPI.SubtypeName, - }, - ProtoService: desc2.GetFullyQualifiedName(), - }, - } - - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return respWith } - injectRobot.ResourceNamesFunc = func() []resource.Name { return nil } - - typesResp, err = server.ResourceRPCSubtypes(context.Background(), &pb.ResourceRPCSubtypesRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, typesResp.ResourceRpcSubtypes, test.ShouldResemble, expectedResp) - }) - - t.Run("GetOperations", func(t *testing.T) { - logger := logging.NewTestLogger(t) - injectRobot := &inject.Robot{} - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return nil } - injectRobot.LoggerFunc = func() logging.Logger { - return logger - } - server := server.New(injectRobot) - - opsResp, err := server.GetOperations(context.Background(), &pb.GetOperationsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, opsResp, test.ShouldResemble, &pb.GetOperationsResponse{ - Operations: []*pb.Operation{}, - }) - - sess1 := session.New(context.Background(), "owner1", time.Minute, nil) - sess2 := session.New(context.Background(), "owner2", time.Minute, nil) - sess1Ctx := session.ToContext(context.Background(), sess1) - sess2Ctx := session.ToContext(context.Background(), sess2) - op1, cancel1 := injectRobot.OperationManager().Create(context.Background(), "something1", nil) - defer cancel1() - - op2, cancel2 := injectRobot.OperationManager().Create(sess1Ctx, "something2", nil) - defer cancel2() - - op3, cancel3 := injectRobot.OperationManager().Create(sess2Ctx, "something3", nil) - defer cancel3() - - opsResp, err = server.GetOperations(context.Background(), &pb.GetOperationsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, opsResp.Operations, test.ShouldHaveLength, 3) - - for idx, searchFor := range []struct { - opID uuid.UUID - sessID uuid.UUID - }{ - {operation.Get(op1).ID, uuid.Nil}, - {operation.Get(op2).ID, sess1.ID()}, - {operation.Get(op3).ID, sess2.ID()}, - } { - t.Run(fmt.Sprintf("check op=%d", idx), func(t *testing.T) { - for _, op := range opsResp.Operations { - if op.Id == searchFor.opID.String() { - if searchFor.sessID == uuid.Nil { - test.That(t, op.SessionId, test.ShouldBeNil) - return - } - test.That(t, op.SessionId, test.ShouldNotBeNil) - test.That(t, *op.SessionId, test.ShouldEqual, searchFor.sessID.String()) - return - } - } - t.Fail() - }) - } - }) - - t.Run("GetSessions", func(t *testing.T) { - sessMgr := &sessionManager{} - injectRobot := &inject.Robot{SessMgr: sessMgr} - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceNamesFunc = func() []resource.Name { return nil } - server := server.New(injectRobot) - - sessResp, err := server.GetSessions(context.Background(), &pb.GetSessionsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, sessResp, test.ShouldResemble, &pb.GetSessionsResponse{ - Sessions: []*pb.Session{}, - }) - - ownerID1 := "owner1" - remoteAddr1 := &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 5} - ctx1 := peer.NewContext(context.Background(), &peer.Peer{ - Addr: remoteAddr1, - }) - remoteAddr2 := &net.TCPAddr{IP: net.IPv4(2, 2, 3, 8), Port: 9} - ctx2 := peer.NewContext(context.Background(), &peer.Peer{ - Addr: remoteAddr2, - }) - - ownerID2 := "owner2" - dur := time.Second - - sessions := []*session.Session{ - session.New(ctx1, ownerID1, dur, nil), - session.New(ctx2, ownerID2, dur, nil), - } - - sessMgr.mu.Lock() - sessMgr.sessions = sessions - sessMgr.mu.Unlock() - - sessResp, err = server.GetSessions(context.Background(), &pb.GetSessionsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, sessResp, test.ShouldResemble, &pb.GetSessionsResponse{ - Sessions: []*pb.Session{ - { - Id: sessions[0].ID().String(), - PeerConnectionInfo: sessions[0].PeerConnectionInfo(), - }, - { - Id: sessions[1].ID().String(), - PeerConnectionInfo: sessions[1].PeerConnectionInfo(), - }, - }, - }) - }) -} - -func TestServerFrameSystemConfig(t *testing.T) { - injectRobot := &inject.Robot{} - - o1 := &spatialmath.R4AA{Theta: math.Pi / 2, RZ: 1} - o1Cfg, err := spatialmath.NewOrientationConfig(o1) - test.That(t, err, test.ShouldBeNil) - - // test working config function - t.Run("test working config function", func(t *testing.T) { - l1 := &referenceframe.LinkConfig{ - ID: "frame1", - Parent: referenceframe.World, - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Orientation: o1Cfg, - Geometry: &spatialmath.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, - } - lif1, err := l1.ParseConfig() - test.That(t, err, test.ShouldBeNil) - l2 := &referenceframe.LinkConfig{ - ID: "frame2", - Parent: "frame1", - Translation: r3.Vector{X: 1, Y: 2, Z: 3}, - Geometry: &spatialmath.GeometryConfig{Type: "box", X: 1, Y: 2, Z: 1}, - } - lif2, err := l2.ParseConfig() - test.That(t, err, test.ShouldBeNil) - fsConfigs := []*referenceframe.FrameSystemPart{ - { - FrameConfig: lif1, - }, - { - FrameConfig: lif2, - }, - } - - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{Parts: fsConfigs}, nil - } - server := server.New(injectRobot) - req := &pb.FrameSystemConfigRequest{} - resp, err := server.FrameSystemConfig(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.FrameSystemConfigs), test.ShouldEqual, len(fsConfigs)) - test.That(t, resp.FrameSystemConfigs[0].Frame.ReferenceFrame, test.ShouldEqual, fsConfigs[0].FrameConfig.Name()) - test.That( - t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.ReferenceFrame, - test.ShouldEqual, - fsConfigs[0].FrameConfig.Parent(), - ) - test.That(t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.Pose.X, - test.ShouldAlmostEqual, - fsConfigs[0].FrameConfig.Pose().Point().X, - ) - test.That(t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.Pose.Y, - test.ShouldAlmostEqual, - fsConfigs[0].FrameConfig.Pose().Point().Y, - ) - test.That(t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.Pose.Z, - test.ShouldAlmostEqual, - fsConfigs[0].FrameConfig.Pose().Point().Z, - ) - pose := fsConfigs[0].FrameConfig.Pose() - test.That(t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.Pose.OX, - test.ShouldAlmostEqual, - pose.Orientation().OrientationVectorDegrees().OX, - ) - test.That(t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.Pose.OY, - test.ShouldAlmostEqual, - pose.Orientation().OrientationVectorDegrees().OY, - ) - test.That(t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.Pose.OZ, - test.ShouldAlmostEqual, - pose.Orientation().OrientationVectorDegrees().OZ, - ) - test.That(t, - resp.FrameSystemConfigs[0].Frame.PoseInObserverFrame.Pose.Theta, - test.ShouldAlmostEqual, - pose.Orientation().OrientationVectorDegrees().Theta, - ) - }) - - t.Run("test failing config function", func(t *testing.T) { - expectedErr := errors.New("failed to retrieve config") - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return nil, expectedErr - } - req := &pb.FrameSystemConfigRequest{} - server := server.New(injectRobot) - resp, err := server.FrameSystemConfig(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, expectedErr) - }) - - injectRobot = &inject.Robot{} - - t.Run("test failing on nonexistent server", func(t *testing.T) { - req := &pb.FrameSystemConfigRequest{} - server := server.New(injectRobot) - test.That(t, func() { server.FrameSystemConfig(context.Background(), req) }, test.ShouldPanic) - }) -} - -func TestServerGetStatus(t *testing.T) { - // Sample lastReconfigured times to be used across status tests. - lastReconfigured, err := time.Parse("2006-01-02 15:04:05", "1998-04-30 19:08:00") - test.That(t, err, test.ShouldBeNil) - lastReconfigured2, err := time.Parse("2006-01-02 15:04:05", "2011-11-11 00:00:00") - test.That(t, err, test.ShouldBeNil) - - t.Run("failed GetStatus", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - passedErr := errors.New("can't get status") - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - return nil, passedErr - } - _, err := server.GetStatus(context.Background(), &pb.GetStatusRequest{}) - test.That(t, err, test.ShouldBeError, passedErr) - }) - - t.Run("bad status response", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - aStatus := robot.Status{Name: arm.Named("arm"), Status: 1} - readings := []robot.Status{aStatus} - injectRobot.StatusFunc = func(ctx context.Context, status []resource.Name) ([]robot.Status, error) { - return readings, nil - } - req := &pb.GetStatusRequest{ - ResourceNames: []*commonpb.ResourceName{}, - } - - _, err := server.GetStatus(context.Background(), req) - test.That( - t, - err, - test.ShouldBeError, - errors.New( - "unable to convert interface 1 to a form acceptable to structpb.NewStruct: "+ - "data of type int and kind int not a struct or a map-like object", - ), - ) - }) - - t.Run("working one status", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - aStatus := robot.Status{ - Name: arm.Named("arm"), - LastReconfigured: lastReconfigured, - Status: struct{}{}, - } - readings := []robot.Status{aStatus} - expected := map[resource.Name]interface{}{ - aStatus.Name: map[string]interface{}{}, - } - expectedLR := map[resource.Name]time.Time{ - aStatus.Name: lastReconfigured, - } - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - test.That( - t, - testutils.NewResourceNameSet(resourceNames...), - test.ShouldResemble, - testutils.NewResourceNameSet(aStatus.Name), - ) - return readings, nil - } - req := &pb.GetStatusRequest{ - ResourceNames: []*commonpb.ResourceName{protoutils.ResourceNameToProto(aStatus.Name)}, - } - - resp, err := server.GetStatus(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.Status), test.ShouldEqual, 1) - - observed := map[resource.Name]interface{}{ - protoutils.ResourceNameFromProto(resp.Status[0].Name): resp.Status[0].Status.AsMap(), - } - observedLR := map[resource.Name]time.Time{ - protoutils.ResourceNameFromProto(resp.Status[0].Name): resp.Status[0]. - LastReconfigured.AsTime(), - } - test.That(t, observed, test.ShouldResemble, expected) - test.That(t, observedLR, test.ShouldResemble, expectedLR) - }) - - t.Run("working many statuses", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - gStatus := robot.Status{ - Name: movementsensor.Named("gps"), - LastReconfigured: lastReconfigured, - Status: map[string]interface{}{"efg": []string{"hello"}}, - } - aStatus := robot.Status{ - Name: arm.Named("arm"), - LastReconfigured: lastReconfigured2, - Status: struct{}{}, - } - statuses := []robot.Status{gStatus, aStatus} - expected := map[resource.Name]interface{}{ - gStatus.Name: map[string]interface{}{"efg": []interface{}{"hello"}}, - aStatus.Name: map[string]interface{}{}, - } - expectedLRs := map[resource.Name]time.Time{ - gStatus.Name: lastReconfigured, - aStatus.Name: lastReconfigured2, - } - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - test.That( - t, - testutils.NewResourceNameSet(resourceNames...), - test.ShouldResemble, - testutils.NewResourceNameSet(gStatus.Name, aStatus.Name), - ) - return statuses, nil - } - req := &pb.GetStatusRequest{ - ResourceNames: []*commonpb.ResourceName{ - protoutils.ResourceNameToProto(gStatus.Name), - protoutils.ResourceNameToProto(aStatus.Name), - }, - } - - resp, err := server.GetStatus(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.Status), test.ShouldEqual, 2) - - observed := map[resource.Name]interface{}{ - protoutils.ResourceNameFromProto(resp.Status[0].Name): resp.Status[0].Status.AsMap(), - protoutils.ResourceNameFromProto(resp.Status[1].Name): resp.Status[1].Status.AsMap(), - } - observedLRs := map[resource.Name]time.Time{ - protoutils.ResourceNameFromProto(resp.Status[0].Name): resp.Status[0]. - LastReconfigured.AsTime(), - protoutils.ResourceNameFromProto(resp.Status[1].Name): resp.Status[1]. - LastReconfigured.AsTime(), - } - test.That(t, observed, test.ShouldResemble, expected) - test.That(t, observedLRs, test.ShouldResemble, expectedLRs) - }) - - t.Run("failed StreamStatus", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - err1 := errors.New("whoops") - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - return nil, err1 - } - - cancelCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - messageCh := make(chan *pb.StreamStatusResponse) - streamServer := &statusStreamServer{ - ctx: cancelCtx, - messageCh: messageCh, - } - err := server.StreamStatus(&pb.StreamStatusRequest{Every: durationpb.New(time.Second)}, streamServer) - test.That(t, err, test.ShouldEqual, err1) - }) - - t.Run("failed StreamStatus server send", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - return []robot.Status{{arm.Named("arm"), time.Time{}, struct{}{}}}, nil - } - - cancelCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - messageCh := make(chan *pb.StreamStatusResponse) - streamServer := &statusStreamServer{ - ctx: cancelCtx, - messageCh: messageCh, - fail: true, - } - dur := 100 * time.Millisecond - err := server.StreamStatus(&pb.StreamStatusRequest{Every: durationpb.New(dur)}, streamServer) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "send fail") - }) - - t.Run("timed out StreamStatus", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - return []robot.Status{{arm.Named("arm"), time.Time{}, struct{}{}}}, nil - } - - timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - streamServer := &statusStreamServer{ - ctx: timeoutCtx, - messageCh: nil, - } - dur := 100 * time.Millisecond - - streamErr := server.StreamStatus(&pb.StreamStatusRequest{Every: durationpb.New(dur)}, streamServer) - test.That(t, streamErr, test.ShouldResemble, context.DeadlineExceeded) - }) - - t.Run("working StreamStatus", func(t *testing.T) { - injectRobot := &inject.Robot{} - server := server.New(injectRobot) - injectRobot.StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - return []robot.Status{ - { - Name: arm.Named("arm"), - LastReconfigured: lastReconfigured, - Status: struct{}{}, - }, - }, nil - } - - cancelCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - messageCh := make(chan *pb.StreamStatusResponse) - streamServer := &statusStreamServer{ - ctx: cancelCtx, - messageCh: messageCh, - fail: false, - } - dur := 100 * time.Millisecond - var streamErr error - start := time.Now() - done := make(chan struct{}) - go func() { - streamErr = server.StreamStatus(&pb.StreamStatusRequest{Every: durationpb.New(dur)}, streamServer) - close(done) - }() - expectedStatus, err := vprotoutils.StructToStructPb(map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - var messages []*pb.StreamStatusResponse - messages = append(messages, <-messageCh) - messages = append(messages, <-messageCh) - messages = append(messages, <-messageCh) - expectedRobotStatus := []*pb.Status{ - { - Name: protoutils.ResourceNameToProto(arm.Named("arm")), - LastReconfigured: timestamppb.New(lastReconfigured), - Status: expectedStatus, - }, - } - test.That(t, messages, test.ShouldResemble, []*pb.StreamStatusResponse{ - {Status: expectedRobotStatus}, - {Status: expectedRobotStatus}, - {Status: expectedRobotStatus}, - }) - test.That(t, time.Since(start), test.ShouldBeGreaterThanOrEqualTo, 3*dur) - test.That(t, time.Since(start), test.ShouldBeLessThanOrEqualTo, 6*dur) - cancel() - <-done - test.That(t, streamErr, test.ShouldEqual, context.Canceled) - }) -} - -type statusStreamServer struct { - grpc.ServerStream // not set - ctx context.Context - messageCh chan<- *pb.StreamStatusResponse - fail bool -} - -func (x *statusStreamServer) Context() context.Context { - return x.ctx -} - -func (x *statusStreamServer) Send(m *pb.StreamStatusResponse) error { - if x.fail { - return errors.New("send fail") - } - if x.messageCh == nil { - return nil - } - x.messageCh <- m - return nil -} - -type sessionManager struct { - mu sync.Mutex - sessions []*session.Session -} - -func (mgr *sessionManager) Start(ctx context.Context, ownerID string) (*session.Session, error) { - panic("unimplemented") -} - -func (mgr *sessionManager) All() []*session.Session { - mgr.mu.Lock() - defer mgr.mu.Unlock() - return mgr.sessions -} - -func (mgr *sessionManager) FindByID(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - panic("unimplemented") -} - -func (mgr *sessionManager) AssociateResource(id uuid.UUID, resourceName resource.Name) { - panic("unimplemented") -} - -func (mgr *sessionManager) Close() { -} - -func (mgr *sessionManager) ServerInterceptors() session.ServerInterceptors { - panic("unimplemented") -} diff --git a/robot/server/verify_main_test.go b/robot/server/verify_main_test.go deleted file mode 100644 index 58bba7621bd..00000000000 --- a/robot/server/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package server - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/robot/session_manager_test.go b/robot/session_manager_test.go deleted file mode 100644 index 7970b44f128..00000000000 --- a/robot/session_manager_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package robot_test - -import ( - "context" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/session" - "go.viam.com/rdk/testutils/inject" -) - -func TestSessionManager(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - r := &inject.Robot{} - - r.LoggerFunc = func() logging.Logger { - return logger - } - - sm := robot.NewSessionManager(r, config.DefaultSessionHeartbeatWindow) - // The deferred Close is necessary in case any of the asserts between here and the second Close fail. - // Double closing the session manager will not cause issues. - defer sm.Close() - - // Start two arbitrary sessions. - fooSess, err := sm.Start(ctx, "foo") - test.That(t, err, test.ShouldBeNil) - - barSess, err := sm.Start(ctx, "bar") - test.That(t, err, test.ShouldBeNil) - - // Assert that FindByID requires correct owner ID. - foundSess, err := sm.FindByID(ctx, fooSess.ID(), "bar") - test.That(t, foundSess, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, session.ErrNoSession) - - // Assert that fooSess and barSess can be found with FindByID. - foundFooSess, err := sm.FindByID(ctx, fooSess.ID(), "foo") - test.That(t, err, test.ShouldBeNil) - test.That(t, foundFooSess, test.ShouldEqual, fooSess) - - foundBarSess, err := sm.FindByID(ctx, barSess.ID(), "bar") - test.That(t, err, test.ShouldBeNil) - test.That(t, foundBarSess, test.ShouldEqual, barSess) - - // Assert that fooSess and barSess can be found with All. - allSessions := sm.All() - - // The following test assertions use reflection to deep-equals the session objects. - // The session manager's expireLoop might be writing to those fields while reflection is reading those fields. - // Thus we close the session manager before proceeding to avoid a data race. - sm.Close() - - test.That(t, len(allSessions), test.ShouldEqual, 2) - test.That(t, allSessions[0], test.ShouldBeIn, fooSess, barSess) - test.That(t, allSessions[1], test.ShouldBeIn, fooSess, barSess) -} - -func TestSessionManagerExpiredSessions(t *testing.T) { - ctx := context.Background() - logger, logs := logging.NewObservedTestLogger(t) - r := &inject.Robot{} - - r.LoggerFunc = func() logging.Logger { - return logger - } - - // Use a negative duration to cause immediate heartbeat timeout and - // session expiration. - sm := robot.NewSessionManager(r, time.Duration(-1)) - defer sm.Close() - - _, err := sm.Start(ctx, "foo") - test.That(t, err, test.ShouldBeNil) - - testutils.WaitForAssertion(t, func(tb testing.TB) { - tb.Helper() - test.That(tb, logs.FilterMessageSnippet("sessions expired").Len(), - test.ShouldEqual, 1) - }) -} diff --git a/robot/session_test.go b/robot/session_test.go deleted file mode 100644 index 68c1386973e..00000000000 --- a/robot/session_test.go +++ /dev/null @@ -1,1057 +0,0 @@ -package robot_test - -import ( - "context" - "fmt" - "io" - "reflect" - "runtime" - "strings" - "sync" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - echopb "go.viam.com/api/component/testecho/v1" - robotpb "go.viam.com/api/robot/v1" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/rpc" - "go.viam.com/utils/testutils" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/motor" - _ "go.viam.com/rdk/components/register" - "go.viam.com/rdk/config" - "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/client" - robotimpl "go.viam.com/rdk/robot/impl" - _ "go.viam.com/rdk/services/register" - "go.viam.com/rdk/session" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/robottestutils" -) - -var someBaseName1 = base.Named("base1") - -var echoAPI = resource.APINamespaceRDK.WithComponentType("echo") - -func init() { - resource.RegisterAPI(echoAPI, resource.APIRegistration[resource.Resource]{ - RPCServiceServerConstructor: func(apiResColl resource.APIResourceCollection[resource.Resource]) interface{} { - return &echoServer{coll: apiResColl} - }, - RPCServiceHandler: echopb.RegisterTestEchoServiceHandlerFromEndpoint, - RPCServiceDesc: &echopb.TestEchoService_ServiceDesc, - RPCClient: func( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, - ) (resource.Resource, error) { - return NewClientFromConn(ctx, conn, remoteName, name, logger), nil - }, - }) -} - -// TODO(RSDK-5435): This test suite checks if stopping a client also stops any -// components that were started by that client. We also should implement a benchmark -// suite that measures how long it takes for components to stop. -// -// We should NOT add a strict deadline to this test - we had that before and it resulted -// in flaky tests (see RSDK-2493). -func TestSessions(t *testing.T) { - for _, windowSize := range []time.Duration{ - config.DefaultSessionHeartbeatWindow, - time.Second * 5, - } { - t.Run(fmt.Sprintf("window size=%s", windowSize), func(t *testing.T) { - logger := logging.NewTestLogger(t) - - stopChs := map[string]*StopChan{ - "motor1": {make(chan struct{}), "motor1"}, - "motor2": {make(chan struct{}), "motor2"}, - "echo1": {make(chan struct{}), "echo1"}, - "base1": {make(chan struct{}), "base1"}, - } - stopChNames := make([]string, 0, len(stopChs)) - for name := range stopChs { - stopChNames = append(stopChNames, name) - } - - ensureStop := makeEnsureStop(stopChs) - - motor1Name := motor.Named("motor1") - motor2Name := motor.Named("motor2") - base1Name := base.Named("base1") - echo1Name := resource.NewName(echoAPI, "echo1") - - model := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - streamModel := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - dummyMotor1 := dummyMotor{Named: motor1Name.AsNamed(), stopCh: stopChs["motor1"].Chan} - dummyMotor2 := dummyMotor{Named: motor2Name.AsNamed(), stopCh: stopChs["motor2"].Chan} - dummyEcho1 := dummyEcho{ - Named: echo1Name.AsNamed(), - stopCh: stopChs["echo1"].Chan, - } - dummyBase1 := dummyBase{Named: base1Name.AsNamed(), stopCh: stopChs["base1"].Chan} - resource.RegisterComponent( - motor.API, - model, - resource.Registration[motor.Motor, resource.NoNativeConfig]{Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - if conf.Name == "motor1" { - return &dummyMotor1, nil - } - return &dummyMotor2, nil - }}) - resource.RegisterComponent( - echoAPI, - streamModel, - resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return &dummyEcho1, nil - }, - }, - ) - resource.RegisterComponent( - base.API, - model, - resource.Registration[base.Base, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (base.Base, error) { - return &dummyBase1, nil - }, - }, - ) - - roboConfig := fmt.Sprintf(`{ - "network":{ - "sessions": { - "heartbeat_window": %[1]q - } - }, - "components": [ - { - "model": "%[2]s", - "name": "motor1", - "type": "motor" - }, - { - "model": "%[2]s", - "name": "motor2", - "type": "motor" - }, - { - "model": "%[3]s", - "name": "echo1", - "type": "echo" - }, - { - "model": "%[2]s", - "name": "base1", - "type": "base" - } - ] - } - `, windowSize, model, streamModel) - - cfg, err := config.FromReader(context.Background(), "", strings.NewReader(roboConfig), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - r, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - roboClient, err := client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor1, err := motor.FromRobot(roboClient, "motor1") - test.That(t, err, test.ShouldBeNil) - - t.Log("get position of motor1 which will not be safety monitored") - pos, err := motor1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 2.0) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - // kind of racy but it's okay - ensureStop(t, "", stopChNames) - - roboClient, err = client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor1, err = motor.FromRobot(roboClient, "motor1") - test.That(t, err, test.ShouldBeNil) - - t.Log("set power of motor1 which will be safety monitored") - test.That(t, motor1.SetPower(ctx, 50, nil), test.ShouldBeNil) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - ensureStop(t, "motor1", stopChNames) - - dummyMotor1.mu.Lock() - stopChs["motor1"].Chan = make(chan struct{}) - dummyMotor1.stopCh = stopChs["motor1"].Chan - dummyMotor1.mu.Unlock() - - roboClient, err = client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor2, err := motor.FromRobot(roboClient, "motor2") - test.That(t, err, test.ShouldBeNil) - - t.Log("set power of motor2 which will be safety monitored") - test.That(t, motor2.SetPower(ctx, 50, nil), test.ShouldBeNil) - - echo1Client, err := roboClient.ResourceByName(echo1Name) - test.That(t, err, test.ShouldBeNil) - echo1Conn := echo1Client.(*dummyClient) - - t.Log("echo multiple of echo1 which will be safety monitored") - echoMultiClient, err := echo1Conn.client.EchoMultiple(ctx, &echopb.EchoMultipleRequest{Name: "echo1"}) - test.That(t, err, test.ShouldBeNil) - _, err = echoMultiClient.Recv() // EOF; okay - test.That(t, err, test.ShouldBeError, io.EOF) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - checkAgainst := []string{"motor1"} - ensureStop(t, "motor2", checkAgainst) - ensureStop(t, "echo1", checkAgainst) - ensureStop(t, "base1", checkAgainst) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - test.That(t, r.Close(ctx), test.ShouldBeNil) - }) - } -} - -func TestSessionsWithRemote(t *testing.T) { - logger := logging.NewTestLogger(t) - - stopChs := map[string]*StopChan{ - "remMotor1": {make(chan struct{}), "remMotor1"}, - "remMotor2": {make(chan struct{}), "remMotor2"}, - "remEcho1": {make(chan struct{}), "remEcho1"}, - "remBase1": {make(chan struct{}), "remBase1"}, - "motor1": {make(chan struct{}), "motor1"}, - "base1": {make(chan struct{}), "base1"}, - } - stopChNames := make([]string, 0, len(stopChs)) - for name := range stopChs { - stopChNames = append(stopChNames, name) - } - - ensureStop := makeEnsureStop(stopChs) - - model := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - streamModel := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - motor1Name := motor.Named("motor1") - motor2Name := motor.Named("motor2") - base1Name := base.Named("base1") - echo1Name := resource.NewName(echoAPI, "echo1") - dummyRemMotor1 := dummyMotor{Named: motor1Name.AsNamed(), stopCh: stopChs["remMotor1"].Chan} - dummyRemMotor2 := dummyMotor{Named: motor2Name.AsNamed(), stopCh: stopChs["remMotor2"].Chan} - dummyRemEcho1 := dummyEcho{Named: echo1Name.AsNamed(), stopCh: stopChs["remEcho1"].Chan} - dummyRemBase1 := dummyBase{Named: base1Name.AsNamed(), stopCh: stopChs["remBase1"].Chan} - dummyMotor1 := dummyMotor{Named: motor1Name.AsNamed(), stopCh: stopChs["motor1"].Chan} - dummyBase1 := dummyBase{Named: base1Name.AsNamed(), stopCh: stopChs["base1"].Chan} - resource.RegisterComponent( - motor.API, - model, - resource.Registration[motor.Motor, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - if conf.Attributes.Bool("rem", false) { - if conf.Name == "motor1" { - return &dummyRemMotor1, nil - } - return &dummyRemMotor2, nil - } - return &dummyMotor1, nil - }, - }) - resource.RegisterComponent( - echoAPI, - streamModel, - resource.Registration[resource.Resource, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (resource.Resource, error) { - return &dummyRemEcho1, nil - }, - }, - ) - resource.RegisterComponent( - base.API, - model, - resource.Registration[base.Base, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (base.Base, error) { - if conf.Attributes.Bool("rem", false) { - return &dummyRemBase1, nil - } - return &dummyBase1, nil - }, - }, - ) - - remoteConfig := fmt.Sprintf(`{ - "components": [ - { - "model": "%[1]s", - "name": "motor1", - "type": "motor", - "attributes": { - "rem": true - } - }, - { - "model": "%[1]s", - "name": "motor2", - "type": "motor", - "attributes": { - "rem": true - } - }, - { - "model": "%[2]s", - "name": "echo1", - "type": "echo", - "attributes": { - "rem": true - } - }, - { - "model": "%[1]s", - "name": "base1", - "type": "base", - "attributes": { - "rem": true - } - } - ] - } - `, model, streamModel) - - cfg, err := config.FromReader(context.Background(), "", strings.NewReader(remoteConfig), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - remoteRobot, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, remoteAddr := robottestutils.CreateBaseOptionsAndListener(t) - err = remoteRobot.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - roboConfig := fmt.Sprintf(`{ - "remotes": [ - { - "name": "rem1", - "address": %q - } - ], - "components": [ - { - "model": "%[2]s", - "name": "motor1", - "type": "motor" - }, - { - "model": "%[2]s", - "name": "base1", - "type": "base" - } - ] - } - `, remoteAddr, model) - - cfg, err = config.FromReader(context.Background(), "", strings.NewReader(roboConfig), logger) - test.That(t, err, test.ShouldBeNil) - - r, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - roboClient, err := client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor1, err := motor.FromRobot(roboClient, "rem1:motor1") - if err != nil { - bufSize := 1 << 20 - traces := make([]byte, bufSize) - traceSize := runtime.Stack(traces, true) - message := fmt.Sprintf("error accessing remote from roboClient: %s. logging stack trace for debugging purposes", err.Error()) - if traceSize == bufSize { - message = fmt.Sprintf("%s (warning: backtrace truncated to %v bytes)", message, bufSize) - } - logger.Errorf("%s,\n %s", message, traces) - } - test.That(t, err, test.ShouldBeNil) - - t.Log("get position of rem1:motor1 which will not be safety monitored") - pos, err := motor1.Position(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, pos, test.ShouldEqual, 2.0) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - // kind of racy but it's okay - ensureStop(t, "", stopChNames) - - roboClient, err = client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor1, err = motor.FromRobot(roboClient, "rem1:motor1") - test.That(t, err, test.ShouldBeNil) - - // this should cause safety monitoring - t.Log("set power of rem1:motor1 which will be safety monitored") - test.That(t, motor1.SetPower(ctx, 50, nil), test.ShouldBeNil) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - ensureStop(t, "remMotor1", stopChNames) - - dummyRemMotor1.mu.Lock() - stopChs["remMotor1"].Chan = make(chan struct{}) - dummyRemMotor1.stopCh = stopChs["remMotor1"].Chan - dummyRemMotor1.mu.Unlock() - - logger.Info("close robot instead of client") - - roboClient, err = client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor1, err = motor.FromRobot(roboClient, "rem1:motor1") - test.That(t, err, test.ShouldBeNil) - - t.Log("set power of rem1:motor1 which will be safety monitored") - test.That(t, motor1.SetPower(ctx, 50, nil), test.ShouldBeNil) - - test.That(t, r.Close(ctx), test.ShouldBeNil) - - ensureStop(t, "remMotor1", stopChNames) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - dummyRemMotor1.mu.Lock() - stopChs["remMotor1"].Chan = make(chan struct{}) - dummyRemMotor1.stopCh = stopChs["remMotor1"].Chan - dummyRemMotor1.mu.Unlock() - - r, err = robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, addr = robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - roboClient, err = client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor2, err := motor.FromRobot(roboClient, "rem1:motor2") - if err != nil { - bufSize := 1 << 20 - traces := make([]byte, bufSize) - traceSize := runtime.Stack(traces, true) - message := fmt.Sprintf("error accessing remote from roboClient: %s. logging stack trace for debugging purposes", err.Error()) - if traceSize == bufSize { - message = fmt.Sprintf("%s (warning: backtrace truncated to %v bytes)", message, bufSize) - } - logger.Errorf("%s,\n %s", message, traces) - } - test.That(t, err, test.ShouldBeNil) - - t.Log("set power of rem1:motor2 which will be safety monitored") - test.That(t, motor2.SetPower(ctx, 50, nil), test.ShouldBeNil) - - dummyName := resource.NewName(echoAPI, "echo1") - echo1Client, err := roboClient.ResourceByName(dummyName) - test.That(t, err, test.ShouldBeNil) - echo1Conn := echo1Client.(*dummyClient) - - t.Log("echo multiple of remEcho1 which will be safety monitored") - echoMultiClient, err := echo1Conn.client.EchoMultiple(ctx, &echopb.EchoMultipleRequest{Name: "echo1"}) - test.That(t, err, test.ShouldBeNil) - _, err = echoMultiClient.Recv() // EOF; okay - test.That(t, err, test.ShouldBeError, io.EOF) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - checkAgainst := []string{"remMotor1", "motor1", "base1"} - ensureStop(t, "remMotor2", checkAgainst) - ensureStop(t, "remBase1", checkAgainst) - ensureStop(t, "remEcho1", checkAgainst) - - test.That(t, roboClient.Close(ctx), test.ShouldBeNil) - - test.That(t, r.Close(ctx), test.ShouldBeNil) - test.That(t, remoteRobot.Close(ctx), test.ShouldBeNil) -} - -func TestSessionsMixedClients(t *testing.T) { - logger := logging.NewTestLogger(t) - stopChMotor1 := make(chan struct{}) - - model := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - motor1Name := motor.Named("motor1") - dummyMotor1 := dummyMotor{Named: motor1Name.AsNamed(), stopCh: stopChMotor1} - resource.RegisterComponent( - motor.API, - model, - resource.Registration[motor.Motor, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - return &dummyMotor1, nil - }, - }) - - roboConfig := fmt.Sprintf(`{ - "components": [ - { - "model": "%s", - "name": "motor1", - "type": "motor" - } - ] - } - `, model) - - cfg, err := config.FromReader(context.Background(), "", strings.NewReader(roboConfig), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - r, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - roboClient1, err := client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - roboClient2, err := client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - motor1Client1, err := motor.FromRobot(roboClient1, "motor1") - test.That(t, err, test.ShouldBeNil) - motor1Client2, err := motor.FromRobot(roboClient2, "motor1") - test.That(t, err, test.ShouldBeNil) - - test.That(t, motor1Client1.SetPower(ctx, 50, nil), test.ShouldBeNil) - time.Sleep(time.Second) - // now client 2 is the last caller - test.That(t, motor1Client2.GoFor(ctx, 1, 2, nil), test.ShouldBeNil) - - test.That(t, roboClient1.Close(ctx), test.ShouldBeNil) - - timer := time.NewTimer(config.DefaultSessionHeartbeatWindow * 2) - select { - case <-stopChMotor1: - panic("unexpected") - case <-timer.C: - timer.Stop() - } - - test.That(t, roboClient2.Close(ctx), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - select { - case <-stopChMotor1: - return - default: - tb.Fail() - } - }) - - test.That(t, r.Close(ctx), test.ShouldBeNil) -} - -func TestSessionsMixedOwnersNoAuth(t *testing.T) { - logger := logging.NewTestLogger(t) - stopChMotor1 := make(chan struct{}) - - model := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - motor1Name := motor.Named("motor1") - dummyMotor1 := dummyMotor{Named: motor1Name.AsNamed(), stopCh: stopChMotor1} - resource.RegisterComponent( - motor.API, - model, - resource.Registration[motor.Motor, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - return &dummyMotor1, nil - }, - }) - - roboConfig := fmt.Sprintf(`{ - "components": [ - { - "model": "%s", - "name": "motor1", - "type": "motor" - } - ] - } - `, model) - - cfg, err := config.FromReader(context.Background(), "", strings.NewReader(roboConfig), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - r, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // with no auth turned on, we will have no session owner, meaning mixing sessions technically works, for now - roboClient1, err := client.New(ctx, addr, logger, client.WithDialOptions(rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - Disable: true, - }))) - test.That(t, err, test.ShouldBeNil) - - roboClientConn2, err := grpc.Dial(ctx, addr, logger, rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - Disable: true, - })) - test.That(t, err, test.ShouldBeNil) - - motor1Client1, err := motor.FromRobot(roboClient1, "motor1") - test.That(t, err, test.ShouldBeNil) - motor1Client2, err := motor.NewClientFromConn(ctx, roboClientConn2, "", motor.Named("motor1"), logger) - test.That(t, err, test.ShouldBeNil) - - test.That(t, motor1Client1.SetPower(ctx, 50, nil), test.ShouldBeNil) - time.Sleep(time.Second) - - sessions := r.SessionManager().All() - test.That(t, sessions, test.ShouldHaveLength, 1) - sessID := sessions[0].ID().String() - - // now client 2 is the last caller but the sessions are the same - client2Ctx := metadata.AppendToOutgoingContext(ctx, session.IDMetadataKey, sessID) - test.That(t, motor1Client2.GoFor(client2Ctx, 1, 2, nil), test.ShouldBeNil) - - // this would just heartbeat it - resp, err := robotpb.NewRobotServiceClient(roboClientConn2).StartSession(ctx, &robotpb.StartSessionRequest{ - Resume: sessID, - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Id, test.ShouldEqual, sessID) - - // this is the only one heartbeating so we expect a stop - test.That(t, roboClient1.Close(ctx), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - select { - case <-stopChMotor1: - return - default: - tb.Fail() - } - }) - - test.That(t, roboClientConn2.Close(), test.ShouldBeNil) - test.That(t, r.Close(ctx), test.ShouldBeNil) -} - -// TODO(RSDK-890): add explicit auth test once subjects are actually unique. -func TestSessionsMixedOwnersImplicitAuth(t *testing.T) { - logger := logging.NewTestLogger(t) - stopChMotor1 := make(chan struct{}) - - model := resource.DefaultModelFamily.WithModel(utils.RandomAlphaString(8)) - motor1Name := motor.Named("motor1") - dummyMotor1 := dummyMotor{Named: motor1Name.AsNamed(), stopCh: stopChMotor1} - resource.RegisterComponent( - motor.API, - model, - resource.Registration[motor.Motor, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (motor.Motor, error) { - return &dummyMotor1, nil - }, - }) - - roboConfig := fmt.Sprintf(`{ - "components": [ - { - "model": "%s", - "name": "motor1", - "type": "motor" - } - ] - } - `, model) - - cfg, err := config.FromReader(context.Background(), "", strings.NewReader(roboConfig), logger) - test.That(t, err, test.ShouldBeNil) - - ctx := context.Background() - r, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = r.StartWeb(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // TODO(RSDK-890): using WebRTC (the default) gives us an implicit auth subject, for now - roboClient1, err := client.New(ctx, addr, logger) - test.That(t, err, test.ShouldBeNil) - - roboClientConn2, err := grpc.Dial(ctx, addr, logger, rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - Disable: true, - })) - test.That(t, err, test.ShouldBeNil) - - // This test sets up two clients to the same motor. The test sets up this (contrived) scenario: - // 1) Client 1 will first `SetPower` with a session heartbeat. This operation returns with the - // motor engaged. - // 2) Client 2 will "override" that operation with a very slow `GoFor`. - // 3) Client 1 will disconnect. This results in the client ceasing heartbeats for the - // `SetPower` operation. - // - Despite `SetPower` not being the "active" operation, it's expected for the robot's - // heartbeat thread to call `motor1.Stop`. - motor1Client1, err := motor.FromRobot(roboClient1, "motor1") - test.That(t, err, test.ShouldBeNil) - motor1Client2, err := motor.NewClientFromConn(ctx, roboClientConn2, "", motor.Named("motor1"), logger) - test.That(t, err, test.ShouldBeNil) - - // Set the power on client1. - test.That(t, motor1Client1.SetPower(ctx, 50, nil), test.ShouldBeNil) - time.Sleep(time.Second) - - sessions := r.SessionManager().All() - test.That(t, sessions, test.ShouldHaveLength, 1) - sessID := sessions[0].ID().String() - - // cannot share here - client2Ctx := metadata.AppendToOutgoingContext(ctx, session.IDMetadataKey, sessID) - err = motor1Client2.GoFor(client2Ctx, 1, 2, nil) - test.That(t, err, test.ShouldNotBeNil) - statusErr, ok := status.FromError(err) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, statusErr.Code(), test.ShouldEqual, session.StatusNoSession.Code()) - test.That(t, statusErr.Message(), test.ShouldEqual, session.StatusNoSession.Message()) - - // this should give us a new session instead since we cannot see it - resp, err := robotpb.NewRobotServiceClient(roboClientConn2).StartSession(ctx, &robotpb.StartSessionRequest{ - Resume: sessID, - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Id, test.ShouldNotEqual, sessID) - test.That(t, resp.Id, test.ShouldNotEqual, "") - - // Assert that closing `roboClient1` results in heartbeats stopping that propagates to `Stop`ing - // the motor operation. We observe this with a message over the `stopChMotor1` channel. - test.That(t, roboClient1.Close(ctx), test.ShouldBeNil) - testutils.WaitForAssertion(t, func(tb testing.TB) { - select { - case <-stopChMotor1: - return - default: - tb.Fail() - } - }) - - test.That(t, roboClientConn2.Close(), test.ShouldBeNil) - test.That(t, r.Close(ctx), test.ShouldBeNil) -} - -type dummyMotor struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - mu sync.Mutex - stopCh chan struct{} -} - -func (dm *dummyMotor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - return nil -} - -func (dm *dummyMotor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - return nil -} - -func (dm *dummyMotor) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - return nil -} - -func (dm *dummyMotor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 2, nil -} - -func (dm *dummyMotor) Stop(ctx context.Context, extra map[string]interface{}) error { - dm.mu.Lock() - defer dm.mu.Unlock() - close(dm.stopCh) - return nil -} - -func (dm *dummyMotor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - return nil -} - -func (dm *dummyMotor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - return motor.Properties{}, nil -} - -func (dm *dummyMotor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - return false, 0, nil -} - -func (dm *dummyMotor) IsMoving(context.Context) (bool, error) { - return false, nil -} - -type dummyBase struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - mu sync.Mutex - stopCh chan struct{} -} - -func (db *dummyBase) SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return nil -} - -func (db *dummyBase) Stop(ctx context.Context, extra map[string]interface{}) error { - db.mu.Lock() - defer db.mu.Unlock() - close(db.stopCh) - return nil -} - -func (db *dummyBase) MoveStraight(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - return nil -} - -func (db *dummyBase) Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - return nil -} - -func (db *dummyBase) SetVelocity(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return nil -} - -func (db *dummyBase) IsMoving(context.Context) (bool, error) { - return false, nil -} - -func (db *dummyBase) Properties(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{}, nil -} - -func (db *dummyBase) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - return []spatialmath.Geometry{}, nil -} - -// NewClientFromConn constructs a new client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) resource.Resource { - c := echopb.NewTestEchoServiceClient(conn) - return &dummyClient{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: c, - } -} - -type dummyClient struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - name string - client echopb.TestEchoServiceClient -} - -func (c *dummyClient) Stop(ctx context.Context, extra map[string]interface{}) error { - _, err := c.client.Stop(ctx, &echopb.StopRequest{Name: c.name}) - return err -} - -func (c *dummyClient) IsMoving(context.Context) (bool, error) { - return false, nil -} - -type dummyEcho struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable - mu sync.Mutex - stopCh chan struct{} -} - -func (e *dummyEcho) EchoMultiple(ctx context.Context) error { - session.SafetyMonitorResourceName(ctx, someBaseName1) - return nil -} - -func (e *dummyEcho) Stop(ctx context.Context, extra map[string]interface{}) error { - e.mu.Lock() - defer e.mu.Unlock() - close(e.stopCh) - return nil -} - -func (e *dummyEcho) IsMoving(context.Context) (bool, error) { - return false, nil -} - -type echoServer struct { - echopb.UnimplementedTestEchoServiceServer - coll resource.APIResourceCollection[resource.Resource] -} - -func (srv *echoServer) EchoMultiple( - req *echopb.EchoMultipleRequest, - server echopb.TestEchoService_EchoMultipleServer, -) error { - res, err := srv.coll.Resource(req.Name) - if err != nil { - return err - } - - switch actual := res.(type) { - case *dummyEcho: - return actual.EchoMultiple(server.Context()) - case *dummyClient: - echoClient, err := actual.client.EchoMultiple(server.Context(), &echopb.EchoMultipleRequest{Name: actual.name}) - if err != nil { - return err - } - if _, err := echoClient.Recv(); err != nil { - if errors.Is(err, io.EOF) { - return nil - } - return err - } - return nil - default: - // force an error - return actual.(*dummyEcho).EchoMultiple(server.Context()) - } -} - -func (srv *echoServer) Stop(ctx context.Context, req *echopb.StopRequest) (*echopb.StopResponse, error) { - res, err := srv.coll.Resource(req.Name) - if err != nil { - return nil, err - } - if actuator, ok := res.(resource.Actuator); ok { - if err := actuator.Stop(ctx, nil); err != nil { - return nil, err - } - } - return &echopb.StopResponse{}, nil -} - -type StopChan struct { - Chan chan struct{} - Name string -} - -func makeEnsureStop(stopChs map[string]*StopChan) func(t *testing.T, name string, checkAgainst []string) { - return func(t *testing.T, name string, checkAgainst []string) { - t.Helper() - stopCases := make([]reflect.SelectCase, 0, len(checkAgainst)) - for _, checkName := range checkAgainst { - test.That(t, stopChs, test.ShouldContainKey, checkName) - if stopChs[checkName].Name == name { - continue - } - stopCases = append(stopCases, reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(stopChs[checkName].Chan), - }) - } - - if name == "" { - t.Log("checking nothing stops") - stopCases = append(stopCases, reflect.SelectCase{ - Dir: reflect.SelectDefault, - }) - } else { - test.That(t, stopChs, test.ShouldContainKey, name) - expectedCh := stopChs[name] - - stopCases = append(stopCases, reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(expectedCh.Chan), - }) - t.Logf("waiting for %q to stop", name) - } - - choice, _, _ := reflect.Select(stopCases) - if choice == len(stopCases)-1 { - return - } - for _, ch := range stopChs { - if ch.Chan == stopCases[choice].Chan.Interface() { - t.Fatalf("expected %q to stop but got %q", name, ch.Name) - } - } - t.Fatal("unreachable; bug") - } -} diff --git a/robot/verify_main_test.go b/robot/verify_main_test.go deleted file mode 100644 index 6cd426becfb..00000000000 --- a/robot/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package robot - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/robot/web/stream/server.go b/robot/web/stream/server.go index c48aa58772f..32b5e5274a8 100644 --- a/robot/web/stream/server.go +++ b/robot/web/stream/server.go @@ -5,26 +5,12 @@ import ( "fmt" "sync" - "github.com/pion/webrtc/v3" - "github.com/pkg/errors" - "go.opencensus.io/trace" - "go.uber.org/multierr" streampb "go.viam.com/api/stream/v1" - "go.viam.com/utils" - "go.viam.com/utils/rpc" - "go.viam.com/rdk/gostream" "go.viam.com/rdk/logging" "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/web/stream/state" - rutils "go.viam.com/rdk/utils" ) -type peerState struct { - streamState *state.StreamState - senders []*webrtc.RTPSender -} - // Server implements the gRPC audio/video streaming service. type Server struct { streampb.UnimplementedStreamServiceServer @@ -33,8 +19,6 @@ type Server struct { mu sync.RWMutex streamNames []string - nameToStreamState map[string]*state.StreamState - activePeerStreams map[*webrtc.PeerConnection]map[string]*peerState activeBackgroundWorkers sync.WaitGroup isAlive bool } @@ -42,23 +26,15 @@ type Server struct { // NewServer returns a server that will run on the given port and initially starts with the given // stream. func NewServer( - streams []gostream.Stream, r robot.Robot, logger logging.Logger, ) (*Server, error) { ss := &Server{ - r: r, - logger: logger, - nameToStreamState: map[string]*state.StreamState{}, - activePeerStreams: map[*webrtc.PeerConnection]map[string]*peerState{}, - isAlive: true, + r: r, + logger: logger, + isAlive: true, } - for _, stream := range streams { - if err := ss.add(stream); err != nil { - return nil, err - } - } return ss, nil } @@ -72,211 +48,18 @@ func (e *StreamAlreadyRegisteredError) Error() string { return fmt.Sprintf("stream %q already registered", e.name) } -// NewStream informs the stream server of new streams that are capable of being streamed. -func (ss *Server) NewStream(config gostream.StreamConfig) (gostream.Stream, error) { - ss.mu.Lock() - defer ss.mu.Unlock() - - if _, ok := ss.nameToStreamState[config.Name]; ok { - return nil, &StreamAlreadyRegisteredError{config.Name} - } - - stream, err := gostream.NewStream(config) - if err != nil { - return nil, err - } - - if err = ss.add(stream); err != nil { - return nil, err - } - - return stream, nil -} - // ListStreams implements part of the StreamServiceServer. func (ss *Server) ListStreams(ctx context.Context, req *streampb.ListStreamsRequest) (*streampb.ListStreamsResponse, error) { - _, span := trace.StartSpan(ctx, "stream::server::ListStreams") - defer span.End() - ss.mu.RLock() - defer ss.mu.RUnlock() - - names := make([]string, 0, len(ss.streamNames)) - for _, name := range ss.streamNames { - streamState := ss.nameToStreamState[name] - names = append(names, streamState.Stream.Name()) - } - return &streampb.ListStreamsResponse{Names: names}, nil + return &streampb.ListStreamsResponse{}, nil } // AddStream implements part of the StreamServiceServer. func (ss *Server) AddStream(ctx context.Context, req *streampb.AddStreamRequest) (*streampb.AddStreamResponse, error) { - ctx, span := trace.StartSpan(ctx, "stream::server::AddStream") - defer span.End() - // Get the peer connection - pc, ok := rpc.ContextPeerConnection(ctx) - if !ok { - return nil, errors.New("can only add a stream over a WebRTC based connection") - } - - ss.mu.Lock() - defer ss.mu.Unlock() - - streamStateToAdd, ok := ss.nameToStreamState[req.Name] - - // return error if there is no stream for that camera - if !ok { - err := fmt.Errorf("no stream for %q", req.Name) - ss.logger.Error(err.Error()) - return nil, err - } - - // return error if the caller's peer connection is already being sent video data - if _, ok := ss.activePeerStreams[pc][req.Name]; ok { - err := errors.New("stream already active") - ss.logger.Error(err.Error()) - return nil, err - } - nameToPeerState, ok := ss.activePeerStreams[pc] - // if there is no active video data being sent, set up a callback to remove the peer connection from - // the active streams & stop the stream from doing h264 encode if this is the last peer connection - // subcribed to the camera's video feed - // the callback fires when the peer connection state changes & runs the cleanup routine when the - // peer connection is in a terminal state. - if !ok { - nameToPeerState = map[string]*peerState{} - pc.OnConnectionStateChange(func(peerConnectionState webrtc.PeerConnectionState) { - ss.logger.Debugf("%s pc.OnConnectionStateChange state: %s", req.Name, peerConnectionState) - switch peerConnectionState { - case webrtc.PeerConnectionStateDisconnected, - webrtc.PeerConnectionStateFailed, - webrtc.PeerConnectionStateClosed: - - ss.mu.Lock() - defer ss.mu.Unlock() - - if ss.isAlive { - // Dan: This conditional closing on `isAlive` is a hack to avoid a data - // race. Shutting down a robot causes the PeerConnection to be closed - // concurrently with this `stream.Server`. Thus, `stream.Server.Close` waiting - // on the `activeBackgroundWorkers` WaitGroup can race with adding a new - // "worker". Given `Close` is expected to `Stop` remaining streams, we can elide - // spinning off the below goroutine. - // - // Given this is an existing race, I'm choosing to add to the tech - // debt rather than architect how shutdown should holistically work. Revert this - // change and run `TestRobotPeerConnect` (double check the test name at PR time) - // to reproduce the race. - ss.activeBackgroundWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer ss.activeBackgroundWorkers.Done() - ss.mu.Lock() - defer ss.mu.Unlock() - defer delete(ss.activePeerStreams, pc) - var errs error - for _, ps := range ss.activePeerStreams[pc] { - ctx, cancel := context.WithTimeout(context.Background(), state.UnsubscribeTimeout) - errs = multierr.Combine(errs, ps.streamState.Decrement(ctx)) - cancel() - } - // We don't want to log this if the streamState was closed (as it only happens if viam-server is terminating) - if errs != nil && !errors.Is(errs, state.ErrClosed) { - ss.logger.Errorw("error(s) stopping the streamState", "errs", errs) - } - }) - } - case webrtc.PeerConnectionStateConnected, - webrtc.PeerConnectionStateConnecting, - webrtc.PeerConnectionStateNew: - fallthrough - default: - return - } - }) - ss.activePeerStreams[pc] = nameToPeerState - } - - ps, ok := nameToPeerState[req.Name] - // if the active peer stream doesn't have a peerState, add one containing the stream in question - if !ok { - ps = &peerState{streamState: streamStateToAdd} - nameToPeerState[req.Name] = ps - } - - guard := rutils.NewGuard(func() { - for _, sender := range ps.senders { - utils.UncheckedError(pc.RemoveTrack(sender)) - } - }) - defer guard.OnFail() - - addTrack := func(track webrtc.TrackLocal) error { - sender, err := pc.AddTrack(track) - if err != nil { - return err - } - ps.senders = append(ps.senders, sender) - return nil - } - - // if the stream supports video, add the video track - if trackLocal, haveTrackLocal := streamStateToAdd.Stream.VideoTrackLocal(); haveTrackLocal { - if err := addTrack(trackLocal); err != nil { - ss.logger.Error(err.Error()) - return nil, err - } - } - // if the stream supports audio, add the audio track - if trackLocal, haveTrackLocal := streamStateToAdd.Stream.AudioTrackLocal(); haveTrackLocal { - if err := addTrack(trackLocal); err != nil { - ss.logger.Error(err.Error()) - return nil, err - } - } - if err := streamStateToAdd.Increment(ctx); err != nil { - ss.logger.Error(err.Error()) - return nil, err - } - - guard.Success() return &streampb.AddStreamResponse{}, nil } // RemoveStream implements part of the StreamServiceServer. func (ss *Server) RemoveStream(ctx context.Context, req *streampb.RemoveStreamRequest) (*streampb.RemoveStreamResponse, error) { - ctx, span := trace.StartSpan(ctx, "stream::server::RemoveStream") - defer span.End() - pc, ok := rpc.ContextPeerConnection(ctx) - if !ok { - return nil, errors.New("can only remove a stream over a WebRTC based connection") - } - - ss.mu.Lock() - defer ss.mu.Unlock() - - streamToRemove, ok := ss.nameToStreamState[req.Name] - if !ok { - return nil, fmt.Errorf("no stream for %q", req.Name) - } - - if _, ok := ss.activePeerStreams[pc][req.Name]; !ok { - return nil, errors.New("stream already inactive") - } - - var errs error - for _, sender := range ss.activePeerStreams[pc][req.Name].senders { - errs = multierr.Combine(errs, pc.RemoveTrack(sender)) - } - if errs != nil { - ss.logger.Error(errs.Error()) - return nil, errs - } - - if err := streamToRemove.Decrement(ctx); err != nil { - ss.logger.Error(err.Error()) - return nil, err - } - - delete(ss.activePeerStreams[pc], req.Name) return &streampb.RemoveStreamResponse{}, nil } @@ -285,27 +68,7 @@ func (ss *Server) Close() error { ss.mu.Lock() ss.isAlive = false - var errs error - for _, name := range ss.streamNames { - errs = multierr.Combine(errs, ss.nameToStreamState[name].Close()) - } - if errs != nil { - ss.logger.Errorf("Stream Server Close > StreamState.Close() errs: %s", errs) - } ss.mu.Unlock() ss.activeBackgroundWorkers.Wait() - return errs -} - -func (ss *Server) add(stream gostream.Stream) error { - streamName := stream.Name() - if _, ok := ss.nameToStreamState[streamName]; ok { - return &StreamAlreadyRegisteredError{streamName} - } - - newStreamState := state.New(stream, ss.r, ss.logger) - newStreamState.Init() - ss.nameToStreamState[streamName] = newStreamState - ss.streamNames = append(ss.streamNames, streamName) return nil } diff --git a/robot/web/stream/state/state.go b/robot/web/stream/state/state.go deleted file mode 100644 index 2f68699ff55..00000000000 --- a/robot/web/stream/state/state.go +++ /dev/null @@ -1,471 +0,0 @@ -// Package state controls the source of the RTP packets being written to the stream's subscribers -// and ensures there is only one active at a time while there are peer connections to receive RTP packets. -package state - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/pion/rtp" - "github.com/pkg/errors" - "go.uber.org/multierr" - "go.viam.com/utils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/robot" -) - -var ( - subscribeRTPTimeout = time.Second * 5 - // UnsubscribeTimeout is the timeout used when unsubscribing from an rtppassthrough subscription. - UnsubscribeTimeout = time.Second * 5 - // ErrRTPPassthroughNotSupported indicates that rtp_passthrough is not supported by the stream's camera. - ErrRTPPassthroughNotSupported = errors.New("RTP Passthrough Not Supported") - // ErrClosed indicates that the StreamState is already closed. - ErrClosed = errors.New("StreamState already closed") - // ErrUninitialized indicates that Init() has not been called on StreamState prior to Increment or Decrement being called. - ErrUninitialized = errors.New("uniniialized") -) - -// StreamState controls the source of the RTP packets being written to the stream's subscribers -// and ensures there is only one active at a time while there are subsribers. -type StreamState struct { - // Stream is the StreamState's stream - Stream gostream.Stream - robot robot.Robot - closedCtx context.Context - closedFn context.CancelFunc - wg sync.WaitGroup - logger logging.Logger - initialized atomic.Bool - - msgChan chan msg - restartChan chan struct{} - - activePeers int - streamSource streamSource - // streamSourceSub is only non nil if streamSource == streamSourcePassthrough - streamSourceSub rtppassthrough.Subscription -} - -// New returns a new *StreamState. -// rtpPassthroughSource is allowed to be nil -// if the camere does not implement rtppassthrough.Source. -func New( - stream gostream.Stream, - r robot.Robot, - logger logging.Logger, -) *StreamState { - ctx, cancel := context.WithCancel(context.Background()) - return &StreamState{ - Stream: stream, - closedCtx: ctx, - closedFn: cancel, - robot: r, - msgChan: make(chan msg), - restartChan: make(chan struct{}), - logger: logger, - } -} - -// Init initializes the StreamState -// Init must be called before any other methods. -func (ss *StreamState) Init() { - ss.wg.Add(1) - utils.ManagedGo(ss.initEventHandler, ss.wg.Done) - ss.wg.Add(1) - utils.ManagedGo(ss.initStreamSourceMonitor, ss.wg.Done) - ss.initialized.Store(true) -} - -// Increment increments the peer connections subscribed to the stream. -func (ss *StreamState) Increment(ctx context.Context) error { - if !ss.initialized.Load() { - return ErrUninitialized - } - if err := ss.closedCtx.Err(); err != nil { - return multierr.Combine(ErrClosed, err) - } - return ss.send(ctx, msgTypeIncrement) -} - -// Decrement decrements the peer connections subscribed to the stream. -func (ss *StreamState) Decrement(ctx context.Context) error { - if !ss.initialized.Load() { - return ErrUninitialized - } - if err := ss.closedCtx.Err(); err != nil { - return multierr.Combine(ErrClosed, err) - } - return ss.send(ctx, msgTypeDecrement) -} - -// Restart restarts the stream source after it has terminated. -func (ss *StreamState) Restart(ctx context.Context) { - if err := ss.closedCtx.Err(); err != nil { - return - } - utils.UncheckedError(ss.send(ctx, msgTypeRestart)) -} - -// Close closes the StreamState. -func (ss *StreamState) Close() error { - ss.closedFn() - ss.wg.Wait() - return nil -} - -// Internals - -const rtpBufferSize int = 512 - -type streamSource uint8 - -const ( - streamSourceUnknown streamSource = iota - streamSourceGoStream - streamSourcePassthrough -) - -func (s streamSource) String() string { - switch s { - case streamSourceGoStream: - return "GoStream" - case streamSourcePassthrough: - return "RTP Passthrough" - case streamSourceUnknown: - fallthrough - default: - return "Unknown" - } -} - -type msgType uint8 - -const ( - msgTypeUnknown msgType = iota - msgTypeIncrement - msgTypeDecrement - msgTypeRestart -) - -func (mt msgType) String() string { - switch mt { - case msgTypeIncrement: - return "Increment" - case msgTypeDecrement: - return "Decrement" - case msgTypeRestart: - return "Restart" - case msgTypeUnknown: - fallthrough - default: - return "Unknown" - } -} - -type msg struct { - msgType msgType - ctx context.Context - respChan chan error -} - -// events (Inc Dec Restart). -func (ss *StreamState) initEventHandler() { - ss.logger.Debug("StreamState initEventHandler booted") - defer ss.logger.Debug("StreamState initEventHandler terminated") - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), UnsubscribeTimeout) - defer cancel() - utils.UncheckedError(ss.stopBasedOnSub(ctx)) - }() - for { - if ss.closedCtx.Err() != nil { - return - } - - select { - case <-ss.closedCtx.Done(): - return - case msg := <-ss.msgChan: - ss.handleMsg(msg) - } - } -} - -func (ss *StreamState) initStreamSourceMonitor() { - for { - select { - case <-ss.closedCtx.Done(): - return - case <-ss.restartChan: - ctx, cancel := context.WithTimeout(ss.closedCtx, subscribeRTPTimeout) - ss.Restart(ctx) - cancel() - } - } -} - -func (ss *StreamState) monitorSubscription(sub rtppassthrough.Subscription) { - if ss.streamSource == streamSourceGoStream { - ss.logger.Debugf("monitorSubscription stopping gostream %s", ss.Stream.Name()) - // if we were streaming using gostream, stop streaming using gostream as we are now using passthrough - ss.Stream.Stop() - } - ss.streamSourceSub = sub - ss.streamSource = streamSourcePassthrough - monitorSubFunc := func() { - // if the stream state is shutting down, terminate - if ss.closedCtx.Err() != nil { - return - } - - select { - case <-ss.closedCtx.Done(): - return - case <-sub.Terminated.Done(): - select { - case ss.restartChan <- struct{}{}: - case <-ss.closedCtx.Done(): - } - return - } - } - - ss.wg.Add(1) - utils.ManagedGo(monitorSubFunc, ss.wg.Done) -} - -// caller must be holding ss.mu. -func (ss *StreamState) stopBasedOnSub(ctx context.Context) error { - switch ss.streamSource { - case streamSourceGoStream: - ss.logger.Debugf("%s stopBasedOnSub stopping GoStream", ss.Stream.Name()) - ss.Stream.Stop() - ss.streamSource = streamSourceUnknown - return nil - case streamSourcePassthrough: - ss.logger.Debugf("%s stopBasedOnSub stopping passthrough", ss.Stream.Name()) - err := ss.unsubscribeH264Passthrough(ctx, ss.streamSourceSub.ID) - if err != nil { - return err - } - ss.streamSourceSub = rtppassthrough.NilSubscription - ss.streamSource = streamSourceUnknown - return nil - - case streamSourceUnknown: - fallthrough - default: - return nil - } -} - -func (ss *StreamState) send(ctx context.Context, msgType msgType) error { - if err := ctx.Err(); err != nil { - return err - } - if err := ss.closedCtx.Err(); err != nil { - return err - } - msg := msg{ - ctx: ctx, - msgType: msgType, - respChan: make(chan error), - } - select { - case ss.msgChan <- msg: - select { - case err := <-msg.respChan: - return err - case <-ctx.Done(): - return ctx.Err() - case <-ss.closedCtx.Done(): - return ss.closedCtx.Err() - } - case <-ctx.Done(): - return ctx.Err() - case <-ss.closedCtx.Done(): - return ss.closedCtx.Err() - } -} - -func (ss *StreamState) handleMsg(msg msg) { - switch msg.msgType { - case msgTypeIncrement: - err := ss.inc(msg.ctx) - select { - case msg.respChan <- err: - case <-ss.closedCtx.Done(): - return - } - case msgTypeRestart: - ss.restart(msg.ctx) - select { - case msg.respChan <- nil: - case <-ss.closedCtx.Done(): - return - } - case msgTypeDecrement: - err := ss.dec(msg.ctx) - msg.respChan <- err - case msgTypeUnknown: - fallthrough - default: - ss.logger.Error("Invalid StreamState msg type received: %s", msg.msgType) - } -} - -func (ss *StreamState) inc(ctx context.Context) error { - ss.logger.Debugf("increment %s START activePeers: %d", ss.Stream.Name(), ss.activePeers) - defer func() { ss.logger.Debugf("increment %s END activePeers: %d", ss.Stream.Name(), ss.activePeers) }() - if ss.activePeers == 0 { - if ss.streamSource != streamSourceUnknown { - return fmt.Errorf("unexpected stream %s source %s", ss.Stream.Name(), ss.streamSource) - } - // this is the first subscription, attempt passthrough - ss.logger.CDebugw(ctx, "attempting to subscribe to rtp_passthrough", "name", ss.Stream.Name()) - err := ss.streamH264Passthrough(ctx) - if err != nil { - ss.logger.CDebugw(ctx, "rtp_passthrough not possible, falling back to GoStream", "err", err.Error(), "name", ss.Stream.Name()) - // if passthrough failed, fall back to gostream based approach - ss.Stream.Start() - ss.streamSource = streamSourceGoStream - } - ss.activePeers++ - return nil - } - - switch ss.streamSource { - case streamSourcePassthrough: - ss.logger.CDebugw(ctx, "continuing using rtp_passthrough", "name", ss.Stream.Name()) - // noop as we are already subscribed - case streamSourceGoStream: - ss.logger.CDebugw(ctx, "currently using gostream, trying upgrade to rtp_passthrough", "name", ss.Stream.Name()) - // attempt to cut over to passthrough - err := ss.streamH264Passthrough(ctx) - if err != nil { - ss.logger.CDebugw(ctx, "rtp_passthrough not possible, continuing with gostream", "err", err.Error(), "name", ss.Stream.Name()) - } - case streamSourceUnknown: - fallthrough - default: - err := fmt.Errorf("%s streamSource in unexpected state %s", ss.Stream.Name(), ss.streamSource) - ss.logger.Error(err.Error()) - return err - } - ss.activePeers++ - return nil -} - -func (ss *StreamState) dec(ctx context.Context) error { - ss.logger.Debugf("decrement START %s activePeers: %d", ss.Stream.Name(), ss.activePeers) - defer func() { ss.logger.Debugf("decrement END %s activePeers: %d", ss.Stream.Name(), ss.activePeers) }() - - var err error - defer func() { - if err != nil { - ss.logger.Errorf("decrement %s hit error: %s", ss.Stream.Name(), err.Error()) - return - } - ss.activePeers-- - if ss.activePeers <= 0 { - ss.activePeers = 0 - } - }() - if ss.activePeers == 1 { - ss.logger.Debugf("decrement %s calling stopBasedOnSub", ss.Stream.Name()) - err = ss.stopBasedOnSub(ctx) - if err != nil { - ss.logger.Error(err.Error()) - return err - } - } - return nil -} - -func (ss *StreamState) restart(ctx context.Context) { - ss.logger.Debugf("restart %s START activePeers: %d", ss.Stream.Name(), ss.activePeers) - defer func() { ss.logger.Debugf("restart %s END activePeers: %d", ss.Stream.Name(), ss.activePeers) }() - - if ss.activePeers == 0 { - // nothing to do if we don't have any active peers - return - } - - if ss.streamSource == streamSourceGoStream { - // nothing to do if stream source is gostream - return - } - - if ss.streamSourceSub != rtppassthrough.NilSubscription && ss.streamSourceSub.Terminated.Err() == nil { - // if the stream is still healthy, do nothing - return - } - - err := ss.streamH264Passthrough(ctx) - if err != nil { - ss.logger.CDebugw(ctx, "rtp_passthrough not possible, falling back to GoStream", "err", err.Error(), "name", ss.Stream.Name()) - // if passthrough failed, fall back to gostream based approach - ss.Stream.Start() - ss.streamSource = streamSourceGoStream - - return - } - // passthrough succeeded, listen for when subscription end and call start again if so -} - -func (ss *StreamState) streamH264Passthrough(ctx context.Context) error { - cam, err := camera.FromRobot(ss.robot, ss.Stream.Name()) - if err != nil { - return err - } - - rtpPassthroughSource, ok := cam.(rtppassthrough.Source) - if !ok { - err := fmt.Errorf("expected %s to implement rtppassthrough.Source", ss.Stream.Name()) - return errors.Wrap(ErrRTPPassthroughNotSupported, err.Error()) - } - - cb := func(pkts []*rtp.Packet) { - for _, pkt := range pkts { - if err := ss.Stream.WriteRTP(pkt); err != nil { - ss.logger.Debugw("stream.WriteRTP", "name", ss.Stream.Name(), "err", err.Error()) - } - } - } - - sub, err := rtpPassthroughSource.SubscribeRTP(ctx, rtpBufferSize, cb) - if err != nil { - return errors.Wrap(ErrRTPPassthroughNotSupported, err.Error()) - } - ss.logger.CWarnw(ctx, "Stream using experimental H264 passthrough", "name", ss.Stream.Name()) - ss.monitorSubscription(sub) - - return nil -} - -func (ss *StreamState) unsubscribeH264Passthrough(ctx context.Context, id rtppassthrough.SubscriptionID) error { - cam, err := camera.FromRobot(ss.robot, ss.Stream.Name()) - if err != nil { - return err - } - - rtpPassthroughSource, ok := cam.(rtppassthrough.Source) - if !ok { - err := fmt.Errorf("expected %s to implement rtppassthrough.Source", ss.Stream.Name()) - return errors.Wrap(ErrRTPPassthroughNotSupported, err.Error()) - } - - if err := rtpPassthroughSource.Unsubscribe(ctx, id); err != nil { - return err - } - - return nil -} diff --git a/robot/web/stream/state/state_test.go b/robot/web/stream/state/state_test.go deleted file mode 100644 index fe36c0f5c2f..00000000000 --- a/robot/web/stream/state/state_test.go +++ /dev/null @@ -1,823 +0,0 @@ -package state_test - -import ( - "context" - "errors" - "image" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/google/uuid" - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "github.com/pion/rtp" - "github.com/pion/webrtc/v3" - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/web/stream/state" - "go.viam.com/rdk/testutils/inject" -) - -type mockStream struct { - name string - t *testing.T - startFunc func() - stopFunc func() - writeRTPFunc func(*rtp.Packet) error -} - -func (mS *mockStream) Name() string { - return mS.name -} - -func (mS *mockStream) Start() { - mS.startFunc() -} - -func (mS *mockStream) Stop() { - mS.stopFunc() -} - -func (mS *mockStream) WriteRTP(pkt *rtp.Packet) error { - return mS.writeRTPFunc(pkt) -} - -// BEGIN Not tested gostream functions. -func (mS *mockStream) StreamingReady() (<-chan struct{}, context.Context) { - mS.t.Log("unimplemented") - mS.t.FailNow() - return nil, context.Background() -} - -func (mS *mockStream) InputVideoFrames(props prop.Video) (chan<- gostream.MediaReleasePair[image.Image], error) { - mS.t.Log("unimplemented") - mS.t.FailNow() - return nil, errors.New("unimplemented") -} - -func (mS *mockStream) InputAudioChunks(props prop.Audio) (chan<- gostream.MediaReleasePair[wave.Audio], error) { - mS.t.Log("unimplemented") - mS.t.FailNow() - return make(chan gostream.MediaReleasePair[wave.Audio]), nil -} - -func (mS *mockStream) VideoTrackLocal() (webrtc.TrackLocal, bool) { - mS.t.Log("unimplemented") - mS.t.FailNow() - return nil, false -} - -func (mS *mockStream) AudioTrackLocal() (webrtc.TrackLocal, bool) { - mS.t.Log("unimplemented") - mS.t.FailNow() - return nil, false -} - -type mockRTPPassthroughSource struct { - subscribeRTPFunc func( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, - ) (rtppassthrough.Subscription, error) - unsubscribeFunc func( - ctx context.Context, - id rtppassthrough.SubscriptionID, - ) error -} - -func (s *mockRTPPassthroughSource) SubscribeRTP( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, -) (rtppassthrough.Subscription, error) { - return s.subscribeRTPFunc(ctx, bufferSize, packetsCB) -} - -func (s *mockRTPPassthroughSource) Unsubscribe( - ctx context.Context, - id rtppassthrough.SubscriptionID, -) error { - return s.unsubscribeFunc(ctx, id) -} - -var camName = "my-cam" - -// END Not tested gostream functions. -func mockRobot(s rtppassthrough.Source) robot.Robot { - robot := &inject.Robot{} - robot.MockResourcesFromMap(map[resource.Name]resource.Resource{ - camera.Named(camName): &inject.Camera{RTPPassthroughSource: s}, - }) - return robot -} - -func TestStreamState(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - t.Run("Stream returns the provided stream", func(t *testing.T) { - mockRTPPassthroughSource := &mockRTPPassthroughSource{} - robot := mockRobot(mockRTPPassthroughSource) - streamMock := &mockStream{name: camName, t: t} - s := state.New(streamMock, robot, logger) - test.That(t, s.Stream, test.ShouldEqual, streamMock) - }) - - t.Run("close succeeds if no methods have been called", func(t *testing.T) { - mockRTPPassthroughSource := &mockRTPPassthroughSource{} - robot := mockRobot(mockRTPPassthroughSource) - streamMock := &mockStream{name: camName, t: t} - s := state.New(streamMock, robot, logger) - test.That(t, s.Close(), test.ShouldBeNil) - }) - - t.Run("Increment() returns an error if Init() is not called first", func(t *testing.T) { - mockRTPPassthroughSource := &mockRTPPassthroughSource{} - robot := mockRobot(mockRTPPassthroughSource) - streamMock := &mockStream{name: camName, t: t} - s := state.New(streamMock, robot, logger) - test.That(t, s.Increment(ctx), test.ShouldBeError, state.ErrUninitialized) - }) - - t.Run("Decrement() returns an error if Init() is not called first", func(t *testing.T) { - mockRTPPassthroughSource := &mockRTPPassthroughSource{} - robot := mockRobot(mockRTPPassthroughSource) - streamMock := &mockStream{name: camName, t: t} - s := state.New(streamMock, robot, logger) - test.That(t, s.Decrement(ctx), test.ShouldBeError, state.ErrUninitialized) - }) - - t.Run("Increment() returns an error if called after Close()", func(t *testing.T) { - mockRTPPassthroughSource := &mockRTPPassthroughSource{} - robot := mockRobot(mockRTPPassthroughSource) - streamMock := &mockStream{name: camName, t: t} - s := state.New(streamMock, robot, logger) - s.Init() - s.Close() - test.That(t, s.Increment(ctx), test.ShouldWrap, state.ErrClosed) - }) - - t.Run("Decrement() returns an error if called after Close()", func(t *testing.T) { - mockRTPPassthroughSource := &mockRTPPassthroughSource{} - robot := mockRobot(mockRTPPassthroughSource) - streamMock := &mockStream{name: camName, t: t} - s := state.New(streamMock, robot, logger) - s.Init() - s.Close() - test.That(t, s.Decrement(ctx), test.ShouldWrap, state.ErrClosed) - }) - - t.Run("when rtppassthrough.Souce is provided but SubscribeRTP always returns an error", func(t *testing.T) { - var startCount atomic.Int64 - var stopCount atomic.Int64 - streamMock := &mockStream{ - name: camName, - t: t, - startFunc: func() { - startCount.Add(1) - }, - stopFunc: func() { - stopCount.Add(1) - }, - writeRTPFunc: func(pkt *rtp.Packet) error { - t.Log("should not happen") - t.FailNow() - return nil - }, - } - - var subscribeRTPCount atomic.Int64 - - subscribeRTPFunc := func( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, - ) (rtppassthrough.Subscription, error) { - subscribeRTPCount.Add(1) - return rtppassthrough.NilSubscription, errors.New("unimplemented") - } - - unsubscribeFunc := func(ctx context.Context, id rtppassthrough.SubscriptionID) error { - t.Log("should not happen") - t.FailNow() - return errors.New("unimplemented") - } - - mockRTPPassthroughSource := &mockRTPPassthroughSource{ - subscribeRTPFunc: subscribeRTPFunc, - unsubscribeFunc: unsubscribeFunc, - } - robot := mockRobot(mockRTPPassthroughSource) - s := state.New(streamMock, robot, logger) - defer func() { utils.UncheckedError(s.Close()) }() - s.Init() - - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 0) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("the first Increment() calls SubscribeRTP and then calls Start() when an error is reurned") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 1) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("subsequent Increment() all calls call SubscribeRTP trying to determine " + - "if they can upgrade but don't call any other gostream methods as SubscribeRTP returns an error") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("as long as the number of Decrement() calls is less than the number of Increment() calls, no gostream methods are called") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("when the number of Decrement() calls is equal to the number of Increment() calls stop is called") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 1) - - t.Log("then when the number of Increment() calls exceeds Decrement(), both SubscribeRTP & Start are called again") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 4) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 1) - - t.Log("calling Decrement() more times than Increment() has a floor of zero") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 4) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // multiple Decrement() calls when the count is already at zero doesn't call any methods - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 4) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // once the count is at zero , calling Increment() again calls SubscribeRTP and when it returns an error Start - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 5) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // set count back to zero - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 5) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - t.Log("calling Increment() with a cancelled context returns an error & does not call any gostream or rtppassthrough.Source methods") - canceledCtx, cancelFn := context.WithCancel(context.Background()) - cancelFn() - test.That(t, s.Increment(canceledCtx), test.ShouldBeError, context.Canceled) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 5) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - // make it so that non cancelled Decrement() would call stop to confirm that does not happen when context is cancelled - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 6) - test.That(t, startCount.Load(), test.ShouldEqual, 4) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - t.Log("calling Decrement() with a cancelled context returns an error & does not call any gostream methods") - test.That(t, s.Decrement(canceledCtx), test.ShouldBeError, context.Canceled) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 6) - test.That(t, startCount.Load(), test.ShouldEqual, 4) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - }) - - t.Run("when rtppassthrough.Souce is provided and SubscribeRTP doesn't return an error", func(t *testing.T) { - writeRTPCalledCtx, writeRTPCalledFunc := context.WithCancel(ctx) - streamMock := &mockStream{ - name: camName, - t: t, - startFunc: func() { - t.Logf("should not be called") - t.FailNow() - }, - stopFunc: func() { - t.Logf("should not be called") - t.FailNow() - }, - writeRTPFunc: func(pkt *rtp.Packet) error { - // Test that WriteRTP is eventually called when SubscribeRTP is called - writeRTPCalledFunc() - return nil - }, - } - - var subscribeRTPCount atomic.Int64 - var unsubscribeCount atomic.Int64 - type subAndCancel struct { - sub rtppassthrough.Subscription - cancelFn context.CancelFunc - wg *sync.WaitGroup - } - - var subsAndCancelByIDMu sync.Mutex - subsAndCancelByID := map[rtppassthrough.SubscriptionID]subAndCancel{} - - subscribeRTPFunc := func( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, - ) (rtppassthrough.Subscription, error) { - subsAndCancelByIDMu.Lock() - defer subsAndCancelByIDMu.Unlock() - defer subscribeRTPCount.Add(1) - terminatedCtx, terminatedFn := context.WithCancel(context.Background()) - id := uuid.New() - sub := rtppassthrough.Subscription{ID: id, Terminated: terminatedCtx} - subsAndCancelByID[id] = subAndCancel{sub: sub, cancelFn: terminatedFn, wg: &sync.WaitGroup{}} - subsAndCancelByID[id].wg.Add(1) - utils.ManagedGo(func() { - for terminatedCtx.Err() == nil { - packetsCB([]*rtp.Packet{{}}) - time.Sleep(time.Millisecond * 50) - } - }, subsAndCancelByID[id].wg.Done) - return sub, nil - } - - unsubscribeFunc := func(ctx context.Context, id rtppassthrough.SubscriptionID) error { - subsAndCancelByIDMu.Lock() - defer subsAndCancelByIDMu.Unlock() - defer unsubscribeCount.Add(1) - subAndCancel, ok := subsAndCancelByID[id] - if !ok { - t.Logf("Unsubscribe called with unknown id: %s", id.String()) - t.FailNow() - } - subAndCancel.cancelFn() - return nil - } - - mockRTPPassthroughSource := &mockRTPPassthroughSource{ - subscribeRTPFunc: subscribeRTPFunc, - unsubscribeFunc: unsubscribeFunc, - } - robot := mockRobot(mockRTPPassthroughSource) - s := state.New(streamMock, robot, logger) - defer func() { utils.UncheckedError(s.Close()) }() - - s.Init() - - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 0) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, writeRTPCalledCtx.Err(), test.ShouldBeNil) - - t.Log("the first Increment() call calls SubscribeRTP()") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 1) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - // WriteRTP is called - <-writeRTPCalledCtx.Done() - - t.Log("subsequent Increment() calls don't call any other rtppassthrough.Source methods") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 1) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - - t.Log("as long as the number of Decrement() calls is less than the number " + - "of Increment() calls, no rtppassthrough.Source methods are called") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 1) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - - t.Log("when the number of Decrement() calls is equal to the number of Increment() calls stop is called") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 1) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 1) - - t.Log("then when the number of Increment() calls exceeds Decrement(), SubscribeRTP is called again") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 2) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 1) - - t.Log("calling Decrement() more times than Increment() has a floor of zero") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 2) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 2) - - // multiple Decrement() calls when the count is already at zero doesn't call any methods - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 2) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 2) - - // once the count is at zero , calling Increment() again calls start - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 2) - - // set count back to zero - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 3) - - t.Log("calling Increment() with a cancelled context returns an error & does not call any rtppassthrough.Source methods") - canceledCtx, cancelFn := context.WithCancel(context.Background()) - cancelFn() - test.That(t, s.Increment(canceledCtx), test.ShouldBeError, context.Canceled) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 3) - - // make it so that non cancelled Decrement() would call stop to confirm that does not happen when context is cancelled - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 4) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 3) - - t.Log("calling Decrement() with a cancelled context returns an error & does not call any rtppassthrough.Source methods") - test.That(t, s.Decrement(canceledCtx), test.ShouldBeError, context.Canceled) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 4) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 3) - - t.Log("when the subscription terminates while there are still subscribers, SubscribeRTP is called again") - var cancelledSubs int - subsAndCancelByIDMu.Lock() - for _, subAndCancel := range subsAndCancelByID { - if subAndCancel.sub.Terminated.Err() == nil { - subAndCancel.cancelFn() - cancelledSubs++ - } - } - subsAndCancelByIDMu.Unlock() - test.That(t, cancelledSubs, test.ShouldEqual, 1) - - // wait unil SubscribeRTP is called - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - for { - if timeoutCtx.Err() != nil { - t.Log("timed out waiting for a new sub to be created after an in progress one terminated unexpectedly") - t.FailNow() - } - if subscribeRTPCount.Load() == 5 { - break - } - time.Sleep(time.Millisecond * 50) - } - - // cancel all mock subs - subsAndCancelByIDMu.Lock() - for _, subAndCancel := range subsAndCancelByID { - subAndCancel.cancelFn() - subAndCancel.wg.Wait() - } - subsAndCancelByIDMu.Unlock() - }) - - t.Run("when rtppassthrough.Souce is provided and sometimes returns an errors "+ - "(test rtp_passthrough/gostream upgrade/downgrade path)", func(t *testing.T) { - var startCount atomic.Int64 - var stopCount atomic.Int64 - streamMock := &mockStream{ - name: camName, - t: t, - startFunc: func() { - startCount.Add(1) - }, - stopFunc: func() { - stopCount.Add(1) - }, - writeRTPFunc: func(pkt *rtp.Packet) error { - return nil - }, - } - - var subscribeRTPCount atomic.Int64 - var unsubscribeCount atomic.Int64 - type subAndCancel struct { - sub rtppassthrough.Subscription - cancelFn context.CancelFunc - } - var subsAndCancelByIDMu sync.Mutex - subsAndCancelByID := map[rtppassthrough.SubscriptionID]subAndCancel{} - - var subscribeRTPReturnError atomic.Bool - subscribeRTPFunc := func( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, - ) (rtppassthrough.Subscription, error) { - subsAndCancelByIDMu.Lock() - defer subsAndCancelByIDMu.Unlock() - defer subscribeRTPCount.Add(1) - if subscribeRTPReturnError.Load() { - return rtppassthrough.NilSubscription, errors.New("SubscribeRTP returned error") - } - terminatedCtx, terminatedFn := context.WithCancel(context.Background()) - id := uuid.New() - sub := rtppassthrough.Subscription{ID: id, Terminated: terminatedCtx} - subsAndCancelByID[id] = subAndCancel{sub: sub, cancelFn: terminatedFn} - return sub, nil - } - - unsubscribeFunc := func(ctx context.Context, id rtppassthrough.SubscriptionID) error { - subsAndCancelByIDMu.Lock() - defer subsAndCancelByIDMu.Unlock() - defer unsubscribeCount.Add(1) - subAndCancel, ok := subsAndCancelByID[id] - if !ok { - t.Logf("Unsubscribe called with unknown id: %s", id.String()) - t.FailNow() - } - subAndCancel.cancelFn() - return nil - } - - mockRTPPassthroughSource := &mockRTPPassthroughSource{ - subscribeRTPFunc: subscribeRTPFunc, - unsubscribeFunc: unsubscribeFunc, - } - robot := mockRobot(mockRTPPassthroughSource) - s := state.New(streamMock, robot, logger) - defer func() { utils.UncheckedError(s.Close()) }() - - // start with RTPPassthrough being supported - s.Init() - - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 0) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 0) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("the first Increment() call calls SubscribeRTP() which returns a success") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 1) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 0) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("subsequent Increment() calls don't call any other rtppassthrough.Source or gostream methods") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 1) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 0) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("if the subscription terminates and SubscribeRTP() returns an error, starts gostream") - subscribeRTPReturnError.Store(true) - subsAndCancelByIDMu.Lock() - test.That(t, len(subsAndCancelByID), test.ShouldEqual, 1) - for _, s := range subsAndCancelByID { - s.cancelFn() - } - subsAndCancelByIDMu.Unlock() - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - for { - if timeoutCtx.Err() != nil { - t.Log("timed out waiting for gostream start to be called on stream which terminated unexpectedly") - t.FailNow() - } - if subscribeRTPCount.Load() == 2 { - break - } - time.Sleep(time.Millisecond * 50) - } - - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 2) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("when the number of Decrement() calls is less than the number of " + - "Increment() calls no rtppassthrough.Source or gostream methods are called") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 2) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("when the number of Decrement() calls is equal to the number of " + - "Increment() calls stop is called (as gostream is the data source)") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 2) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 1) - - t.Log("then when the number of Increment() calls exceeds Decrement(), " + - "SubscribeRTP is called again followed by Start if it returns an error") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 1) - - t.Log("calling Decrement() more times than Increment() has a floor of zero and calls Stop()") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // multiple Decrement() calls when the count is already at zero doesn't call any methods - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 3) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // once the count is at zero , calling Increment() again calls SubscribeRTP followed by Start - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 4) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - t.Log("if while gostream is being used Increment is called and SubscribeRTP succeeds, Stop is called") - subscribeRTPReturnError.Store(false) - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 5) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - // calling Decrement() fewer times than Increment() doesn't call any methods - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 5) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - t.Log("calling Decrement() more times than Increment() has a floor of zero and calls Unsubscribe()") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 5) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 1) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - // multiple Decrement() calls when the count is already at zero doesn't call any methods - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 5) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 1) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - t.Log("if while rtp_passthrough is being used the the subscription " + - "terminates & afterwards rtp_passthrough is no longer supported, Start is called") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 6) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 1) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - subscribeRTPReturnError.Store(true) - - subsAndCancelByIDMu.Lock() - for _, s := range subsAndCancelByID { - s.cancelFn() - } - subsAndCancelByIDMu.Unlock() - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - for { - if timeoutCtx.Err() != nil { - t.Log("timed out waiting for Start() to be called after an in progress sub terminated unexpectedly") - t.FailNow() - } - if startCount.Load() == 4 { - break - } - time.Sleep(time.Millisecond * 50) - } - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 7) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 1) - test.That(t, startCount.Load(), test.ShouldEqual, 4) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - // Decrement() calls Stop() as gostream is the data source - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 7) - test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 1) - test.That(t, startCount.Load(), test.ShouldEqual, 4) - test.That(t, stopCount.Load(), test.ShouldEqual, 4) - }) - - t.Run("when the camera does not implement rtppassthrough.Souce", func(t *testing.T) { - var startCount atomic.Int64 - var stopCount atomic.Int64 - streamMock := &mockStream{ - name: "my-cam", - t: t, - startFunc: func() { - startCount.Add(1) - }, - stopFunc: func() { - stopCount.Add(1) - }, - writeRTPFunc: func(pkt *rtp.Packet) error { - t.Log("should not happen") - t.FailNow() - return nil - }, - } - robot := mockRobot(nil) - s := state.New(streamMock, robot, logger) - defer func() { utils.UncheckedError(s.Close()) }() - s.Init() - - t.Log("the first Increment() -> Start()") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("subsequent Increment() all calls don't call any other gostream methods") - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("as long as the number of Decrement() calls is less than the number of Increment() calls, no gostream methods are called") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 0) - - t.Log("when the number of Decrement() calls is equal to the number of Increment() calls stop is called") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 1) - test.That(t, stopCount.Load(), test.ShouldEqual, 1) - test.That(t, s.Increment(ctx), test.ShouldBeNil) - - t.Log("then when the number of Increment() calls exceeds Decrement(), Start is called again") - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 1) - - t.Log("calling Decrement() more times than Increment() has a floor of zero") - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // multiple Decrement() calls when the count is already at zero doesn't call any methods - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 2) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // once the count is at zero , calling Increment() again calls start - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 2) - - // set count back to zero - test.That(t, s.Decrement(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - t.Log("calling Increment() with a cancelled context returns an error & does not call any gostream methods") - canceledCtx, cancelFn := context.WithCancel(context.Background()) - cancelFn() - test.That(t, s.Increment(canceledCtx), test.ShouldBeError, context.Canceled) - test.That(t, startCount.Load(), test.ShouldEqual, 3) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - // make it so that non cancelled Decrement() would call stop to confirm that does not happen when context is cancelled - test.That(t, s.Increment(ctx), test.ShouldBeNil) - test.That(t, startCount.Load(), test.ShouldEqual, 4) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - - t.Log("calling Decrement() with a cancelled context returns an error & does not call any gostream methods") - test.That(t, s.Decrement(canceledCtx), test.ShouldBeError, context.Canceled) - test.That(t, startCount.Load(), test.ShouldEqual, 4) - test.That(t, stopCount.Load(), test.ShouldEqual, 3) - }) -} diff --git a/robot/web/stream/stream.go b/robot/web/stream/stream.go deleted file mode 100644 index 554d19d15e3..00000000000 --- a/robot/web/stream/stream.go +++ /dev/null @@ -1,92 +0,0 @@ -//go:build !no_cgo || android - -// Package webstream provides controls for streaming from the web server. -package webstream - -import ( - "context" - "errors" - "math" - "time" - - "go.viam.com/utils" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" -) - -// StreamVideoSource starts a stream from a video source with a throttled error handler. -func StreamVideoSource( - ctx context.Context, - source gostream.VideoSource, - stream gostream.Stream, - backoffOpts *BackoffTuningOptions, - logger logging.Logger, -) error { - return gostream.StreamVideoSourceWithErrorHandler(ctx, source, stream, backoffOpts.getErrorThrottledHandler(logger)) -} - -// StreamAudioSource starts a stream from an audio source with a throttled error handler. -func StreamAudioSource( - ctx context.Context, - source gostream.AudioSource, - stream gostream.Stream, - backoffOpts *BackoffTuningOptions, - logger logging.Logger, -) error { - return gostream.StreamAudioSourceWithErrorHandler(ctx, source, stream, backoffOpts.getErrorThrottledHandler(logger)) -} - -// BackoffTuningOptions represents a set of parameters for determining exponential -// backoff when receiving multiple simultaneous errors. -// -// BaseSleep is the duration to wait after receiving a new error. After that, the wait -// time doubles for every subsequent, consecutive error of the same type, until the wait -// duration reaches the MaxSleep duration. -type BackoffTuningOptions struct { - // BaseSleep sets the initial amount of time to wait after an error. - BaseSleep time.Duration - // MaxSleep determines the maximum amount of time that streamSource is - // permitted to a sleep after receiving a single error. - MaxSleep time.Duration - // Cooldown sets how long since the last error that we can reset our backoff. This - // should be greater than MaxSleep. This prevents a scenario where we haven't made - // a call to read for a long time and the error may go away sooner. - Cooldown time.Duration -} - -// GetSleepTimeFromErrorCount returns a sleep time from an error count. -func (opts *BackoffTuningOptions) GetSleepTimeFromErrorCount(errorCount int) time.Duration { - if errorCount < 1 || opts == nil { - return 0 - } - multiplier := math.Pow(2, float64(errorCount-1)) - uncappedSleep := opts.BaseSleep * time.Duration(multiplier) - sleep := math.Min(float64(uncappedSleep), float64(opts.MaxSleep)) - return time.Duration(sleep) -} - -func (opts *BackoffTuningOptions) getErrorThrottledHandler(logger logging.Logger) func(context.Context, error) { - var prevErr error - var errorCount int - lastErrTime := time.Now() - - return func(ctx context.Context, err error) { - now := time.Now() - if now.Sub(lastErrTime) > opts.Cooldown { - errorCount = 0 - } - lastErrTime = now - - if errors.Is(prevErr, err) { - errorCount++ - } else { - prevErr = err - errorCount = 1 - } - - sleep := opts.GetSleepTimeFromErrorCount(errorCount) - logger.Errorw("error getting media", "error", err, "count", errorCount, "sleep", sleep) - utils.SelectContextOrWait(ctx, sleep) - } -} diff --git a/robot/web/stream/stream_test.go b/robot/web/stream/stream_test.go deleted file mode 100644 index 7518598767a..00000000000 --- a/robot/web/stream/stream_test.go +++ /dev/null @@ -1,313 +0,0 @@ -//go:build !no_cgo - -package webstream_test - -import ( - "context" - "errors" - "image" - "strings" - "sync" - "testing" - "time" - - "github.com/pion/mediadevices/pkg/prop" - "github.com/pion/mediadevices/pkg/wave" - "github.com/pion/rtp" - "github.com/pion/webrtc/v3" - streampb "go.viam.com/api/stream/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - "go.viam.com/utils/testutils" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec/opus" - "go.viam.com/rdk/gostream/codec/x264" - rgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/robot/web" - webstream "go.viam.com/rdk/robot/web/stream" - "go.viam.com/rdk/testutils/robottestutils" -) - -var errImageRetrieval = errors.New("image retrieval failed") - -type mockErrorVideoSource struct { - callsLeft int - wg sync.WaitGroup -} - -func newMockErrorVideoReader(expectedCalls int) *mockErrorVideoSource { - mock := &mockErrorVideoSource{callsLeft: expectedCalls} - mock.wg.Add(expectedCalls) - return mock -} - -func (videoSource *mockErrorVideoSource) Read(ctx context.Context) (image.Image, func(), error) { - if videoSource.callsLeft > 0 { - videoSource.wg.Done() - videoSource.callsLeft-- - } - return nil, nil, errImageRetrieval -} - -func (videoSource *mockErrorVideoSource) Close(ctx context.Context) error { - return nil -} - -type mockStream struct { - name string - streamingReadyFunc func() <-chan struct{} - inputFramesFunc func() (chan<- gostream.MediaReleasePair[image.Image], error) -} - -func (mS *mockStream) StreamingReady() (<-chan struct{}, context.Context) { - return mS.streamingReadyFunc(), context.Background() -} - -func (mS *mockStream) InputVideoFrames(props prop.Video) (chan<- gostream.MediaReleasePair[image.Image], error) { - return mS.inputFramesFunc() -} - -func (mS *mockStream) InputAudioChunks(props prop.Audio) (chan<- gostream.MediaReleasePair[wave.Audio], error) { - return make(chan gostream.MediaReleasePair[wave.Audio]), nil -} - -func (mS *mockStream) Name() string { - return mS.name -} - -func (mS *mockStream) Start() { -} - -func (mS *mockStream) Stop() { -} - -func (mS *mockStream) WriteRTP(*rtp.Packet) error { - return nil -} - -func (mS *mockStream) VideoTrackLocal() (webrtc.TrackLocal, bool) { - return nil, false -} - -func (mS *mockStream) AudioTrackLocal() (webrtc.TrackLocal, bool) { - return nil, false -} - -func TestStreamSourceErrorBackoff(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, cancel := context.WithCancel(context.Background()) - - backoffOpts := &webstream.BackoffTuningOptions{ - BaseSleep: 50 * time.Microsecond, - MaxSleep: 250 * time.Millisecond, - Cooldown: time.Second, - } - calls := 25 - videoReader := newMockErrorVideoReader(calls) - videoSrc := gostream.NewVideoSource(videoReader, prop.Video{}) - defer func() { - test.That(t, videoSrc.Close(context.Background()), test.ShouldBeNil) - }() - - totalExpectedSleep := int64(0) - // Note that we do not add the expected sleep duration for the last error since the - // streaming context will be cancelled during that error. - for i := 1; i < calls; i++ { - totalExpectedSleep += backoffOpts.GetSleepTimeFromErrorCount(i).Nanoseconds() - } - str := &mockStream{} - readyChan := make(chan struct{}) - inputChan := make(chan gostream.MediaReleasePair[image.Image]) - str.streamingReadyFunc = func() <-chan struct{} { - return readyChan - } - str.inputFramesFunc = func() (chan<- gostream.MediaReleasePair[image.Image], error) { - return inputChan, nil - } - - go webstream.StreamVideoSource(ctx, videoSrc, str, backoffOpts, logger) - start := time.Now() - readyChan <- struct{}{} - videoReader.wg.Wait() - cancel() - - duration := time.Since(start).Nanoseconds() - test.That(t, duration, test.ShouldBeGreaterThanOrEqualTo, totalExpectedSleep) -} - -// setupRealRobot creates a robot from the input config and starts a WebRTC server with video -// streaming capabilities. -// -//nolint:lll -func setupRealRobot(t *testing.T, robotConfig *config.Config, logger logging.Logger) (context.Context, robot.LocalRobot, string, web.Service) { - t.Helper() - - ctx := context.Background() - robot, err := robotimpl.RobotFromConfig(ctx, robotConfig, logger) - test.That(t, err, test.ShouldBeNil) - - // We initialize with a stream config such that the stream server is capable of creating video stream and - // audio stream data. - webSvc := web.New(robot, logger, web.WithStreamConfig(gostream.StreamConfig{ - AudioEncoderFactory: opus.NewEncoderFactory(), - VideoEncoderFactory: x264.NewEncoderFactory(), - })) - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err = webSvc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - return ctx, robot, addr, webSvc -} - -// TestAudioTrackIsNotCreatedForVideoStream asserts that starting a video stream from a camera does -// not create an audio stream. It's not a business requirement that camera streams remain -// silent. Rather there was intentional code for cameras at robot startup to only register -// themselves as video stream sources. But cameras added after robot startup would not behave the -// same. This test asserts both paths to adding a camera agree on whether to create WebRTC audio -// tracks. -func TestAudioTrackIsNotCreatedForVideoStream(t *testing.T) { - // Pion has a bug where `PeerConnection.AddTrack` is non-deterministic w.r.t creating a media - // track in the SDP due to races in their renegotiation piggy-backing algorithm. This is easy to - // reproduce by runnnig the test in a loop. - t.Skip("RSDK-7492") - logger := logging.NewTestLogger(t).Sublogger("TestWebReconfigure") - - origCfg := &config.Config{Components: []resource.Config{ - { - Name: "origCamera", - API: resource.NewAPI("rdk", "component", "camera"), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - Animated: true, - Width: 100, - Height: 50, - }, - }, - }} - - // Create a robot with a single fake camera. - ctx, robot, addr, webSvc := setupRealRobot(t, origCfg, logger) - - defer robot.Close(ctx) - defer webSvc.Close(ctx) - - // Create a client connection to the robot. Disable direct GRPC to force a WebRTC - // connection. Fail if a WebRTC connection cannot be made. - conn, err := rgrpc.Dial(context.Background(), addr, logger.Sublogger("TestDial"), rpc.WithDisableDirectGRPC()) - //nolint - defer conn.Close() - test.That(t, err, test.ShouldBeNil) - - // Get a handle on the camera client named in the robot config. - cam, err := camera.NewClientFromConn(context.Background(), conn, "", camera.Named("origCamera"), logger) - test.That(t, err, test.ShouldBeNil) - defer cam.Close(ctx) - - // Test that getting a single image succeeds. - _, _, err = cam.Images(ctx) - test.That(t, err, test.ShouldBeNil) - - // Create a stream client. Listing the streams should give back a single stream named `origCamera`; - // after our component name. - livestreamClient := streampb.NewStreamServiceClient(conn) - listResp, err := livestreamClient.ListStreams(ctx, &streampb.ListStreamsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, listResp.Names, test.ShouldResemble, []string{"origCamera"}) - - // Assert there are no video/audio tracks on the peer connection. - test.That(t, conn.PeerConn().LocalDescription().SDP, test.ShouldNotContainSubstring, "m=video") - test.That(t, conn.PeerConn().LocalDescription().SDP, test.ShouldNotContainSubstring, "m=audio") - - logger.Info("Adding stream `origCamera`") - // Call the `AddStream` endpoint to request that the server start streaming video backed by the - // robot component named `origCamera`. This should result in a non-error response. - _, err = livestreamClient.AddStream(ctx, &streampb.AddStreamRequest{ - Name: "origCamera", - }) - test.That(t, err, test.ShouldBeNil) - - // Upon receiving the `AddStreamRequest`, the robot server will renegotiate the PeerConnection - // to add a video track. This happens asynchronously. Wait for the video track to appear. - testutils.WaitForAssertion(t, func(tb testing.TB) { - test.That(tb, conn.PeerConn().CurrentLocalDescription().SDP, test.ShouldContainSubstring, "m=video") - }) - // Until we support sending a camera's video and audio data, sending an `AddStreamRequest` for a - // camera should only create a video track. Assert the audio track does not exist. - test.That(t, conn.PeerConn().CurrentLocalDescription().SDP, test.ShouldNotContainSubstring, "m=audio") - - // Dan: While testing adding a new camera + stream, it made sense to me to call `RemoveStream` - // on `origCamera`. However, I couldn't assert any observable state change on the client - // PeerConnection. Specifically, when adding the first stream we can see the client peer - // connection object go from not having any video tracks to having one video track with the - // property of `a=recvonly`. The server similarly creates a video track, but it takes on the - // property of `a=sendrecv`. - // - // When removing the stream, the server keeps the video track, but its state is now in - // `a=recvonly`. Which I suppose is reasonable. And while I am experimentally observing that - // this does result in an "offer" being presented to the client, the client ultimately does not - // change its video track state. The client's video track remains in the SDP and it stays in the - // `a=recvonly` state. I believe I've observed in chrome the video track gets moved into an - // `a=inactive` state. - // - // Given the client SDP seems to remain logically the same, I've commented out this part of the - // test. - // - // logger.Info("Removing stream `origCamera`") - // _, err = livestreamClient.RemoveStream(ctx, &streampb.RemoveStreamRequest{ - // Name: "origCamera", - // }) - - logger.Info("Reconfiguring with `newCamera`") - // Update the config to add a `newCamera`. - origCfg.Components = append(origCfg.Components, resource.Config{ - Name: "newCamera", - API: resource.NewAPI("rdk", "component", "camera"), - Model: resource.DefaultModelFamily.WithModel("fake"), - ConvertedAttributes: &fake.Config{ - Animated: true, - Width: 100, - Height: 50, - }, - }) - // And reconfigure the robot which will create a `newCamera` component. - robot.Reconfigure(ctx, origCfg) - - // Reconfigure the websvc to update the available video streams. Such that `newCamera` is picked - // up. - // - // Dan: It's unclear to me if this is the intended way to do this. As signified by the nil/empty - // inputs. - webSvc.Reconfigure(ctx, nil, resource.Config{}) - - // Listing the streams now should return both `origCamera` and `newCamera`. - listResp, err = livestreamClient.ListStreams(ctx, &streampb.ListStreamsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, listResp.Names, test.ShouldResemble, []string{"origCamera", "newCamera"}) - - logger.Info("Adding stream `newCamera`") - // Call the `AddStream` endpoint to request that the server start streaming video backed by the - // robot component named `newCamera`. This should result in a non-error response. - _, err = livestreamClient.AddStream(ctx, &streampb.AddStreamRequest{ - Name: "newCamera", - }) - test.That(t, err, test.ShouldBeNil) - - // Upon receiving the `AddStreamRequest`, the robot server will renegotiate the PeerConnection - // to add a video track. This happens asynchronously. Wait for the video track to appear. - testutils.WaitForAssertion(t, func(tb testing.TB) { - videoCnt := strings.Count(conn.PeerConn().CurrentLocalDescription().SDP, "m=video") - test.That(tb, videoCnt, test.ShouldEqual, 2) - }) - - // Until we support sending a camera's video and audio data, sending an `AddStreamRequest` for a - // camera should only create a video track. Assert the audio track does not exist. - test.That(t, conn.PeerConn().CurrentLocalDescription().SDP, test.ShouldNotContainSubstring, "m=audio") -} diff --git a/robot/web/verify_main_test.go b/robot/web/verify_main_test.go deleted file mode 100644 index 20b56063689..00000000000 --- a/robot/web/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package web - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/robot/web/web_c.go b/robot/web/web_c.go index 5c8cb21dac9..ac978c24843 100644 --- a/robot/web/web_c.go +++ b/robot/web/web_c.go @@ -5,27 +5,18 @@ package web import ( "bytes" "context" - "math" "net/http" - "runtime" - "slices" "sync" - "time" "github.com/pkg/errors" streampb "go.viam.com/api/stream/v1" - "go.viam.com/utils" "go.viam.com/utils/rpc" - "go.viam.com/rdk/components/audioinput" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/gostream" "go.viam.com/rdk/logging" "go.viam.com/rdk/resource" "go.viam.com/rdk/robot" weboptions "go.viam.com/rdk/robot/web/options" webstream "go.viam.com/rdk/robot/web/stream" - rutils "go.viam.com/rdk/utils" ) // StreamServer manages streams and displays. @@ -37,11 +28,7 @@ type StreamServer struct { } // New returns a new web service for the given robot. -func New(r robot.Robot, logger logging.Logger, opts ...Option) Service { - var wOpts options - for _, opt := range opts { - opt.apply(&wOpts) - } +func New(r robot.Robot, logger logging.Logger) Service { webSvc := &webService{ Named: InternalServiceName.AsNamed(), r: r, @@ -49,9 +36,6 @@ func New(r robot.Robot, logger logging.Logger, opts ...Option) Service { rpcServer: nil, streamServer: nil, services: map[resource.API]resource.APIResourceCollection[resource.Resource]{}, - opts: wOpts, - videoSources: map[string]gostream.HotSwappableVideoSource{}, - audioSources: map[string]gostream.HotSwappableAudioSource{}, } return webSvc } @@ -65,7 +49,6 @@ type webService struct { modServer rpc.Server streamServer *StreamServer services map[resource.API]resource.APIResourceCollection[resource.Resource] - opts options addr string modAddr string logger logging.Logger @@ -74,249 +57,11 @@ type webService struct { isRunning bool webWorkers sync.WaitGroup modWorkers sync.WaitGroup - - videoSources map[string]gostream.HotSwappableVideoSource - audioSources map[string]gostream.HotSwappableAudioSource -} - -func (svc *webService) streamInitialized() bool { - return svc.streamServer != nil && svc.streamServer.Server != nil -} - -func (svc *webService) addNewStreams(ctx context.Context) error { - if !svc.streamInitialized() { - return nil - } - svc.refreshVideoSources() - svc.refreshAudioSources() - if svc.opts.streamConfig == nil { - if len(svc.videoSources) != 0 || len(svc.audioSources) != 0 { - svc.logger.Debug("not starting streams due to no stream config being set") - } - return nil - } - - newStream := func(name string, isVideo bool) (gostream.Stream, bool, error) { - // Configure new stream - config := gostream.StreamConfig{ - Name: name, - } - - if isVideo { - config.VideoEncoderFactory = svc.opts.streamConfig.VideoEncoderFactory - } else { - config.AudioEncoderFactory = svc.opts.streamConfig.AudioEncoderFactory - } - stream, err := svc.streamServer.Server.NewStream(config) - - // Skip if stream is already registered, otherwise raise any other errors - var registeredError *webstream.StreamAlreadyRegisteredError - if errors.As(err, ®isteredError) { - return nil, true, nil - } else if err != nil { - return nil, false, err - } - - if !svc.streamServer.HasStreams { - svc.streamServer.HasStreams = true - } - return stream, false, nil - } - - for name, source := range svc.videoSources { - const isVideo = true - stream, alreadyRegistered, err := newStream(name, isVideo) - if err != nil { - return err - } else if alreadyRegistered { - continue - } - - svc.startVideoStream(ctx, source, stream) - } - - for name, source := range svc.audioSources { - const isVideo = false - stream, alreadyRegistered, err := newStream(name, isVideo) - if err != nil { - return err - } else if alreadyRegistered { - continue - } - - svc.startAudioStream(ctx, source, stream) - } - - return nil } func (svc *webService) makeStreamServer(ctx context.Context) (*StreamServer, error) { - svc.refreshVideoSources() - svc.refreshAudioSources() - var streams []gostream.Stream - var streamTypes []bool - - if svc.opts.streamConfig == nil || (len(svc.videoSources) == 0 && len(svc.audioSources) == 0) { - if len(svc.videoSources) != 0 || len(svc.audioSources) != 0 { - svc.logger.Debug("not starting streams due to no stream config being set") - } - noopServer, err := webstream.NewServer(streams, svc.r, svc.logger) - return &StreamServer{noopServer, false}, err - } - - addStream := func(streams []gostream.Stream, name string, isVideo bool) ([]gostream.Stream, error) { - config := gostream.StreamConfig{ - Name: name, - } - if isVideo { - config.VideoEncoderFactory = svc.opts.streamConfig.VideoEncoderFactory - - // set TargetFrameRate to the framerate of the video source if available - props, err := svc.videoSources[name].MediaProperties(ctx) - if err != nil { - svc.logger.Warnw("failed to get video source properties", "name", name, "error", err) - } else if props.FrameRate > 0.0 { - // round float up to nearest int - config.TargetFrameRate = int(math.Ceil(float64(props.FrameRate))) - } - // default to 60fps if the video source doesn't have a framerate - if config.TargetFrameRate == 0 { - config.TargetFrameRate = 60 - } - - if runtime.GOOS == "windows" { - // TODO(RSDK-1771): support video on windows - svc.logger.Warnw("not starting video stream since not supported on Windows yet", "name", name) - return streams, nil - } - } else { - config.AudioEncoderFactory = svc.opts.streamConfig.AudioEncoderFactory - } - stream, err := gostream.NewStream(config) - if err != nil { - return streams, err - } - return append(streams, stream), nil - } - for name := range svc.videoSources { - var err error - streams, err = addStream(streams, name, true) - if err != nil { - return nil, err - } - streamTypes = append(streamTypes, true) - } - for name := range svc.audioSources { - var err error - streams, err = addStream(streams, name, false) - if err != nil { - return nil, err - } - streamTypes = append(streamTypes, false) - } - - streamServer, err := webstream.NewServer(streams, svc.r, svc.logger) - if err != nil { - return nil, err - } - - for idx, stream := range streams { - if streamTypes[idx] { - svc.startVideoStream(ctx, svc.videoSources[stream.Name()], stream) - } else { - svc.startAudioStream(ctx, svc.audioSources[stream.Name()], stream) - } - } - - return &StreamServer{streamServer, true}, nil -} - -func (svc *webService) startStream(streamFunc func(opts *webstream.BackoffTuningOptions) error) { - waitCh := make(chan struct{}) - svc.webWorkers.Add(1) - utils.PanicCapturingGo(func() { - defer svc.webWorkers.Done() - close(waitCh) - opts := &webstream.BackoffTuningOptions{ - BaseSleep: 50 * time.Microsecond, - MaxSleep: 2 * time.Second, - Cooldown: 5 * time.Second, - } - if err := streamFunc(opts); err != nil { - if utils.FilterOutError(err, context.Canceled) != nil { - svc.logger.Errorw("error streaming", "error", err) - } - } - }) - <-waitCh -} - -func (svc *webService) propertiesFromStream(ctx context.Context, stream gostream.Stream) (camera.Properties, error) { - res, err := svc.r.ResourceByName(camera.Named(stream.Name())) - if err != nil { - return camera.Properties{}, err - } - - cam, ok := res.(camera.Camera) - if !ok { - return camera.Properties{}, errors.Errorf("cannot convert resource (type %T) to type (%T)", res, camera.Camera(nil)) - } - - return cam.Properties(ctx) -} - -func (svc *webService) startVideoStream(ctx context.Context, source gostream.VideoSource, stream gostream.Stream) { - svc.startStream(func(opts *webstream.BackoffTuningOptions) error { - streamVideoCtx, _ := utils.MergeContext(svc.cancelCtx, ctx) - // Use H264 for cameras that support it; but do not override upstream values. - if props, err := svc.propertiesFromStream(ctx, stream); err == nil && slices.Contains(props.MimeTypes, rutils.MimeTypeH264) { - streamVideoCtx = gostream.WithMIMETypeHint(streamVideoCtx, rutils.WithLazyMIMEType(rutils.MimeTypeH264)) - } - - return webstream.StreamVideoSource(streamVideoCtx, source, stream, opts, svc.logger) - }) -} - -func (svc *webService) startAudioStream(ctx context.Context, source gostream.AudioSource, stream gostream.Stream) { - svc.startStream(func(opts *webstream.BackoffTuningOptions) error { - // Merge ctx that may be coming from a Reconfigure. - streamAudioCtx, _ := utils.MergeContext(svc.cancelCtx, ctx) - return webstream.StreamAudioSource(streamAudioCtx, source, stream, opts, svc.logger) - }) -} - -// refreshVideoSources checks and initializes every possible video source that could be viewed from the robot. -func (svc *webService) refreshVideoSources() { - for _, name := range camera.NamesFromRobot(svc.r) { - cam, err := camera.FromRobot(svc.r, name) - if err != nil { - continue - } - existing, ok := svc.videoSources[validSDPTrackName(name)] - if ok { - existing.Swap(cam) - continue - } - newSwapper := gostream.NewHotSwappableVideoSource(cam) - svc.videoSources[validSDPTrackName(name)] = newSwapper - } -} - -// refreshAudioSources checks and initializes every possible audio source that could be viewed from the robot. -func (svc *webService) refreshAudioSources() { - for _, name := range audioinput.NamesFromRobot(svc.r) { - input, err := audioinput.FromRobot(svc.r, name) - if err != nil { - continue - } - existing, ok := svc.audioSources[validSDPTrackName(name)] - if ok { - existing.Swap(input) - continue - } - newSwapper := gostream.NewHotSwappableAudioSource(input) - svc.audioSources[validSDPTrackName(name)] = newSwapper - } + noopServer, err := webstream.NewServer(svc.r, svc.logger) + return &StreamServer{noopServer, false}, err } // Update updates the web service when the robot has changed. @@ -329,7 +74,7 @@ func (svc *webService) Reconfigure(ctx context.Context, deps resource.Dependenci if !svc.isRunning { return nil } - return svc.addNewStreams(svc.cancelCtx) + return nil } func (svc *webService) closeStreamServer() { diff --git a/robot/web/web_options.go b/robot/web/web_options.go deleted file mode 100644 index def4b7034b6..00000000000 --- a/robot/web/web_options.go +++ /dev/null @@ -1,23 +0,0 @@ -package web - -// Option configures how we set up the web service. -// Cribbed from https://github.com/grpc/grpc-go/blob/aff571cc86e6e7e740130dbbb32a9741558db805/dialoptions.go#L41 -type Option interface { - apply(*options) -} - -// funcOption wraps a function that modifies options into an -// implementation of the Option interface. -type funcOption struct { - f func(*options) -} - -func (fdo *funcOption) apply(do *options) { - fdo.f(do) -} - -func newFuncOption(f func(*options)) *funcOption { - return &funcOption{ - f: f, - } -} diff --git a/robot/web/web_options_cgo.go b/robot/web/web_options_cgo.go deleted file mode 100644 index 06121e42ced..00000000000 --- a/robot/web/web_options_cgo.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !no_cgo || android - -package web - -import "go.viam.com/rdk/gostream" - -// options configures a web service. -type options struct { - // streamConfig is used to enable audio/video streaming over WebRTC. - streamConfig *gostream.StreamConfig -} - -// WithStreamConfig returns an Option which sets the streamConfig -// used to enable audio/video streaming over WebRTC. -func WithStreamConfig(config gostream.StreamConfig) Option { - return newFuncOption(func(o *options) { - o.streamConfig = &config - }) -} diff --git a/robot/web/web_test.go b/robot/web/web_test.go deleted file mode 100644 index 99fd12a965d..00000000000 --- a/robot/web/web_test.go +++ /dev/null @@ -1,1411 +0,0 @@ -package web_test - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/tls" - "crypto/x509" - "fmt" - "net" - "os" - "testing" - "time" - - "github.com/golang-jwt/jwt/v4" - "github.com/golang/geo/r3" - "github.com/google/uuid" - "github.com/jhump/protoreflect/grpcreflect" - "github.com/lestrrat-go/jwx/jwk" - "go.mongodb.org/mongo-driver/bson/primitive" - echopb "go.viam.com/api/component/testecho/v1" - robotpb "go.viam.com/api/robot/v1" - streampb "go.viam.com/api/stream/v1" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/rpc" - "go.viam.com/utils/testutils" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/audioinput" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/config" - gizmopb "go.viam.com/rdk/examples/customresources/apis/proto/api/component/gizmo/v1" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec/opus" - "go.viam.com/rdk/gostream/codec/x264" - rgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/robot/web" - weboptions "go.viam.com/rdk/robot/web/options" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/testutils/robottestutils" - rutils "go.viam.com/rdk/utils" -) - -const arm1String = "arm1" - -var resources = []resource.Name{arm.Named(arm1String)} - -var pos = spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 2, Z: 3}) - -func TestWebStart(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, injectRobot := setupRobotCtx(t) - - svc := web.New(injectRobot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - conn, err := rgrpc.Dial(context.Background(), addr, logger) - test.That(t, err, test.ShouldBeNil) - arm1, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - err = svc.Start(context.Background(), weboptions.New()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "already started") - - err = svc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) -} - -func TestModule(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, injectRobot := setupRobotCtx(t) - - svc := web.New(injectRobot, logger) - - err := svc.StartModule(ctx) - test.That(t, err, test.ShouldBeNil) - - conn1, err := rgrpc.Dial(context.Background(), "unix://"+svc.ModuleAddress(), logger) - test.That(t, err, test.ShouldBeNil) - - arm1, err := arm.NewClientFromConn(context.Background(), conn1, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - err = svc.StartModule(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "already started") - - options, _, _ := robottestutils.CreateBaseOptionsAndListener(t) - - err = svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - conn2, err := rgrpc.Dial(context.Background(), svc.Address(), logger) - test.That(t, err, test.ShouldBeNil) - arm2, err := arm.NewClientFromConn(context.Background(), conn2, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm2Position, err := arm2.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm2Position, test.ShouldResemble, pos) - - svc.Stop() - time.Sleep(time.Second) - - _, err = arm2.EndPosition(ctx, nil) - test.That(t, err, test.ShouldNotBeNil) - - _, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - err = svc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - _, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldNotBeNil) - - test.That(t, conn1.Close(), test.ShouldBeNil) - test.That(t, conn2.Close(), test.ShouldBeNil) -} - -func TestWebStartOptions(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, injectRobot := setupRobotCtx(t) - - svc := web.New(injectRobot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - - options.Network.BindAddress = "woop" - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "only set one of") - options.Network.BindAddress = "" - - err = svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - conn, err := rgrpc.Dial(context.Background(), addr, logger) - test.That(t, err, test.ShouldBeNil) - arm1, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, conn.Close(), test.ShouldBeNil) - err = svc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) -} - -func TestWebWithAuth(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, injectRobot := setupRobotCtx(t) - - for _, tc := range []struct { - Case string - Managed bool - EntityName string - }{ - {Case: "unmanaged and default host"}, - {Case: "unmanaged and specific host", EntityName: "something-different"}, - {Case: "managed and default host", Managed: true}, - {Case: "managed and specific host", Managed: true, EntityName: "something-different"}, - } { - t.Run(tc.Case, func(t *testing.T) { - svc := web.New(injectRobot, logger) - - keyset := jwk.NewSet() - privKeyForWebAuth, err := rsa.GenerateKey(rand.Reader, 4096) - test.That(t, err, test.ShouldBeNil) - publicKeyForWebAuth, err := jwk.New(privKeyForWebAuth.PublicKey) - test.That(t, err, test.ShouldBeNil) - publicKeyForWebAuth.Set("alg", "RS256") - publicKeyForWebAuth.Set(jwk.KeyIDKey, "key-id-1") - test.That(t, keyset.Add(publicKeyForWebAuth), test.ShouldBeTrue) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - options.Managed = tc.Managed - options.FQDN = tc.EntityName - options.LocalFQDN = primitive.NewObjectID().Hex() - legacyAPIKey := "sosecret" - apiKeyID1 := uuid.New().String() - apiKey1 := utils.RandomAlphaString(32) - apiKeyID2 := uuid.New().String() - apiKey2 := utils.RandomAlphaString(32) - locationSecrets := []string{"locsosecret", "locsec2"} - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: rpc.CredentialsTypeAPIKey, - Config: rutils.AttributeMap{ - "key": legacyAPIKey, - apiKeyID1: apiKey1, - apiKeyID2: apiKey2, - "keys": []string{apiKeyID1, apiKeyID2}, - }, - }, - { - Type: rutils.CredentialsTypeRobotLocationSecret, - Config: rutils.AttributeMap{ - "secrets": locationSecrets, - }, - }, - } - options.Auth.ExternalAuthConfig = &config.ExternalAuthConfig{ - ValidatedKeySet: keyset, - } - if tc.Managed { - options.BakedAuthEntity = "blah" - options.BakedAuthCreds = rpc.Credentials{Type: "blah"} - } - - err = svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - _, err = rgrpc.Dial(context.Background(), addr, logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "authentication required") - - if tc.Managed { - _, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials("wrong", rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: legacyAPIKey, - })) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid credentials") - - _, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials("wrong", rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecrets[0], - }), - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid credentials") - - entityName := tc.EntityName - if entityName == "" { - entityName = options.LocalFQDN - } - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err := rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(entityName, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: legacyAPIKey, - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - arm1, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(entityName, rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecrets[0], - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(entityName, rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecrets[1], - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - _, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(apiKeyID1, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey2, - })) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid credentials") - - _, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(entityName, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey1, - })) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid credentials") - - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(apiKeyID1, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey1, - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(apiKeyID2, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey2, - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - if tc.EntityName != "" { - t.Run("can connect with external auth", func(t *testing.T) { - accessToken, err := signJWKBasedExternalAccessToken( - privKeyForWebAuth, - entityName, - options.FQDN, - "iss", - "key-id-1", - ) - test.That(t, err, test.ShouldBeNil) - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithStaticAuthenticationMaterial(accessToken), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - } - } else { - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err := rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithCredentials(rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: legacyAPIKey, - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - - arm1, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithCredentials(rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecrets[0], - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - } - - err = svc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - }) - } -} - -func TestWebWithTLSAuth(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, injectRobot := setupRobotCtx(t) - - svc := web.New(injectRobot, logger) - - altName := primitive.NewObjectID().Hex() - cert, certFile, keyFile, certPool, err := testutils.GenerateSelfSignedCertificate("somename", altName) - test.That(t, err, test.ShouldBeNil) - t.Cleanup(func() { - os.Remove(certFile) - os.Remove(keyFile) - }) - - leaf, err := x509.ParseCertificate(cert.Certificate[0]) - test.That(t, err, test.ShouldBeNil) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - options.Network.TLSConfig = &tls.Config{ - RootCAs: certPool, - ClientCAs: certPool, - Certificates: []tls.Certificate{cert}, - MinVersion: tls.VersionTLS12, - ClientAuth: tls.VerifyClientCertIfGiven, - } - options.Auth.TLSAuthEntities = leaf.DNSNames - options.Managed = true - options.FQDN = altName - options.LocalFQDN = "localhost" // this will allow authentication to work in unmanaged, default host - locationSecret := "locsosecret" - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: rutils.CredentialsTypeRobotLocationSecret, - Config: rutils.AttributeMap{ - "secret": locationSecret, - }, - }, - } - options.BakedAuthEntity = "blah" - options.BakedAuthCreds = rpc.Credentials{Type: "blah"} - - err = svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - clientTLSConfig := options.Network.TLSConfig.Clone() - clientTLSConfig.Certificates = nil - clientTLSConfig.ServerName = "somename" - - _, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithTLSConfig(clientTLSConfig), - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "authentication required") - - _, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithTLSConfig(clientTLSConfig), - rpc.WithEntityCredentials("wrong", rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret, - }), - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid credentials") - - // use secret - conn, err := rgrpc.Dial(context.Background(), addr, logger, - rpc.WithTLSConfig(clientTLSConfig), - rpc.WithEntityCredentials(options.FQDN, rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret, - }), - ) - test.That(t, err, test.ShouldBeNil) - - arm1, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - test.That(t, conn.Close(), test.ShouldBeNil) - - // use cert - clientTLSConfig.Certificates = []tls.Certificate{cert} - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithTLSConfig(clientTLSConfig), - ) - test.That(t, err, test.ShouldBeNil) - - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - test.That(t, conn.Close(), test.ShouldBeNil) - - // use cert with mDNS - conn, err = rgrpc.Dial(context.Background(), options.FQDN, logger, - rpc.WithDialDebug(), - rpc.WithTLSConfig(clientTLSConfig), - ) - test.That(t, err, test.ShouldBeNil) - - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - test.That(t, conn.Close(), test.ShouldBeNil) - - // use signaling creds - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithDialDebug(), - rpc.WithTLSConfig(clientTLSConfig), - rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - SignalingServerAddress: addr, - SignalingAuthEntity: options.FQDN, - SignalingCreds: rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret, - }, - }), - ) - test.That(t, err, test.ShouldBeNil) - - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - test.That(t, conn.Close(), test.ShouldBeNil) - - // use cert with mDNS while signaling present - conn, err = rgrpc.Dial(context.Background(), options.FQDN, logger, - rpc.WithDialDebug(), - rpc.WithTLSConfig(clientTLSConfig), - rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{ - SignalingServerAddress: addr, - SignalingAuthEntity: options.FQDN, - SignalingCreds: rpc.Credentials{ - Type: rutils.CredentialsTypeRobotLocationSecret, - Payload: locationSecret + "bad", - }, - }), - rpc.WithDialMulticastDNSOptions(rpc.DialMulticastDNSOptions{ - RemoveAuthCredentials: true, - }), - ) - test.That(t, err, test.ShouldBeNil) - - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - err = svc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) -} - -func TestWebWithBadAuthHandlers(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, injectRobot := setupRobotCtx(t) - - svc := web.New(injectRobot, logger) - - options, _, _ := robottestutils.CreateBaseOptionsAndListener(t) - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: "unknown", - }, - } - - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "do not know how") - test.That(t, err.Error(), test.ShouldContainSubstring, "unknown") - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) - - svc = web.New(injectRobot, logger) - - options, _, _ = robottestutils.CreateBaseOptionsAndListener(t) - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: rpc.CredentialsTypeAPIKey, - }, - } - - err = svc.Start(ctx, options) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "non-empty") - test.That(t, err.Error(), test.ShouldContainSubstring, "api-key") - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) -} - -func TestWebWithOnlyNewAPIKeyAuthHandlers(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, injectRobot := setupRobotCtx(t) - - svc := web.New(injectRobot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - apiKeyID1 := uuid.New().String() - apiKey1 := utils.RandomAlphaString(32) - apiKeyID2 := uuid.New().String() - apiKey2 := utils.RandomAlphaString(32) - options.Auth.Handlers = []config.AuthHandlerConfig{ - { - Type: rpc.CredentialsTypeAPIKey, - Config: rutils.AttributeMap{ - apiKeyID1: apiKey1, - apiKeyID2: apiKey2, - "keys": []string{apiKeyID1, apiKeyID2}, - }, - }, - } - - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - _, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(apiKeyID1, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey2, - })) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid credentials") - - _, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials("something-different", rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey1, - })) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "invalid credentials") - - conn, err := rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(apiKeyID1, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey1, - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - arm1, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - conn, err = rgrpc.Dial(context.Background(), addr, logger, - rpc.WithAllowInsecureWithCredentialsDowngrade(), - rpc.WithEntityCredentials(apiKeyID2, rpc.Credentials{ - Type: rpc.CredentialsTypeAPIKey, - Payload: apiKey2, - }), - rpc.WithForceDirectGRPC(), - ) - test.That(t, err, test.ShouldBeNil) - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) -} - -func TestWebReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, robot := setupRobotCtx(t) - - svc := web.New(robot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err := rgrpc.Dial(context.Background(), addr, logger, rpc.WithForceDirectGRPC()) - test.That(t, err, test.ShouldBeNil) - - arm1, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err := arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, pos) - test.That(t, conn.Close(), test.ShouldBeNil) - - // add arm to robot and then update - injectArm := &inject.Arm{} - newPos := spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 3, Z: 6}) - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return newPos, nil - } - rs := map[resource.Name]resource.Resource{arm.Named(arm1String): injectArm} - err = svc.Reconfigure(context.Background(), rs, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithForceDirectGRPC()) - test.That(t, err, test.ShouldBeNil) - aClient, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - position, err := aClient.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, position, test.ShouldResemble, newPos) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, svc.Close(context.Background()), test.ShouldBeNil) - test.That(t, aClient.Close(context.Background()), test.ShouldBeNil) - - // now start it with the arm already in it - ctx, robot2 := setupRobotCtx(t) - robot2.(*inject.Robot).ResourceNamesFunc = func() []resource.Name { return resources } - robot2.(*inject.Robot).ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - return injectArm, nil - } - - svc2 := web.New(robot2, logger) - - listener := testutils.ReserveRandomListener(t) - addr = listener.Addr().String() - options.Network.Listener = listener - - err = svc2.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithForceDirectGRPC()) - test.That(t, err, test.ShouldBeNil) - - arm1, err = arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - - arm1Position, err = arm1.EndPosition(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, arm1Position, test.ShouldResemble, newPos) - test.That(t, conn.Close(), test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithForceDirectGRPC()) - test.That(t, err, test.ShouldBeNil) - aClient2, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm1String), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - position, err = aClient2.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, position, test.ShouldResemble, newPos) - - // add a second arm - arm2 := "arm2" - injectArm2 := &inject.Arm{} - pos2 := spatialmath.NewPoseFromPoint(r3.Vector{X: 2, Y: 3, Z: 4}) - injectArm2.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return pos2, nil - } - rs[arm.Named(arm2)] = injectArm2 - err = svc2.Reconfigure(context.Background(), rs, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - position, err = aClient2.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, position, test.ShouldResemble, newPos) - - aClient3, err := arm.NewClientFromConn(context.Background(), conn, "", arm.Named(arm2), logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - position, err = aClient3.EndPosition(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, position, test.ShouldResemble, pos2) - - test.That(t, arm1.Close(context.Background()), test.ShouldBeNil) - test.That(t, svc2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) -} - -func TestWebWithStreams(t *testing.T) { - const ( - camera1Key = "camera1" - camera2Key = "camera2" - audioKey = "audio" - ) - - // Start a robot with a camera - robot := &inject.Robot{} - cam1 := &inject.Camera{ - PropertiesFunc: func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - }, - } - rs := map[resource.Name]resource.Resource{camera.Named(camera1Key): cam1} - robot.MockResourcesFromMap(rs) - - ctx, cancel := context.WithCancel(context.Background()) - - // Start service - logger := logging.NewTestLogger(t) - robot.LoggerFunc = func() logging.Logger { return logger } - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - svc := web.New(robot, logger, web.WithStreamConfig(gostream.StreamConfig{ - AudioEncoderFactory: opus.NewEncoderFactory(), - VideoEncoderFactory: x264.NewEncoderFactory(), - })) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // Start a stream service client - conn, err := rgrpc.Dial(context.Background(), addr, logger) - test.That(t, err, test.ShouldBeNil) - streamClient := streampb.NewStreamServiceClient(conn) - - // Test that only one stream is available - resp, err := streamClient.ListStreams(ctx, &streampb.ListStreamsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Names, test.ShouldContain, camera1Key) - test.That(t, resp.Names, test.ShouldHaveLength, 1) - - // Add another camera and update - cam2 := &inject.Camera{ - PropertiesFunc: func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - }, - } - robot.Mu.Lock() - rs[camera.Named(camera2Key)] = cam2 - robot.Mu.Unlock() - robot.MockResourcesFromMap(rs) - err = svc.Reconfigure(context.Background(), rs, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - // Add an audio stream - audio := &inject.AudioInput{} - robot.Mu.Lock() - rs[audioinput.Named(audioKey)] = audio - robot.Mu.Unlock() - robot.MockResourcesFromMap(rs) - err = svc.Reconfigure(context.Background(), rs, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - // Test that new streams are available - resp, err = streamClient.ListStreams(ctx, &streampb.ListStreamsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Names, test.ShouldContain, camera1Key) - test.That(t, resp.Names, test.ShouldContain, camera2Key) - test.That(t, resp.Names, test.ShouldHaveLength, 3) - - // We need to cancel otherwise we are stuck waiting for WebRTC to start streaming. - cancel() - test.That(t, svc.Close(ctx), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - <-ctx.Done() -} - -func TestWebAddFirstStream(t *testing.T) { - const ( - camera1Key = "camera1" - ) - - // Start a robot without a camera - robot := &inject.Robot{} - rs := map[resource.Name]resource.Resource{} - robot.MockResourcesFromMap(rs) - - ctx, cancel := context.WithCancel(context.Background()) - - // Start service - logger := logging.NewTestLogger(t) - robot.LoggerFunc = func() logging.Logger { return logger } - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - svc := web.New(robot, logger, web.WithStreamConfig(x264.DefaultStreamConfig)) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // Start a stream service client - conn, err := rgrpc.Dial(context.Background(), addr, logger) - test.That(t, err, test.ShouldBeNil) - streamClient := streampb.NewStreamServiceClient(conn) - - // Test that there are no streams available - resp, err := streamClient.ListStreams(ctx, &streampb.ListStreamsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Names, test.ShouldHaveLength, 0) - - // Add first camera and update - cam1 := &inject.Camera{ - PropertiesFunc: func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - }, - } - robot.Mu.Lock() - rs[camera.Named(camera1Key)] = cam1 - robot.Mu.Unlock() - robot.MockResourcesFromMap(rs) - err = svc.Reconfigure(ctx, rs, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - // Test that new streams are available - resp, err = streamClient.ListStreams(ctx, &streampb.ListStreamsRequest{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Names, test.ShouldContain, camera1Key) - test.That(t, resp.Names, test.ShouldHaveLength, 1) - - // We need to cancel otherwise we are stuck waiting for WebRTC to start streaming. - cancel() - test.That(t, svc.Close(ctx), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - <-ctx.Done() -} - -func TestWebStreamImmediateClose(t *testing.T) { - // Primarily a regression test for RSDK-2418 - - // Start a robot with a camera - robot := &inject.Robot{} - cam1 := &inject.Camera{ - PropertiesFunc: func(ctx context.Context) (camera.Properties, error) { - return camera.Properties{}, nil - }, - } - rs := map[resource.Name]resource.Resource{camera.Named("camera1"): cam1} - robot.MockResourcesFromMap(rs) - - ctx, cancel := context.WithCancel(context.Background()) - - // Start service - logger := logging.NewTestLogger(t) - robot.LoggerFunc = func() logging.Logger { return logger } - options, _, _ := robottestutils.CreateBaseOptionsAndListener(t) - svc := web.New(robot, logger, web.WithStreamConfig(x264.DefaultStreamConfig)) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // Immediately Close service. - cancel() - test.That(t, svc.Close(ctx), test.ShouldBeNil) - <-ctx.Done() -} - -func setupRobotCtx(t *testing.T) (context.Context, robot.Robot) { - t.Helper() - - injectArm := &inject.Arm{} - injectArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return pos, nil - } - injectRobot := &inject.Robot{} - injectRobot.ConfigFunc = func() *config.Config { return &config.Config{} } - injectRobot.ResourceNamesFunc = func() []resource.Name { return resources } - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { return nil } - injectRobot.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - return injectArm, nil - } - injectRobot.LoggerFunc = func() logging.Logger { return logging.NewTestLogger(t) } - injectRobot.FrameSystemConfigFunc = func(ctx context.Context) (*framesystem.Config, error) { - return &framesystem.Config{}, nil - } - - return context.Background(), injectRobot -} - -func TestForeignResource(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, robot := setupRobotCtx(t) - - svc := web.New(robot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err := rgrpc.Dial(context.Background(), addr, logger, rpc.WithForceDirectGRPC()) - test.That(t, err, test.ShouldBeNil) - - myCompClient := gizmopb.NewGizmoServiceClient(conn) - _, err = myCompClient.DoOne(ctx, &gizmopb.DoOneRequest{Name: "thing1", Arg1: "hello"}) - test.That(t, err, test.ShouldNotBeNil) - errStatus, ok := status.FromError(err) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, errStatus.Code(), test.ShouldEqual, codes.Unimplemented) - - test.That(t, svc.Close(ctx), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - - remoteServer := grpc.NewServer() - gizmopb.RegisterGizmoServiceServer(remoteServer, &myCompServer{}) - - listenerR, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - go remoteServer.Serve(listenerR) - defer remoteServer.Stop() - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - remoteConn, err := rgrpc.Dial(context.Background(), listenerR.Addr().String(), - logger, rpc.WithForceDirectGRPC()) - test.That(t, err, test.ShouldBeNil) - - resourceAPI := resource.NewAPI( - "acme", - "component", - "mycomponent", - ) - resName := resource.NewName(resourceAPI, "thing1") - - foreignRes := rgrpc.NewForeignResource(resName, remoteConn) - - svcDesc, err := grpcreflect.LoadServiceDescriptor(&gizmopb.GizmoService_ServiceDesc) - test.That(t, err, test.ShouldBeNil) - - injectRobot := &inject.Robot{} - injectRobot.LoggerFunc = func() logging.Logger { return logger } - injectRobot.ConfigFunc = func() *config.Config { return &config.Config{} } - injectRobot.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{ - resource.NewName(resourceAPI, "thing1"), - } - } - injectRobot.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - return foreignRes, nil - } - - listener := testutils.ReserveRandomListener(t) - addr = listener.Addr().String() - options.Network.Listener = listener - svc = web.New(injectRobot, logger) - err = svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // TODO(RSDK-4473) Reenable WebRTC when we figure out why multiple - // WebRTC connections across unix sockets can create deadlock in CI. - conn, err = rgrpc.Dial(context.Background(), addr, logger, rpc.WithForceDirectGRPC()) - test.That(t, err, test.ShouldBeNil) - - myCompClient = gizmopb.NewGizmoServiceClient(conn) - - injectRobot.Mu.Lock() - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { - return nil - } - injectRobot.Mu.Unlock() - - _, err = myCompClient.DoOne(ctx, &gizmopb.DoOneRequest{Name: "thing1", Arg1: "hello"}) - test.That(t, err, test.ShouldNotBeNil) - errStatus, ok = status.FromError(err) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, errStatus.Code(), test.ShouldEqual, codes.Unimplemented) - - injectRobot.Mu.Lock() - injectRobot.ResourceRPCAPIsFunc = func() []resource.RPCAPI { - return []resource.RPCAPI{ - { - API: resourceAPI, - Desc: svcDesc, - }, - } - } - injectRobot.Mu.Unlock() - - resp, err := myCompClient.DoOne(ctx, &gizmopb.DoOneRequest{Name: "thing1", Arg1: "hello"}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Ret1, test.ShouldBeTrue) - - test.That(t, svc.Close(ctx), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - test.That(t, remoteConn.Close(), test.ShouldBeNil) -} - -type myCompServer struct { - gizmopb.UnimplementedGizmoServiceServer -} - -func (s *myCompServer) DoOne(ctx context.Context, req *gizmopb.DoOneRequest) (*gizmopb.DoOneResponse, error) { - return &gizmopb.DoOneResponse{Ret1: req.Arg1 == "hello"}, nil -} - -func TestRawClientOperation(t *testing.T) { - // Need an unfiltered streaming call to test interceptors - echoAPI := resource.NewAPI("rdk", "component", "echo") - resource.RegisterAPI(echoAPI, resource.APIRegistration[resource.Resource]{ - RPCServiceServerConstructor: func(apiResColl resource.APIResourceCollection[resource.Resource]) interface{} { return &echoServer{} }, - RPCServiceHandler: echopb.RegisterTestEchoServiceHandlerFromEndpoint, - RPCServiceDesc: &echopb.TestEchoService_ServiceDesc, - }) - defer resource.DeregisterAPI(echoAPI) - - logger := logging.NewTestLogger(t) - ctx, iRobot := setupRobotCtx(t) - - svc := web.New(iRobot, logger) - - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - iRobot.(*inject.Robot).StatusFunc = func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - return []robot.Status{}, nil - } - - checkOpID := func(md metadata.MD, expected bool) { - t.Helper() - if expected { - test.That(t, md["opid"], test.ShouldHaveLength, 1) - _, err = uuid.Parse(md["opid"][0]) - test.That(t, err, test.ShouldBeNil) - } else { - // StreamStatus is in operations' list of filtered methods, so expect no opID. - test.That(t, md["opid"], test.ShouldHaveLength, 0) - } - } - - conn, err := rgrpc.Dial(context.Background(), addr, logger, rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{Disable: true})) - test.That(t, err, test.ShouldBeNil) - client := robotpb.NewRobotServiceClient(conn) - - var hdr metadata.MD - _, err = client.GetStatus(ctx, &robotpb.GetStatusRequest{}, grpc.Header(&hdr)) - test.That(t, err, test.ShouldBeNil) - checkOpID(hdr, true) - - streamClient, err := client.StreamStatus(ctx, &robotpb.StreamStatusRequest{}) - test.That(t, err, test.ShouldBeNil) - md, err := streamClient.Header() - test.That(t, err, test.ShouldBeNil) - checkOpID(md, false) // StreamStatus is in the filtered method list, so doesn't get an opID - test.That(t, conn.Close(), test.ShouldBeNil) - - // test with a simple echo proto as well - conn, err = rgrpc.Dial(context.Background(), addr, logger) - test.That(t, err, test.ShouldBeNil) - echoclient := echopb.NewTestEchoServiceClient(conn) - - hdr = metadata.MD{} - trailers := metadata.MD{} // won't do anything but helps test goutils - _, err = echoclient.Echo(ctx, &echopb.EchoRequest{}, grpc.Header(&hdr), grpc.Trailer(&trailers)) - test.That(t, err, test.ShouldBeNil) - checkOpID(hdr, true) - - echoStreamClient, err := echoclient.EchoMultiple(ctx, &echopb.EchoMultipleRequest{}) - test.That(t, err, test.ShouldBeNil) - md, err = echoStreamClient.Header() - test.That(t, err, test.ShouldBeNil) - checkOpID(md, true) // EchoMultiple is NOT filtered, so should have an opID - test.That(t, conn.Close(), test.ShouldBeNil) - - test.That(t, svc.Close(ctx), test.ShouldBeNil) -} - -func TestInboundMethodTimeout(t *testing.T) { - logger := logging.NewTestLogger(t) - ctx, iRobot := setupRobotCtx(t) - - t.Run("web start", func(t *testing.T) { - t.Run("default timeout", func(t *testing.T) { - svc := web.New(iRobot, logger) - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // Use an injected status function to check that the default deadline was added - // to the context. - iRobot.(*inject.Robot).StatusFunc = func(ctx context.Context, - resourceNames []resource.Name, - ) ([]robot.Status, error) { - deadline, deadlineSet := ctx.Deadline() - test.That(t, deadlineSet, test.ShouldBeTrue) - // Assert that deadline is between 9 and 10 minutes from now (some time will - // have elapsed). - test.That(t, deadline, test.ShouldHappenBetween, - time.Now().Add(time.Minute*9), time.Now().Add(time.Minute*10)) - - return []robot.Status{}, nil - } - - conn, err := rgrpc.Dial(context.Background(), addr, logger, - rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{Disable: true})) - test.That(t, err, test.ShouldBeNil) - client := robotpb.NewRobotServiceClient(conn) - - // Use GetStatus to call injected status function. - _, err = client.GetStatus(ctx, &robotpb.GetStatusRequest{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - t.Run("overridden timeout", func(t *testing.T) { - svc := web.New(iRobot, logger) - options, _, addr := robottestutils.CreateBaseOptionsAndListener(t) - - err := svc.Start(ctx, options) - test.That(t, err, test.ShouldBeNil) - - // Use an injected status function to check that the default deadline was not - // added to the context, and the deadline passed to GetStatus was used instead. - iRobot.(*inject.Robot).StatusFunc = func(ctx context.Context, - resourceNames []resource.Name, - ) ([]robot.Status, error) { - deadline, deadlineSet := ctx.Deadline() - test.That(t, deadlineSet, test.ShouldBeTrue) - // Assert that deadline is between 4 and 5 minutes from now (some time will - // have elapsed). - test.That(t, deadline, test.ShouldHappenBetween, - time.Now().Add(time.Minute*4), time.Now().Add(time.Minute*5)) - return []robot.Status{}, nil - } - - conn, err := rgrpc.Dial(context.Background(), addr, logger, - rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{Disable: true})) - test.That(t, err, test.ShouldBeNil) - client := robotpb.NewRobotServiceClient(conn) - - // Use GetStatus and a context with a deadline to call injected status function. - overrideCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - _, err = client.GetStatus(overrideCtx, &robotpb.GetStatusRequest{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - }) - t.Run("module start", func(t *testing.T) { - t.Run("default timeout", func(t *testing.T) { - svc := web.New(iRobot, logger) - - err := svc.StartModule(ctx) - test.That(t, err, test.ShouldBeNil) - - // Use an injected status function to check that the default deadline was added - // to the context. - iRobot.(*inject.Robot).StatusFunc = func(ctx context.Context, - resourceNames []resource.Name, - ) ([]robot.Status, error) { - deadline, deadlineSet := ctx.Deadline() - test.That(t, deadlineSet, test.ShouldBeTrue) - // Assert that deadline is between 9 and 10 minutes from now (some time will - // have elapsed). - test.That(t, deadline, test.ShouldHappenBetween, - time.Now().Add(time.Minute*9), time.Now().Add(time.Minute*10)) - - return []robot.Status{}, nil - } - - conn, err := rgrpc.Dial(context.Background(), "unix://"+svc.ModuleAddress(), - logger, rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{Disable: true})) - test.That(t, err, test.ShouldBeNil) - client := robotpb.NewRobotServiceClient(conn) - - // Use GetStatus to call injected status function. - _, err = client.GetStatus(ctx, &robotpb.GetStatusRequest{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - t.Run("overridden timeout", func(t *testing.T) { - svc := web.New(iRobot, logger) - - err := svc.StartModule(ctx) - test.That(t, err, test.ShouldBeNil) - - // Use an injected status function to check that the default deadline was not - // added to the context, and the deadline passed to GetStatus was used instead. - iRobot.(*inject.Robot).StatusFunc = func(ctx context.Context, - resourceNames []resource.Name, - ) ([]robot.Status, error) { - deadline, deadlineSet := ctx.Deadline() - test.That(t, deadlineSet, test.ShouldBeTrue) - // Assert that deadline is between 4 and 5 minutes from now (some time will - // have elapsed). - test.That(t, deadline, test.ShouldHappenBetween, - time.Now().Add(time.Minute*4), time.Now().Add(time.Minute*5)) - return []robot.Status{}, nil - } - - conn, err := rgrpc.Dial(context.Background(), "unix://"+svc.ModuleAddress(), - logger, rpc.WithWebRTCOptions(rpc.DialWebRTCOptions{Disable: true})) - test.That(t, err, test.ShouldBeNil) - client := robotpb.NewRobotServiceClient(conn) - - // Use GetStatus and a context with a deadline to call injected status function. - overrideCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - _, err = client.GetStatus(overrideCtx, &robotpb.GetStatusRequest{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - }) -} - -type echoServer struct { - echopb.UnimplementedTestEchoServiceServer -} - -func (srv *echoServer) EchoMultiple( - req *echopb.EchoMultipleRequest, - server echopb.TestEchoService_EchoMultipleServer, -) error { - return server.Send(&echopb.EchoMultipleResponse{}) -} - -func (srv *echoServer) Echo(context.Context, *echopb.EchoRequest) (*echopb.EchoResponse, error) { - return &echopb.EchoResponse{}, nil -} - -// signJWKBasedExternalAccessToken returns an access jwt access token typically returned by an OIDC provider. -func signJWKBasedExternalAccessToken( - key *rsa.PrivateKey, - entity, aud, iss, keyID string, -) (string, error) { - token := &jwt.Token{ - Header: map[string]interface{}{ - "typ": "JWT", - "alg": jwt.SigningMethodRS256.Alg(), - "kid": keyID, - }, - Claims: rpc.JWTClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - Audience: []string{aud}, - Issuer: iss, - Subject: fmt.Sprintf("someauthprovider/%s", entity), - IssuedAt: jwt.NewNumericDate(time.Now()), - }, - AuthCredentialsType: rpc.CredentialsTypeExternal, - }, - Method: jwt.SigningMethodRS256, - } - - return token.SignedString(key) -} diff --git a/ros/README.md b/ros/README.md deleted file mode 100644 index e3940b741b9..00000000000 --- a/ros/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# ROS package -The ROS package implements functionality that bridges the gap between `rdk` and `ROS`. - -## Plane Segmentation -Plane segmentation works with ROS bags that contain the `/L515_ImageWithDepth` rostopic that publishes Intel Realsense L515 RGBD data. - -It saves png images of the rgbd data, as well as segmented planes. - -Run `rosbag_parser/cmd`: -```bash -go run rosbag_parser/cmd/main.go -``` \ No newline at end of file diff --git a/ros/data/intel515_parameters.json b/ros/data/intel515_parameters.json deleted file mode 100644 index 780b1002df0..00000000000 --- a/ros/data/intel515_parameters.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "color": { - "height_px": 720, - "width_px": 1280, - "fx": 900.538000, - "fy": 900.818000, - "ppx": 648.934000, - "ppy": 367.736000, - "distortion": { - "rk1": 0.158701, - "rk2": -0.485405, - "rk3": 0.435342, - "tp1": -0.00143327, - "tp2": -0.000705919 - } - }, - "depth": { - "height_px": 768, - "width_px": 1024, - "fx": 734.938, - "fy": 735.516, - "ppx": 542.078, - "ppy": 398.016, - "distortion": { - "rk1": 0.0, - "rk2": 0.0, - "rk3": 0.0, - "tp1": 0.0, - "tp2": 0.0 - } - }, - "extrinsics_depth_to_color": { - "rotation_rads": [ - 0.999958, - -0.00838489, - 0.00378392, - 0.00824708, - 0.999351, - 0.0350734, - -0.00407554, - -0.0350407, - 0.999378 - ], - "translation_mm": [ - -0.000828434, - 0.0139185, - -0.0033418 - ] - } -} diff --git a/ros/messages.go b/ros/messages.go deleted file mode 100644 index d2a5ed4ad3c..00000000000 --- a/ros/messages.go +++ /dev/null @@ -1,75 +0,0 @@ -// Package ros implements functionality that bridges the gap between `rdk` and ROS -package ros - -// TimeStamp contains the timestamp expressed as: -// * TimeStamp.Secs: seconds since epoch -// * TimeStamp.Nsecs: nanoseconds since TimeStamp.Secs. -type TimeStamp struct { - Secs int - Nsecs int -} - -// MultiArrayDimension is a ROS std_msgs/MultiArrayDimension message. -type MultiArrayDimension struct { - Label string - Size uint32 - Stride uint32 -} - -// MultiArrayLayout is a ROS std_msgs/MultiArrayLayout message. -type MultiArrayLayout struct { - Dim []MultiArrayDimension - DataOffset int `json:"data_offset"` -} - -// ByteMultiArray is a ROS std_msgs/ByteMultiArray message. -type ByteMultiArray struct { - Layout MultiArrayLayout - Data []byte -} - -// MessageHeader is a ROS std_msgs/Header message. -type MessageHeader struct { - Seq int - Stamp TimeStamp - FrameID string `json:"frame_id"` -} - -// Quaternion is a ROS geometry_msgs/Quaternion message. -type Quaternion struct { - X float64 - Y float64 - Z float64 - W float64 -} - -// Vector3 is a ROS geometry_msgs/Vector3 message. -type Vector3 struct { - X float64 - Y float64 - Z float64 -} - -// L515Message reflects the JSON data format for rosbag Intel Realsense data. -type L515Message struct { - Meta TimeStamp - ColorData ByteMultiArray - DepthData ByteMultiArray -} - -// ImuData contains the IMU data. -type ImuData struct { - Header MessageHeader - Orientation Quaternion - OrientationCovariance [9]int `json:"orientation_covariance"` - AngularVelocity Vector3 `json:"angular_velocity"` - AngularVelocityCovariance [9]int `json:"angular_velocity_covariance"` - LinearAcceleration Vector3 `json:"linear_acceleration"` - LinearAccelerationCovariance [9]int `json:"linear_acceleration_covariance"` -} - -// ImuMessage reflects the JSON data format for rosbag imu data. -type ImuMessage struct { - Meta TimeStamp - Data ImuData -} diff --git a/ros/rosbag_parser.go b/ros/rosbag_parser.go deleted file mode 100644 index 4ad64aeb172..00000000000 --- a/ros/rosbag_parser.go +++ /dev/null @@ -1,104 +0,0 @@ -// Package ros implements functionality that bridges the gap between `rdk` and ROS -package ros - -import ( - "encoding/json" - "io" - "os" - - "github.com/edaniels/gobag/rosbag" - "github.com/pkg/errors" - "go.viam.com/utils" -) - -// ReadBag reads the contents of a rosbag into a gobag data structure. -func ReadBag(filename string) (*rosbag.RosBag, error) { - //nolint:gosec - f, err := os.Open(filename) - defer utils.UncheckedErrorFunc(f.Close) - if err != nil { - return nil, errors.Wrapf(err, "unable to open input file") - } - - rb := rosbag.NewRosBag() - - if err := rb.Read(f); err != nil { - return nil, errors.Wrapf(err, "unable to create ros bag, error") - } - - return rb, nil -} - -// WriteTopicsJSON writes data from a rosbag into JSON files, filtered and sorted by topic. -func WriteTopicsJSON(rb *rosbag.RosBag, startTime, endTime int64, topicsFilter []string) error { - var timeFilterFunc func(int64) bool - if startTime == 0 || endTime == 0 { - timeFilterFunc = func(timestamp int64) bool { - return true - } - } else { - timeFilterFunc = func(timestamp int64) bool { - return timestamp >= startTime && timestamp <= endTime - } - } - - var topicFilterFunc func(string) bool - if len(topicsFilter) == 0 { - topicFilterFunc = func(string) bool { - return true - } - } else { - topicsFilterMap := make(map[string]bool) - for _, topic := range topicsFilter { - topicsFilterMap[topic] = true - } - topicFilterFunc = func(topic string) bool { - _, ok := topicsFilterMap[topic] - return ok - } - } - - if err := rb.ParseTopicsToJSON("", timeFilterFunc, topicFilterFunc, false); err != nil { - return errors.Wrapf(err, "error while parsing bag to JSON") - } - - return nil -} - -// AllMessagesForTopic returns all messages for a specific topic in the ros bag. -func AllMessagesForTopic(rb *rosbag.RosBag, topic string) ([]map[string]interface{}, error) { - if err := rb.ParseTopicsToJSON( - "", - func(int64) bool { return true }, - func(t string) bool { return t == topic }, - false, - ); err != nil { - return nil, errors.Wrapf(err, "error while parsing bag to JSON") - } - - msgs := rb.TopicsAsJSON[topic] - if msgs == nil { - return nil, errors.Errorf("no messages for topic %s", topic) - } - - all := []map[string]interface{}{} - - for { - data, err := msgs.ReadBytes('\n') - if err != nil { - if errors.Is(err, io.EOF) { - break - } - return nil, err - } - message := map[string]interface{}{} - err = json.Unmarshal(data, &message) - if err != nil { - return nil, err - } - - all = append(all, message) - } - - return all, nil -} diff --git a/ros/rosbag_parser/cmd/main.go b/ros/rosbag_parser/cmd/main.go deleted file mode 100644 index af08ab39a7a..00000000000 --- a/ros/rosbag_parser/cmd/main.go +++ /dev/null @@ -1,163 +0,0 @@ -// Package main is a rosbag parser. -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "image" - "image/png" - "io" - "os" - - "github.com/pkg/errors" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/ros" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/segmentation" -) - -var logger = logging.NewDebugLogger("rosbag_parser") - -// Arguments for the rosbag parser. -type Arguments struct { - RosbagFile string `flag:"0"` -} - -func main() { - goutils.ContextualMain(mainWithArgs, logger) -} - -// saveImageAsPng saves image as png in current directory. -func saveImageAsPng(img image.Image, filename string) error { - path := "" - //nolint:gosec - f, err := os.Create(path + filename) - if err != nil { - return err - } - err = png.Encode(f, img) - if err != nil { - return err - } - err = f.Close() - if err != nil { - return err - } - return nil -} - -// extractPlanes extract planes from an image and depth map. -func extractPlanes(ctx context.Context, img *rimage.Image, dm *rimage.DepthMap) (*segmentation.SegmentedImage, error) { - // Set camera matrices in image-with-depth - camera, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(utils.ResolveFile("ros/data/intel515_parameters.json")) - if err != nil { - return nil, err - } - - // Get the pointcloud from the image-with-depth - pcl, err := camera.RGBDToPointCloud(img, dm) - if err != nil { - return nil, err - } - - // Extract the planes from the point cloud - planeSeg := segmentation.NewPointCloudPlaneSegmentation(pcl, 50, 150000) - planes, _, err := planeSeg.FindPlanes(ctx) - if err != nil { - return nil, err - } - - // Project the pointcloud planes into an image - segments := make([]pointcloud.PointCloud, 0, len(planes)) - for _, plane := range planes { - cloud, err := plane.PointCloud() - if err != nil { - return nil, err - } - segments = append(segments, cloud) - } - segImage, err := segmentation.PointCloudSegmentsToMask(camera.ColorCamera, segments) - if err != nil { - return nil, err - } - - return segImage, nil -} - -func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error { - var argsParsed Arguments - - if err := goutils.ParseFlags(args, &argsParsed); err != nil { - return err - } - - rb, err := ros.ReadBag(argsParsed.RosbagFile) - if err != nil { - return err - } - - topics := []string{"/L515_ImageWithDepth"} - err = ros.WriteTopicsJSON(rb, 0, 0, topics) - if err != nil { - return err - } - - var message ros.L515Message - for _, v := range rb.TopicsAsJSON { - count := 0 - for { - // Read bytes into JSON structure - measurement, err := v.ReadBytes('\n') - if err != nil { - if errors.Is(err, io.EOF) { - break - } - return err - } - err = json.Unmarshal(measurement[:len(measurement)-1], &message) - if err != nil { - return err - } - - // Create & display image - img1, _, err := image.Decode(bytes.NewReader(message.ColorData.Data)) - if err != nil { - return err - } - img2, _, err := image.Decode(bytes.NewReader(message.DepthData.Data)) - if err != nil { - return err - } - img := rimage.ConvertImage(img1) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), img2) - if err != nil { - return err - } - imgNrgba := rimage.Overlay(img, dm) - err = saveImageAsPng(imgNrgba, "img_"+fmt.Sprint(count)+".png") - if err != nil { - return err - } - - // Apply plane segmentation on image - segImg, err := extractPlanes(ctx, img, dm) - if err != nil { - return err - } - err = saveImageAsPng(segImg, "seg_img_"+fmt.Sprint(count)+".png") - if err != nil { - return err - } - - count++ - } - } - return nil -} diff --git a/services/baseremotecontrol/base_remote_control.go b/services/baseremotecontrol/base_remote_control.go deleted file mode 100644 index a68da7c74dc..00000000000 --- a/services/baseremotecontrol/base_remote_control.go +++ /dev/null @@ -1,39 +0,0 @@ -// Package baseremotecontrol implements a remote control for a base. -package baseremotecontrol - -import ( - "context" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -// SubtypeName is the name of the type of service. -const SubtypeName = "base_remote_control" - -// API is a variable that identifies the remote control resource API. -var API = resource.APINamespaceRDK.WithServiceType(SubtypeName) - -// Named is a helper for getting the named base remote control service's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named base remote control service from the given Robot. -func FromRobot(r robot.Robot, name string) (Service, error) { - return robot.ResourceFromRobot[Service](r, Named(name)) -} - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Service]{}) -} - -// A Service is the basis for the base remote control. -type Service interface { - resource.Resource - // Close out of all remote control related systems. - Close(ctx context.Context) error - // controllerInputs returns the list of inputs from the controller that are being monitored for that control mode. - ControllerInputs() []input.Control -} diff --git a/services/baseremotecontrol/builtin/builtin.go b/services/baseremotecontrol/builtin/builtin.go deleted file mode 100644 index 6972d263d41..00000000000 --- a/services/baseremotecontrol/builtin/builtin.go +++ /dev/null @@ -1,583 +0,0 @@ -// Package builtin implements a remote control for a base. -package builtin - -import ( - "context" - "math" - "sync" - "sync/atomic" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - vutils "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/baseremotecontrol" - "go.viam.com/rdk/session" -) - -// Constants for the system including the max speed and angle (TBD: allow to be set as config vars) -// as well as the various control modes including oneJoystick (control via a joystick), triggerSpeed -// (triggers control speed and joystick angle), button (four buttons X, Y, A, B to control speed and -// angle) and arrow (arrows buttons used to control speed and angle). -const ( - joyStickControl = controlMode(iota) - triggerSpeedControl - buttonControl - arrowControl - droneControl -) - -func init() { - resource.RegisterService(baseremotecontrol.API, resource.DefaultServiceModel, resource.Registration[baseremotecontrol.Service, *Config]{ - Constructor: NewBuiltIn, - }) -} - -// ControlMode is the control type for the remote control. -type controlMode uint8 - -// Config describes how to configure the service. -type Config struct { - BaseName string `json:"base"` - InputControllerName string `json:"input_controller"` - ControlModeName string `json:"control_mode,omitempty"` - MaxAngularVelocity float64 `json:"max_angular_deg_per_sec,omitempty"` - MaxLinearVelocity float64 `json:"max_linear_mm_per_sec,omitempty"` -} - -// Validate creates the list of implicit dependencies. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - if conf.InputControllerName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "input_controller") - } - deps = append(deps, conf.InputControllerName) - - if conf.BaseName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "base") - } - deps = append(deps, conf.BaseName) - - return deps, nil -} - -// builtIn is the structure of the remote service. -type builtIn struct { - resource.Named - - mu sync.RWMutex - base base.Base - inputController input.Controller - controlMode controlMode - config *Config - - state throttleState - logger logging.Logger - cancel func() - cancelCtx context.Context - activeBackgroundWorkers sync.WaitGroup - events chan (struct{}) - instance atomic.Int64 -} - -// NewBuiltIn returns a new remote control service for the given robot. -func NewBuiltIn( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (baseremotecontrol.Service, error) { - cancelCtx, cancel := context.WithCancel(context.Background()) - remoteSvc := &builtIn{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - cancelCtx: cancelCtx, - cancel: cancel, - events: make(chan struct{}, 1), - } - remoteSvc.state.init() - if err := remoteSvc.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - remoteSvc.eventProcessor() - - return remoteSvc, nil -} - -func (svc *builtIn) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - svcConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - base1, err := base.FromDependencies(deps, svcConfig.BaseName) - if err != nil { - return err - } - controller, err := input.FromDependencies(deps, svcConfig.InputControllerName) - if err != nil { - return err - } - - var controlMode1 controlMode - switch svcConfig.ControlModeName { - case "triggerSpeedControl": - controlMode1 = triggerSpeedControl - case "buttonControl": - controlMode1 = buttonControl - case "joystickControl": - controlMode1 = joyStickControl - case "droneControl": - controlMode1 = droneControl - default: - controlMode1 = arrowControl - } - - svc.mu.Lock() - svc.base = base1 - svc.inputController = controller - svc.controlMode = controlMode1 - svc.config = svcConfig - svc.mu.Unlock() - svc.instance.Add(1) - - if err := svc.registerCallbacks(ctx, &svc.state); err != nil { - return errors.Errorf("error with starting remote control service: %q", err) - } - - return nil -} - -// registerCallbacks registers events from controller to base. -func (svc *builtIn) registerCallbacks(ctx context.Context, state *throttleState) error { - var lastTS time.Time - lastTSPerEvent := map[input.Control]map[input.EventType]time.Time{} - var onlyOneAtATime sync.Mutex - - instance := svc.instance.Load() - - updateLastEvent := func(event input.Event) bool { - if event.Time.After(lastTS) { - lastTS = event.Time - } - if event.Time.Before(lastTSPerEvent[event.Control][event.Event]) { - return false - } - lastTSPerEventControl := lastTSPerEvent[event.Control] - if lastTSPerEventControl == nil { - lastTSPerEventControl = map[input.EventType]time.Time{} - lastTSPerEvent[event.Control] = lastTSPerEventControl - } - lastTSPerEventControl[event.Event] = event.Time - return true - } - - remoteCtl := func(ctx context.Context, event input.Event) { - onlyOneAtATime.Lock() - defer onlyOneAtATime.Unlock() - - if svc.instance.Load() != instance { - return - } - - if svc.cancelCtx.Err() != nil { - return - } - - if !updateLastEvent(event) { - return - } - - svc.processEvent(ctx, state, event) - } - - connect := func(ctx context.Context, event input.Event) { - onlyOneAtATime.Lock() - defer onlyOneAtATime.Unlock() - - if svc.instance.Load() != instance { - return - } - - // Connect and Disconnect events should both stop the base completely. - svc.mu.RLock() - defer svc.mu.RUnlock() - err := svc.base.Stop(ctx, map[string]interface{}{}) - if err != nil { - svc.logger.CError(ctx, err) - } - - if !updateLastEvent(event) { - return - } - } - - for _, control := range svc.ControllerInputs() { - if err := func() error { - svc.mu.RLock() - defer svc.mu.RUnlock() - var err error - if svc.controlMode == buttonControl { - err = svc.inputController.RegisterControlCallback( - ctx, - control, - []input.EventType{input.ButtonChange}, - remoteCtl, - map[string]interface{}{}, - ) - } else { - err = svc.inputController.RegisterControlCallback(ctx, - control, - []input.EventType{input.PositionChangeAbs}, - remoteCtl, - map[string]interface{}{}, - ) - } - if err != nil { - return err - } - err = svc.inputController.RegisterControlCallback(ctx, - control, - []input.EventType{input.Connect, input.Disconnect}, - connect, - map[string]interface{}{}, - ) - if err != nil { - return err - } - return nil - }(); err != nil { - return err - } - } - return nil -} - -// Close out of all remote control related systems. -func (svc *builtIn) Close(_ context.Context) error { - svc.cancel() - svc.activeBackgroundWorkers.Wait() - return nil -} - -// ControllerInputs returns the list of inputs from the controller that are being monitored for that control mode. -func (svc *builtIn) ControllerInputs() []input.Control { - svc.mu.RLock() - defer svc.mu.RUnlock() - switch svc.controlMode { - case triggerSpeedControl: - return []input.Control{input.AbsoluteX, input.AbsoluteZ, input.AbsoluteRZ} - case arrowControl: - return []input.Control{input.AbsoluteHat0X, input.AbsoluteHat0Y} - case buttonControl: - return []input.Control{input.ButtonNorth, input.ButtonSouth, input.ButtonEast, input.ButtonWest} - case joyStickControl: - return []input.Control{input.AbsoluteX, input.AbsoluteY} - case droneControl: - return []input.Control{input.AbsoluteX, input.AbsoluteY, input.AbsoluteRX, input.AbsoluteRY} - } - return []input.Control{} -} - -func (svc *builtIn) eventProcessor() { - var currentLinear, currentAngular r3.Vector - var nextLinear, nextAngular r3.Vector - var inRetry bool - - svc.activeBackgroundWorkers.Add(1) - vutils.ManagedGo(func() { - for { - if svc.cancelCtx.Err() != nil { - return - } - - if inRetry { - select { - case <-svc.cancelCtx.Done(): - case <-svc.events: - default: - } - } else { - select { - case <-svc.cancelCtx.Done(): - case <-svc.events: - } - } - svc.state.mu.Lock() - nextLinear, nextAngular = svc.state.linearThrottle, svc.state.angularThrottle - svc.state.mu.Unlock() - - if func() bool { - svc.mu.RLock() - defer svc.mu.RUnlock() - - if currentLinear != nextLinear || currentAngular != nextAngular { - if svc.config.MaxAngularVelocity > 0 && svc.config.MaxLinearVelocity > 0 { - if err := svc.base.SetVelocity( - svc.cancelCtx, - r3.Vector{ - X: svc.config.MaxLinearVelocity * nextLinear.X, - Y: svc.config.MaxLinearVelocity * nextLinear.Y, - Z: svc.config.MaxLinearVelocity * nextLinear.Z, - }, - r3.Vector{ - X: svc.config.MaxAngularVelocity * nextAngular.X, - Y: svc.config.MaxAngularVelocity * nextAngular.Y, - Z: svc.config.MaxAngularVelocity * nextAngular.Z, - }, - nil, - ); err != nil { - svc.logger.Errorw("error setting velocity", "error", err) - if !vutils.SelectContextOrWait(svc.cancelCtx, 10*time.Millisecond) { - return true - } - inRetry = true - return false - } - } else { - if err := svc.base.SetPower(svc.cancelCtx, nextLinear, nextAngular, nil); err != nil { - svc.logger.Errorw("error setting power", "error", err) - if !vutils.SelectContextOrWait(svc.cancelCtx, 10*time.Millisecond) { - return true - } - inRetry = true - return false - } - } - inRetry = false - - currentLinear = nextLinear - currentAngular = nextAngular - } - - return false - }() { - return - } - } - }, svc.activeBackgroundWorkers.Done) -} - -func (svc *builtIn) processEvent(ctx context.Context, state *throttleState, event input.Event) { - // Order of who processes what event is *not* guaranteed. It depends on the mutex - // fairness mode. Ordering logic must be handled at a higher level in the robot. - // Other than that, values overwrite each other. - state.mu.Lock() - oldLinear := state.linearThrottle - oldAngular := state.angularThrottle - newLinear := oldLinear - newAngular := oldAngular - - svc.mu.RLock() - defer svc.mu.RUnlock() - - switch svc.controlMode { - case joyStickControl: - newLinear.Y, newAngular.Z = oneJoyStickEvent(event, state.linearThrottle.Y, state.angularThrottle.Z) - case droneControl: - newLinear, newAngular = droneEvent(event, state.linearThrottle, state.angularThrottle) - case triggerSpeedControl: - newLinear.Y, newAngular.Z = triggerSpeedEvent(event, state.linearThrottle.Y, state.angularThrottle.Z) - case buttonControl: - newLinear.Y, newAngular.Z, state.buttons = buttonControlEvent(event, state.buttons) - case arrowControl: - newLinear.Y, newAngular.Z, state.arrows = arrowEvent(event, state.arrows) - } - state.linearThrottle = newLinear - state.angularThrottle = newAngular - state.mu.Unlock() - - if similar(newLinear, oldLinear, .05) && similar(newAngular, oldAngular, .05) { - return - } - - // If we do not manage to send the event, that means the processor - // is working and it is about to see our state change anyway. This - // actls like a condition variable signal. - select { - case <-ctx.Done(): - case svc.events <- struct{}{}: - default: - } - - session.SafetyMonitor(ctx, svc.base) -} - -// triggerSpeedEvent takes inputs from the gamepad allowing the triggers to control speed and the left joystick to -// control the angle. -func triggerSpeedEvent(event input.Event, speed, angle float64) (float64, float64) { - switch event.Control { - case input.AbsoluteZ: - speed -= 0.05 - speed = math.Max(-1, speed) - case input.AbsoluteRZ: - speed += 0.05 - speed = math.Min(1, speed) - case input.AbsoluteX: - angle = event.Value - case input.AbsoluteHat0X, input.AbsoluteHat0Y, input.AbsoluteRX, input.AbsoluteRY, input.AbsoluteY, - input.ButtonEStop, input.ButtonEast, input.ButtonLT, input.ButtonLT2, input.ButtonLThumb, input.ButtonMenu, - input.ButtonNorth, input.ButtonRT, input.ButtonRT2, input.ButtonRThumb, input.ButtonRecord, - input.ButtonSelect, input.ButtonSouth, input.ButtonStart, input.ButtonWest, input.AbsolutePedalAccelerator, - input.AbsolutePedalBrake, input.AbsolutePedalClutch: - fallthrough - default: - } - - return speed, angle -} - -// buttonControlEvent takes inputs from the gamepad allowing the X and B buttons to control speed and Y and A buttons to control angle. -func buttonControlEvent(event input.Event, buttons map[input.Control]bool) (float64, float64, map[input.Control]bool) { - var speed float64 - var angle float64 - - switch event.Event { - case input.ButtonPress: - buttons[event.Control] = true - case input.ButtonRelease: - buttons[event.Control] = false - case input.AllEvents, input.ButtonChange, input.ButtonHold, input.Connect, input.Disconnect, - input.PositionChangeAbs, input.PositionChangeRel: - fallthrough - default: - } - - if buttons[input.ButtonNorth] == buttons[input.ButtonSouth] { - speed = 0.0 - } else { - if buttons[input.ButtonNorth] { - speed = 1.0 - } else { - speed = -1.0 - } - } - - if buttons[input.ButtonEast] == buttons[input.ButtonWest] { - angle = 0.0 - } else { - if buttons[input.ButtonEast] { - angle = -1.0 - } else { - angle = 1.0 - } - } - - return speed, angle, buttons -} - -// arrowControlEvent takes inputs from the gamepad allowing the arrow buttons to control speed and angle. -func arrowEvent(event input.Event, arrows map[input.Control]float64) (float64, float64, map[input.Control]float64) { - arrows[event.Control] = -1.0 * event.Value - - speed := arrows[input.AbsoluteHat0Y] - angle := arrows[input.AbsoluteHat0X] - - return speed, angle, arrows -} - -// oneJoyStickEvent (default) takes inputs from the gamepad allowing the left joystick to control speed and angle. -func oneJoyStickEvent(event input.Event, y, x float64) (float64, float64) { - switch event.Control { - case input.AbsoluteY: - y = -1.0 * event.Value - case input.AbsoluteX: - x = -1.0 * event.Value - case input.AbsoluteHat0X, input.AbsoluteHat0Y, input.AbsoluteRX, input.AbsoluteRY, input.AbsoluteRZ, - input.AbsoluteZ, input.ButtonEStop, input.ButtonEast, input.ButtonLT, input.ButtonLT2, input.ButtonLThumb, - input.ButtonMenu, input.ButtonNorth, input.ButtonRT, input.ButtonRT2, input.ButtonRThumb, - input.ButtonRecord, input.ButtonSelect, input.ButtonSouth, input.ButtonStart, input.ButtonWest, input.AbsolutePedalAccelerator, - input.AbsolutePedalBrake, input.AbsolutePedalClutch: - fallthrough - default: - } - - return scaleThrottle(y), scaleThrottle(x) -} - -// right joystick is forward/back, strafe right/left -// left joystick is spin right/left & up/down. -func droneEvent(event input.Event, linear, angular r3.Vector) (r3.Vector, r3.Vector) { - switch event.Control { - case input.AbsoluteX: - angular.Z = scaleThrottle(-1.0 * event.Value) - case input.AbsoluteY: - linear.Z = scaleThrottle(-1.0 * event.Value) - case input.AbsoluteRX: - linear.X = scaleThrottle(event.Value) - case input.AbsoluteRY: - linear.Y = scaleThrottle(-1.0 * event.Value) - case input.AbsoluteHat0X, input.AbsoluteHat0Y, input.AbsoluteRZ, input.AbsoluteZ, input.ButtonEStop, - input.ButtonEast, input.ButtonLT, input.ButtonLT2, input.ButtonLThumb, input.ButtonMenu, input.ButtonNorth, - input.ButtonRT, input.ButtonRT2, input.ButtonRThumb, input.ButtonRecord, input.ButtonSelect, - input.ButtonSouth, input.ButtonStart, input.ButtonWest, input.AbsolutePedalAccelerator, - input.AbsolutePedalBrake, input.AbsolutePedalClutch: - fallthrough - default: - } - - return linear, angular -} - -func similar(a, b r3.Vector, deltaThreshold float64) bool { - if math.Abs(a.X-b.X) > deltaThreshold { - return false - } - - if math.Abs(a.Y-b.Y) > deltaThreshold { - return false - } - - if math.Abs(a.Z-b.Z) > deltaThreshold { - return false - } - - return true -} - -func scaleThrottle(a float64) float64 { - //nolint:ifshort - neg := a < 0 - - a = math.Abs(a) - if a <= .27 { - return 0 - } - - a = math.Ceil(a*10) / 10.0 - - if neg { - a *= -1 - } - - return a -} - -type throttleState struct { - mu sync.Mutex - linearThrottle, angularThrottle r3.Vector - buttons map[input.Control]bool - arrows map[input.Control]float64 -} - -func (ts *throttleState) init() { - ts.buttons = map[input.Control]bool{ - input.ButtonNorth: false, - input.ButtonSouth: false, - input.ButtonEast: false, - input.ButtonWest: false, - } - - ts.arrows = map[input.Control]float64{ - input.AbsoluteHat0X: 0.0, - input.AbsoluteHat0Y: 0.0, - } -} diff --git a/services/baseremotecontrol/builtin/builtin_ext_test.go b/services/baseremotecontrol/builtin/builtin_ext_test.go deleted file mode 100644 index 06ee1d1ec0d..00000000000 --- a/services/baseremotecontrol/builtin/builtin_ext_test.go +++ /dev/null @@ -1,308 +0,0 @@ -package builtin_test - -import ( - "context" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - "go.viam.com/test" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/components/input/webgamepad" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/baseremotecontrol/builtin" - "go.viam.com/rdk/session" - "go.viam.com/rdk/testutils/inject" -) - -func TestSafetyMonitoring(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - gamepadName := input.Named("barf") - gamepad, err := webgamepad.NewController(ctx, nil, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - - myBaseName := base.Named("warf") - injectBase := inject.NewBase(myBaseName.ShortName()) - - setPowerFirst := make(chan struct{}) - injectBase.SetPowerFunc = func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - close(setPowerFirst) - return nil - } - - svc, err := builtin.NewBuiltIn(ctx, resource.Dependencies{ - gamepadName: gamepad, - myBaseName: injectBase, - }, resource.Config{ - ConvertedAttributes: &builtin.Config{ - BaseName: myBaseName.Name, - InputControllerName: gamepadName.Name, - }, - }, logger) - test.That(t, err, test.ShouldBeNil) - - type triggerer interface { - TriggerEvent(ctx context.Context, event input.Event, extra map[string]interface{}) error - } - test.That(t, gamepad.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 1, - }, nil), test.ShouldBeNil) - - <-setPowerFirst - - safetyFirst := make(chan struct{}) - setPowerSecond := make(chan struct{}) - injectBase.SetPowerFunc = func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - <-safetyFirst - close(setPowerSecond) - return nil - } - - var stored sync.Once - var storedCount int32 - var storedID uuid.UUID - var storedResourceName resource.Name - sess1 := session.New(context.Background(), "ownerID", time.Minute, func(id uuid.UUID, resourceName resource.Name) { - atomic.AddInt32(&storedCount, 1) - stored.Do(func() { - storedID = id - storedResourceName = resourceName - close(safetyFirst) - }) - }) - nextCtx := session.ToContext(context.Background(), sess1) - - test.That(t, gamepad.(triggerer).TriggerEvent(nextCtx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 0, - }, nil), test.ShouldBeNil) - - <-setPowerSecond - - test.That(t, storedID, test.ShouldEqual, sess1.ID()) - test.That(t, storedResourceName, test.ShouldResemble, myBaseName) - test.That(t, atomic.LoadInt32(&storedCount), test.ShouldEqual, 1) - - test.That(t, svc.Close(ctx), test.ShouldBeNil) -} - -func TestConnectStopsBase(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - gamepadName := input.Named("barf") - gamepad, err := webgamepad.NewController(ctx, nil, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - - myBaseName := base.Named("warf") - injectBase := &inject.Base{Base: &fake.Base{ - Named: myBaseName.AsNamed(), - }} - - //nolint:dupl - t.Run("connect", func(t *testing.T) { - // Use an injected Stop function and a channel to ensure stop is called on connect. - stop := make(chan struct{}) - injectBase.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - close(stop) - return nil - } - - svc, err := builtin.NewBuiltIn(ctx, resource.Dependencies{ - gamepadName: gamepad, - myBaseName: injectBase, - }, resource.Config{ - ConvertedAttributes: &builtin.Config{ - BaseName: myBaseName.Name, - InputControllerName: gamepadName.Name, - }, - }, logger) - test.That(t, err, test.ShouldBeNil) - - type triggerer interface { - TriggerEvent(ctx context.Context, event input.Event, extra map[string]interface{}) error - } - test.That(t, gamepad.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.Connect, - Control: input.AbsoluteHat0X, - }, nil), test.ShouldBeNil) - - <-stop - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) - - //nolint:dupl - t.Run("disconnect", func(t *testing.T) { - // Use an injected Stop function and a channel to ensure stop is called on disconnect. - stop := make(chan struct{}) - injectBase.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - close(stop) - return nil - } - - svc, err := builtin.NewBuiltIn(ctx, resource.Dependencies{ - gamepadName: gamepad, - myBaseName: injectBase, - }, resource.Config{ - ConvertedAttributes: &builtin.Config{ - BaseName: myBaseName.Name, - InputControllerName: gamepadName.Name, - }, - }, logger) - test.That(t, err, test.ShouldBeNil) - - type triggerer interface { - TriggerEvent(ctx context.Context, event input.Event, extra map[string]interface{}) error - } - test.That(t, gamepad.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.Disconnect, - Control: input.AbsoluteHat0X, - }, nil), test.ShouldBeNil) - - <-stop - test.That(t, svc.Close(ctx), test.ShouldBeNil) - }) -} - -func TestReconfigure(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - gamepadName := input.Named("barf") - gamepad, err := webgamepad.NewController(ctx, nil, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - - myBaseName := base.Named("warf") - injectBase := inject.NewBase(myBaseName.ShortName()) - - setPowerVal := make(chan r3.Vector) - injectBase.SetPowerFunc = func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - setPowerVal <- angular - return nil - } - - svc, err := builtin.NewBuiltIn(ctx, resource.Dependencies{ - gamepadName: gamepad, - myBaseName: injectBase, - }, resource.Config{ - ConvertedAttributes: &builtin.Config{ - BaseName: myBaseName.Name, - InputControllerName: gamepadName.Name, - }, - }, logger) - test.That(t, err, test.ShouldBeNil) - - type triggerer interface { - TriggerEvent(ctx context.Context, event input.Event, extra map[string]interface{}) error - } - - test.That(t, gamepad.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 1, - }, nil), test.ShouldBeNil) - test.That(t, <-setPowerVal, test.ShouldResemble, r3.Vector{0, 0, -1}) - - err = svc.Reconfigure(ctx, nil, resource.Config{ - ConvertedAttributes: &builtin.Config{}, - }) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "missing from") - - test.That(t, gamepad.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 2, - }, nil), test.ShouldBeNil) - test.That(t, <-setPowerVal, test.ShouldResemble, r3.Vector{0, 0, -2}) - - err = svc.Reconfigure(ctx, resource.Dependencies{ - gamepadName: gamepad, - myBaseName: injectBase, - }, resource.Config{ - ConvertedAttributes: &builtin.Config{ - BaseName: myBaseName.Name, - InputControllerName: gamepadName.Name, - }, - }) - test.That(t, err, test.ShouldBeNil) - - test.That(t, gamepad.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 3, - }, nil), test.ShouldBeNil) - test.That(t, <-setPowerVal, test.ShouldResemble, r3.Vector{0, 0, -3}) - - gamepadName2 := input.Named("hello") - gamepad2, err := webgamepad.NewController(ctx, nil, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - - err = svc.Reconfigure(ctx, resource.Dependencies{ - gamepadName2: gamepad2, - myBaseName: injectBase, - }, resource.Config{ - ConvertedAttributes: &builtin.Config{ - BaseName: myBaseName.Name, - InputControllerName: gamepadName2.Name, - }, - }) - test.That(t, err, test.ShouldBeNil) - - test.That(t, gamepad.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 3, - }, nil), test.ShouldBeNil) - test.That(t, gamepad2.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 4, - }, nil), test.ShouldBeNil) - - test.That(t, <-setPowerVal, test.ShouldResemble, r3.Vector{0, 0, -4}) - - close(setPowerVal) - - myBaseName2 := base.Named("warf2") - injectBase2 := inject.NewBase(myBaseName2.ShortName()) - - setPowerVal2 := make(chan r3.Vector) - injectBase2.SetPowerFunc = func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - setPowerVal2 <- angular - return nil - } - - err = svc.Reconfigure(ctx, resource.Dependencies{ - gamepadName2: gamepad2, - myBaseName2: injectBase2, - }, resource.Config{ - ConvertedAttributes: &builtin.Config{ - BaseName: myBaseName2.Name, - InputControllerName: gamepadName2.Name, - }, - }) - test.That(t, err, test.ShouldBeNil) - - test.That(t, gamepad2.(triggerer).TriggerEvent(ctx, input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteHat0X, - Value: 5, - }, nil), test.ShouldBeNil) - - test.That(t, <-setPowerVal2, test.ShouldResemble, r3.Vector{0, 0, -5}) - - test.That(t, svc.Close(ctx), test.ShouldBeNil) -} diff --git a/services/baseremotecontrol/builtin/builtin_test.go b/services/baseremotecontrol/builtin/builtin_test.go deleted file mode 100644 index 5746fbf25af..00000000000 --- a/services/baseremotecontrol/builtin/builtin_test.go +++ /dev/null @@ -1,376 +0,0 @@ -package builtin - -import ( - "context" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - fakebase "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/baseremotecontrol" - "go.viam.com/rdk/testutils/inject" -) - -func TestBaseRemoteControl(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - deps := make(resource.Dependencies) - cfg := &Config{ - BaseName: "baseTest", - InputControllerName: "inputTest", - ControlModeName: "", - } - - depNames, err := cfg.Validate("") - test.That(t, err, test.ShouldBeNil) - test.That(t, utils.NewStringSet(depNames...), test.ShouldResemble, utils.NewStringSet("baseTest", "inputTest")) - - fakeController := &inject.InputController{} - fakeBase := &fakebase.Base{} - - deps[input.Named(cfg.InputControllerName)] = fakeController - deps[base.Named(cfg.BaseName)] = fakeBase - - fakeController.RegisterControlCallbackFunc = func( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, - ) error { - return nil - } - - // New base_remote_control check - cfg.ControlModeName = "joystickControl" - tmpSvc, err := NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeNil) - svc, ok := tmpSvc.(*builtIn) - test.That(t, ok, test.ShouldBeTrue) - - cfg.ControlModeName = "triggerSpeedControl" - tmpSvc1, err := NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeNil) - svc1, ok := tmpSvc1.(*builtIn) - test.That(t, ok, test.ShouldBeTrue) - - cfg.ControlModeName = "arrowControl" - tmpSvc2, err := NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeNil) - svc2, ok := tmpSvc2.(*builtIn) - test.That(t, ok, test.ShouldBeTrue) - - cfg.ControlModeName = "buttonControl" - tmpSvc3, err := NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeNil) - svc3, ok := tmpSvc3.(*builtIn) - test.That(t, ok, test.ShouldBeTrue) - - cfg.ControlModeName = "fail" - tmpSvc4, err := NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeNil) - svc4, ok := tmpSvc4.(*builtIn) - test.That(t, ok, test.ShouldBeTrue) - - // Controller import failure - delete(deps, input.Named(cfg.InputControllerName)) - deps[base.Named(cfg.BaseName)] = fakeBase - - _, err = NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeError, errors.New("Resource missing from dependencies. Resource: rdk:component:input_controller/inputTest")) - - // Base import failure - deps[input.Named(cfg.InputControllerName)] = fakeController - delete(deps, base.Named(cfg.BaseName)) - - _, err = NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeError, errors.New("Resource missing from dependencies. Resource: rdk:component:base/baseTest")) - - // Deps exist but are incorrect component - deps[input.Named(cfg.InputControllerName)] = fakeController - deps[base.Named(cfg.BaseName)] = fakeController - _, err = NewBuiltIn(ctx, deps, - resource.Config{ - Name: "base_remote_control", - API: baseremotecontrol.API, - ConvertedAttributes: cfg, - }, - logger) - test.That(t, err, test.ShouldBeError, - errors.New("dependency \"rdk:component:base/baseTest\" should be an implementation of base.Base but it was a *inject.InputController")) - - // Controller event by mode - t.Run("controller events joystick control mode", func(t *testing.T) { - i := svc.ControllerInputs() - test.That(t, i[0], test.ShouldEqual, input.AbsoluteX) - test.That(t, i[1], test.ShouldEqual, input.AbsoluteY) - }) - - t.Run("controller events trigger speed control mode", func(t *testing.T) { - i := svc1.ControllerInputs() - test.That(t, i[0], test.ShouldEqual, input.AbsoluteX) - test.That(t, i[1], test.ShouldEqual, input.AbsoluteZ) - test.That(t, i[2], test.ShouldEqual, input.AbsoluteRZ) - }) - - t.Run("controller events arrow control mode", func(t *testing.T) { - i := svc2.ControllerInputs() - test.That(t, i[0], test.ShouldEqual, input.AbsoluteHat0X) - test.That(t, i[1], test.ShouldEqual, input.AbsoluteHat0Y) - }) - - t.Run("controller events button control mode", func(t *testing.T) { - i := svc3.ControllerInputs() - test.That(t, i[0], test.ShouldEqual, input.ButtonNorth) - test.That(t, i[1], test.ShouldEqual, input.ButtonSouth) - test.That(t, i[2], test.ShouldEqual, input.ButtonEast) - test.That(t, i[3], test.ShouldEqual, input.ButtonWest) - }) - - t.Run("controller events button no mode", func(t *testing.T) { - svc4.mu.Lock() - svc4.controlMode = 8 - svc4.mu.Unlock() - i := svc4.ControllerInputs() - test.That(t, len(i), test.ShouldEqual, 0) - }) - - // JoystickControl - eventX := input.Event{ - Control: input.AbsoluteX, - Value: 1.0, - } - - eventY := input.Event{ - Event: input.PositionChangeAbs, - Control: input.AbsoluteY, - Value: 1.0, - } - - eventHat0X := input.Event{ - Control: input.AbsoluteHat0X, - Value: 1.0, - } - - eventHat0Y := input.Event{ - Control: input.AbsoluteHat0Y, - Value: 1.0, - } - - t.Run("joy stick control mode for input X", func(t *testing.T) { - mmPerSec, degsPerSec := oneJoyStickEvent(eventX, 0.5, 0.6) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 0.5, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, -1.0, .001) - }) - - t.Run("joy stick control mode for input Y", func(t *testing.T) { - mmPerSec, degsPerSec := oneJoyStickEvent(eventY, 0.5, 0.6) - test.That(t, mmPerSec, test.ShouldAlmostEqual, -1.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.6, .001) - }) - - // TriggerSpeedControl - eventZ := input.Event{ - Control: input.AbsoluteZ, - Value: 1.0, - } - - eventRZ := input.Event{ - Control: input.AbsoluteRZ, - Value: 1.0, - } - - t.Run("trigger speed control mode for input X", func(t *testing.T) { - mmPerSec, degsPerSec := triggerSpeedEvent(eventX, 0.5, 0.6) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 0.5, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 1.0, .001) - }) - - t.Run("trigger speed control mode for input Z", func(t *testing.T) { - mmPerSec, degsPerSec := triggerSpeedEvent(eventZ, 0.8, 0.8) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 0.75, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.8, .001) - }) - - t.Run("trigger speed control mode for input RZ", func(t *testing.T) { - mmPerSec, degsPerSec := triggerSpeedEvent(eventRZ, 0.8, 0.8) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 0.85, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.8, .001) - }) - - t.Run("trigger speed control mode for input Y (invalid event)", func(t *testing.T) { - mmPerSec, degsPerSec := triggerSpeedEvent(eventY, 0.8, 0.8) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 0.8, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.8, .001) - }) - - // ArrowControl - - arrows := make(map[input.Control]float64) - arrows[input.AbsoluteHat0X] = 0.0 - arrows[input.AbsoluteHat0Y] = 0.0 - - t.Run("arrow control mode for input X", func(t *testing.T) { - mmPerSec, degsPerSec, _ := arrowEvent(eventHat0X, arrows) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, -1.0, .001) - }) - - t.Run("arrow control mode for input Y", func(t *testing.T) { - mmPerSec, degsPerSec, _ := arrowEvent(eventHat0Y, arrows) - test.That(t, mmPerSec, test.ShouldAlmostEqual, -1.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, -1.0, .001) - }) - - // ButtonControl - buttons := make(map[input.Control]bool) - buttons[input.ButtonNorth] = false - buttons[input.ButtonSouth] = false - buttons[input.ButtonEast] = false - buttons[input.ButtonWest] = false - - eventButtonNorthPress := input.Event{ - Event: input.ButtonPress, - Control: input.ButtonNorth, - } - - eventButtonSouthPress := input.Event{ - Event: input.ButtonPress, - Control: input.ButtonSouth, - } - - eventButtonNorthRelease := input.Event{ - Event: input.ButtonRelease, - Control: input.ButtonNorth, - } - - t.Run("button control mode for input X and B", func(t *testing.T) { - mmPerSec, degsPerSec, _ := buttonControlEvent(eventButtonNorthPress, buttons) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 1.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.0, .001) - - mmPerSec, degsPerSec, _ = buttonControlEvent(eventButtonSouthPress, buttons) - test.That(t, mmPerSec, test.ShouldAlmostEqual, 0.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.0, .001) - - mmPerSec, degsPerSec, _ = buttonControlEvent(eventButtonNorthRelease, buttons) - test.That(t, mmPerSec, test.ShouldAlmostEqual, -1.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.0, .001) - }) - - eventButtonEastPress := input.Event{ - Event: input.ButtonPress, - Control: input.ButtonEast, - } - - eventButtonWestPress := input.Event{ - Event: input.ButtonPress, - Control: input.ButtonWest, - } - - eventButtonEastRelease := input.Event{ - Event: input.ButtonRelease, - Control: input.ButtonEast, - } - - t.Run("button control mode for input Y and A", func(t *testing.T) { - mmPerSec, degsPerSec, _ := buttonControlEvent(eventButtonEastPress, buttons) - test.That(t, mmPerSec, test.ShouldAlmostEqual, -1.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, -1.0, .001) - - mmPerSec, degsPerSec, _ = buttonControlEvent(eventButtonWestPress, buttons) - test.That(t, mmPerSec, test.ShouldAlmostEqual, -1.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 0.0, .001) - - mmPerSec, degsPerSec, _ = buttonControlEvent(eventButtonEastRelease, buttons) - test.That(t, mmPerSec, test.ShouldAlmostEqual, -1.0, .001) - test.That(t, degsPerSec, test.ShouldAlmostEqual, 1.0, .001) - }) - - err = svc.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - err = svc1.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - err = svc2.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - err = svc3.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - err = svc4.Close(ctx) - test.That(t, err, test.ShouldBeNil) - - // Close out check - err = svc.Close(ctx) - test.That(t, err, test.ShouldBeNil) -} - -func TestLowLevel(t *testing.T) { - test.That(t, scaleThrottle(.01), test.ShouldAlmostEqual, 0, .001) - test.That(t, scaleThrottle(-.01), test.ShouldAlmostEqual, 0, .001) - - test.That(t, scaleThrottle(.33), test.ShouldAlmostEqual, 0.4, .001) - test.That(t, scaleThrottle(.81), test.ShouldAlmostEqual, 0.9, .001) - test.That(t, scaleThrottle(1.0), test.ShouldAlmostEqual, 1.0, .001) - - test.That(t, scaleThrottle(-.81), test.ShouldAlmostEqual, -0.9, .001) - test.That(t, scaleThrottle(-1.0), test.ShouldAlmostEqual, -1.0, .001) -} - -func TestSimilar(t *testing.T) { - test.That(t, similar(r3.Vector{}, r3.Vector{}, 1), test.ShouldBeTrue) - test.That(t, similar(r3.Vector{X: 2}, r3.Vector{}, 1), test.ShouldBeFalse) - test.That(t, similar(r3.Vector{Y: 2}, r3.Vector{}, 1), test.ShouldBeFalse) - test.That(t, similar(r3.Vector{Z: 2}, r3.Vector{}, 1), test.ShouldBeFalse) -} diff --git a/services/baseremotecontrol/register/register.go b/services/baseremotecontrol/register/register.go deleted file mode 100644 index 866e0f28c03..00000000000 --- a/services/baseremotecontrol/register/register.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package register registers all relevant baseremotecontrol models and also API specific functions -package register - -import ( - // for baseremotecontrol models. - _ "go.viam.com/rdk/services/baseremotecontrol/builtin" -) diff --git a/services/baseremotecontrol/verify_main_test.go b/services/baseremotecontrol/verify_main_test.go deleted file mode 100644 index 20c1d4f0be2..00000000000 --- a/services/baseremotecontrol/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package baseremotecontrol - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/datamanager/builtin/builtin.go b/services/datamanager/builtin/builtin.go deleted file mode 100644 index 89542ed17d8..00000000000 --- a/services/datamanager/builtin/builtin.go +++ /dev/null @@ -1,735 +0,0 @@ -// Package builtin contains a service type that can be used to capture data from a robot's components. -package builtin - -import ( - "context" - "fmt" - "os" - "path/filepath" - "reflect" - "sync" - "time" - - clk "github.com/benbjohnson/clock" - "github.com/pkg/errors" - v1 "go.viam.com/api/app/datasync/v1" - goutils "go.viam.com/utils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/data" - "go.viam.com/rdk/internal/cloud" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/datamanager" - "go.viam.com/rdk/services/datamanager/datacapture" - "go.viam.com/rdk/services/datamanager/datasync" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/utils" -) - -func init() { - resource.RegisterService( - datamanager.API, - resource.DefaultServiceModel, - resource.Registration[datamanager.Service, *Config]{ - Constructor: NewBuiltIn, - WeakDependencies: []resource.Matcher{ - resource.TypeMatcher{Type: resource.APITypeComponentName}, - resource.SubtypeMatcher{Subtype: slam.SubtypeName}, - }, - }) -} - -// TODO: re-determine if queue size is optimal given we now support 10khz+ capture rates -// The Collector's queue should be big enough to ensure that .capture() is never blocked by the queue being -// written to disk. A default value of 250 was chosen because even with the fastest reasonable capture interval (1ms), -// this would leave 250ms for a (buffered) disk write before blocking, which seems sufficient for the size of -// writes this would be performing. -const defaultCaptureQueueSize = 250 - -// Default bufio.Writer buffer size in bytes. -const defaultCaptureBufferSize = 4096 - -// Default time to wait in milliseconds to check if a file has been modified. -const defaultFileLastModifiedMillis = 10000.0 - -// Default time between disk size checks. -const filesystemPollInterval = 30 * time.Second - -var clock = clk.New() - -var errCaptureDirectoryConfigurationDisabled = errors.New("changing the capture directory is prohibited in this environment") - -// Config describes how to configure the service. -type Config struct { - CaptureDir string `json:"capture_dir"` - AdditionalSyncPaths []string `json:"additional_sync_paths"` - SyncIntervalMins float64 `json:"sync_interval_mins"` - CaptureDisabled bool `json:"capture_disabled"` - ScheduledSyncDisabled bool `json:"sync_disabled"` - Tags []string `json:"tags"` - FileLastModifiedMillis int `json:"file_last_modified_millis"` - SelectiveSyncerName string `json:"selective_syncer_name"` -} - -// Validate returns components which will be depended upon weakly due to the above matcher. -func (c *Config) Validate(path string) ([]string, error) { - return []string{cloud.InternalServiceName.String()}, nil -} - -type selectiveSyncer interface { - sensor.Sensor -} - -// readyToSync is a method for getting the bool reading from the selective sync sensor -// for determining whether the key is present and what its value is. -func readyToSync(ctx context.Context, s selectiveSyncer, logger logging.Logger) (readyToSync bool) { - readyToSync = false - readings, err := s.Readings(ctx, nil) - if err != nil { - logger.CErrorw(ctx, "error getting readings from selective syncer", "error", err.Error()) - return - } - readyToSyncVal, ok := readings[datamanager.ShouldSyncKey] - if !ok { - logger.CErrorf(ctx, "value for should sync key %s not present in readings", datamanager.ShouldSyncKey) - return - } - readyToSyncBool, err := utils.AssertType[bool](readyToSyncVal) - if err != nil { - logger.CErrorw(ctx, "error converting should sync key to bool", "key", datamanager.ShouldSyncKey, "error", err.Error()) - return - } - readyToSync = readyToSyncBool - return -} - -// builtIn initializes and orchestrates data capture collectors for registered component/methods. -type builtIn struct { - resource.Named - logger logging.Logger - captureDir string - captureDisabled bool - collectors map[resourceMethodMetadata]*collectorAndConfig - lock sync.Mutex - backgroundWorkers sync.WaitGroup - fileLastModifiedMillis int - - additionalSyncPaths []string - tags []string - syncDisabled bool - syncIntervalMins float64 - syncRoutineCancelFn context.CancelFunc - syncer datasync.Manager - syncerConstructor datasync.ManagerConstructor - cloudConnSvc cloud.ConnectionService - cloudConn rpc.ClientConn - syncTicker *clk.Ticker - - syncSensor selectiveSyncer - selectiveSyncEnabled bool - - componentMethodFrequencyHz map[resourceMethodMetadata]float32 - - fileDeletionRoutineCancelFn context.CancelFunc - fileDeletionBackgroundWorkers *sync.WaitGroup -} - -var viamCaptureDotDir = filepath.Join(os.Getenv("HOME"), ".viam", "capture") - -// NewBuiltIn returns a new data manager service for the given robot. -func NewBuiltIn( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, - logger logging.Logger, -) (datamanager.Service, error) { - svc := &builtIn{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - captureDir: viamCaptureDotDir, - collectors: make(map[resourceMethodMetadata]*collectorAndConfig), - syncIntervalMins: 0, - additionalSyncPaths: []string{}, - tags: []string{}, - fileLastModifiedMillis: defaultFileLastModifiedMillis, - syncerConstructor: datasync.NewManager, - selectiveSyncEnabled: false, - componentMethodFrequencyHz: make(map[resourceMethodMetadata]float32), - } - - if err := svc.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return svc, nil -} - -// Close releases all resources managed by data_manager. -func (svc *builtIn) Close(_ context.Context) error { - svc.lock.Lock() - svc.closeCollectors() - svc.closeSyncer() - if svc.syncRoutineCancelFn != nil { - svc.syncRoutineCancelFn() - } - if svc.fileDeletionRoutineCancelFn != nil { - svc.fileDeletionRoutineCancelFn() - } - - svc.lock.Unlock() - svc.backgroundWorkers.Wait() - - if svc.fileDeletionBackgroundWorkers != nil { - svc.fileDeletionBackgroundWorkers.Wait() - } - - return nil -} - -func (svc *builtIn) closeCollectors() { - var wg sync.WaitGroup - for md, collector := range svc.collectors { - currCollector := collector - wg.Add(1) - go func() { - defer wg.Done() - currCollector.Collector.Close() - }() - delete(svc.collectors, md) - } - wg.Wait() -} - -func (svc *builtIn) flushCollectors() { - var wg sync.WaitGroup - for _, collector := range svc.collectors { - currCollector := collector - wg.Add(1) - go func() { - defer wg.Done() - currCollector.Collector.Flush() - }() - } - wg.Wait() -} - -// Parameters stored for each collector. -type collectorAndConfig struct { - Resource resource.Resource - Collector data.Collector - Config datamanager.DataCaptureConfig -} - -// Identifier for a particular collector: component name, component model, component type, -// method parameters, and method name. -type resourceMethodMetadata struct { - ResourceName string - MethodParams string - MethodMetadata data.MethodMetadata -} - -func (r resourceMethodMetadata) String() string { - return fmt.Sprintf( - "[API: %s, Resource Name: %s, Method Name: %s, Method Params: %s]", - r.MethodMetadata.API, r.ResourceName, r.MethodMetadata.MethodName, r.MethodParams) -} - -// Get time.Duration from hz. -func getDurationFromHz(captureFrequencyHz float32) time.Duration { - if captureFrequencyHz == 0 { - return time.Duration(0) - } - return time.Duration(float32(time.Second) / captureFrequencyHz) -} - -var metadataToAdditionalParamFields = map[string]string{ - generateMetadataKey("rdk:component:board", "Analogs"): "reader_name", - generateMetadataKey("rdk:component:board", "Gpios"): "pin_name", -} - -// Initialize a collector for the component/method or update it if it has previously been created. -// Return the component/method metadata which is used as a key in the collectors map. -func (svc *builtIn) initializeOrUpdateCollector( - res resource.Resource, - md resourceMethodMetadata, - config datamanager.DataCaptureConfig, -) (*collectorAndConfig, error) { - // Build metadata. - captureMetadata, err := datacapture.BuildCaptureMetadata( - config.Name.API, - config.Name.ShortName(), - config.Method, - config.AdditionalParams, - config.Tags, - ) - if err != nil { - return nil, err - } - - // TODO(DATA-451): validate method params - - if storedCollectorAndConfig, ok := svc.collectors[md]; ok { - if storedCollectorAndConfig.Config.Equals(&config) && res == storedCollectorAndConfig.Resource { - // If the attributes have not changed, do nothing and leave the existing collector. - return svc.collectors[md], nil - } - // If the attributes have changed, close the existing collector. - storedCollectorAndConfig.Collector.Close() - } - - // Get collector constructor for the component API and method. - collectorConstructor := data.CollectorLookup(md.MethodMetadata) - if collectorConstructor == nil { - return nil, errors.Errorf("failed to find collector constructor for %s", md.MethodMetadata) - } - - // Parameters to initialize collector. - interval := getDurationFromHz(config.CaptureFrequencyHz) - // Set queue size to defaultCaptureQueueSize if it was not set in the config. - captureQueueSize := config.CaptureQueueSize - if captureQueueSize == 0 { - captureQueueSize = defaultCaptureQueueSize - } - - captureBufferSize := config.CaptureBufferSize - if captureBufferSize == 0 { - captureBufferSize = defaultCaptureBufferSize - } - additionalParamKey, ok := metadataToAdditionalParamFields[generateMetadataKey( - md.MethodMetadata.API.String(), - md.MethodMetadata.MethodName)] - if ok { - if _, ok := config.AdditionalParams[additionalParamKey]; !ok { - return nil, errors.Errorf("failed to validate additional_params for %s, must supply %s", - md.MethodMetadata.API.String(), additionalParamKey) - } - } - methodParams, err := protoutils.ConvertStringMapToAnyPBMap(config.AdditionalParams) - if err != nil { - return nil, err - } - - // Create a collector for this resource and method. - targetDir := datacapture.FilePathWithReplacedReservedChars( - filepath.Join(svc.captureDir, captureMetadata.GetComponentType(), - captureMetadata.GetComponentName(), captureMetadata.GetMethodName())) - if err := os.MkdirAll(targetDir, 0o700); err != nil { - return nil, err - } - params := data.CollectorParams{ - ComponentName: config.Name.ShortName(), - Interval: interval, - MethodParams: methodParams, - Target: datacapture.NewBuffer(targetDir, captureMetadata), - QueueSize: captureQueueSize, - BufferSize: captureBufferSize, - Logger: svc.logger, - Clock: clock, - } - collector, err := (*collectorConstructor)(res, params) - if err != nil { - return nil, err - } - collector.Collect() - - return &collectorAndConfig{res, collector, config}, nil -} - -func (svc *builtIn) closeSyncer() { - if svc.syncer != nil { - // If previously we were syncing, close the old syncer and cancel the old updateCollectors goroutine. - svc.syncer.Close() - svc.syncer = nil - } - if svc.cloudConn != nil { - goutils.UncheckedError(svc.cloudConn.Close()) - } -} - -var grpcConnectionTimeout = 10 * time.Second - -func (svc *builtIn) initSyncer(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, grpcConnectionTimeout) - defer cancel() - - identity, conn, err := svc.cloudConnSvc.AcquireConnection(ctx) - if errors.Is(err, cloud.ErrNotCloudManaged) { - svc.logger.CDebug(ctx, "Using no-op sync manager when not cloud managed") - svc.syncer = datasync.NewNoopManager() - } - if err != nil { - return err - } - - client := v1.NewDataSyncServiceClient(conn) - - syncer, err := svc.syncerConstructor(identity, client, svc.logger, svc.captureDir) - if err != nil { - return errors.Wrap(err, "failed to initialize new syncer") - } - svc.syncer = syncer - svc.cloudConn = conn - return nil -} - -// TODO: Determine desired behavior if sync is disabled. Do we wan to allow manual syncs, then? -// If so, how could a user cancel it? - -// Sync performs a non-scheduled sync of the data in the capture directory. -// If automated sync is also enabled, calling Sync will upload the files, -// regardless of whether or not is the scheduled time. -func (svc *builtIn) Sync(ctx context.Context, _ map[string]interface{}) error { - svc.lock.Lock() - if svc.syncer == nil { - err := svc.initSyncer(ctx) - if err != nil { - svc.lock.Unlock() - return err - } - } - - svc.lock.Unlock() - svc.sync() - return nil -} - -// Reconfigure updates the data manager service when the config has changed. -func (svc *builtIn) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - svc.lock.Lock() - defer svc.lock.Unlock() - svcConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - cloudConnSvc, err := resource.FromDependencies[cloud.ConnectionService](deps, cloud.InternalServiceName) - if err != nil { - return err - } - - reinitSyncer := cloudConnSvc != svc.cloudConnSvc - svc.cloudConnSvc = cloudConnSvc - - captureConfigs, err := svc.updateDataCaptureConfigs(deps, conf, svcConfig.CaptureDir) - if err != nil { - return err - } - - if !utils.IsTrustedEnvironment(ctx) && svcConfig.CaptureDir != "" && svcConfig.CaptureDir != viamCaptureDotDir { - return errCaptureDirectoryConfigurationDisabled - } - - if svcConfig.CaptureDir != "" { - svc.captureDir = svcConfig.CaptureDir - } - if svcConfig.CaptureDir == "" { - svc.captureDir = viamCaptureDotDir - } - svc.captureDisabled = svcConfig.CaptureDisabled - // Service is disabled, so close all collectors and clear the map so we can instantiate new ones if we enable this service. - if svc.captureDisabled { - svc.closeCollectors() - svc.collectors = make(map[resourceMethodMetadata]*collectorAndConfig) - } - - if svc.fileDeletionRoutineCancelFn != nil { - svc.fileDeletionRoutineCancelFn() - } - if svc.fileDeletionBackgroundWorkers != nil { - svc.fileDeletionBackgroundWorkers.Wait() - } - - // Initialize or add collectors based on changes to the component configurations. - newCollectors := make(map[resourceMethodMetadata]*collectorAndConfig) - if !svc.captureDisabled { - for res, resConfs := range captureConfigs { - for _, resConf := range resConfs { - // Create component/method metadata - methodMetadata := data.MethodMetadata{ - API: resConf.Name.API, - MethodName: resConf.Method, - } - - componentMethodMetadata := resourceMethodMetadata{ - ResourceName: resConf.Name.ShortName(), - MethodMetadata: methodMetadata, - MethodParams: fmt.Sprintf("%v", resConf.AdditionalParams), - } - _, ok := svc.componentMethodFrequencyHz[componentMethodMetadata] - - // Only log capture frequency if the component frequency is new or the frequency has changed - // otherwise we'll be logging way too much - if !ok || (ok && resConf.CaptureFrequencyHz != svc.componentMethodFrequencyHz[componentMethodMetadata]) { - syncVal := "will" - if resConf.CaptureFrequencyHz == 0 { - syncVal += " not" - } - svc.logger.Infof( - "capture frequency for %s is set to %.2fHz and %s sync", componentMethodMetadata, resConf.CaptureFrequencyHz, syncVal, - ) - } - - // we need this map to keep track of if state has changed in the configs - // without it, we will be logging the same message over and over for no reason - svc.componentMethodFrequencyHz[componentMethodMetadata] = resConf.CaptureFrequencyHz - - if !resConf.Disabled && resConf.CaptureFrequencyHz > 0 { - // We only use service-level tags. - resConf.Tags = svcConfig.Tags - - newCollectorAndConfig, err := svc.initializeOrUpdateCollector(res, componentMethodMetadata, resConf) - if err != nil { - svc.logger.CErrorw(ctx, "failed to initialize or update collector", "error", err) - } else { - newCollectors[componentMethodMetadata] = newCollectorAndConfig - } - } - } - } - } else { - svc.fileDeletionRoutineCancelFn = nil - svc.fileDeletionBackgroundWorkers = nil - } - - // If a component/method has been removed from the config, close the collector. - for md, collAndConfig := range svc.collectors { - if _, present := newCollectors[md]; !present { - collAndConfig.Collector.Close() - } - } - svc.collectors = newCollectors - svc.additionalSyncPaths = svcConfig.AdditionalSyncPaths - - fileLastModifiedMillis := svcConfig.FileLastModifiedMillis - if fileLastModifiedMillis <= 0 { - fileLastModifiedMillis = defaultFileLastModifiedMillis - } - - var syncSensor sensor.Sensor - if svcConfig.SelectiveSyncerName != "" { - svc.selectiveSyncEnabled = true - syncSensor, err = sensor.FromDependencies(deps, svcConfig.SelectiveSyncerName) - if err != nil { - svc.logger.CErrorw( - ctx, "unable to initialize selective syncer; will not sync at all until fixed or removed from config", "error", err.Error()) - } - } else { - svc.selectiveSyncEnabled = false - } - if svc.syncSensor != syncSensor { - svc.syncSensor = syncSensor - } - - if svc.syncDisabled != svcConfig.ScheduledSyncDisabled || svc.syncIntervalMins != svcConfig.SyncIntervalMins || - !reflect.DeepEqual(svc.tags, svcConfig.Tags) || svc.fileLastModifiedMillis != fileLastModifiedMillis { - svc.syncDisabled = svcConfig.ScheduledSyncDisabled - svc.syncIntervalMins = svcConfig.SyncIntervalMins - svc.tags = svcConfig.Tags - svc.fileLastModifiedMillis = fileLastModifiedMillis - - svc.cancelSyncScheduler() - if !svc.syncDisabled && svc.syncIntervalMins != 0.0 { - if svc.syncer == nil { - if err := svc.initSyncer(ctx); err != nil { - return err - } - } else if reinitSyncer { - svc.closeSyncer() - if err := svc.initSyncer(ctx); err != nil { - return err - } - } - svc.syncer.SetArbitraryFileTags(svc.tags) - svc.startSyncScheduler(svc.syncIntervalMins) - } else { - if svc.syncTicker != nil { - svc.syncTicker.Stop() - svc.syncTicker = nil - } - svc.closeSyncer() - } - } - // if datacapture is enabled, kick off a go routine to check if disk space is filling due to - // cached datacapture files - if !svc.captureDisabled { - fileDeletionCtx, cancelFunc := context.WithCancel(context.Background()) - svc.fileDeletionRoutineCancelFn = cancelFunc - svc.fileDeletionBackgroundWorkers = &sync.WaitGroup{} - svc.fileDeletionBackgroundWorkers.Add(1) - go pollFilesystem(fileDeletionCtx, svc.fileDeletionBackgroundWorkers, svc.captureDir, &svc.syncer, svc.logger) - } - - return nil -} - -// startSyncScheduler starts the goroutine that calls Sync repeatedly if scheduled sync is enabled. -func (svc *builtIn) startSyncScheduler(intervalMins float64) { - cancelCtx, fn := context.WithCancel(context.Background()) - svc.syncRoutineCancelFn = fn - svc.uploadData(cancelCtx, intervalMins) -} - -// cancelSyncScheduler cancels the goroutine that calls Sync repeatedly if scheduled sync is enabled. -// It does not close the syncer or any in progress uploads. -func (svc *builtIn) cancelSyncScheduler() { - if svc.syncRoutineCancelFn != nil { - svc.syncRoutineCancelFn() - svc.backgroundWorkers.Wait() - svc.syncRoutineCancelFn = nil - } -} - -func (svc *builtIn) uploadData(cancelCtx context.Context, intervalMins float64) { - // time.Duration loses precision at low floating point values, so turn intervalMins to milliseconds. - intervalMillis := 60000.0 * intervalMins - // The ticker must be created before uploadData returns to prevent race conditions between clock.Ticker and - // clock.Add in sync_test.go. - svc.syncTicker = clock.Ticker(time.Millisecond * time.Duration(intervalMillis)) - svc.backgroundWorkers.Add(1) - goutils.PanicCapturingGo(func() { - defer svc.backgroundWorkers.Done() - defer svc.syncTicker.Stop() - - for { - if err := cancelCtx.Err(); err != nil { - if !errors.Is(err, context.Canceled) { - svc.logger.Errorw("data manager context closed unexpectedly", "error", err) - } - return - } - - select { - case <-cancelCtx.Done(): - return - case <-svc.syncTicker.C: - svc.lock.Lock() - if svc.syncer != nil { - // If selective sync is disabled, sync. If it is enabled, check the condition below. - shouldSync := !svc.selectiveSyncEnabled - // If selective sync is enabled and the sensor has been properly initialized, - // try to get the reading from the selective sensor that indicates whether to sync - if svc.syncSensor != nil && svc.selectiveSyncEnabled { - shouldSync = readyToSync(cancelCtx, svc.syncSensor, svc.logger) - } - svc.lock.Unlock() - - if shouldSync { - svc.sync() - } - } else { - svc.lock.Unlock() - } - } - } - }) -} - -func (svc *builtIn) sync() { - svc.flushCollectors() - - svc.lock.Lock() - toSync := getAllFilesToSync(svc.captureDir, svc.fileLastModifiedMillis) - for _, ap := range svc.additionalSyncPaths { - toSync = append(toSync, getAllFilesToSync(ap, svc.fileLastModifiedMillis)...) - } - svc.lock.Unlock() - - for _, p := range toSync { - svc.syncer.SyncFile(p) - } -} - -//nolint -func getAllFilesToSync(dir string, lastModifiedMillis int) []string { - var filePaths []string - _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - // Do not sync the files in the corrupted data directory. - if info.IsDir() && info.Name() == datasync.FailedDir { - return filepath.SkipDir - } - if info.IsDir() { - return nil - } - // If a file was modified within the past lastModifiedMillis, do not sync it (data - // may still be being written). - timeSinceMod := clock.Since(info.ModTime()) - // When using a mock clock in tests, this can be negative since the file system will still use the system clock. - // Take max(timeSinceMod, 0) to account for this. - if timeSinceMod < 0 { - timeSinceMod = 0 - } - isStuckInProgressCaptureFile := filepath.Ext(path) == datacapture.InProgressFileExt && - timeSinceMod >= defaultFileLastModifiedMillis*time.Millisecond - isNonCaptureFileThatIsNotBeingWrittenTo := filepath.Ext(path) != datacapture.InProgressFileExt && - timeSinceMod >= time.Duration(lastModifiedMillis)*time.Millisecond - isCompletedCaptureFile := filepath.Ext(path) == datacapture.FileExt - if isCompletedCaptureFile || isStuckInProgressCaptureFile || isNonCaptureFileThatIsNotBeingWrittenTo { - filePaths = append(filePaths, path) - } - return nil - }) - return filePaths -} - -// Build the component configs associated with the data manager service. -func (svc *builtIn) updateDataCaptureConfigs( - resources resource.Dependencies, - conf resource.Config, - captureDir string, -) (map[resource.Resource][]datamanager.DataCaptureConfig, error) { - resourceCaptureConfigMap := make(map[resource.Resource][]datamanager.DataCaptureConfig) - for name, assocCfg := range conf.AssociatedAttributes { - associatedConf, err := utils.AssertType[*datamanager.AssociatedConfig](assocCfg) - if err != nil { - return nil, err - } - - res, err := resources.Lookup(name) - if err != nil { - svc.logger.Debugw("failed to lookup resource", "error", err) - continue - } - - captureCopies := make([]datamanager.DataCaptureConfig, len(associatedConf.CaptureMethods)) - for _, method := range associatedConf.CaptureMethods { - method.CaptureDirectory = captureDir - captureCopies = append(captureCopies, method) - } - resourceCaptureConfigMap[res] = captureCopies - } - return resourceCaptureConfigMap, nil -} - -func generateMetadataKey(component, method string) string { - return fmt.Sprintf("%s/%s", component, method) -} - -//nolint:unparam -func pollFilesystem(ctx context.Context, wg *sync.WaitGroup, captureDir string, syncer *datasync.Manager, logger logging.Logger) { - t := time.NewTicker(filesystemPollInterval) - defer t.Stop() - defer wg.Done() - for { - if err := ctx.Err(); err != nil { - if !errors.Is(err, context.Canceled) { - logger.Errorw("data manager context closed unexpectedly in filesystem polling", "error", err) - } - return - } - select { - case <-ctx.Done(): - return - case <-t.C: - } - } -} diff --git a/services/datamanager/builtin/builtin_test.go b/services/datamanager/builtin/builtin_test.go deleted file mode 100644 index da444ad38f3..00000000000 --- a/services/datamanager/builtin/builtin_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package builtin - -import ( - "bytes" - "context" - "image" - "image/png" - "os" - "path/filepath" - "testing" - "time" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/config" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/internal/cloud" - cloudinject "go.viam.com/rdk/internal/testutils/inject" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/datamanager" - "go.viam.com/rdk/services/datamanager/internal" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -func getInjectedRobot() *inject.Robot { - r := &inject.Robot{} - rs := map[resource.Name]resource.Resource{} - - rs[cloud.InternalServiceName] = &cloudinject.CloudConnectionService{ - Named: cloud.InternalServiceName.AsNamed(), - } - - injectedArm := &inject.Arm{} - injectedArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 2, Z: 3}), nil - } - rs[arm.Named("arm1")] = injectedArm - - injectedRemoteArm := &inject.Arm{} - injectedRemoteArm.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - return spatialmath.NewZeroPose(), nil - } - rs[arm.Named("remote1:remoteArm")] = injectedRemoteArm - - injectedCam := &inject.Camera{} - img := image.NewNRGBA(image.Rect(0, 0, 4, 4)) - var imgBuf bytes.Buffer - png.Encode(&imgBuf, img) - imgPng, _ := png.Decode(bytes.NewReader(imgBuf.Bytes())) - injectedCam.StreamFunc = func(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) { - return gostream.NewEmbeddedVideoStreamFromReader( - gostream.VideoReaderFunc(func(ctx context.Context) (image.Image, func(), error) { - return imgPng, func() {}, nil - }), - ), nil - } - rs[camera.Named("c1")] = injectedCam - - r.MockResourcesFromMap(rs) - return r -} - -func newTestDataManager(t *testing.T) (internal.DMService, robot.Robot) { - t.Helper() - dmCfg := &Config{} - cfgService := resource.Config{ - API: datamanager.API, - ConvertedAttributes: dmCfg, - } - logger := logging.NewTestLogger(t) - - // Create local robot with injected arm and remote. - r := getInjectedRobot() - remoteRobot := getInjectedRobot() - r.RemoteByNameFunc = func(name string) (robot.Robot, bool) { - return remoteRobot, true - } - - resources := resourcesFromDeps(t, r, []string{cloud.InternalServiceName.String()}) - svc, err := NewBuiltIn(context.Background(), resources, cfgService, logger) - if err != nil { - t.Log(err) - t.FailNow() - } - return svc.(internal.DMService), r -} - -func setupConfig(t *testing.T, relativePath string) (*Config, map[resource.Name]resource.AssociatedConfig, []string) { - t.Helper() - logger := logging.NewTestLogger(t) - testCfg, err := config.Read(context.Background(), utils.ResolveFile(relativePath), logger) - test.That(t, err, test.ShouldBeNil) - return getServiceConfig(t, testCfg) -} - -func getServiceConfig(t *testing.T, cfg *config.Config) (*Config, map[resource.Name]resource.AssociatedConfig, []string) { - t.Helper() - for _, c := range cfg.Services { - // Compare service type and name. - if c.API == datamanager.API && c.ConvertedAttributes != nil { - svcConfig, ok := c.ConvertedAttributes.(*Config) - test.That(t, ok, test.ShouldBeTrue) - - var deps []string - for _, resConf := range c.AssociatedAttributes { - assocConf, ok := resConf.(*datamanager.AssociatedConfig) - test.That(t, ok, test.ShouldBeTrue) - if len(assocConf.CaptureMethods) == 0 { - continue - } - deps = append(deps, assocConf.CaptureMethods[0].Name.String()) - } - deps = append(deps, c.ImplicitDependsOn...) - return svcConfig, c.AssociatedAttributes, deps - } - } - - t.Log("no service config") - return nil, nil, nil -} - -func TestGetDurationFromHz(t *testing.T) { - test.That(t, GetDurationFromHz(0.1), test.ShouldEqual, time.Second*10) - test.That(t, GetDurationFromHz(0.5), test.ShouldEqual, time.Second*2) - test.That(t, GetDurationFromHz(1), test.ShouldEqual, time.Second) - test.That(t, GetDurationFromHz(1000), test.ShouldEqual, time.Millisecond) - test.That(t, GetDurationFromHz(0), test.ShouldEqual, 0) -} - -func TestEmptyConfig(t *testing.T) { - // Data manager should not be enabled implicitly, an empty config will not result in a data manager being configured. - initConfig, associations, deps := setupConfig(t, enabledTabularCollectorEmptyConfigPath) - test.That(t, initConfig, test.ShouldBeNil) - test.That(t, associations, test.ShouldBeNil) - test.That(t, deps, test.ShouldBeNil) -} - -func TestUntrustedEnv(t *testing.T) { - dmsvc, r := newTestDataManager(t) - defer dmsvc.Close(context.Background()) - - config, associations, deps := setupConfig(t, enabledTabularCollectorConfigPath) - ctx, err := utils.WithTrustedEnvironment(context.Background(), false) - test.That(t, err, test.ShouldBeNil) - - resources := resourcesFromDeps(t, r, deps) - err = dmsvc.Reconfigure(ctx, resources, resource.Config{ - ConvertedAttributes: config, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldEqual, errCaptureDirectoryConfigurationDisabled) -} - -func getAllFileInfos(dir string) []os.FileInfo { - var files []os.FileInfo - _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { - // ignore errors/unreadable files and directories - //nolint:nilerr - return nil - } - files = append(files, info) - return nil - }) - return files -} diff --git a/services/datamanager/builtin/capture_test.go b/services/datamanager/builtin/capture_test.go deleted file mode 100644 index 966c2fc2fbb..00000000000 --- a/services/datamanager/builtin/capture_test.go +++ /dev/null @@ -1,372 +0,0 @@ -package builtin - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - "github.com/golang/geo/r3" - "github.com/pkg/errors" - v1 "go.viam.com/api/app/datasync/v1" - "go.viam.com/test" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/datamanager/datacapture" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -var ( - // Robot config which specifies data manager service. - enabledTabularCollectorConfigPath = "services/datamanager/data/fake_robot_with_data_manager.json" - enabledTabularCollectorEmptyConfigPath = "services/datamanager/data/fake_robot_with_data_manager_empty.json" - disabledTabularCollectorConfigPath = "services/datamanager/data/fake_robot_with_disabled_collector.json" - enabledBinaryCollectorConfigPath = "services/datamanager/data/robot_with_cam_capture.json" - infrequentCaptureTabularCollectorConfigPath = "services/datamanager/data/fake_robot_with_infrequent_capture.json" - remoteCollectorConfigPath = "services/datamanager/data/fake_robot_with_remote_and_data_manager.json" - emptyFileBytesSize = 100 // size of leading metadata message - captureInterval = time.Millisecond * 10 -) - -func TestDataCaptureEnabled(t *testing.T) { - tests := []struct { - name string - initialServiceDisableStatus bool - newServiceDisableStatus bool - initialCollectorDisableStatus bool - newCollectorDisableStatus bool - remoteCollector bool - }{ - { - name: "data capture service disabled, should capture nothing", - initialServiceDisableStatus: true, - newServiceDisableStatus: true, - initialCollectorDisableStatus: true, - newCollectorDisableStatus: true, - }, - { - name: "data capture service enabled and a configured collector, should capture data", - initialServiceDisableStatus: false, - newServiceDisableStatus: false, - initialCollectorDisableStatus: false, - newCollectorDisableStatus: false, - }, - - { - name: "disabling data capture service should cause all data capture to stop", - initialServiceDisableStatus: false, - newServiceDisableStatus: true, - initialCollectorDisableStatus: false, - newCollectorDisableStatus: false, - }, - { - name: "enabling data capture should cause all enabled collectors to start capturing data", - initialServiceDisableStatus: true, - newServiceDisableStatus: false, - initialCollectorDisableStatus: false, - newCollectorDisableStatus: false, - }, - { - name: "enabling a collector should not trigger data capture if the service is disabled", - initialServiceDisableStatus: true, - newServiceDisableStatus: true, - initialCollectorDisableStatus: true, - newCollectorDisableStatus: false, - }, - { - name: "disabling an individual collector should stop it", - initialServiceDisableStatus: false, - newServiceDisableStatus: false, - initialCollectorDisableStatus: false, - newCollectorDisableStatus: true, - }, - { - name: "enabling an individual collector should start it", - initialServiceDisableStatus: false, - newServiceDisableStatus: false, - initialCollectorDisableStatus: true, - newCollectorDisableStatus: false, - }, - { - name: "capture should work for remotes too", - remoteCollector: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up capture directories. - initCaptureDir := t.TempDir() - updatedCaptureDir := t.TempDir() - mockClock := clk.NewMock() - // Make mockClock the package level clock used by the dmsvc so that we can simulate time's passage - clock = mockClock - - // Set up robot config. - var initConfig *Config - var associations map[resource.Name]resource.AssociatedConfig - var deps []string - switch { - case tc.remoteCollector: - initConfig, associations, deps = setupConfig(t, remoteCollectorConfigPath) - case tc.initialCollectorDisableStatus: - initConfig, associations, deps = setupConfig(t, disabledTabularCollectorConfigPath) - default: - initConfig, associations, deps = setupConfig(t, enabledTabularCollectorConfigPath) - } - - // further set up service config. - initConfig.CaptureDisabled = tc.initialServiceDisableStatus - initConfig.ScheduledSyncDisabled = true - initConfig.CaptureDir = initCaptureDir - - // Build and start data manager. - dmsvc, r := newTestDataManager(t) - defer func() { - test.That(t, dmsvc.Close(context.Background()), test.ShouldBeNil) - }() - - resources := resourcesFromDeps(t, r, deps) - err := dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: initConfig, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - passTimeCtx1, cancelPassTime1 := context.WithCancel(context.Background()) - donePassingTime1 := passTime(passTimeCtx1, mockClock, captureInterval) - - if !tc.initialServiceDisableStatus && !tc.initialCollectorDisableStatus { - waitForCaptureFilesToExceedNFiles(initCaptureDir, 0) - testFilesContainSensorData(t, initCaptureDir) - } else { - initialCaptureFiles := getAllFileInfos(initCaptureDir) - test.That(t, len(initialCaptureFiles), test.ShouldEqual, 0) - } - cancelPassTime1() - <-donePassingTime1 - - // Set up updated robot config. - var updatedConfig *Config - if tc.newCollectorDisableStatus { - updatedConfig, associations, deps = setupConfig(t, disabledTabularCollectorConfigPath) - } else { - updatedConfig, associations, deps = setupConfig(t, enabledTabularCollectorConfigPath) - } - - // further set up updated service config. - updatedConfig.CaptureDisabled = tc.newServiceDisableStatus - updatedConfig.ScheduledSyncDisabled = true - updatedConfig.CaptureDir = updatedCaptureDir - - // Update to new config and let it run for a bit. - resources = resourcesFromDeps(t, r, deps) - err = dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: updatedConfig, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - oldCaptureDirFiles := getAllFileInfos(initCaptureDir) - passTimeCtx2, cancelPassTime2 := context.WithCancel(context.Background()) - donePassingTime2 := passTime(passTimeCtx2, mockClock, captureInterval) - - if !tc.newServiceDisableStatus && !tc.newCollectorDisableStatus { - waitForCaptureFilesToExceedNFiles(updatedCaptureDir, 0) - testFilesContainSensorData(t, updatedCaptureDir) - } else { - updatedCaptureFiles := getAllFileInfos(updatedCaptureDir) - test.That(t, len(updatedCaptureFiles), test.ShouldEqual, 0) - oldCaptureDirFilesAfterWait := getAllFileInfos(initCaptureDir) - test.That(t, len(oldCaptureDirFilesAfterWait), test.ShouldEqual, len(oldCaptureDirFiles)) - for i := range oldCaptureDirFiles { - test.That(t, oldCaptureDirFiles[i].Size(), test.ShouldEqual, oldCaptureDirFilesAfterWait[i].Size()) - } - } - cancelPassTime2() - <-donePassingTime2 - }) - } -} - -func TestSwitchResource(t *testing.T) { - captureDir := t.TempDir() - mockClock := clk.NewMock() - // Make mockClock the package level clock used by the dmsvc so that we can simulate time's passage - clock = mockClock - - // Set up robot config. - config, associations, deps := setupConfig(t, enabledTabularCollectorConfigPath) - config.CaptureDisabled = false - config.ScheduledSyncDisabled = true - config.CaptureDir = captureDir - - // Build and start data manager. - dmsvc, r := newTestDataManager(t) - defer func() { - test.That(t, dmsvc.Close(context.Background()), test.ShouldBeNil) - }() - - resources := resourcesFromDeps(t, r, deps) - err := dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: config, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - passTimeCtx1, cancelPassTime1 := context.WithCancel(context.Background()) - donePassingTime1 := passTime(passTimeCtx1, mockClock, captureInterval) - - waitForCaptureFilesToExceedNFiles(captureDir, 0) - testFilesContainSensorData(t, captureDir) - - cancelPassTime1() - <-donePassingTime1 - - // Change the resource named arm1 to show that the data manager recognizes that the collector has changed with no other config changes. - for resource := range resources { - if resource.Name == "arm1" { - newResource := inject.NewArm(resource.Name) - newResource.EndPositionFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - // Return a different value from the initial arm1 resource. - return spatialmath.NewPoseFromPoint(r3.Vector{X: 888, Y: 888, Z: 888}), nil - } - resources[resource] = newResource - } - } - - err = dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: config, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - dataBeforeSwitch, err := getSensorData(captureDir) - test.That(t, err, test.ShouldBeNil) - - passTimeCtx2, cancelPassTime2 := context.WithCancel(context.Background()) - donePassingTime2 := passTime(passTimeCtx2, mockClock, captureInterval) - - // Test that sensor data is captured from the new collector. - waitForCaptureFilesToExceedNFiles(captureDir, len(getAllFileInfos(captureDir))) - testFilesContainSensorData(t, captureDir) - - filePaths := getAllFilePaths(captureDir) - test.That(t, len(filePaths), test.ShouldEqual, 2) - - initialData, err := datacapture.SensorDataFromFilePath(filePaths[0]) - test.That(t, err, test.ShouldBeNil) - for _, d := range initialData { - // Each resource's mocked capture method outputs a different value. - // Assert that we see the expected data captured by the initial arm1 resource. - test.That( - t, - d.GetStruct().GetFields()["pose"].GetStructValue().GetFields()["o_z"].GetNumberValue(), test.ShouldEqual, - float64(1), - ) - } - // Assert that the initial arm1 resource isn't capturing any more data. - test.That(t, len(initialData), test.ShouldEqual, len(dataBeforeSwitch)) - - newData, err := datacapture.SensorDataFromFilePath(filePaths[1]) - test.That(t, err, test.ShouldBeNil) - for _, d := range newData { - // Assert that we see the expected data captured by the updated arm1 resource. - test.That( - t, - d.GetStruct().GetFields()["pose"].GetStructValue().GetFields()["x"].GetNumberValue(), - test.ShouldEqual, - float64(888), - ) - } - // Assert that the updated arm1 resource is capturing data. - test.That(t, len(newData), test.ShouldBeGreaterThan, 0) - - cancelPassTime2() - <-donePassingTime2 -} - -// passTime repeatedly increments mc by interval until the context is canceled. -func passTime(ctx context.Context, mc *clk.Mock, interval time.Duration) chan struct{} { - done := make(chan struct{}) - go func() { - for { - select { - case <-ctx.Done(): - close(done) - return - default: - time.Sleep(10 * time.Millisecond) - mc.Add(interval) - } - } - }() - return done -} - -func getSensorData(dir string) ([]*v1.SensorData, error) { - var sd []*v1.SensorData - filePaths := getAllFilePaths(dir) - for _, path := range filePaths { - d, err := datacapture.SensorDataFromFilePath(path) - // It's possible a file was closed (and so its extension changed) in between the points where we gathered - // file names and here. So if the file does not exist, check if the extension has just been changed. - if errors.Is(err, os.ErrNotExist) { - path = strings.TrimSuffix(path, filepath.Ext(path)) + datacapture.FileExt - d, err = datacapture.SensorDataFromFilePath(path) - if err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - - sd = append(sd, d...) - } - return sd, nil -} - -// testFilesContainSensorData verifies that the files in `dir` contain sensor data. -func testFilesContainSensorData(t *testing.T, dir string) { - t.Helper() - - sd, err := getSensorData(dir) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(sd), test.ShouldBeGreaterThan, 0) - for _, d := range sd { - test.That(t, d.GetStruct(), test.ShouldNotBeNil) - test.That(t, d.GetMetadata(), test.ShouldNotBeNil) - } -} - -// waitForCaptureFilesToExceedNFiles returns once `captureDir` contains more than `n` files. -func waitForCaptureFilesToExceedNFiles(captureDir string, n int) { - totalWait := time.Second * 2 - waitPerCheck := time.Millisecond * 10 - iterations := int(totalWait / waitPerCheck) - files := getAllFileInfos(captureDir) - for i := 0; i < iterations; i++ { - if len(files) > n && files[n].Size() > int64(emptyFileBytesSize) { - return - } - time.Sleep(waitPerCheck) - files = getAllFileInfos(captureDir) - } -} - -func resourcesFromDeps(t *testing.T, r robot.Robot, deps []string) resource.Dependencies { - t.Helper() - resources := resource.Dependencies{} - for _, dep := range deps { - resName, err := resource.NewFromString(dep) - test.That(t, err, test.ShouldBeNil) - res, err := r.ResourceByName(resName) - if err == nil { - // some resources are weakly linked - resources[resName] = res - } - } - return resources -} diff --git a/services/datamanager/builtin/export_test.go b/services/datamanager/builtin/export_test.go deleted file mode 100644 index 670c08b59c6..00000000000 --- a/services/datamanager/builtin/export_test.go +++ /dev/null @@ -1,19 +0,0 @@ -// export_test.go adds functionality to the builtin package that we only want to use and expose during testing. -package builtin - -import ( - "go.viam.com/rdk/services/datamanager/datasync" -) - -// SetSyncerConstructor sets the syncer constructor for the data manager to use when creating its syncer. -func (svc *builtIn) SetSyncerConstructor(fn datasync.ManagerConstructor) { - svc.syncerConstructor = fn -} - -// SetFileLastModifiedMillis sets the wait time for the syncer to use when initialized/changed in Service.Update. -func (svc *builtIn) SetFileLastModifiedMillis(s int) { - svc.fileLastModifiedMillis = s -} - -// Make getDurationFromHz global for tests. -var GetDurationFromHz = getDurationFromHz diff --git a/services/datamanager/builtin/sync_test.go b/services/datamanager/builtin/sync_test.go deleted file mode 100644 index 6b0f8e41dd3..00000000000 --- a/services/datamanager/builtin/sync_test.go +++ /dev/null @@ -1,925 +0,0 @@ -package builtin - -import ( - "context" - "io" - "os" - "path/filepath" - "sort" - "sync/atomic" - "testing" - "time" - - clk "github.com/benbjohnson/clock" - "github.com/pkg/errors" - v1 "go.viam.com/api/app/datasync/v1" - "go.viam.com/test" - "google.golang.org/grpc" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/datamanager/datacapture" - "go.viam.com/rdk/services/datamanager/datasync" -) - -const ( - syncIntervalMins = 0.0008 - syncInterval = time.Millisecond * 50 -) - -// TODO DATA-849: Add a test that validates that sync interval is accurately respected. -func TestSyncEnabled(t *testing.T) { - captureInterval := time.Millisecond * 10 - tests := []struct { - name string - initialServiceDisableStatus bool - newServiceDisableStatus bool - }{ - { - name: "config with sync disabled should sync nothing", - initialServiceDisableStatus: true, - newServiceDisableStatus: true, - }, - { - name: "config with sync enabled should sync", - initialServiceDisableStatus: false, - newServiceDisableStatus: false, - }, - { - name: "disabling sync should stop syncing", - initialServiceDisableStatus: false, - newServiceDisableStatus: true, - }, - { - name: "enabling sync should trigger syncing to start", - initialServiceDisableStatus: true, - newServiceDisableStatus: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up server. - mockClock := clk.NewMock() - // Make mockClock the package level clock used by the dmsvc so that we can simulate time's passage - clock = mockClock - tmpDir := t.TempDir() - - // Set up data manager. - dmsvc, r := newTestDataManager(t) - defer dmsvc.Close(context.Background()) - mockClient := mockDataSyncServiceClient{ - succesfulDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - failedDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - fail: &atomic.Bool{}, - } - dmsvc.SetSyncerConstructor(getTestSyncerConstructorMock(mockClient)) - cfg, associations, deps := setupConfig(t, enabledBinaryCollectorConfigPath) - - // Set up service config. - cfg.CaptureDisabled = false - cfg.ScheduledSyncDisabled = tc.initialServiceDisableStatus - cfg.CaptureDir = tmpDir - cfg.SyncIntervalMins = syncIntervalMins - - resources := resourcesFromDeps(t, r, deps) - err := dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - mockClock.Add(captureInterval) - waitForCaptureFilesToExceedNFiles(tmpDir, 0) - mockClock.Add(syncInterval) - var sentReq bool - wait := time.After(time.Second) - select { - case <-wait: - case <-mockClient.succesfulDCRequests: - sentReq = true - } - - if !tc.initialServiceDisableStatus { - test.That(t, sentReq, test.ShouldBeTrue) - } else { - test.That(t, sentReq, test.ShouldBeFalse) - } - - // Set up service config. - cfg.CaptureDisabled = false - cfg.ScheduledSyncDisabled = tc.newServiceDisableStatus - cfg.CaptureDir = tmpDir - cfg.SyncIntervalMins = syncIntervalMins - - resources = resourcesFromDeps(t, r, deps) - err = dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - // Drain any requests that were already sent before Update returned. - for len(mockClient.succesfulDCRequests) > 0 { - <-mockClient.succesfulDCRequests - } - var sentReqAfterUpdate bool - mockClock.Add(captureInterval) - waitForCaptureFilesToExceedNFiles(tmpDir, 0) - mockClock.Add(syncInterval) - wait = time.After(time.Second) - select { - case <-wait: - case <-mockClient.succesfulDCRequests: - sentReqAfterUpdate = true - } - err = dmsvc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - if !tc.newServiceDisableStatus { - test.That(t, sentReqAfterUpdate, test.ShouldBeTrue) - } else { - test.That(t, sentReqAfterUpdate, test.ShouldBeFalse) - } - }) - } -} - -// TODO DATA-849: Test concurrent capture and sync more thoroughly. -func TestDataCaptureUploadIntegration(t *testing.T) { - // Set exponential factor to 1 and retry wait time to 20ms so retries happen very quickly. - datasync.RetryExponentialFactor.Store(int32(1)) - datasync.InitialWaitTimeMillis.Store(int32(20)) - - tests := []struct { - name string - dataType v1.DataType - manualSync bool - scheduledSyncDisabled bool - failTransiently bool - emptyFile bool - }{ - { - name: "previously captured tabular data should be synced at start up", - dataType: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - }, - { - name: "previously captured binary data should be synced at start up", - dataType: v1.DataType_DATA_TYPE_BINARY_SENSOR, - }, - { - name: "manual sync should successfully sync captured tabular data", - dataType: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - manualSync: true, - scheduledSyncDisabled: true, - }, - { - name: "manual sync should successfully sync captured binary data", - dataType: v1.DataType_DATA_TYPE_BINARY_SENSOR, - manualSync: true, - scheduledSyncDisabled: true, - }, - { - name: "running manual and scheduled sync concurrently should not cause data races or duplicate uploads", - dataType: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - manualSync: true, - }, - { - name: "if tabular uploads fail transiently, they should be retried until they succeed", - dataType: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - failTransiently: true, - }, - { - name: "if binary uploads fail transiently, they should be retried until they succeed", - dataType: v1.DataType_DATA_TYPE_BINARY_SENSOR, - failTransiently: true, - }, - { - name: "files with no sensor data should not be synced", - emptyFile: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up server. - mockClock := clk.NewMock() - clock = mockClock - tmpDir := t.TempDir() - - // Set up data manager. - dmsvc, r := newTestDataManager(t) - defer dmsvc.Close(context.Background()) - var cfg *Config - var associations map[resource.Name]resource.AssociatedConfig - var deps []string - captureInterval := time.Millisecond * 10 - if tc.emptyFile { - cfg, associations, deps = setupConfig(t, infrequentCaptureTabularCollectorConfigPath) - } else { - if tc.dataType == v1.DataType_DATA_TYPE_TABULAR_SENSOR { - cfg, associations, deps = setupConfig(t, enabledTabularCollectorConfigPath) - } else { - cfg, associations, deps = setupConfig(t, enabledBinaryCollectorConfigPath) - } - } - - // Set up service config with only capture enabled. - cfg.CaptureDisabled = false - cfg.ScheduledSyncDisabled = true - cfg.SyncIntervalMins = syncIntervalMins - cfg.CaptureDir = tmpDir - - resources := resourcesFromDeps(t, r, deps) - err := dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - // Let it capture a bit, then close. - for i := 0; i < 20; i++ { - mockClock.Add(captureInterval) - } - err = dmsvc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - // Get all captured data. - numFiles, capturedData, err := getCapturedData(tmpDir) - test.That(t, err, test.ShouldBeNil) - if tc.emptyFile { - test.That(t, len(capturedData), test.ShouldEqual, 0) - } else { - test.That(t, len(capturedData), test.ShouldBeGreaterThan, 0) - } - - // Turn dmsvc back on with capture disabled. - newDMSvc, r := newTestDataManager(t) - defer newDMSvc.Close(context.Background()) - mockClient := mockDataSyncServiceClient{ - succesfulDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - failedDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - fail: &atomic.Bool{}, - } - newDMSvc.SetSyncerConstructor(getTestSyncerConstructorMock(mockClient)) - cfg.CaptureDisabled = true - cfg.ScheduledSyncDisabled = tc.scheduledSyncDisabled - cfg.SyncIntervalMins = syncIntervalMins - resources = resourcesFromDeps(t, r, deps) - err = newDMSvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - if tc.failTransiently { - // Simulate the backend returning errors some number of times, and validate that the dmsvc is continuing - // to retry. - numFails := 3 - mockClient.fail.Store(true) - for i := 0; i < numFails; i++ { - mockClock.Add(syncInterval) - // Each time we sync, we should get a sync request for each file. - for j := 0; j < numFiles; j++ { - wait := time.After(time.Second * 5) - select { - case <-wait: - t.Fatalf("timed out waiting for sync request") - case <-mockClient.failedDCRequests: - } - } - } - } - - mockClient.fail.Store(false) - // If testing manual sync, call sync. Call it multiple times to ensure concurrent calls are safe. - if tc.manualSync { - err = newDMSvc.Sync(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - err = newDMSvc.Sync(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - } - - var successfulReqs []*v1.DataCaptureUploadRequest - // Get the successful requests - mockClock.Add(syncInterval) - for i := 0; i < numFiles; i++ { - wait := time.After(time.Second * 5) - select { - case <-wait: - t.Fatalf("timed out waiting for sync request") - case r := <-mockClient.succesfulDCRequests: - successfulReqs = append(successfulReqs, r) - } - } - - // Give it time to delete files after upload. - waitUntilNoFiles(tmpDir) - err = newDMSvc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - // Validate that all captured data was synced. - syncedData := getUploadedData(successfulReqs) - compareSensorData(t, tc.dataType, syncedData, capturedData) - - // After all uploads succeed, all files should be deleted. - test.That(t, len(getAllFileInfos(tmpDir)), test.ShouldEqual, 0) - }) - } -} - -func TestArbitraryFileUpload(t *testing.T) { - // Set exponential factor to 1 and retry wait time to 20ms so retries happen very quickly. - datasync.RetryExponentialFactor.Store(int32(1)) - fileName := "some_file_name.txt" - fileExt := ".txt" - // Disable the check to see if the file was modified recently, - // since we are testing instanteous arbitrary file uploads. - datasync.SetFileLastModifiedMillis(0) - tests := []struct { - name string - manualSync bool - scheduleSyncDisabled bool - serviceFail bool - }{ - { - name: "scheduled sync of arbitrary files should work", - manualSync: false, - scheduleSyncDisabled: false, - }, - { - name: "manual sync of arbitrary files should work", - manualSync: true, - scheduleSyncDisabled: true, - }, - { - name: "running manual and scheduled sync concurrently should work and not lead to duplicate uploads", - manualSync: true, - scheduleSyncDisabled: false, - }, - { - name: "if an error response is received from the backend, local files should not be deleted", - manualSync: false, - scheduleSyncDisabled: false, - serviceFail: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up server. - mockClock := clk.NewMock() - clock = mockClock - additionalPathsDir := t.TempDir() - captureDir := t.TempDir() - - // Set up dmsvc config. - dmsvc, r := newTestDataManager(t) - defer dmsvc.Close(context.Background()) - f := atomic.Bool{} - f.Store(tc.serviceFail) - mockClient := mockDataSyncServiceClient{ - succesfulDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - failedDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - fileUploads: make(chan *mockFileUploadClient, 100), - fail: &f, - } - dmsvc.SetSyncerConstructor(getTestSyncerConstructorMock(mockClient)) - cfg, associations, deps := setupConfig(t, disabledTabularCollectorConfigPath) - cfg.ScheduledSyncDisabled = tc.scheduleSyncDisabled - cfg.SyncIntervalMins = syncIntervalMins - cfg.AdditionalSyncPaths = []string{additionalPathsDir} - cfg.CaptureDir = captureDir - - // Start dmsvc. - resources := resourcesFromDeps(t, r, deps) - err := dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - // Ensure that we don't wait to sync files. - dmsvc.SetFileLastModifiedMillis(0) - - // Write file to the path. - var fileContents []byte - for i := 0; i < 1000; i++ { - fileContents = append(fileContents, []byte("happy cows come from california\n")...) - } - tmpFile, err := os.Create(filepath.Join(additionalPathsDir, fileName)) - test.That(t, err, test.ShouldBeNil) - _, err = tmpFile.Write(fileContents) - test.That(t, err, test.ShouldBeNil) - test.That(t, tmpFile.Close(), test.ShouldBeNil) - - // Advance the clock syncInterval so it tries to sync the files. - mockClock.Add(syncInterval) - - // Call manual sync. - if tc.manualSync { - err = dmsvc.Sync(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - } - - // Wait for upload requests. - var fileUploads []*mockFileUploadClient - var urs []*v1.FileUploadRequest - // Get the successful requests - wait := time.After(time.Second * 3) - select { - case <-wait: - if !tc.serviceFail { - t.Fatalf("timed out waiting for sync request") - } - case r := <-mockClient.fileUploads: - fileUploads = append(fileUploads, r) - select { - case <-wait: - t.Fatalf("timed out waiting for sync request") - case <-r.closed: - urs = append(urs, r.urs...) - } - } - - waitUntilNoFiles(additionalPathsDir) - if !tc.serviceFail { - // Validate first metadata message. - test.That(t, len(fileUploads), test.ShouldEqual, 1) - test.That(t, len(urs), test.ShouldBeGreaterThan, 0) - actMD := urs[0].GetMetadata() - test.That(t, actMD, test.ShouldNotBeNil) - test.That(t, actMD.Type, test.ShouldEqual, v1.DataType_DATA_TYPE_FILE) - test.That(t, filepath.Base(actMD.FileName), test.ShouldEqual, fileName) - test.That(t, actMD.FileExtension, test.ShouldEqual, fileExt) - test.That(t, actMD.PartId, test.ShouldNotBeBlank) - - // Validate ensuing data messages. - dataRequests := urs[1:] - var actData []byte - for _, d := range dataRequests { - actData = append(actData, d.GetFileContents().GetData()...) - } - test.That(t, actData, test.ShouldResemble, fileContents) - - // Validate file no longer exists. - test.That(t, len(getAllFileInfos(additionalPathsDir)), test.ShouldEqual, 0) - test.That(t, dmsvc.Close(context.Background()), test.ShouldBeNil) - } else { - // Validate no files were successfully uploaded. - test.That(t, len(fileUploads), test.ShouldEqual, 0) - // Validate file still exists. - test.That(t, len(getAllFileInfos(additionalPathsDir)), test.ShouldEqual, 1) - } - }) - } -} - -func TestStreamingDCUpload(t *testing.T) { - tests := []struct { - name string - serviceFail bool - }{ - { - name: "A data capture file greater than MaxUnaryFileSize should be successfully uploaded" + - "via the streaming rpc.", - serviceFail: false, - }, - { - name: "if an error response is received from the backend, local files should not be deleted", - serviceFail: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up server. - mockClock := clk.NewMock() - clock = mockClock - tmpDir := t.TempDir() - - // Set up data manager. - dmsvc, r := newTestDataManager(t) - defer dmsvc.Close(context.Background()) - var cfg *Config - var associations map[resource.Name]resource.AssociatedConfig - var deps []string - captureInterval := time.Millisecond * 10 - cfg, associations, deps = setupConfig(t, enabledBinaryCollectorConfigPath) - - // Set up service config with just capture enabled. - cfg.CaptureDisabled = false - cfg.ScheduledSyncDisabled = true - cfg.SyncIntervalMins = syncIntervalMins - cfg.CaptureDir = tmpDir - - resources := resourcesFromDeps(t, r, deps) - err := dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - // Capture an image, then close. - mockClock.Add(captureInterval) - waitForCaptureFilesToExceedNFiles(tmpDir, 0) - err = dmsvc.Close(context.Background()) - test.That(t, err, test.ShouldBeNil) - - // Get all captured data. - _, capturedData, err := getCapturedData(tmpDir) - test.That(t, err, test.ShouldBeNil) - - // Turn dmsvc back on with capture disabled. - newDMSvc, r := newTestDataManager(t) - defer newDMSvc.Close(context.Background()) - f := atomic.Bool{} - f.Store(tc.serviceFail) - mockClient := mockDataSyncServiceClient{ - streamingDCUploads: make(chan *mockStreamingDCClient, 10), - fail: &f, - } - // Set max unary file size to 1 byte, so it uses the streaming rpc. - datasync.MaxUnaryFileSize = 1 - newDMSvc.SetSyncerConstructor(getTestSyncerConstructorMock(mockClient)) - cfg.CaptureDisabled = true - cfg.ScheduledSyncDisabled = true - resources = resourcesFromDeps(t, r, deps) - err = newDMSvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - // Call sync. - err = newDMSvc.Sync(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - - // Wait for upload requests. - var uploads []*mockStreamingDCClient - var urs []*v1.StreamingDataCaptureUploadRequest - // Get the successful requests - wait := time.After(time.Second * 3) - select { - case <-wait: - if !tc.serviceFail { - t.Fatalf("timed out waiting for sync request") - } - case r := <-mockClient.streamingDCUploads: - uploads = append(uploads, r) - select { - case <-wait: - t.Fatalf("timed out waiting for sync request") - case <-r.closed: - urs = append(urs, r.reqs...) - } - } - waitUntilNoFiles(tmpDir) - - // Validate error and URs. - remainingFiles := getAllFilePaths(tmpDir) - if tc.serviceFail { - // Validate no files were successfully uploaded. - test.That(t, len(uploads), test.ShouldEqual, 0) - // Error case, file should not be deleted. - test.That(t, len(remainingFiles), test.ShouldEqual, 1) - } else { - // Validate first metadata message. - test.That(t, len(uploads), test.ShouldEqual, 1) - test.That(t, len(urs), test.ShouldBeGreaterThan, 0) - actMD := urs[0].GetMetadata() - test.That(t, actMD, test.ShouldNotBeNil) - test.That(t, actMD.GetUploadMetadata(), test.ShouldNotBeNil) - test.That(t, actMD.GetSensorMetadata(), test.ShouldNotBeNil) - test.That(t, actMD.GetUploadMetadata().Type, test.ShouldEqual, v1.DataType_DATA_TYPE_BINARY_SENSOR) - test.That(t, actMD.GetUploadMetadata().PartId, test.ShouldNotBeBlank) - - // Validate ensuing data messages. - dataRequests := urs[1:] - var actData []byte - for _, d := range dataRequests { - actData = append(actData, d.GetData()...) - } - test.That(t, actData, test.ShouldResemble, capturedData[0].GetBinary()) - - // Validate file no longer exists. - test.That(t, len(getAllFileInfos(tmpDir)), test.ShouldEqual, 0) - } - test.That(t, dmsvc.Close(context.Background()), test.ShouldBeNil) - }) - } -} - -func TestSyncConfigUpdateBehavior(t *testing.T) { - newSyncIntervalMins := 0.009 - tests := []struct { - name string - initSyncDisabled bool - initSyncIntervalMins float64 - newSyncDisabled bool - newSyncIntervalMins float64 - }{ - { - name: "all sync config stays the same, syncer should not cancel, ticker stays the same", - initSyncDisabled: false, - initSyncIntervalMins: syncIntervalMins, - newSyncDisabled: false, - newSyncIntervalMins: syncIntervalMins, - }, - { - name: "sync config changes, new ticker should be created for sync", - initSyncDisabled: false, - initSyncIntervalMins: syncIntervalMins, - newSyncDisabled: false, - newSyncIntervalMins: newSyncIntervalMins, - }, - { - name: "sync gets disabled, syncer should be nil", - initSyncDisabled: false, - initSyncIntervalMins: syncIntervalMins, - newSyncDisabled: true, - newSyncIntervalMins: syncIntervalMins, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up server. - mockClock := clk.NewMock() - // Make mockClock the package level clock used by the dmsvc so that we can simulate time's passage - clock = mockClock - tmpDir := t.TempDir() - - // Set up data manager. - dmsvc, r := newTestDataManager(t) - defer dmsvc.Close(context.Background()) - mockClient := mockDataSyncServiceClient{ - succesfulDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - failedDCRequests: make(chan *v1.DataCaptureUploadRequest, 100), - fail: &atomic.Bool{}, - } - dmsvc.SetSyncerConstructor(getTestSyncerConstructorMock(mockClient)) - cfg, associations, deps := setupConfig(t, enabledBinaryCollectorConfigPath) - - // Set up service config. - cfg.CaptureDisabled = false - cfg.ScheduledSyncDisabled = tc.initSyncDisabled - cfg.CaptureDir = tmpDir - cfg.SyncIntervalMins = tc.initSyncIntervalMins - - resources := resourcesFromDeps(t, r, deps) - err := dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - builtInSvc := dmsvc.(*builtIn) - initTicker := builtInSvc.syncTicker - - // Reconfigure the dmsvc with new sync configs - cfg.ScheduledSyncDisabled = tc.newSyncDisabled - cfg.SyncIntervalMins = tc.newSyncIntervalMins - - err = dmsvc.Reconfigure(context.Background(), resources, resource.Config{ - ConvertedAttributes: cfg, - AssociatedAttributes: associations, - }) - test.That(t, err, test.ShouldBeNil) - - newBuildInSvc := dmsvc.(*builtIn) - newTicker := newBuildInSvc.syncTicker - newSyncer := newBuildInSvc.syncer - newFileDeletionBackgroundWorker := newBuildInSvc.fileDeletionBackgroundWorkers - - if tc.newSyncDisabled { - test.That(t, newSyncer, test.ShouldBeNil) - } - test.That(t, newFileDeletionBackgroundWorker, test.ShouldNotBeNil) - if tc.initSyncDisabled != tc.newSyncDisabled || - tc.initSyncIntervalMins != tc.newSyncIntervalMins { - test.That(t, initTicker, test.ShouldNotEqual, newTicker) - } - }) - } -} - -func getAllFilePaths(dir string) []string { - var filePaths []string - - _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil || info.IsDir() { - // ignore errors/unreadable files and directories - //nolint:nilerr - return nil - } - filePaths = append(filePaths, path) - return nil - }) - return filePaths -} - -func getCapturedData(dir string) (int, []*v1.SensorData, error) { - var allFiles []*datacapture.File - filePaths := getAllFilePaths(dir) - var numFiles int - - for _, f := range filePaths { - osFile, err := os.Open(f) - if err != nil { - return 0, nil, err - } - dcFile, err := datacapture.ReadFile(osFile) - if err != nil { - return 0, nil, err - } - allFiles = append(allFiles, dcFile) - } - - var ret []*v1.SensorData - for _, dcFile := range allFiles { - containsData := false - for { - next, err := dcFile.ReadNext() - if errors.Is(err, io.EOF) { - break - } - containsData = true - if err != nil { - return 0, nil, err - } - ret = append(ret, next) - } - if containsData { - numFiles++ - } - } - return numFiles, ret, nil -} - -func getUploadedData(urs []*v1.DataCaptureUploadRequest) []*v1.SensorData { - var syncedData []*v1.SensorData - for _, ur := range urs { - sd := ur.GetSensorContents() - syncedData = append(syncedData, sd...) - } - return syncedData -} - -func compareSensorData(t *testing.T, dataType v1.DataType, act, exp []*v1.SensorData) { - if len(act) == 0 && len(exp) == 0 { - return - } - - // Sort both by time requested. - sort.SliceStable(act, func(i, j int) bool { - diffRequested := act[j].GetMetadata().GetTimeRequested().AsTime().Sub(act[i].GetMetadata().GetTimeRequested().AsTime()) - switch { - case diffRequested > 0: - return true - case diffRequested == 0: - return act[j].GetMetadata().GetTimeReceived().AsTime().Sub(act[i].GetMetadata().GetTimeReceived().AsTime()) > 0 - default: - return false - } - }) - sort.SliceStable(exp, func(i, j int) bool { - diffRequested := exp[j].GetMetadata().GetTimeRequested().AsTime().Sub(exp[i].GetMetadata().GetTimeRequested().AsTime()) - switch { - case diffRequested > 0: - return true - case diffRequested == 0: - return exp[j].GetMetadata().GetTimeReceived().AsTime().Sub(exp[i].GetMetadata().GetTimeReceived().AsTime()) > 0 - default: - return false - } - }) - - test.That(t, len(act), test.ShouldEqual, len(exp)) - - for i := range act { - test.That(t, act[i].GetMetadata(), test.ShouldResemble, exp[i].GetMetadata()) - if dataType == v1.DataType_DATA_TYPE_TABULAR_SENSOR { - test.That(t, act[i].GetStruct(), test.ShouldResemble, exp[i].GetStruct()) - } else { - test.That(t, act[i].GetBinary(), test.ShouldResemble, exp[i].GetBinary()) - } - } -} - -type mockDataSyncServiceClient struct { - succesfulDCRequests chan *v1.DataCaptureUploadRequest - failedDCRequests chan *v1.DataCaptureUploadRequest - fileUploads chan *mockFileUploadClient - streamingDCUploads chan *mockStreamingDCClient - fail *atomic.Bool -} - -func (c mockDataSyncServiceClient) DataCaptureUpload( - ctx context.Context, - ur *v1.DataCaptureUploadRequest, - opts ...grpc.CallOption, -) (*v1.DataCaptureUploadResponse, error) { - if c.fail.Load() { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case c.failedDCRequests <- ur: - return nil, errors.New("oh no error") - } - } - select { - case <-ctx.Done(): - return nil, ctx.Err() - case c.succesfulDCRequests <- ur: - return &v1.DataCaptureUploadResponse{}, nil - } -} - -func (c mockDataSyncServiceClient) FileUpload(ctx context.Context, opts ...grpc.CallOption) (v1.DataSyncService_FileUploadClient, error) { - if c.fail.Load() { - return nil, errors.New("oh no error") - } - ret := &mockFileUploadClient{closed: make(chan struct{})} - select { - case <-ctx.Done(): - return nil, ctx.Err() - case c.fileUploads <- ret: - } - return ret, nil -} - -func (c mockDataSyncServiceClient) StreamingDataCaptureUpload(ctx context.Context, - opts ...grpc.CallOption, -) (v1.DataSyncService_StreamingDataCaptureUploadClient, error) { - if c.fail.Load() { - return nil, errors.New("oh no error") - } - ret := &mockStreamingDCClient{closed: make(chan struct{})} - select { - case <-ctx.Done(): - return nil, ctx.Err() - case c.streamingDCUploads <- ret: - } - return ret, nil -} - -type mockFileUploadClient struct { - urs []*v1.FileUploadRequest - closed chan struct{} - grpc.ClientStream -} - -func (m *mockFileUploadClient) Send(req *v1.FileUploadRequest) error { - m.urs = append(m.urs, req) - return nil -} - -func (m *mockFileUploadClient) CloseAndRecv() (*v1.FileUploadResponse, error) { - m.closed <- struct{}{} - return &v1.FileUploadResponse{}, nil -} - -func (m *mockFileUploadClient) CloseSend() error { - m.closed <- struct{}{} - return nil -} - -type mockStreamingDCClient struct { - reqs []*v1.StreamingDataCaptureUploadRequest - closed chan struct{} - grpc.ClientStream -} - -func (m *mockStreamingDCClient) Send(req *v1.StreamingDataCaptureUploadRequest) error { - m.reqs = append(m.reqs, req) - return nil -} - -func (m *mockStreamingDCClient) CloseAndRecv() (*v1.StreamingDataCaptureUploadResponse, error) { - m.closed <- struct{}{} - return &v1.StreamingDataCaptureUploadResponse{}, nil -} - -func (m *mockStreamingDCClient) CloseSend() error { - m.closed <- struct{}{} - return nil -} - -func getTestSyncerConstructorMock(client mockDataSyncServiceClient) datasync.ManagerConstructor { - return func(identity string, _ v1.DataSyncServiceClient, logger logging.Logger, viamCaptureDotDir string) (datasync.Manager, error) { - return datasync.NewManager(identity, client, logger, viamCaptureDotDir) - } -} - -func waitUntilNoFiles(dir string) { - totalWait := time.Second * 3 - waitPerCheck := time.Millisecond * 10 - iterations := int(totalWait / waitPerCheck) - files := getAllFileInfos(dir) - for i := 0; i < iterations; i++ { - if len(files) == 0 { - return - } - time.Sleep(waitPerCheck) - files = getAllFileInfos(dir) - } -} diff --git a/services/datamanager/builtin/verify_main_test.go b/services/datamanager/builtin/verify_main_test.go deleted file mode 100644 index c8e5bbecc77..00000000000 --- a/services/datamanager/builtin/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package builtin - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/datamanager/client_test.go b/services/datamanager/client_test.go deleted file mode 100644 index 25821e6f898..00000000000 --- a/services/datamanager/client_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package datamanager_test - -import ( - "context" - "net" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/datamanager" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const testDataManagerServiceName = "DataManager1" - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - var extraOptions map[string]interface{} - - injectDS := &inject.DataManagerService{} - svcName := datamanager.Named(testDataManagerServiceName) - resourceMap := map[resource.Name]datamanager.Service{ - svcName: injectDS, - } - svc, err := resource.NewAPIResourceCollection(datamanager.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[datamanager.Service](datamanager.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - test.That(t, err, test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - // working - t.Run("datamanager client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := datamanager.NewClientFromConn(context.Background(), conn, "", svcName, logger) - test.That(t, err, test.ShouldBeNil) - - injectDS.SyncFunc = func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - return nil - } - extra := map[string]interface{}{"foo": "Sync"} - err = client.Sync(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, extra) - - // DoCommand - injectDS.DoCommandFunc = testutils.EchoFunc - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - // broken - t.Run("datamanager client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", svcName, logger) - test.That(t, err, test.ShouldBeNil) - - passedErr := errors.New("fake sync error") - injectDS.SyncFunc = func(ctx context.Context, extra map[string]interface{}) error { - return passedErr - } - - err = client2.Sync(context.Background(), map[string]interface{}{}) - test.That(t, err.Error(), test.ShouldContainSubstring, passedErr.Error()) - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/datamanager/data/fake_robot_with_data_manager.json b/services/datamanager/data/fake_robot_with_data_manager.json deleted file mode 100644 index 9d3a2104771..00000000000 --- a/services/datamanager/data/fake_robot_with_data_manager.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "network": { - "fqdn": "something-unique", - "bind_address": ":8080" - }, - "components": [ - { - "name": "arm1", - "type": "arm", - "model": "fake", - "service_configs": [ - { - "type": "data_manager", - "attributes": { - "capture_methods": [ - { - "method": "EndPosition", - "capture_frequency_hz": 100, - "tags": [ - "a", - "b" - ] - } - ] - } - } - ] - } - ], - "services": [ - { - "name": "data_manager1", - "type": "data_manager", - "model": "builtin", - "attributes": { - "sync_disabled": false, - "sync_interval_mins": 0, - "capture_dir": "/tmp/capture", - "capture_disabled": false - } - } - ] -} diff --git a/services/datamanager/data/fake_robot_with_data_manager_empty.json b/services/datamanager/data/fake_robot_with_data_manager_empty.json deleted file mode 100644 index 1450987fb02..00000000000 --- a/services/datamanager/data/fake_robot_with_data_manager_empty.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "network": { - "fqdn": "something-unique", - "bind_address": ":8080" - }, - "components": [ - { - "name": "arm1", - "type": "arm", - "model": "fake", - "service_configs": [ - { - "type": "data_manager", - "attributes": { - "capture_methods": [ - { - "method": "EndPosition", - "capture_frequency_hz": 100, - "tags": [ - "a", - "b" - ] - } - ] - } - } - ] - } - ] -} diff --git a/services/datamanager/data/fake_robot_with_disabled_collector.json b/services/datamanager/data/fake_robot_with_disabled_collector.json deleted file mode 100644 index b3b0a41ac75..00000000000 --- a/services/datamanager/data/fake_robot_with_disabled_collector.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "network": { - "fqdn": "something-unique", - "bind_address": ":8080" - }, - "components": [ - { - "name": "arm1", - "type": "arm", - "model": "fake", - "service_configs": [ - { - "type": "data_manager", - "attributes": { - "capture_methods": [ - { - "method": "EndPosition", - "capture_frequency_hz": 500, - "tags": [ - "a", - "b" - ], - "disabled": true - } - ] - } - } - ] - } - ], - "services": [ - { - "name": "data_manager1", - "type": "data_manager", - "model": "builtin", - "attributes": { - "sync_disabled": true, - "sync_interval_mins": 1, - "capture_dir": "/tmp/capture", - "capture_disabled": false - } - } - ] -} diff --git a/services/datamanager/data/fake_robot_with_infrequent_capture.json b/services/datamanager/data/fake_robot_with_infrequent_capture.json deleted file mode 100644 index 2f7e95017c8..00000000000 --- a/services/datamanager/data/fake_robot_with_infrequent_capture.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "network": { - "fqdn": "something-unique", - "bind_address": ":8080" - }, - "components": [ - { - "name": "arm1", - "type": "arm", - "model": "fake", - "service_configs": [ - { - "type": "data_manager", - "attributes": { - "capture_methods": [ - { - "method": "EndPosition", - "capture_frequency_hz": 0.001, - "tags": [ - "a", - "b" - ] - } - ] - } - } - ] - } - ], - "services": [ - { - "name": "data_manager1", - "type": "data_manager", - "model": "builtin", - "attributes": { - "sync_disabled": false, - "sync_interval_mins": 0, - "capture_dir": "/tmp/capture", - "capture_disabled": false - } - } - ] -} diff --git a/services/datamanager/data/fake_robot_with_remote_and_data_manager.json b/services/datamanager/data/fake_robot_with_remote_and_data_manager.json deleted file mode 100644 index cb519f82f0f..00000000000 --- a/services/datamanager/data/fake_robot_with_remote_and_data_manager.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "network": { - "fqdn": "something-unique", - "bind_address": ":8080" - }, - "services": [ - { - "name": "data_manager1", - "type": "data_manager", - "model": "builtin", - "attributes": { - "capture_dir": "/tmp/capture" - } - } - ], - "remotes": [ - { - "name": "remote1", - "address": "localhost:8081", - "service_configs": [ - { - "name": "data_manager2", - "type": "data_manager", - "attributes": { - "capture_methods": [ - { - "name": "rdk:component:arm/remoteArm", - "method": "EndPosition", - "capture_frequency_hz": 100 - }, - { - "name": "rdk:component:arm/nonotthere", - "method": "EndPosition", - "capture_frequency_hz": 100 - } - ] - } - } - ] - } - ] -} diff --git a/services/datamanager/data/robot_with_cam_capture.json b/services/datamanager/data/robot_with_cam_capture.json deleted file mode 100644 index 60c0f242995..00000000000 --- a/services/datamanager/data/robot_with_cam_capture.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "components": [ - { - "name": "c1", - "type": "camera", - "model": "fake", - "service_configs": [ - { - "type": "data_manager", - "attributes": { - "capture_methods": [ - { - "method": "ReadImage", - "capture_frequency_hz": 100, - "additional_params": { - "mime_type": "image/png" - } - } - ] - } - } - ] - } - ], - "services": [ - { - "name": "data_manager1", - "type": "data_manager", - "model": "builtin", - "attributes": { - "capture_dir": "/tmp/capture" - } - } - ] -} diff --git a/services/datamanager/datacapture/data_capture_buffer_test.go b/services/datamanager/datacapture/data_capture_buffer_test.go deleted file mode 100644 index 58b18b57798..00000000000 --- a/services/datamanager/datacapture/data_capture_buffer_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package datacapture - -import ( - "os" - "path/filepath" - "testing" - - v1 "go.viam.com/api/app/datasync/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/protobuf/types/known/structpb" -) - -type structReading struct { - Field1 bool -} - -func (r structReading) toProto() *structpb.Struct { - msg, err := protoutils.StructToStructPb(r) - if err != nil { - return nil - } - return msg -} - -var ( - structSensorData = &v1.SensorData{ - Metadata: &v1.SensorMetadata{}, - Data: &v1.SensorData_Struct{Struct: structReading{}.toProto()}, - } - binarySensorData = &v1.SensorData{ - Metadata: &v1.SensorMetadata{}, - Data: &v1.SensorData_Binary{ - Binary: []byte("this sure is binary data, yup it is"), - }, - } -) - -// TODO: rewrite tests. -func TestCaptureQueue(t *testing.T) { - MaxFileSize = 50 - tests := []struct { - name string - dataType v1.DataType - pushCount int - expCompleteFiles int - expInProgressFiles int - }{ - { - name: "Files that are still being written to should have the InProgressFileExt extension.", - dataType: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - pushCount: 1, - expCompleteFiles: 0, - expInProgressFiles: 1, - }, - { - name: "Pushing N binary data should write N files.", - dataType: v1.DataType_DATA_TYPE_BINARY_SENSOR, - pushCount: 2, - expCompleteFiles: 2, - expInProgressFiles: 0, - }, - { - name: "Pushing > MaxFileSize + 1 worth of struct data should write two files.", - dataType: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - // MaxFileSize / size(structSensorData) = ceil(50 / 19) = 3 readings per file => 2 files, one in progress - pushCount: 4, - expCompleteFiles: 1, - expInProgressFiles: 1, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tmpDir := t.TempDir() - md := &v1.DataCaptureMetadata{Type: tc.dataType} - sut := NewBuffer(tmpDir, md) - var pushValue *v1.SensorData - if tc.dataType == v1.DataType_DATA_TYPE_BINARY_SENSOR { - pushValue = binarySensorData - } else { - pushValue = structSensorData - } - - for i := 0; i < tc.pushCount; i++ { - err := sut.Write(pushValue) - test.That(t, err, test.ShouldBeNil) - } - - dcFiles, inProgressFiles := getCaptureFiles(tmpDir) - test.That(t, len(dcFiles), test.ShouldEqual, tc.expCompleteFiles) - test.That(t, len(inProgressFiles), test.ShouldEqual, tc.expInProgressFiles) - - // Test that sync is respected: after closing, all files should no longer be in progress. - err := sut.Flush() - test.That(t, err, test.ShouldBeNil) - completeFiles, remainingProgFiles := getCaptureFiles(tmpDir) - test.That(t, len(remainingProgFiles), test.ShouldEqual, 0) - - // Validate correct values were written. - var actCaptures []*v1.SensorData - for i := 0; i < len(completeFiles); i++ { - c, err := SensorDataFromFilePath(completeFiles[i]) - test.That(t, err, test.ShouldBeNil) - actCaptures = append(actCaptures, c...) - } - test.That(t, len(actCaptures), test.ShouldEqual, tc.pushCount) - for _, capture := range actCaptures { - if tc.dataType == v1.DataType_DATA_TYPE_BINARY_SENSOR { - test.That(t, capture.GetBinary(), test.ShouldNotBeNil) - test.That(t, capture.GetBinary(), test.ShouldResemble, binarySensorData.GetBinary()) - } - if tc.dataType == v1.DataType_DATA_TYPE_TABULAR_SENSOR { - test.That(t, capture.GetStruct(), test.ShouldNotBeNil) - test.That(t, capture.GetStruct(), test.ShouldResemble, structSensorData.GetStruct()) - } - } - }) - } -} - -//nolint -func getCaptureFiles(dir string) (dcFiles, progFiles []string) { - _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if info.IsDir() { - return nil - } - if filepath.Ext(path) == FileExt { - dcFiles = append(dcFiles, path) - } - if filepath.Ext(path) == InProgressFileExt { - progFiles = append(progFiles, path) - } - return nil - }) - return dcFiles, progFiles -} diff --git a/services/datamanager/datacapture/data_capture_file_test.go b/services/datamanager/datacapture/data_capture_file_test.go deleted file mode 100644 index e8c8039d062..00000000000 --- a/services/datamanager/datacapture/data_capture_file_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package datacapture - -import ( - "testing" - - v1 "go.viam.com/api/app/datasync/v1" - "go.viam.com/test" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" -) - -func TestBuildCaptureMetadata(t *testing.T) { - tests := []struct { - name string - componentType string - componentName string - method string - additionalParams map[string]string - dataType v1.DataType - fileExtension string - tags []string - }{ - { - name: "Metadata for arm positions stored in a length delimited proto file", - componentType: "arm", - componentName: "arm1", - method: "EndPosition", - additionalParams: make(map[string]string), - dataType: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - fileExtension: ".dat", - tags: []string{"tagA", "tagB"}, - }, - { - name: "Metadata for a camera Next() image stored as a binary .jpeg file", - componentType: "camera", - componentName: "cam1", - method: readImage, - additionalParams: map[string]string{"mime_type": utils.MimeTypeJPEG}, - dataType: v1.DataType_DATA_TYPE_BINARY_SENSOR, - fileExtension: ".jpeg", - tags: []string{}, - }, - { - name: "Metadata for a LiDAR Next() point cloud stored as a binary .pcd file", - componentType: "camera", - componentName: "cam1", - method: readImage, - additionalParams: map[string]string{"mime_type": utils.MimeTypePCD}, - dataType: v1.DataType_DATA_TYPE_BINARY_SENSOR, - fileExtension: ".pcd", - tags: []string{}, - }, - { - name: "Metadata for a LiDAR NextPointCloud() stored as a binary .pcd file", - componentType: "camera", - componentName: "cam1", - method: nextPointCloud, - additionalParams: make(map[string]string), - dataType: v1.DataType_DATA_TYPE_BINARY_SENSOR, - fileExtension: ".pcd", - tags: []string{}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - actualMetadata, err := BuildCaptureMetadata( - resource.APINamespaceRDK.WithComponentType(tc.componentType), - tc.componentName, tc.method, tc.additionalParams, tc.tags) - test.That(t, err, test.ShouldEqual, nil) - - methodParams, err := protoutils.ConvertStringMapToAnyPBMap(tc.additionalParams) - test.That(t, err, test.ShouldEqual, nil) - - expectedMetadata := v1.DataCaptureMetadata{ - ComponentType: resource.APINamespaceRDK.WithComponentType(tc.componentType).String(), - ComponentName: tc.componentName, - MethodName: tc.method, - Type: tc.dataType, - MethodParameters: methodParams, - FileExtension: tc.fileExtension, - Tags: tc.tags, - } - test.That(t, actualMetadata.String(), test.ShouldEqual, expectedMetadata.String()) - }) - } -} - -// TestReadCorruptedFile ensures that if a file ends with invalid data (which can occur if a robot is killed uncleanly -// during a write, e.g. if the power is cut), the file is still successfully read up until that point. -func TestReadCorruptedFile(t *testing.T) { - dir := t.TempDir() - md := &v1.DataCaptureMetadata{ - Type: v1.DataType_DATA_TYPE_TABULAR_SENSOR, - } - f, err := NewFile(dir, md) - test.That(t, err, test.ShouldBeNil) - numReadings := 100 - for i := 0; i < numReadings; i++ { - err := f.WriteNext(&v1.SensorData{ - Metadata: &v1.SensorMetadata{}, - Data: &v1.SensorData_Struct{Struct: &structpb.Struct{}}, - }) - test.That(t, err, test.ShouldBeNil) - } - _, err = f.writer.Write([]byte("invalid data")) - test.That(t, err, test.ShouldBeNil) - test.That(t, f.writer.Flush(), test.ShouldBeNil) - - // Should still be able to successfully read all the successfully written data. - sd, err := SensorDataFromFilePath(f.GetPath()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(sd), test.ShouldEqual, numReadings) -} diff --git a/services/datamanager/datacapture/verify_main_test.go b/services/datamanager/datacapture/verify_main_test.go deleted file mode 100644 index bfb1c999225..00000000000 --- a/services/datamanager/datacapture/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package datacapture - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/datamanager/datasync/verify_main_test.go b/services/datamanager/datasync/verify_main_test.go deleted file mode 100644 index e605a95a0ba..00000000000 --- a/services/datamanager/datasync/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package datasync - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/datamanager/register/register.go b/services/datamanager/register/register.go deleted file mode 100644 index 7c7b85f0a7d..00000000000 --- a/services/datamanager/register/register.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package register registers all relevant datamanager models and also API specific functions -package register - -import ( - // for datamanager models. - _ "go.viam.com/rdk/services/datamanager/builtin" -) diff --git a/services/datamanager/server_test.go b/services/datamanager/server_test.go deleted file mode 100644 index ce0167c562a..00000000000 --- a/services/datamanager/server_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package datamanager_test - -import ( - "context" - "errors" - "testing" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/datamanager/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/datamanager" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func newServer(resourceMap map[resource.Name]datamanager.Service) (pb.DataManagerServiceServer, error) { - coll, err := resource.NewAPIResourceCollection(datamanager.API, resourceMap) - if err != nil { - return nil, err - } - return datamanager.NewRPCServiceServer(coll).(pb.DataManagerServiceServer), nil -} - -func TestServerSync(t *testing.T) { - var extraOptions map[string]interface{} - - tests := map[string]struct { - resourceMap map[resource.Name]datamanager.Service - expectedError error - }{ - "missing datamanager": { - resourceMap: map[resource.Name]datamanager.Service{}, - expectedError: errors.New("resource \"rdk:service:data_manager/DataManager1\" not found"), - }, - "returns error": { - resourceMap: map[resource.Name]datamanager.Service{ - datamanager.Named(testDataManagerServiceName): &inject.DataManagerService{ - SyncFunc: func(ctx context.Context, extra map[string]interface{}) error { - return errors.New("fake sync error") - }, - }, - }, - expectedError: errors.New("fake sync error"), - }, - "returns response": { - resourceMap: map[resource.Name]datamanager.Service{ - datamanager.Named(testDataManagerServiceName): &inject.DataManagerService{ - SyncFunc: func(ctx context.Context, extra map[string]interface{}) error { - extraOptions = extra - return nil - }, - }, - }, - expectedError: nil, - }, - } - extra := map[string]interface{}{"foo": "Sync"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - syncRequest := &pb.SyncRequest{Name: testDataManagerServiceName, Extra: ext} - // put resource - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - resourceMap := tc.resourceMap - server, err := newServer(resourceMap) - test.That(t, err, test.ShouldBeNil) - _, err = server.Sync(context.Background(), syncRequest) - if tc.expectedError != nil { - test.That(t, err, test.ShouldBeError, tc.expectedError) - } else { - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, extra) - } - }) - } -} - -func TestServerDoCommand(t *testing.T) { - resourceMap := map[resource.Name]datamanager.Service{ - datamanager.Named(testDataManagerServiceName): &inject.DataManagerService{ - DoCommandFunc: testutils.EchoFunc, - }, - } - server, err := newServer(resourceMap) - test.That(t, err, test.ShouldBeNil) - - cmd, err := protoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - doCommandRequest := &commonpb.DoCommandRequest{ - Name: testDataManagerServiceName, - Command: cmd, - } - doCommandResponse, err := server.DoCommand(context.Background(), doCommandRequest) - test.That(t, err, test.ShouldBeNil) - - // Assert that do command response is an echoed request. - respMap := doCommandResponse.Result.AsMap() - test.That(t, respMap["command"], test.ShouldResemble, "test") - test.That(t, respMap["data"], test.ShouldResemble, 500.0) -} diff --git a/services/datamanager/verify_main_test.go b/services/datamanager/verify_main_test.go deleted file mode 100644 index 6bf5a6bf77b..00000000000 --- a/services/datamanager/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package datamanager - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/generic/client_test.go b/services/generic/client_test.go deleted file mode 100644 index efb6e039bcb..00000000000 --- a/services/generic/client_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package generic_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/generic" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testGenericName = "gen1" - failGenericName = "gen2" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - workingGeneric := &inject.GenericService{} - failingGeneric := &inject.GenericService{} - - workingGeneric.DoFunc = testutils.EchoFunc - failingGeneric.DoFunc = func( - ctx context.Context, - cmd map[string]interface{}, - ) ( - map[string]interface{}, - error, - ) { - return nil, errDoFailed - } - - resourceMap := map[resource.Name]resource.Resource{ - generic.Named(testGenericName): workingGeneric, - generic.Named(failGenericName): failingGeneric, - } - genericSvc, err := resource.NewAPIResourceCollection(generic.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[resource.Resource](generic.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, genericSvc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("client tests for working generic", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingGenericClient, err := generic.NewClientFromConn(context.Background(), conn, "", generic.Named(testGenericName), logger) - test.That(t, err, test.ShouldBeNil) - - resp, err := workingGenericClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, workingGenericClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("client tests for failing generic", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingGenericClient, err := generic.NewClientFromConn(context.Background(), conn, "", generic.Named(failGenericName), logger) - test.That(t, err, test.ShouldBeNil) - - _, err = failingGenericClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errDoFailed.Error()) - - test.That(t, failingGenericClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client tests for working generic", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := resourceAPI.RPCClient(context.Background(), conn, "", generic.Named(testGenericName), logger) - test.That(t, err, test.ShouldBeNil) - - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/generic/fake/fake_test.go b/services/generic/fake/fake_test.go deleted file mode 100644 index 2d86671a015..00000000000 --- a/services/generic/fake/fake_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package fake - -import ( - "context" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/services/generic" -) - -func TestDoCommand(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - gen := newGeneric(generic.Named("foo"), logger) - cmd := map[string]interface{}{"bar": "baz"} - resp, err := gen.DoCommand(ctx, cmd) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, cmd) -} diff --git a/services/generic/server_test.go b/services/generic/server_test.go deleted file mode 100644 index 15c7f1a04c4..00000000000 --- a/services/generic/server_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package generic_test - -import ( - "context" - "errors" - "testing" - - commonpb "go.viam.com/api/common/v1" - genericpb "go.viam.com/api/service/generic/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/generic" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var errDoFailed = errors.New("do failed") - -func newServer() (genericpb.GenericServiceServer, *inject.GenericService, *inject.GenericService, error) { - injectGeneric := &inject.GenericService{} - injectGeneric2 := &inject.GenericService{} - resourceMap := map[resource.Name]resource.Resource{ - generic.Named(testGenericName): injectGeneric, - generic.Named(failGenericName): injectGeneric2, - } - injectSvc, err := resource.NewAPIResourceCollection(generic.API, resourceMap) - if err != nil { - return nil, nil, nil, err - } - return generic.NewRPCServiceServer(injectSvc).(genericpb.GenericServiceServer), injectGeneric, injectGeneric2, nil -} - -func TestGenericDo(t *testing.T) { - genericServer, workingGeneric, failingGeneric, err := newServer() - test.That(t, err, test.ShouldBeNil) - - workingGeneric.DoFunc = func( - ctx context.Context, - cmd map[string]interface{}, - ) ( - map[string]interface{}, - error, - ) { - return cmd, nil - } - failingGeneric.DoFunc = func( - ctx context.Context, - cmd map[string]interface{}, - ) ( - map[string]interface{}, - error, - ) { - return nil, errDoFailed - } - - commandStruct, err := protoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - - req := commonpb.DoCommandRequest{Name: testGenericName, Command: commandStruct} - resp, err := genericServer.DoCommand(context.Background(), &req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, resp.Result.AsMap()["cmd"], test.ShouldEqual, testutils.TestCommand["cmd"]) - test.That(t, resp.Result.AsMap()["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - req = commonpb.DoCommandRequest{Name: failGenericName, Command: commandStruct} - resp, err = genericServer.DoCommand(context.Background(), &req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errDoFailed.Error()) - test.That(t, resp, test.ShouldBeNil) -} diff --git a/services/mlmodel/client_test.go b/services/mlmodel/client_test.go deleted file mode 100644 index ed601b547e2..00000000000 --- a/services/mlmodel/client_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package mlmodel_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - "gorgonia.org/tensor" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/ml" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/mlmodel" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testMLModelServiceName = "mlmodel1" - testMLModelServiceName2 = "mlmodel2" -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - fakeModel := inject.NewMLModelService(testMLModelServiceName) - fakeModel.MetadataFunc = injectedMetadataFunc - fakeModel.InferFunc = injectedInferFunc - resources := map[resource.Name]mlmodel.Service{ - mlmodel.Named(testMLModelServiceName): fakeModel, - } - svc, err := resource.NewAPIResourceCollection(mlmodel.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[mlmodel.Service](mlmodel.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - inputTensors := ml.Tensors{} - inputTensors["image"] = tensor.New(tensor.WithShape(3, 3), tensor.WithBacking([]uint8{10, 10, 255, 0, 0, 255, 255, 0, 100})) - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // context canceled - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - // working - t.Run("ml model client infer", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := mlmodel.NewClientFromConn(context.Background(), conn, "", mlmodel.Named(testMLModelServiceName), logger) - test.That(t, err, test.ShouldBeNil) - // Infer Command - result, err := client.Infer(context.Background(), inputTensors) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(result), test.ShouldEqual, 4) - test.That(t, err, test.ShouldBeNil) - detections, err := result["n_detections"].At(0) - test.That(t, err, test.ShouldBeNil) - test.That(t, detections, test.ShouldEqual, 3) - confidenceScores, err := result["confidence_scores"].Slice(tensor.S(0, 1), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, confidenceScores.Size(), test.ShouldEqual, 3) - labels, err := result["labels"].Slice(tensor.S(0, 1), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, labels.Size(), test.ShouldEqual, 3) - location0, err := result["locations"].At(0, 0, 0) - test.That(t, err, test.ShouldBeNil) - test.That(t, location0, test.ShouldEqual, 0.1) - locations, err := result["locations"].Slice(tensor.S(0, 1), tensor.S(0, 1), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, locations.Size(), test.ShouldEqual, 4) - test.That(t, locations.Data().([]float32), test.ShouldResemble, []float32{0.1, 0.4, 0.22, 0.4}) - // nil data should work too - result, err = client.Infer(context.Background(), nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(result), test.ShouldEqual, 4) - // close the client - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - t.Run("ml model client metadata", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := mlmodel.NewClientFromConn(context.Background(), conn, "", mlmodel.Named(testMLModelServiceName), logger) - test.That(t, err, test.ShouldBeNil) - // Metadata Command - meta, err := client.Metadata(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, meta.ModelName, test.ShouldEqual, "fake_detector") - test.That(t, meta.ModelType, test.ShouldEqual, "object_detector") - test.That(t, meta.ModelDescription, test.ShouldEqual, "desc") - t.Logf("inputs: %v", meta.Inputs) - test.That(t, len(meta.Inputs), test.ShouldEqual, 1) - test.That(t, len(meta.Outputs), test.ShouldEqual, 4) - test.That(t, meta.Inputs[0].Shape, test.ShouldResemble, []int{300, 200}) - outInfo := meta.Outputs - test.That(t, outInfo[0].Name, test.ShouldEqual, "n_detections") - test.That(t, len(outInfo[0].AssociatedFiles), test.ShouldEqual, 0) - test.That(t, outInfo[2].Name, test.ShouldEqual, "labels") - test.That(t, len(outInfo[2].AssociatedFiles), test.ShouldEqual, 1) - test.That(t, outInfo[2].AssociatedFiles[0].Name, test.ShouldEqual, "category_labels.txt") - test.That(t, outInfo[2].AssociatedFiles[0].LabelType, test.ShouldEqual, mlmodel.LabelTypeTensorValue) - test.That(t, outInfo[3].Name, test.ShouldEqual, "locations") - test.That(t, outInfo[3].Shape, test.ShouldResemble, []int{4, 3, 1}) - - // close the client - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/mlmodel/mlmodel_test.go b/services/mlmodel/mlmodel_test.go deleted file mode 100644 index fb60f304b94..00000000000 --- a/services/mlmodel/mlmodel_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package mlmodel - -import ( - "testing" - - "go.viam.com/test" - "gorgonia.org/tensor" -) - -func TestTensorRoundTrip(t *testing.T) { - testCases := []struct { - name string - tensor *tensor.Dense - }{ - {"int8", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]int8{-1, 2, 3, 4, -5, 6}))}, - {"uint8", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]uint8{1, 2, 3, 4, 5, 6}))}, - {"int16", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]int16{-1, 2, 3, 4, -5, 6}))}, - {"uint16", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]uint16{1, 2, 3, 4, 5, 6}))}, - {"int32", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]int32{-1, 2, 3, 4, -5, 6}))}, - {"uint32", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]uint32{1, 2, 3, 4, 5, 6}))}, - {"int64", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]int64{-1, 2, 3, 4, -5, 6}))}, - {"uint64", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]uint64{1, 2, 3, 4, 5, 6}))}, - {"float32", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]float32{1.1, 2.0, 3.0, 4.0, 5.0, -6.0}))}, - {"float64", tensor.New(tensor.WithShape(2, 3), tensor.WithBacking([]float64{-1.1, 2.0, -3.0, 4.5, 5.0, 6.0}))}, - } - - for _, tensor := range testCases { - t.Run(tensor.name, func(t *testing.T) { - resp, err := tensorToProto(tensor.tensor) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Shape, test.ShouldHaveLength, 2) - test.That(t, resp.Shape[0], test.ShouldEqual, 2) - test.That(t, resp.Shape[1], test.ShouldEqual, 3) - back, err := createNewTensor(resp) - test.That(t, err, test.ShouldBeNil) - test.That(t, back.Shape(), test.ShouldResemble, tensor.tensor.Shape()) - test.That(t, back.Data(), test.ShouldResemble, tensor.tensor.Data()) - }) - } -} diff --git a/services/mlmodel/server_test.go b/services/mlmodel/server_test.go deleted file mode 100644 index d3db338e483..00000000000 --- a/services/mlmodel/server_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package mlmodel_test - -import ( - "context" - "testing" - - "github.com/pkg/errors" - pb "go.viam.com/api/service/mlmodel/v1" - "go.viam.com/test" - "gorgonia.org/tensor" - - "go.viam.com/rdk/ml" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/mlmodel" - "go.viam.com/rdk/testutils/inject" -) - -func newServer(resources map[resource.Name]mlmodel.Service) (pb.MLModelServiceServer, error) { - coll, err := resource.NewAPIResourceCollection(mlmodel.API, resources) - if err != nil { - return nil, err - } - return mlmodel.NewRPCServiceServer(coll).(pb.MLModelServiceServer), nil -} - -func TestServerNotFound(t *testing.T) { - metadataRequest := &pb.MetadataRequest{ - Name: testMLModelServiceName, - } - resources := map[resource.Name]mlmodel.Service{} - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - _, err = server.Metadata(context.Background(), metadataRequest) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:mlmodel/mlmodel1\" not found")) -} - -func TestServerMetadata(t *testing.T) { - metadataRequest := &pb.MetadataRequest{ - Name: testMLModelServiceName, - } - - mockSrv := inject.NewMLModelService(testMLModelServiceName) - mockSrv.MetadataFunc = injectedMetadataFunc - resources := map[resource.Name]mlmodel.Service{ - mlmodel.Named(testMLModelServiceName): mockSrv, - } - - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - resp, err := server.Metadata(context.Background(), metadataRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Metadata.GetName(), test.ShouldEqual, "fake_detector") - test.That(t, resp.Metadata.GetType(), test.ShouldEqual, "object_detector") - test.That(t, resp.Metadata.GetDescription(), test.ShouldEqual, "desc") - test.That(t, len(resp.Metadata.GetInputInfo()), test.ShouldEqual, 1) - test.That(t, len(resp.Metadata.GetOutputInfo()), test.ShouldEqual, 4) - outInfo := resp.Metadata.GetOutputInfo() - test.That(t, outInfo[0].GetName(), test.ShouldEqual, "n_detections") - test.That(t, len(outInfo[0].GetAssociatedFiles()), test.ShouldEqual, 0) - test.That(t, outInfo[2].GetName(), test.ShouldEqual, "labels") - test.That(t, len(outInfo[2].GetAssociatedFiles()), test.ShouldEqual, 1) - test.That(t, outInfo[2].GetAssociatedFiles()[0].GetName(), test.ShouldEqual, "category_labels.txt") - test.That(t, outInfo[2].GetAssociatedFiles()[0].GetLabelType(), test.ShouldEqual, 1) - test.That(t, outInfo[3].GetName(), test.ShouldEqual, "locations") - test.That(t, outInfo[3].GetShape(), test.ShouldResemble, []int32{4, 3, 1}) - - // Multiple Services names Valid - resources = map[resource.Name]mlmodel.Service{ - mlmodel.Named(testMLModelServiceName): mockSrv, - mlmodel.Named(testMLModelServiceName2): mockSrv, - } - server, err = newServer(resources) - test.That(t, err, test.ShouldBeNil) - resp, err = server.Metadata(context.Background(), metadataRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Metadata.GetName(), test.ShouldEqual, "fake_detector") - test.That(t, resp.Metadata.GetType(), test.ShouldEqual, "object_detector") - test.That(t, resp.Metadata.GetDescription(), test.ShouldEqual, "desc") - - metadataRequest2 := &pb.MetadataRequest{ - Name: testMLModelServiceName2, - } - resp, err = server.Metadata(context.Background(), metadataRequest2) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Metadata.GetName(), test.ShouldEqual, "fake_detector") - test.That(t, resp.Metadata.GetType(), test.ShouldEqual, "object_detector") - test.That(t, resp.Metadata.GetDescription(), test.ShouldEqual, "desc") -} - -var injectedMetadataFunc = func(ctx context.Context) (mlmodel.MLMetadata, error) { - md := mlmodel.MLMetadata{ - ModelName: "fake_detector", - ModelType: "object_detector", - ModelDescription: "desc", - } - md.Inputs = []mlmodel.TensorInfo{ - {Name: "image", Description: "i0", DataType: "uint8", Shape: []int{300, 200}}, - } - md.Outputs = []mlmodel.TensorInfo{ - {Name: "n_detections", Description: "o0", DataType: "int32", Shape: []int{1}}, - {Name: "confidence_scores", Description: "o1", DataType: "float32", Shape: []int{3, 1}}, - { - Name: "labels", - Description: "o2", - DataType: "int32", - Shape: []int{3, 1}, - AssociatedFiles: []mlmodel.File{ - { - Name: "category_labels.txt", - Description: "these labels represent types of plants", - LabelType: mlmodel.LabelTypeTensorValue, - }, - }, - }, - {Name: "locations", Description: "o3", DataType: "float32", Shape: []int{4, 3, 1}}, - } - return md, nil -} - -var injectedInferFunc = func( - ctx context.Context, - tensors ml.Tensors, -) (ml.Tensors, error) { - // this is a possible form of what a detection tensor with 3 detection in 1 image would look like - outputMap := ml.Tensors{} - outputMap["n_detections"] = tensor.New( - tensor.WithShape(1), - tensor.WithBacking([]int32{3}), - ) - outputMap["confidence_scores"] = tensor.New( - tensor.WithShape(1, 3), - tensor.WithBacking([]float32{0.9084375, 0.7359375, 0.33984375}), - ) - outputMap["labels"] = tensor.New( - tensor.WithShape(1, 3), - tensor.WithBacking([]int32{0, 0, 4}), - ) - outputMap["locations"] = tensor.New( - tensor.WithShape(1, 3, 4), - tensor.WithBacking([]float32{0.1, 0.4, 0.22, 0.4, 0.02, 0.22, 0.77, 0.90, 0.40, 0.50, 0.40, 0.50}), - ) - return outputMap, nil -} - -func TestServerInfer(t *testing.T) { - // input tensors to proto - inputTensors := ml.Tensors{} - inputTensors["image"] = tensor.New(tensor.WithShape(3, 3), tensor.WithBacking([]uint8{10, 10, 255, 0, 0, 255, 255, 0, 100})) - tensorsProto, err := mlmodel.TensorsToProto(inputTensors) - test.That(t, err, test.ShouldBeNil) - inferRequest := &pb.InferRequest{ - Name: testMLModelServiceName, - InputTensors: tensorsProto, - } - - mockSrv := inject.NewMLModelService(testMLModelServiceName) - mockSrv.InferFunc = injectedInferFunc - resources := map[resource.Name]mlmodel.Service{ - mlmodel.Named(testMLModelServiceName): mockSrv, - } - - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - resp, err := server.Infer(context.Background(), inferRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.OutputTensors.Tensors), test.ShouldEqual, 4) - protoTensors := resp.OutputTensors.Tensors - // n detections - test.That(t, protoTensors["n_detections"].GetShape(), test.ShouldResemble, []uint64{1}) - nDetections := protoTensors["n_detections"].GetInt32Tensor() - test.That(t, nDetections, test.ShouldNotBeNil) - test.That(t, nDetections.Data[0], test.ShouldEqual, 3) - // confidence scores - test.That(t, protoTensors["confidence_scores"].GetShape(), test.ShouldResemble, []uint64{1, 3}) - confScores := protoTensors["confidence_scores"].GetFloatTensor() - test.That(t, confScores, test.ShouldNotBeNil) - test.That(t, confScores.Data, test.ShouldHaveLength, 3) - // labels - test.That(t, protoTensors["labels"].GetShape(), test.ShouldResemble, []uint64{1, 3}) - labels := protoTensors["labels"].GetInt32Tensor() - test.That(t, labels, test.ShouldNotBeNil) - test.That(t, labels.Data, test.ShouldHaveLength, 3) - // locations - test.That(t, protoTensors["locations"].GetShape(), test.ShouldResemble, []uint64{1, 3, 4}) - locations := protoTensors["locations"].GetFloatTensor() - test.That(t, locations, test.ShouldNotBeNil) - test.That(t, locations.Data[0:4], test.ShouldResemble, []float32{0.1, 0.4, 0.22, 0.4}) -} diff --git a/services/mlmodel/tflitecpu/tflite_cpu_test.go b/services/mlmodel/tflitecpu/tflite_cpu_test.go deleted file mode 100644 index 8a9b0db6557..00000000000 --- a/services/mlmodel/tflitecpu/tflite_cpu_test.go +++ /dev/null @@ -1,282 +0,0 @@ -//go:build !no_tflite - -package tflitecpu - -import ( - "context" - "net" - "testing" - - "github.com/nfnt/resize" - "go.viam.com/test" - "go.viam.com/utils/artifact" - "go.viam.com/utils/rpc" - "gorgonia.org/tensor" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/ml" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/services/mlmodel" -) - -func TestEmptyTFLiteConfig(t *testing.T) { - ctx := context.Background() - emptyCfg := TFLiteConfig{} // empty config - - // Test that empty config gives error about loading model - emptyGot, err := NewTFLiteCPUModel(ctx, &emptyCfg, mlmodel.Named("fakeModel")) - test.That(t, emptyGot, test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "could not add model") -} - -func TestTFLiteCPUDetector(t *testing.T) { - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/effdet0.tflite") - cfg := TFLiteConfig{ // detector config - ModelPath: modelLoc, - NumThreads: 2, - } - // Test that a detector would give the expected output on the dog image - // Creating the model should populate model and attrs, but not metadata - out, err := NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("myDet")) - got := out.(*Model) - test.That(t, err, test.ShouldBeNil) - test.That(t, got.model, test.ShouldNotBeNil) - test.That(t, got.conf, test.ShouldNotBeNil) - test.That(t, got.metadata, test.ShouldBeNil) - - // Test that the Metadata() works on detector - gotMD, err := got.Metadata(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotMD, test.ShouldNotBeNil) - test.That(t, got.metadata, test.ShouldNotBeNil) - - test.That(t, gotMD.Inputs[0].Name, test.ShouldResemble, "image") - test.That(t, gotMD.Outputs[0].Name, test.ShouldResemble, "location") - test.That(t, gotMD.Outputs[1].Name, test.ShouldResemble, "category") - test.That(t, gotMD.Outputs[2].Name, test.ShouldResemble, "score") - test.That(t, gotMD.Inputs[0].DataType, test.ShouldResemble, "uint8") - test.That(t, gotMD.Outputs[0].DataType, test.ShouldResemble, "float32") - test.That(t, gotMD.Outputs[1].AssociatedFiles[0].Name, test.ShouldResemble, "labelmap.txt") - - // Test that the Infer() works on detector - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/dogscute.jpeg")) - test.That(t, err, test.ShouldBeNil) - resized := resize.Resize(uint(got.metadata.Inputs[0].Shape[1]), uint(got.metadata.Inputs[0].Shape[2]), pic, resize.Bilinear) - imgBytes := rimage.ImageToUInt8Buffer(resized, false) - test.That(t, imgBytes, test.ShouldNotBeNil) - inputMap := ml.Tensors{} - inputMap["image"] = tensor.New( - tensor.WithShape(got.metadata.Inputs[0].Shape[1], got.metadata.Inputs[0].Shape[2], 3), - tensor.WithBacking(imgBytes), - ) - gotOutput, err := got.Infer(ctx, inputMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotOutput, test.ShouldNotBeNil) - - test.That(t, len(gotOutput), test.ShouldEqual, 4) - // n detections - test.That(t, gotOutput["number of detections"], test.ShouldNotBeNil) - test.That(t, gotOutput["number of detections"].Data(), test.ShouldResemble, []float32{25}) - // score - test.That(t, gotOutput["score"], test.ShouldNotBeNil) - test.That(t, gotOutput["score"].Shape(), test.ShouldResemble, tensor.Shape{1, 25}) - // category - test.That(t, gotOutput["category"], test.ShouldNotBeNil) - test.That(t, gotOutput["category"].Shape(), test.ShouldResemble, tensor.Shape{1, 25}) - result, err := gotOutput["category"].At(0, 0) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldEqual, 17) // 17 is dog - // location - test.That(t, gotOutput["location"], test.ShouldNotBeNil) - test.That(t, gotOutput["location"].Shape(), test.ShouldResemble, tensor.Shape{1, 25, 4}) -} - -func TestTFLiteCPUClassifier(t *testing.T) { - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/effnet0.tflite") - cfg := TFLiteConfig{ // classifier config - ModelPath: modelLoc, - NumThreads: 2, - } - - // Test that the tflite classifier gives the expected output on the lion image - out, err := NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("myClass")) - got := out.(*Model) - test.That(t, err, test.ShouldBeNil) - test.That(t, got.model, test.ShouldNotBeNil) - test.That(t, got.conf, test.ShouldNotBeNil) - test.That(t, got.metadata, test.ShouldBeNil) - - // Test that the Metadata() works on classifier - gotMD, err := got.Metadata(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotMD, test.ShouldNotBeNil) - - test.That(t, gotMD.Inputs[0].Name, test.ShouldResemble, "image") - test.That(t, gotMD.Outputs[0].Name, test.ShouldResemble, "probability") - test.That(t, gotMD.Inputs[0].DataType, test.ShouldResemble, "uint8") - test.That(t, gotMD.Outputs[0].DataType, test.ShouldResemble, "uint8") - test.That(t, gotMD.Outputs[0].AssociatedFiles[0].Name, test.ShouldContainSubstring, ".txt") - test.That(t, gotMD.Outputs[0].AssociatedFiles[0].LabelType, test.ShouldResemble, mlmodel.LabelTypeTensorAxis) - - // Test that the Infer() works on a classifier - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/lion.jpeg")) - test.That(t, err, test.ShouldBeNil) - resized := resize.Resize(uint(got.metadata.Inputs[0].Shape[1]), uint(got.metadata.Inputs[0].Shape[2]), pic, resize.Bilinear) - imgBytes := rimage.ImageToUInt8Buffer(resized, false) - test.That(t, imgBytes, test.ShouldNotBeNil) - inputMap := ml.Tensors{} - inputMap["images"] = tensor.New( - tensor.WithShape(got.metadata.Inputs[0].Shape[1], got.metadata.Inputs[0].Shape[2], 3), - tensor.WithBacking(imgBytes), - ) - - gotOutput, err := got.Infer(ctx, inputMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotOutput, test.ShouldNotBeNil) - - test.That(t, len(gotOutput), test.ShouldEqual, 1) - test.That(t, gotOutput["probability"], test.ShouldNotBeNil) - result, err := gotOutput["probability"].At(0, 290) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldEqual, 0) - result, err = gotOutput["probability"].At(0, 291) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldBeGreaterThan, 200) // 291 is lion - result, err = gotOutput["probability"].At(0, 292) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldEqual, 0) -} - -func TestTFLiteCPUTextModel(t *testing.T) { - // Setup - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/mobilebert_1_default_1.tflite") - - cfg := TFLiteConfig{ // text classifier config - ModelPath: modelLoc, - NumThreads: 1, - } - - // Test that a text classifier gives an output with good input - out, err := NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("myTextModel")) - got := out.(*Model) - test.That(t, err, test.ShouldBeNil) - test.That(t, got.model, test.ShouldNotBeNil) - test.That(t, got.conf, test.ShouldNotBeNil) - test.That(t, got.metadata, test.ShouldBeNil) - - // Test that the Metadata() does not error even when there is none - // Should still populate with something - _, err = got.Metadata(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, got.metadata, test.ShouldNotBeNil) - - // Test that the Infer() works even on a text classifier - zeros := make([]int32, got.model.Info.InputHeight) - inputMap := ml.Tensors{} - inputMap["input_ids"] = makeExampleTensor(got.model.Info.InputHeight) - test.That(t, inputMap["input_ids"].Shape(), test.ShouldResemble, tensor.Shape{384}) - inputMap["input_mask"] = tensor.New(tensor.WithShape(384), tensor.WithBacking(zeros)) - inputMap["segment_ids"] = tensor.New(tensor.WithShape(384), tensor.WithBacking(zeros)) - gotOutput, err := got.Infer(ctx, inputMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotOutput, test.ShouldNotBeNil) - test.That(t, len(gotOutput), test.ShouldEqual, 2) - test.That(t, gotOutput["end_logits"], test.ShouldNotBeNil) - test.That(t, gotOutput["start_logits"], test.ShouldNotBeNil) - test.That(t, gotOutput["end_logits"].Shape(), test.ShouldResemble, tensor.Shape{1, 384}) - test.That(t, gotOutput["start_logits"].Shape(), test.ShouldResemble, tensor.Shape{1, 384}) -} - -func TestTFLiteCPUClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - modelParams := TFLiteConfig{ // classifier config - ModelPath: artifact.MustPath("vision/tflite/effdet0.tflite"), - NumThreads: 2, - } - myModel, err := NewTFLiteCPUModel(context.Background(), &modelParams, mlmodel.Named("myModel")) - test.That(t, err, test.ShouldBeNil) - test.That(t, myModel, test.ShouldNotBeNil) - - resources := map[resource.Name]mlmodel.Service{ - mlmodel.Named("testName"): myModel, - } - svc, err := resource.NewAPIResourceCollection(mlmodel.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[mlmodel.Service](mlmodel.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // Prep img - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/dogscute.jpeg")) - test.That(t, err, test.ShouldBeNil) - resized := resize.Resize(320, 320, pic, resize.Bilinear) - imgBytes := rimage.ImageToUInt8Buffer(resized, false) - test.That(t, imgBytes, test.ShouldNotBeNil) - inputMap := ml.Tensors{} - inputMap["image"] = tensor.New( - tensor.WithShape(320, 320, 3), - tensor.WithBacking(imgBytes), - ) - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := mlmodel.NewClientFromConn(context.Background(), conn, "", mlmodel.Named("testName"), logger) - test.That(t, err, test.ShouldBeNil) - // Test call to Metadata - gotMD, err := client.Metadata(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotMD, test.ShouldNotBeNil) - test.That(t, gotMD.ModelType, test.ShouldEqual, "tflite_detector") - test.That(t, gotMD.Inputs[0].Name, test.ShouldResemble, "image") - test.That(t, gotMD.Outputs[0].Name, test.ShouldResemble, "location") - test.That(t, gotMD.Outputs[1].Name, test.ShouldResemble, "category") - test.That(t, gotMD.Outputs[2].Name, test.ShouldResemble, "score") - test.That(t, gotMD.Outputs[3].Name, test.ShouldResemble, "number of detections") - test.That(t, gotMD.Outputs[1].Description, test.ShouldContainSubstring, "categories of the detected boxes") - test.That(t, gotMD.Inputs[0].DataType, test.ShouldResemble, "uint8") - test.That(t, gotMD.Outputs[0].DataType, test.ShouldResemble, "float32") - test.That(t, gotMD.Outputs[1].AssociatedFiles[0].Name, test.ShouldResemble, "labelmap.txt") - - // Test call to Infer - gotOutput, err := client.Infer(context.Background(), inputMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotOutput, test.ShouldNotBeNil) - test.That(t, len(gotOutput), test.ShouldEqual, 4) - // n detections - test.That(t, gotOutput["number of detections"], test.ShouldNotBeNil) - test.That(t, gotOutput["number of detections"].Data(), test.ShouldResemble, []float32{25}) - // score - test.That(t, gotOutput["score"], test.ShouldNotBeNil) - test.That(t, gotOutput["score"].Shape(), test.ShouldResemble, tensor.Shape{1, 25}) - // category - test.That(t, gotOutput["category"], test.ShouldNotBeNil) - test.That(t, gotOutput["category"].Shape(), test.ShouldResemble, tensor.Shape{1, 25}) - result, err := gotOutput["category"].At(0, 0) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldEqual, 17) // 17 is dog - // location - test.That(t, gotOutput["location"], test.ShouldNotBeNil) - test.That(t, gotOutput["location"].Shape(), test.ShouldResemble, tensor.Shape{1, 25, 4}) -} - -func makeExampleTensor(length int) *tensor.Dense { - out := make([]int32, 0, length) - for i := 0; i < length; i++ { - out = append(out, int32(i)) - } - return tensor.New(tensor.WithShape(length), tensor.WithBacking(out)) -} diff --git a/services/mlmodel/verify_main_test.go b/services/mlmodel/verify_main_test.go deleted file mode 100644 index bc88f598d24..00000000000 --- a/services/mlmodel/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package mlmodel - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/motion/builtin/builtin.go b/services/motion/builtin/builtin.go deleted file mode 100644 index bcb334b1499..00000000000 --- a/services/motion/builtin/builtin.go +++ /dev/null @@ -1,395 +0,0 @@ -// Package builtin implements a motion service. -package builtin - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - "github.com/pkg/errors" - servicepb "go.viam.com/api/service/motion/v1" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/motion/builtin/state" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -var ( - stateTTL = time.Hour * 24 - stateTTLCheckInterval = time.Minute -) - -func init() { - resource.RegisterDefaultService( - motion.API, - resource.DefaultServiceModel, - resource.Registration[motion.Service, *Config]{ - Constructor: NewBuiltIn, - WeakDependencies: []resource.Matcher{ - resource.TypeMatcher{Type: resource.APITypeComponentName}, - resource.SubtypeMatcher{Subtype: slam.SubtypeName}, - resource.SubtypeMatcher{Subtype: vision.SubtypeName}, - }, - }, - ) -} - -const ( - builtinOpLabel = "motion-service" - maxTravelDistanceMM = 5e6 // this is equivalent to 5km - lookAheadDistanceMM float64 = 5e6 - defaultSmoothIter = 30 - defaultAngularDegsPerSec = 20. - defaultLinearMPerSec = 0.3 - defaultObstaclePollingHz = 1. - defaultSlamPlanDeviationM = 1. - defaultGlobePlanDeviationM = 2.6 - defaultPositionPollingHz = 1. -) - -// inputEnabledActuator is an actuator that interacts with the frame system. -// This allows us to figure out where the actuator currently is and then -// move it. Input units are always in meters or radians. -type inputEnabledActuator interface { - resource.Actuator - referenceframe.InputEnabled -} - -// ErrNotImplemented is thrown when an unreleased function is called. -var ErrNotImplemented = errors.New("function coming soon but not yet implemented") - -// Config describes how to configure the service; currently only used for specifying dependency on framesystem service. -type Config struct { - LogFilePath string `json:"log_file_path"` -} - -// Validate here adds a dependency on the internal framesystem service. -func (c *Config) Validate(path string) ([]string, error) { - return []string{framesystem.InternalServiceName.String()}, nil -} - -// NewBuiltIn returns a new move and grab service for the given robot. -func NewBuiltIn( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (motion.Service, error) { - ms := &builtIn{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - - if err := ms.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return ms, nil -} - -// Reconfigure updates the motion service when the config has changed. -func (ms *builtIn) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) error { - ms.mu.Lock() - defer ms.mu.Unlock() - - config, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - if config.LogFilePath != "" { - logger, err := rdkutils.NewFilePathDebugLogger(config.LogFilePath, "motion") - if err != nil { - return err - } - ms.logger = logger - } - movementSensors := make(map[resource.Name]movementsensor.MovementSensor) - slamServices := make(map[resource.Name]slam.Service) - visionServices := make(map[resource.Name]vision.Service) - components := make(map[resource.Name]resource.Resource) - for name, dep := range deps { - switch dep := dep.(type) { - case framesystem.Service: - ms.fsService = dep - case movementsensor.MovementSensor: - movementSensors[name] = dep - case slam.Service: - slamServices[name] = dep - case vision.Service: - visionServices[name] = dep - default: - components[name] = dep - } - } - ms.movementSensors = movementSensors - ms.slamServices = slamServices - ms.visionServices = visionServices - ms.components = components - if ms.state != nil { - ms.state.Stop() - } - - state, err := state.NewState(stateTTL, stateTTLCheckInterval, ms.logger) - if err != nil { - return err - } - ms.state = state - return nil -} - -type builtIn struct { - resource.Named - mu sync.RWMutex - fsService framesystem.Service - movementSensors map[resource.Name]movementsensor.MovementSensor - slamServices map[resource.Name]slam.Service - visionServices map[resource.Name]vision.Service - components map[resource.Name]resource.Resource - logger logging.Logger - state *state.State -} - -func (ms *builtIn) Close(ctx context.Context) error { - ms.mu.Lock() - defer ms.mu.Unlock() - if ms.state != nil { - ms.state.Stop() - } - return nil -} - -// Move takes a goal location and will plan and execute a movement to move a component specified by its name to that destination. -func (ms *builtIn) Move( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *servicepb.Constraints, - extra map[string]interface{}, -) (bool, error) { - ms.mu.RLock() - defer ms.mu.RUnlock() - - operation.CancelOtherWithLabel(ctx, builtinOpLabel) - - // get goal frame - goalFrameName := destination.Parent() - ms.logger.CDebugf(ctx, "goal given in frame of %q", goalFrameName) - - frameSys, err := ms.fsService.FrameSystem(ctx, worldState.Transforms()) - if err != nil { - return false, err - } - - // build maps of relevant components and inputs from initial inputs - fsInputs, resources, err := ms.fsService.CurrentInputs(ctx) - if err != nil { - return false, err - } - - movingFrame := frameSys.Frame(componentName.ShortName()) - - ms.logger.CDebugf(ctx, "frame system inputs: %v", fsInputs) - if movingFrame == nil { - return false, fmt.Errorf("component named %s not found in robot frame system", componentName.ShortName()) - } - - // re-evaluate goalPose to be in the frame of World - solvingFrame := referenceframe.World // TODO(erh): this should really be the parent of rootName - tf, err := frameSys.Transform(fsInputs, destination, solvingFrame) - if err != nil { - return false, err - } - goalPose, _ := tf.(*referenceframe.PoseInFrame) - - // the goal is to move the component to goalPose which is specified in coordinates of goalFrameName - plan, err := motionplan.PlanMotion(ctx, &motionplan.PlanRequest{ - Logger: ms.logger, - Goal: goalPose, - Frame: movingFrame, - StartConfiguration: fsInputs, - FrameSystem: frameSys, - WorldState: worldState, - ConstraintSpecs: constraints, - Options: extra, - }) - if err != nil { - return false, err - } - - // move all the components - for _, step := range plan.Trajectory() { - for name, inputs := range step { - if len(inputs) == 0 { - continue - } - r := resources[name] - if err := r.GoToInputs(ctx, inputs); err != nil { - // If there is an error on GoToInputs, stop the component if possible before returning the error - if actuator, ok := r.(inputEnabledActuator); ok { - if stopErr := actuator.Stop(ctx, nil); stopErr != nil { - return false, errors.Wrap(err, stopErr.Error()) - } - } - return false, err - } - } - } - return true, nil -} - -func (ms *builtIn) MoveOnMap(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - ms.mu.RLock() - defer ms.mu.RUnlock() - ms.logger.CDebugf(ctx, "MoveOnMap called with %s", req) - - // TODO: Deprecated: remove once no motion apis use the opid system - operation.CancelOtherWithLabel(ctx, builtinOpLabel) - - id, err := state.StartExecution(ctx, ms.state, req.ComponentName, req, ms.newMoveOnMapRequest) - if err != nil { - return uuid.Nil, err - } - - return id, nil -} - -type validatedExtra struct { - maxReplans int - replanCostFactor float64 - motionProfile string - extra map[string]interface{} -} - -func newValidatedExtra(extra map[string]interface{}) (validatedExtra, error) { - maxReplans := -1 - replanCostFactor := defaultReplanCostFactor - motionProfile := "" - v := validatedExtra{} - if extra == nil { - v.extra = map[string]interface{}{"smooth_iter": defaultSmoothIter} - return v, nil - } - if replansRaw, ok := extra["max_replans"]; ok { - if replans, ok := replansRaw.(int); ok { - maxReplans = replans - } - } - if profile, ok := extra["motion_profile"]; ok { - motionProfile, ok = profile.(string) - if !ok { - return v, errors.New("could not interpret motion_profile field as string") - } - } - if costFactorRaw, ok := extra["replan_cost_factor"]; ok { - costFactor, ok := costFactorRaw.(float64) - if !ok { - return validatedExtra{}, errors.New("could not interpret replan_cost_factor field as float") - } - replanCostFactor = costFactor - } - - if _, ok := extra["smooth_iter"]; !ok { - extra["smooth_iter"] = defaultSmoothIter - } - - return validatedExtra{ - maxReplans: maxReplans, - motionProfile: motionProfile, - replanCostFactor: replanCostFactor, - extra: extra, - }, nil -} - -func (ms *builtIn) MoveOnGlobe(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - ms.mu.RLock() - defer ms.mu.RUnlock() - ms.logger.CDebugf(ctx, "MoveOnGlobe called with %s", req) - // TODO: Deprecated: remove once no motion apis use the opid system - operation.CancelOtherWithLabel(ctx, builtinOpLabel) - - id, err := state.StartExecution(ctx, ms.state, req.ComponentName, req, ms.newMoveOnGlobeRequest) - if err != nil { - return uuid.Nil, err - } - - return id, nil -} - -func (ms *builtIn) GetPose( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransforms []*referenceframe.LinkInFrame, - extra map[string]interface{}, -) (*referenceframe.PoseInFrame, error) { - ms.mu.RLock() - defer ms.mu.RUnlock() - if destinationFrame == "" { - destinationFrame = referenceframe.World - } - return ms.fsService.TransformPose( - ctx, - referenceframe.NewPoseInFrame( - componentName.ShortName(), - spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 0}), - ), - destinationFrame, - supplementalTransforms, - ) -} - -func (ms *builtIn) StopPlan( - ctx context.Context, - req motion.StopPlanReq, -) error { - if err := ctx.Err(); err != nil { - return err - } - ms.mu.RLock() - defer ms.mu.RUnlock() - return ms.state.StopExecutionByResource(req.ComponentName) -} - -func (ms *builtIn) ListPlanStatuses( - ctx context.Context, - req motion.ListPlanStatusesReq, -) ([]motion.PlanStatusWithID, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - ms.mu.RLock() - defer ms.mu.RUnlock() - return ms.state.ListPlanStatuses(req) -} - -func (ms *builtIn) PlanHistory( - ctx context.Context, - req motion.PlanHistoryReq, -) ([]motion.PlanWithStatus, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - ms.mu.RLock() - defer ms.mu.RUnlock() - return ms.state.PlanHistory(req) -} diff --git a/services/motion/builtin/builtin_test.go b/services/motion/builtin/builtin_test.go deleted file mode 100644 index 016032b2622..00000000000 --- a/services/motion/builtin/builtin_test.go +++ /dev/null @@ -1,2772 +0,0 @@ -package builtin - -import ( - "context" - "fmt" - "math" - "runtime" - "strings" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - - "go.viam.com/rdk/components/arm" - armFake "go.viam.com/rdk/components/arm/fake" - ur "go.viam.com/rdk/components/arm/universalrobots" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/components/movementsensor" - _ "go.viam.com/rdk/components/register" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/motion/builtin/state" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - viz "go.viam.com/rdk/vision" -) - -func TestMoveResponseString(t *testing.T) { - type testCase struct { - description string - expected string - moveResponse moveResponse - } - testCases := []testCase{ - { - "when executeResponse.Replan is false & ReplanReason is empty and error is not nil", - "builtin.moveResponse{executeResponse: state.ExecuteResponse{Replan:false, ReplanReason:\"\"}, err: an error}", - moveResponse{err: errors.New("an error")}, - }, - { - "when executeResponse.Replan is true & ReplanReason is not empty and error is not nil", - "builtin.moveResponse{executeResponse: state.ExecuteResponse{Replan:true, ReplanReason:\"some reason\"}, err: an error}", - moveResponse{executeResponse: state.ExecuteResponse{Replan: true, ReplanReason: "some reason"}, err: errors.New("an error")}, - }, - { - "when executeResponse.Replan is true & ReplanReason is not empty and error is nil", - "builtin.moveResponse{executeResponse: state.ExecuteResponse{Replan:true, ReplanReason:\"some reason\"}, err: }", - moveResponse{executeResponse: state.ExecuteResponse{Replan: true, ReplanReason: "some reason"}}, - }, - { - "when executeResponse.Replan is false & ReplanReason is empty and error is nil", - "builtin.moveResponse{executeResponse: state.ExecuteResponse{Replan:false, ReplanReason:\"\"}, err: }", - moveResponse{}, - }, - } - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - test.That(t, tc.moveResponse.String(), test.ShouldEqual, tc.expected) - }) - } -} - -func TestReplanResponseString(t *testing.T) { - type testCase struct { - description string - expected string - replanResponse replanResponse - } - testCases := []testCase{ - { - "when replan is true and reason is non empty and error is nil", - "builtin.replanResponse{executeResponse: state.ExecuteResponse{Replan:true, ReplanReason:\"some reason\"}, err: }", - replanResponse{executeResponse: state.ExecuteResponse{Replan: true, ReplanReason: "some reason"}}, - }, - { - "when replan is true and reason is non empty and error is not nil", - "builtin.replanResponse{executeResponse: state.ExecuteResponse{Replan:true, ReplanReason:\"some reason\"}, err: an error}", - replanResponse{executeResponse: state.ExecuteResponse{Replan: true, ReplanReason: "some reason"}, err: errors.New("an error")}, - }, - { - "when replan is false and error is nil", - "builtin.replanResponse{executeResponse: state.ExecuteResponse{Replan:false, ReplanReason:\"\"}, err: }", - replanResponse{}, - }, - { - "when replan is false and error is not nil", - "builtin.replanResponse{executeResponse: state.ExecuteResponse{Replan:false, ReplanReason:\"\"}, err: an error}", - replanResponse{err: errors.New("an error")}, - }, - } - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - test.That(t, tc.replanResponse.String(), test.ShouldEqual, tc.expected) - }) - } -} - -func TestMoveFailures(t *testing.T) { - var err error - ms, teardown := setupMotionServiceFromConfig(t, "../data/arm_gantry.json") - defer teardown() - ctx := context.Background() - t.Run("fail on not finding gripper", func(t *testing.T) { - grabPose := referenceframe.NewPoseInFrame("fakeCamera", spatialmath.NewPoseFromPoint(r3.Vector{X: 10.0, Y: 10.0, Z: 10.0})) - _, err = ms.Move(ctx, camera.Named("fake"), grabPose, nil, nil, nil) - test.That(t, err, test.ShouldNotBeNil) - }) - - t.Run("fail on disconnected supplemental frames in world state", func(t *testing.T) { - testPose := spatialmath.NewPose( - r3.Vector{X: 1., Y: 2., Z: 3.}, - &spatialmath.R4AA{Theta: math.Pi / 2, RX: 0., RY: 1., RZ: 0.}, - ) - transforms := []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame("noParent", testPose, "frame2", nil), - } - worldState, err := referenceframe.NewWorldState(nil, transforms) - test.That(t, err, test.ShouldBeNil) - poseInFrame := referenceframe.NewPoseInFrame("frame2", spatialmath.NewZeroPose()) - _, err = ms.Move(ctx, arm.Named("arm1"), poseInFrame, worldState, nil, nil) - test.That(t, err, test.ShouldBeError, referenceframe.NewParentFrameMissingError("frame2", "noParent")) - }) -} - -func TestMove(t *testing.T) { - var err error - ctx := context.Background() - - t.Run("succeeds when all frame info in config", func(t *testing.T) { - ms, teardown := setupMotionServiceFromConfig(t, "../data/moving_arm.json") - defer teardown() - grabPose := referenceframe.NewPoseInFrame("c", spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: -30, Z: -50})) - _, err = ms.Move(ctx, gripper.Named("pieceGripper"), grabPose, nil, nil, nil) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("succeeds when mobile component can be solved for destinations in own frame", func(t *testing.T) { - ms, teardown := setupMotionServiceFromConfig(t, "../data/moving_arm.json") - defer teardown() - grabPose := referenceframe.NewPoseInFrame("pieceArm", spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: -30, Z: -50})) - _, err = ms.Move(ctx, arm.Named("pieceArm"), grabPose, nil, nil, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("succeeds when immobile component can be solved for destinations in own frame", func(t *testing.T) { - ms, teardown := setupMotionServiceFromConfig(t, "../data/moving_arm.json") - defer teardown() - grabPose := referenceframe.NewPoseInFrame("pieceGripper", spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: -30, Z: -50})) - _, err = ms.Move(ctx, gripper.Named("pieceGripper"), grabPose, nil, nil, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("succeeds with supplemental info in world state", func(t *testing.T) { - ms, teardown := setupMotionServiceFromConfig(t, "../data/moving_arm.json") - defer teardown() - testPose := spatialmath.NewPose( - r3.Vector{X: 1., Y: 2., Z: 3.}, - &spatialmath.R4AA{Theta: math.Pi / 2, RX: 0., RY: 1., RZ: 0.}, - ) - - transforms := []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame(referenceframe.World, testPose, "testFrame2", nil), - referenceframe.NewLinkInFrame("pieceArm", testPose, "testFrame", nil), - } - - worldState, err := referenceframe.NewWorldState(nil, transforms) - test.That(t, err, test.ShouldBeNil) - grabPose := referenceframe.NewPoseInFrame("testFrame2", spatialmath.NewPoseFromPoint(r3.Vector{X: -20, Y: -130, Z: -40})) - _, err = ms.Move(context.Background(), gripper.Named("pieceGripper"), grabPose, worldState, nil, nil) - test.That(t, err, test.ShouldBeNil) - }) -} - -func TestMoveWithObstacles(t *testing.T) { - ms, teardown := setupMotionServiceFromConfig(t, "../data/moving_arm.json") - defer teardown() - - t.Run("check a movement that should not succeed due to obstacles", func(t *testing.T) { - testPose1 := spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 370}) - testPose2 := spatialmath.NewPoseFromPoint(r3.Vector{X: 300, Y: 300, Z: -3500}) - _ = testPose2 - grabPose := referenceframe.NewPoseInFrame("world", spatialmath.NewPoseFromPoint(r3.Vector{X: -600, Y: -400, Z: 460})) - obsMsgs := []*commonpb.GeometriesInFrame{ - { - ReferenceFrame: "world", - Geometries: []*commonpb.Geometry{ - { - Center: spatialmath.PoseToProtobuf(testPose2), - GeometryType: &commonpb.Geometry_Box{ - Box: &commonpb.RectangularPrism{DimsMm: &commonpb.Vector3{ - X: 20, - Y: 40, - Z: 40, - }}, - }, - }, - }, - }, - { - ReferenceFrame: "world", - Geometries: []*commonpb.Geometry{ - { - Center: spatialmath.PoseToProtobuf(testPose1), - GeometryType: &commonpb.Geometry_Box{ - Box: &commonpb.RectangularPrism{DimsMm: &commonpb.Vector3{ - X: 2000, - Y: 2000, - Z: 20, - }}, - }, - }, - }, - }, - } - worldState, err := referenceframe.WorldStateFromProtobuf(&commonpb.WorldState{Obstacles: obsMsgs}) - test.That(t, err, test.ShouldBeNil) - _, err = ms.Move(context.Background(), gripper.Named("pieceArm"), grabPose, worldState, nil, nil) - // This fails due to a large obstacle being in the way - test.That(t, err, test.ShouldNotBeNil) - }) -} - -func TestMoveOnMapAskewIMU(t *testing.T) { - t.Parallel() - extraPosOnly := map[string]interface{}{"smooth_iter": 5, "motion_profile": "position_only"} - t.Run("Askew but valid base should be able to plan", func(t *testing.T) { - t.Parallel() - logger := logging.NewTestLogger(t) - ctx := context.Background() - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 1, OY: 1, OZ: 1, Theta: 35} - askewOrientCorrected := &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -22.988} - // goal x-position of 1.32m is scaled to be in mm - goal1SLAMFrame := spatialmath.NewPose(r3.Vector{X: 1.32 * 1000, Y: 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 55}) - goal1BaseFrame := spatialmath.Compose(goal1SLAMFrame, motion.SLAMOrientationAdjustment) - - kb, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, spatialmath.NewPoseFromOrientation(askewOrient)) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: goal1SLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: extraPosOnly, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*15) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*15) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPIF, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - // We need to transform the endPos by the corrected orientation in order to properly place it, otherwise it will go off in +Z somewhere. - // In a real robot this will be taken care of by gravity. - correctedPose := spatialmath.NewPoseFromOrientation(askewOrientCorrected) - endPos := spatialmath.Compose(correctedPose, spatialmath.PoseBetween(spatialmath.NewPoseFromOrientation(askewOrient), endPIF.Pose())) - logger.Debug(spatialmath.PoseToProtobuf(endPos)) - logger.Debug(spatialmath.PoseToProtobuf(goal1BaseFrame)) - - test.That(t, spatialmath.PoseAlmostEqualEps(endPos, goal1BaseFrame, 10), test.ShouldBeTrue) - }) - t.Run("Upside down base should fail to plan", func(t *testing.T) { - t.Parallel() - ctx := context.Background() - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 1, OY: 1, OZ: -1, Theta: 55} - // goal x-position of 1.32m is scaled to be in mm - goal1SLAMFrame := spatialmath.NewPose(r3.Vector{X: 1.32 * 1000, Y: 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 55}) - - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, spatialmath.NewPoseFromOrientation(askewOrient)) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: goal1SLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: extraPosOnly, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*15) - defer timeoutFn() - _, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, "base appears to be upside down, check your movement sensor") - }) -} - -func TestPositionalReplanning(t *testing.T) { - t.Parallel() - ctx := context.Background() - - gpsPoint := geo.NewPoint(0, 0) - dst := geo.NewPoint(gpsPoint.Lat(), gpsPoint.Lng()+1e-5) - epsilonMM := 15. - motionCfg := &motion.MotionConfiguration{PositionPollingFreqHz: 100, ObstaclePollingFreqHz: 1, PlanDeviationMM: epsilonMM} - - type testCase struct { - name string - noise r3.Vector - expectedSuccess bool - expectedErr string - extra map[string]interface{} - } - - testCases := []testCase{ - { - name: "check we dont replan with a good sensor", - noise: r3.Vector{Y: epsilonMM - 0.1}, - expectedSuccess: true, - extra: map[string]interface{}{"smooth_iter": 5}, - }, - // TODO(RSDK-5634): this should be uncommented when this bug is fixed - // { - // // This also checks that `replan` is called under default conditions when "max_replans" is not set - // name: "check we fail to replan with a low cost factor", - // noise: r3.Vector{Y: epsilonMM + 0.1}, - // expectedErr: "unable to create a new plan within replanCostFactor from the original", - // expectedSuccess: false, - // extra: map[string]interface{}{"replan_cost_factor": 0.01, "smooth_iter": 5}, - // }, - { - name: "check we replan with a noisy sensor", - noise: r3.Vector{Y: epsilonMM + 0.1}, - expectedErr: fmt.Sprintf("exceeded maximum number of replans: %d: plan failed", 4), - expectedSuccess: false, - extra: map[string]interface{}{"replan_cost_factor": 10.0, "max_replans": 4, "smooth_iter": 5}, - }, - } - - testFn := func(t *testing.T, tc testCase) { - t.Helper() - injectedMovementSensor, _, kb, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, spatialmath.NewPoseFromPoint(tc.noise), 5) - defer ms.Close(ctx) - - req := motion.MoveOnGlobeReq{ - ComponentName: kb.Name(), - Destination: dst, - MovementSensorName: injectedMovementSensor.Name(), - MotionCfg: motionCfg, - Extra: tc.extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Minute*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - - if tc.expectedSuccess { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err.Error(), test.ShouldEqual, tc.expectedErr) - } - } - - for _, tc := range testCases { - c := tc // needed to workaround loop variable not being captured by func literals - t.Run(c.name, func(t *testing.T) { - t.Parallel() - testFn(t, c) - }) - } -} - -func TestObstacleReplanningGlobe(t *testing.T) { - t.Parallel() - ctx := context.Background() - - gpsOrigin := geo.NewPoint(0, 0) - dst := geo.NewPoint(gpsOrigin.Lat(), gpsOrigin.Lng()+1e-5) - epsilonMM := 15. - - type testCase struct { - name string - getPCfunc func(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) - expectedSuccess bool - expectedErr string - } - - obstacleDetectorSlice := []motion.ObstacleDetectorName{ - {VisionServiceName: vision.Named("injectedVisionSvc"), CameraName: camera.Named("injectedCamera")}, - } - - cfg := &motion.MotionConfiguration{ - PositionPollingFreqHz: 1, ObstaclePollingFreqHz: 100, PlanDeviationMM: epsilonMM, ObstacleDetectors: obstacleDetectorSlice, - } - - extra := map[string]interface{}{"max_replans": 0, "max_ik_solutions": 1, "smooth_iter": 1} - - // ~ i := 0 - j := 0 - - testCases := []testCase{ - { - name: "ensure no replan from discovered obstacles", - getPCfunc: func(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) { - if j == 0 { - j++ - return []*viz.Object{}, nil - } - obstaclePosition := spatialmath.NewPoseFromPoint(r3.Vector{X: -1000, Y: -1000, Z: 0}) - box, err := spatialmath.NewBox(obstaclePosition, r3.Vector{X: 10, Y: 10, Z: 10}, "test-case-2") - test.That(t, err, test.ShouldBeNil) - - detection, err := viz.NewObjectWithLabel(pointcloud.New(), "test-case-2-detection", box.ToProtobuf()) - test.That(t, err, test.ShouldBeNil) - - return []*viz.Object{detection}, nil - }, - expectedSuccess: true, - }, - // TODO(pl): This was disabled as part of course correction. It will need to be re-enabled once a method is developed to surface - // course-corrected plans from the kinematic base to the motion service. - // { - // name: "ensure replan due to obstacle collision", - // getPCfunc: func(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) { - // if i == 0 { - // i++ - // return []*viz.Object{}, nil - // } - // obstaclePosition := spatialmath.NewPoseFromPoint(r3.Vector{X: 300, Y: 0, Z: 0}) - // box, err := spatialmath.NewBox(obstaclePosition, r3.Vector{X: 20, Y: 20, Z: 10}, "test-case-1") - // test.That(t, err, test.ShouldBeNil) - - // detection, err := viz.NewObjectWithLabel(pointcloud.New(), "test-case-1-detection", box.ToProtobuf()) - // test.That(t, err, test.ShouldBeNil) - - // return []*viz.Object{detection}, nil - // }, - // expectedSuccess: false, - // expectedErr: fmt.Sprintf("exceeded maximum number of replans: %d: plan failed", 0), - // }, - } - - testFn := func(t *testing.T, tc testCase) { - t.Helper() - injectedMovementSensor, _, kb, ms := createMoveOnGlobeEnvironment( - ctx, - t, - gpsOrigin, - spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 0}), - 5000, - ) - defer ms.Close(ctx) - - srvc, ok := ms.(*builtIn).visionServices[cfg.ObstacleDetectors[0].VisionServiceName].(*inject.VisionService) - test.That(t, ok, test.ShouldBeTrue) - srvc.GetObjectPointCloudsFunc = tc.getPCfunc - - req := motion.MoveOnGlobeReq{ - ComponentName: kb.Name(), - Destination: dst, - MovementSensorName: injectedMovementSensor.Name(), - MotionCfg: cfg, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Minute*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - - if tc.expectedSuccess { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, tc.expectedErr) - } - } - - for _, tc := range testCases { - c := tc // needed to workaround loop variable not being captured by func literals - t.Run(c.name, func(t *testing.T) { - t.Parallel() - testFn(t, c) - }) - } -} - -func TestObstacleReplanningSlam(t *testing.T) { - cameraToBase := spatialmath.NewPose(r3.Vector{0, 0, 0}, &spatialmath.OrientationVectorDegrees{OY: 1, Theta: -90}) - cameraToBaseInv := spatialmath.PoseInverse(cameraToBase) - - ctx := context.Background() - origin := spatialmath.NewPose( - r3.Vector{X: -900, Y: 0, Z: 0}, - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -90}, - ) - - boxWrld, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 0}), - r3.Vector{X: 50, Y: 50, Z: 50}, "box-obstacle", - ) - test.That(t, err, test.ShouldBeNil) - - kb, ms := createMoveOnMapEnvironment( - ctx, t, - "pointcloud/cardboardOcto.pcd", - 50, origin, - ) - defer ms.Close(ctx) - - visSrvc, ok := ms.(*builtIn).visionServices[vision.Named("test-vision")].(*inject.VisionService) - test.That(t, ok, test.ShouldBeTrue) - i := 0 - visSrvc.GetObjectPointCloudsFunc = func(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) { - if i == 0 { - i++ - return []*viz.Object{}, nil - } - currentPif, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - relativeBox := boxWrld.Transform(spatialmath.PoseInverse(currentPif.Pose())).Transform(cameraToBaseInv) - detection, err := viz.NewObjectWithLabel(pointcloud.New(), "test-case-1-detection", relativeBox.ToProtobuf()) - test.That(t, err, test.ShouldBeNil) - - return []*viz.Object{detection}, nil - } - - obstacleDetectorSlice := []motion.ObstacleDetectorName{ - {VisionServiceName: vision.Named("test-vision"), CameraName: camera.Named("test-camera")}, - } - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 800, Y: 0, Z: 0}), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{ - PositionPollingFreqHz: 1, ObstaclePollingFreqHz: 100, PlanDeviationMM: 1, ObstacleDetectors: obstacleDetectorSlice, - }, - // TODO: add back "max_replans": 1 to extra - Extra: map[string]interface{}{"smooth_iter": 0}, - } - - executionID, err := ms.MoveOnMap(ctx, req) - test.That(t, err, test.ShouldBeNil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*15) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) -} - -func TestMultiplePieces(t *testing.T) { - var err error - ms, teardown := setupMotionServiceFromConfig(t, "../data/fake_tomato.json") - defer teardown() - grabPose := referenceframe.NewPoseInFrame("c", spatialmath.NewPoseFromPoint(r3.Vector{X: -0, Y: -30, Z: -50})) - _, err = ms.Move(context.Background(), gripper.Named("gr"), grabPose, nil, nil, nil) - test.That(t, err, test.ShouldBeNil) -} - -func TestGetPose(t *testing.T) { - var err error - ms, teardown := setupMotionServiceFromConfig(t, "../data/arm_gantry.json") - defer teardown() - - pose, err := ms.GetPose(context.Background(), arm.Named("gantry1"), "", nil, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Parent(), test.ShouldEqual, referenceframe.World) - test.That(t, pose.Pose().Point().X, test.ShouldAlmostEqual, 1.2) - test.That(t, pose.Pose().Point().Y, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Z, test.ShouldAlmostEqual, 0) - - pose, err = ms.GetPose(context.Background(), arm.Named("arm1"), "", nil, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Parent(), test.ShouldEqual, referenceframe.World) - test.That(t, pose.Pose().Point().X, test.ShouldAlmostEqual, 501.2) - test.That(t, pose.Pose().Point().Y, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Z, test.ShouldAlmostEqual, 300) - - pose, err = ms.GetPose(context.Background(), arm.Named("arm1"), "gantry1", nil, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Parent(), test.ShouldEqual, "gantry1") - test.That(t, pose.Pose().Point().X, test.ShouldAlmostEqual, 500) - test.That(t, pose.Pose().Point().Y, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Z, test.ShouldAlmostEqual, 300) - - pose, err = ms.GetPose(context.Background(), arm.Named("gantry1"), "gantry1", nil, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Parent(), test.ShouldEqual, "gantry1") - test.That(t, pose.Pose().Point().X, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Y, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Z, test.ShouldAlmostEqual, 0) - - pose, err = ms.GetPose(context.Background(), arm.Named("arm1"), "arm1", nil, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Parent(), test.ShouldEqual, "arm1") - test.That(t, pose.Pose().Point().X, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Y, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Z, test.ShouldAlmostEqual, 0) - - testPose := spatialmath.NewPoseFromOrientation(&spatialmath.R4AA{Theta: math.Pi / 2, RX: 0., RY: 1., RZ: 0.}) - transforms := []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame(referenceframe.World, testPose, "testFrame", nil), - referenceframe.NewLinkInFrame("testFrame", testPose, "testFrame2", nil), - } - - pose, err = ms.GetPose(context.Background(), arm.Named("arm1"), "testFrame2", transforms, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pose.Pose().Point().X, test.ShouldAlmostEqual, -501.2) - test.That(t, pose.Pose().Point().Y, test.ShouldAlmostEqual, 0) - test.That(t, pose.Pose().Point().Z, test.ShouldAlmostEqual, -300) - test.That(t, pose.Pose().Orientation().AxisAngles().RX, test.ShouldEqual, 0) - test.That(t, pose.Pose().Orientation().AxisAngles().RY, test.ShouldEqual, -1) - test.That(t, pose.Pose().Orientation().AxisAngles().RZ, test.ShouldEqual, 0) - test.That(t, pose.Pose().Orientation().AxisAngles().Theta, test.ShouldAlmostEqual, math.Pi) - - transforms = []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame("noParent", testPose, "testFrame", nil), - } - pose, err = ms.GetPose(context.Background(), arm.Named("arm1"), "testFrame", transforms, map[string]interface{}{}) - test.That(t, err, test.ShouldBeError, referenceframe.NewParentFrameMissingError("testFrame", "noParent")) - test.That(t, pose, test.ShouldBeNil) -} - -func TestStoppableMoveFunctions(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - failToReachGoalError := errors.New("failed to reach goal") - calledStopFunc := false - testIfStoppable := func(t *testing.T, success bool, err, expectedErr error) { - t.Helper() - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, success, test.ShouldBeFalse) - test.That(t, calledStopFunc, test.ShouldBeTrue) - } - extra := map[string]interface{}{"smooth_iter": 5} - - t.Run("successfully stop arms", func(t *testing.T) { - armName := "test-arm" - injectArmName := arm.Named(armName) - goal := referenceframe.NewPoseInFrame( - armName, - spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: -10, Z: -10}), - ) - - // Create an injected Arm - armCfg := resource.Config{ - Name: armName, - API: arm.API, - Model: resource.DefaultModelFamily.WithModel("ur5e"), - ConvertedAttributes: &armFake.Config{ - ArmModel: "ur5e", - }, - Frame: &referenceframe.LinkConfig{ - Parent: "world", - }, - } - - fakeArm, err := armFake.NewArm(ctx, nil, armCfg, logger) - test.That(t, err, test.ShouldBeNil) - - injectArm := &inject.Arm{ - Arm: fakeArm, - } - injectArm.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - calledStopFunc = true - return nil - } - injectArm.GoToInputsFunc = func(ctx context.Context, goal ...[]referenceframe.Input) error { - return failToReachGoalError - } - injectArm.ModelFrameFunc = func() referenceframe.Model { - model, _ := ur.MakeModelFrame("ur5e") - return model - } - injectArm.MoveToPositionFunc = func(ctx context.Context, to spatialmath.Pose, extra map[string]interface{}) error { - return failToReachGoalError - } - - // create arm link - armLink := referenceframe.NewLinkInFrame( - referenceframe.World, - spatialmath.NewZeroPose(), - armName, - nil, - ) - - // Create a motion service - fsParts := []*referenceframe.FrameSystemPart{ - { - FrameConfig: armLink, - ModelFrame: injectArm.ModelFrameFunc(), - }, - } - deps := resource.Dependencies{ - injectArmName: injectArm, - } - - _, err = createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - - conf := resource.Config{ConvertedAttributes: &Config{}} - ms, err := NewBuiltIn(ctx, deps, conf, logger) - test.That(t, err, test.ShouldBeNil) - defer ms.Close(context.Background()) - - t.Run("stop during Move(...) call", func(t *testing.T) { - calledStopFunc = false - success, err := ms.Move(ctx, injectArmName, goal, nil, nil, extra) - testIfStoppable(t, success, err, failToReachGoalError) - }) - }) - - t.Run("successfully stop kinematic bases", func(t *testing.T) { - // Create an injected Base - baseName := "test-base" - - geometry, err := (&spatialmath.GeometryConfig{R: 20}).ParseConfig() - test.That(t, err, test.ShouldBeNil) - - injectBase := inject.NewBase(baseName) - injectBase.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { - return []spatialmath.Geometry{geometry}, nil - } - injectBase.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{ - TurningRadiusMeters: 0, - WidthMeters: 600 * 0.001, - }, nil - } - injectBase.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { - calledStopFunc = true - return nil - } - injectBase.SpinFunc = func(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - return failToReachGoalError - } - injectBase.MoveStraightFunc = func(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - return failToReachGoalError - } - injectBase.SetVelocityFunc = func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - return failToReachGoalError - } - - // Create a base link - baseLink := createBaseLink(t) - - t.Run("stop during MoveOnGlobe(...) call", func(t *testing.T) { - calledStopFunc = false - gpsPoint := geo.NewPoint(-70, 40) - - // Create an injected MovementSensor - movementSensorName := "test-gps" - injectMovementSensor := createInjectedMovementSensor(movementSensorName, gpsPoint) - - // Create a MovementSensor link - movementSensorLink := referenceframe.NewLinkInFrame( - baseLink.Name(), - spatialmath.NewPoseFromPoint(r3.Vector{X: -10, Y: 0, Z: 0}), - movementSensorName, - nil, - ) - - // Create a motion service - fsParts := []*referenceframe.FrameSystemPart{ - {FrameConfig: movementSensorLink}, - {FrameConfig: baseLink}, - } - deps := resource.Dependencies{ - injectBase.Name(): injectBase, - injectMovementSensor.Name(): injectMovementSensor, - } - - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - - conf := resource.Config{ConvertedAttributes: &Config{}} - ms, err := NewBuiltIn(ctx, deps, conf, logger) - test.That(t, err, test.ShouldBeNil) - defer ms.Close(context.Background()) - - ms.(*builtIn).fsService = fsSvc - - goal := geo.NewPoint(gpsPoint.Lat()+1e-4, gpsPoint.Lng()+1e-4) - motionCfg := motion.MotionConfiguration{ - PlanDeviationMM: 10000, - LinearMPerSec: 10, - PositionPollingFreqHz: 4, - ObstaclePollingFreqHz: 1, - } - - req := motion.MoveOnGlobeReq{ - ComponentName: injectBase.Name(), - Destination: goal, - MovementSensorName: injectMovementSensor.Name(), - MotionCfg: &motionCfg, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - - expectedErr := errors.Wrap(errors.New("plan failed"), failToReachGoalError.Error()) - testIfStoppable(t, false, err, expectedErr) - }) - - t.Run("stop during MoveOnMap(...) call", func(t *testing.T) { - calledStopFunc = false - slamName := "test-slam" - - // Create an injected SLAM - injectSlam := createInjectedSlam(slamName, "pointcloud/octagonspace.pcd", nil) - - // Create a motion service - deps := resource.Dependencies{ - injectBase.Name(): injectBase, - injectSlam.Name(): injectSlam, - } - fsParts := []*referenceframe.FrameSystemPart{ - {FrameConfig: baseLink}, - } - - ms, err := NewBuiltIn( - ctx, - deps, - resource.Config{ConvertedAttributes: &Config{}}, - logger, - ) - test.That(t, err, test.ShouldBeNil) - defer ms.Close(context.Background()) - - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - ms.(*builtIn).fsService = fsSvc - - goal := spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 500}) - req := motion.MoveOnMapReq{ - ComponentName: injectBase.Name(), - Destination: goal, - SlamName: injectSlam.Name(), - MotionCfg: &motion.MotionConfiguration{ - PlanDeviationMM: 0.2, - }, - Extra: extra, - } - - executionID, err := ms.MoveOnMap(ctx, req) - test.That(t, err, test.ShouldBeNil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - - expectedErr := errors.Wrap(errors.New("plan failed"), failToReachGoalError.Error()) - testIfStoppable(t, false, err, expectedErr) - }) - - t.Run("stop during MoveOnMap(...) call", func(t *testing.T) { - calledStopFunc = false - slamName := "test-slam" - - // Create an injected SLAM - injectSlam := createInjectedSlam(slamName, "pointcloud/octagonspace.pcd", nil) - - // Create a motion service - deps := resource.Dependencies{ - injectBase.Name(): injectBase, - injectSlam.Name(): injectSlam, - } - fsParts := []*referenceframe.FrameSystemPart{ - {FrameConfig: baseLink}, - } - - ms, err := NewBuiltIn( - ctx, - deps, - resource.Config{ConvertedAttributes: &Config{}}, - logger, - ) - test.That(t, err, test.ShouldBeNil) - defer ms.Close(context.Background()) - - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - ms.(*builtIn).fsService = fsSvc - - req := motion.MoveOnMapReq{ - ComponentName: injectBase.Name(), - Destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 500}), - SlamName: injectSlam.Name(), - MotionCfg: &motion.MotionConfiguration{ - PlanDeviationMM: 1, - }, - Extra: extra, - } - - executionID, err := ms.MoveOnMap(ctx, req) - test.That(t, err, test.ShouldBeNil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - - expectedErr := errors.Wrap(errors.New("plan failed"), failToReachGoalError.Error()) - testIfStoppable(t, false, err, expectedErr) - }) - }) -} - -func TestMoveOnGlobe(t *testing.T) { - ctx := context.Background() - // Near antarctica 🐧 - gpsPoint := geo.NewPoint(-70, 40) - dst := geo.NewPoint(gpsPoint.Lat(), gpsPoint.Lng()+7e-5) - expectedDst := r3.Vector{X: 2662.16, Y: 0, Z: 0} // Relative pose to the starting point of the base; facing north, Y = forwards - epsilonMM := 15. - // create motion config - extra := map[string]interface{}{ - "motion_profile": "position_only", - "timeout": 5., - "smooth_iter": 5., - } - - t.Run("Changes to executions show up in PlanHistory", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Destination: dst, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotBeEmpty) - - // returns the execution just created in the history - ph, err := ms.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph), test.ShouldEqual, 1) - test.That(t, ph[0].Plan.ExecutionID, test.ShouldResemble, executionID) - test.That(t, len(ph[0].StatusHistory), test.ShouldEqual, 1) - test.That(t, ph[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, len(ph[0].Plan.Path()), test.ShouldNotEqual, 0) - - err = ms.StopPlan(ctx, motion.StopPlanReq{ComponentName: fakeBase.Name()}) - test.That(t, err, test.ShouldBeNil) - - ph2, err := ms.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph2), test.ShouldEqual, 1) - test.That(t, ph2[0].Plan.ExecutionID, test.ShouldResemble, executionID) - test.That(t, len(ph2[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, ph2[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateStopped) - test.That(t, ph2[0].StatusHistory[1].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, len(ph2[0].Plan.Path()), test.ShouldNotEqual, 0) - - // Proves that calling StopPlan after the plan has reached a terminal state is idempotent - err = ms.StopPlan(ctx, motion.StopPlanReq{ComponentName: fakeBase.Name()}) - test.That(t, err, test.ShouldBeNil) - ph3, err := ms.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ph3, test.ShouldResemble, ph2) - }) - - t.Run("is able to reach a nearby geo point with empty values", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Destination: dst, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("is able to reach a nearby geo point with a requested NaN heading", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: math.NaN(), - Destination: dst, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("is able to reach a nearby geo point with a requested positive heading", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 10000000, - Destination: dst, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("is able to reach a nearby geo point with a requested negative heading", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: -10000000, - Destination: dst, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("is able to reach a nearby geo point when the motion configuration is empty", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{}, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("is able to reach a nearby geo point when the motion configuration nil", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("ensure success to a nearby geo point", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - motionCfg := &motion.MotionConfiguration{PositionPollingFreqHz: 4, ObstaclePollingFreqHz: 1, PlanDeviationMM: epsilonMM} - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - Destination: dst, - MovementSensorName: injectedMovementSensor.Name(), - Obstacles: []*spatialmath.GeoObstacle{}, - MotionCfg: motionCfg, - Extra: extra, - } - planExecutor, err := ms.(*builtIn).newMoveOnGlobeRequest(ctx, req, nil, 0) - test.That(t, err, test.ShouldBeNil) - - mr, ok := planExecutor.(*moveRequest) - test.That(t, ok, test.ShouldBeTrue) - - test.That(t, mr.planRequest.Goal.Pose().Point().X, test.ShouldAlmostEqual, expectedDst.X, epsilonMM) - test.That(t, mr.planRequest.Goal.Pose().Point().Y, test.ShouldAlmostEqual, expectedDst.Y, epsilonMM) - - planResp, err := mr.Plan(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planResp.Path()), test.ShouldBeGreaterThan, 2) - - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("go around an obstacle", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - motionCfg := &motion.MotionConfiguration{PositionPollingFreqHz: 4, ObstaclePollingFreqHz: 1, PlanDeviationMM: epsilonMM} - - boxPose := spatialmath.NewPoseFromPoint(r3.Vector{X: 50, Y: 0, Z: 0}) - boxDims := r3.Vector{X: 5, Y: 50, Z: 10} - geometries, err := spatialmath.NewBox(boxPose, boxDims, "wall") - test.That(t, err, test.ShouldBeNil) - geoObstacle := spatialmath.NewGeoObstacle(gpsPoint, []spatialmath.Geometry{geometries}) - startPose, err := fakeBase.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - Destination: dst, - MovementSensorName: injectedMovementSensor.Name(), - Obstacles: []*spatialmath.GeoObstacle{geoObstacle}, - MotionCfg: motionCfg, - Extra: extra, - } - mr, err := ms.(*builtIn).newMoveOnGlobeRequest(ctx, req, nil, 0) - test.That(t, err, test.ShouldBeNil) - planResp, err := mr.Plan(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planResp.Path()), test.ShouldBeGreaterThan, 2) - - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPose, err := fakeBase.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - movedPose := spatialmath.PoseBetween(startPose.Pose(), endPose.Pose()) - test.That(t, movedPose.Point().X, test.ShouldAlmostEqual, expectedDst.X, epsilonMM) - test.That(t, movedPose.Point().Y, test.ShouldAlmostEqual, expectedDst.Y, epsilonMM) - }) - - t.Run("fail because of obstacle", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - - // Construct a set of obstacles that entirely enclose the goal point - boxPose := spatialmath.NewPoseFromPoint(r3.Vector{X: 250, Y: 0, Z: 0}) - boxDims := r3.Vector{X: 20, Y: 6660, Z: 10} - geometry1, err := spatialmath.NewBox(boxPose, boxDims, "wall1") - test.That(t, err, test.ShouldBeNil) - boxPose = spatialmath.NewPoseFromPoint(r3.Vector{X: 5000, Y: 0, Z: 0}) - boxDims = r3.Vector{X: 20, Y: 6660, Z: 10} - geometry2, err := spatialmath.NewBox(boxPose, boxDims, "wall2") - test.That(t, err, test.ShouldBeNil) - boxPose = spatialmath.NewPoseFromPoint(r3.Vector{X: 2500, Y: 2500, Z: 0}) - boxDims = r3.Vector{X: 6660, Y: 20, Z: 10} - geometry3, err := spatialmath.NewBox(boxPose, boxDims, "wall3") - test.That(t, err, test.ShouldBeNil) - boxPose = spatialmath.NewPoseFromPoint(r3.Vector{X: 2500, Y: -2500, Z: 0}) - boxDims = r3.Vector{X: 6660, Y: 20, Z: 10} - geometry4, err := spatialmath.NewBox(boxPose, boxDims, "wall4") - test.That(t, err, test.ShouldBeNil) - geoObstacle := spatialmath.NewGeoObstacle(gpsPoint, []spatialmath.Geometry{geometry1, geometry2, geometry3, geometry4}) - - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - Destination: dst, - MovementSensorName: injectedMovementSensor.Name(), - Obstacles: []*spatialmath.GeoObstacle{geoObstacle}, - MotionCfg: &motion.MotionConfiguration{}, - Extra: extra, - } - moveRequest, err := ms.(*builtIn).newMoveOnGlobeRequest(ctx, req, nil, 0) - test.That(t, err, test.ShouldBeNil) - planResp, err := moveRequest.Plan(ctx) - test.That(t, err, test.ShouldBeError) - test.That(t, planResp, test.ShouldBeNil) - }) - - t.Run("check offset constructed correctly", func(t *testing.T) { - _, fsSvc, _, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - baseOrigin := referenceframe.NewPoseInFrame("test-base", spatialmath.NewZeroPose()) - movementSensorToBase, err := fsSvc.TransformPose(ctx, baseOrigin, "test-gps", nil) - if err != nil { - movementSensorToBase = baseOrigin - } - test.That(t, movementSensorToBase.Pose().Point(), test.ShouldResemble, r3.Vector{X: 10, Y: 0, Z: 0}) - }) -} - -func TestMoveOnMapStaticObs(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - extra := map[string]interface{}{ - "motion_profile": "position_only", - "timeout": 5., - "smooth_iter": 10., - } - - baseName := "test-base" - slamName := "test-slam" - - // Create an injected Base - geometry, err := (&spatialmath.GeometryConfig{R: 30}).ParseConfig() - test.That(t, err, test.ShouldBeNil) - - injectBase := inject.NewBase(baseName) - injectBase.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { - return []spatialmath.Geometry{geometry}, nil - } - injectBase.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - return base.Properties{TurningRadiusMeters: 0, WidthMeters: 0.6}, nil - } - - // Create a base link - baseLink := createBaseLink(t) - - // Create an injected SLAM - injectSlam := createInjectedSlam(slamName, "pointcloud/octagonspace.pcd", nil) - injectSlam.PositionFunc = func(ctx context.Context) (spatialmath.Pose, string, error) { - return spatialmath.NewPose( - r3.Vector{X: 0.58772e3, Y: -0.80826e3, Z: 0}, - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 90}, - ), "", nil - } - - // Create a motion service - deps := resource.Dependencies{injectBase.Name(): injectBase, injectSlam.Name(): injectSlam} - fsParts := []*referenceframe.FrameSystemPart{{FrameConfig: baseLink}} - - ms, err := NewBuiltIn(ctx, deps, resource.Config{ConvertedAttributes: &Config{}}, logger) - test.That(t, err, test.ShouldBeNil) - defer ms.Close(context.Background()) - - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - ms.(*builtIn).fsService = fsSvc - - goal := spatialmath.NewPoseFromPoint(r3.Vector{X: 0.6556e3, Y: 0.64152e3}) - - req := motion.MoveOnMapReq{ - ComponentName: injectBase.Name(), - Destination: goal, - SlamName: injectSlam.Name(), - MotionCfg: &motion.MotionConfiguration{PlanDeviationMM: 0.01}, - Extra: extra, - } - - t.Run("one obstacle", func(t *testing.T) { - // WTS: static obstacles are obeyed at plan time. - - // We place an obstacle on the right side of the robot to force our motion planner to return a path - // which veers to the left. We then place an obstacle to the left of the robot and project the - // robot's position across the path. By showing that we have a collision on the path with an - // obstacle on the left we prove that our path does not collide with the original obstacle - // placed on the right. - obstacleLeft, err := spatialmath.NewBox( - spatialmath.NewPose(r3.Vector{X: 0.22981e3, Y: -0.38875e3, Z: 0}, - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 45}), - r3.Vector{X: 900, Y: 10, Z: 10}, - "obstacleLeft", - ) - test.That(t, err, test.ShouldBeNil) - obstacleRight, err := spatialmath.NewBox( - spatialmath.NewPose(r3.Vector{0.89627e3, -0.37192e3, 0}, - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -45}), - r3.Vector{900, 10, 10}, - "obstacleRight", - ) - test.That(t, err, test.ShouldBeNil) - - req.Obstacles = []spatialmath.Geometry{obstacleRight} - - // construct move request - planExecutor, err := ms.(*builtIn).newMoveOnMapRequest(ctx, req, nil, 0) - test.That(t, err, test.ShouldBeNil) - mr, ok := planExecutor.(*moveRequest) - test.That(t, ok, test.ShouldBeTrue) - - // construct plan - plan, err := mr.Plan(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(plan.Path()), test.ShouldBeGreaterThan, 2) - - // place obstacle in opposte position and show that the generate path - // collides with obstacleLeft - - wrldSt, err := referenceframe.NewWorldState( - []*referenceframe.GeometriesInFrame{ - referenceframe.NewGeometriesInFrame( - referenceframe.World, - []spatialmath.Geometry{obstacleLeft}, - ), - }, nil, - ) - test.That(t, err, test.ShouldBeNil) - - err = motionplan.CheckPlan( - mr.planRequest.Frame, - plan, - 1, - wrldSt, - mr.planRequest.FrameSystem, - spatialmath.NewPose( - r3.Vector{X: 0.58772e3, Y: -0.80826e3, Z: 0}, - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 0}, - ), - referenceframe.StartPositions(mr.planRequest.FrameSystem), - spatialmath.NewZeroPose(), - lookAheadDistanceMM, - logger, - ) - test.That(t, err, test.ShouldNotBeNil) - }) - - t.Run("fail due to obstacles enclosing goals", func(t *testing.T) { - // define static obstacles - obstacleTop, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{X: 0.64603e3, Y: 0.77151e3, Z: 0}), - r3.Vector{X: 400, Y: 10, Z: 10}, - "obstacleTop", - ) - test.That(t, err, test.ShouldBeNil) - - obstacleBottom, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{X: 0.64603e3, Y: 0.42479e3, Z: 0}), - r3.Vector{X: 400, Y: 10, Z: 10}, - "obstacleBottom", - ) - test.That(t, err, test.ShouldBeNil) - - obstacleLeft, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{X: 0.47525e3, Y: 0.65091e3, Z: 0}), - r3.Vector{X: 10, Y: 400, Z: 10}, - "obstacleLeft", - ) - test.That(t, err, test.ShouldBeNil) - - obstacleRight, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{X: 0.82183e3, Y: 0.64589e3, Z: 0}), - r3.Vector{X: 10, Y: 400, Z: 10}, - "obstacleRight", - ) - test.That(t, err, test.ShouldBeNil) - - req.Obstacles = []spatialmath.Geometry{obstacleTop, obstacleBottom, obstacleLeft, obstacleRight} - - // construct move request - planExecutor, err := ms.(*builtIn).newMoveOnMapRequest(ctx, req, nil, 0) - test.That(t, err, test.ShouldBeNil) - mr, ok := planExecutor.(*moveRequest) - test.That(t, ok, test.ShouldBeTrue) - - // construct plan - _, err = mr.Plan(ctx) - test.That(t, err, test.ShouldBeError, errors.New("context deadline exceeded")) - }) -} - -func TestMoveOnMap(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - t.Run("Long distance", func(t *testing.T) { - if runtime.GOARCH == "arm" { - t.Skip("skipping on 32-bit ARM, large maps use too much memory") - } - extra := map[string]interface{}{"smooth_iter": 0, "motion_profile": "position_only"} - // goal position is scaled to be in mm - goalInBaseFrame := spatialmath.NewPoseFromPoint(r3.Vector{X: -32.508 * 1000, Y: -2.092 * 1000}) - goalInSLAMFrame := spatialmath.PoseBetweenInverse(motion.SLAMOrientationAdjustment, goalInBaseFrame) - - kb, ms := createMoveOnMapEnvironment( - ctx, - t, - "slam/example_cartographer_outputs/viam-office-02-22-3/pointcloud/pointcloud_4.pcd", - 110, - spatialmath.NewPoseFromPoint(r3.Vector{0, -1600, 0}), - ) - defer ms.Close(ctx) - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: goalInSLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: extra, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*90) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*35) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPos, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, spatialmath.PoseAlmostCoincidentEps(endPos.Pose(), goalInBaseFrame, 15), test.ShouldBeTrue) - }) - - t.Run("Plans", func(t *testing.T) { - // goal x-position of 1.32m is scaled to be in mm - // Orientation theta should be at least 3 degrees away from an integer multiple of 22.5 to ensure the position-only test functions. - goalInBaseFrame := spatialmath.NewPose(r3.Vector{X: 1.32 * 1000, Y: 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 33}) - goalInSLAMFrame := spatialmath.PoseBetweenInverse(motion.SLAMOrientationAdjustment, goalInBaseFrame) - extra := map[string]interface{}{"smooth_iter": 0} - extraPosOnly := map[string]interface{}{"smooth_iter": 5, "motion_profile": "position_only"} - - t.Run("ensure success of movement around obstacle", func(t *testing.T) { - kb, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: -50})) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: goalInSLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: extra, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPos, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, spatialmath.PoseAlmostCoincidentEps(endPos.Pose(), goalInBaseFrame, 15), test.ShouldBeTrue) - }) - t.Run("check that straight line path executes", func(t *testing.T) { - kb, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - easyGoalInBaseFrame := spatialmath.NewPoseFromPoint(r3.Vector{X: 0.277 * 1000, Y: 0.593 * 1000}) - easyGoalInSLAMFrame := spatialmath.PoseBetweenInverse(motion.SLAMOrientationAdjustment, easyGoalInBaseFrame) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: easyGoalInSLAMFrame, - MotionCfg: &motion.MotionConfiguration{ - PlanDeviationMM: 1, - }, - SlamName: slam.Named("test_slam"), - Extra: extra, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPos, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, spatialmath.PoseAlmostEqualEps(endPos.Pose(), easyGoalInBaseFrame, 10), test.ShouldBeTrue) - }) - - t.Run("check that position-only mode executes", func(t *testing.T) { - kb, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: goalInSLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: extraPosOnly, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPos, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, spatialmath.PoseAlmostCoincidentEps(endPos.Pose(), goalInBaseFrame, 15), test.ShouldBeTrue) - // Position only mode should not yield the goal orientation. - test.That(t, spatialmath.OrientationAlmostEqualEps( - endPos.Pose().Orientation(), - goalInBaseFrame.Orientation(), - 0.05), test.ShouldBeFalse) - }) - - t.Run("should fail due to map collision", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: -500})) - defer ms.Close(ctx) - easyGoalInBaseFrame := spatialmath.NewPoseFromPoint(r3.Vector{X: 0.277 * 1000, Y: 0.593 * 1000}) - easyGoalInSLAMFrame := spatialmath.PoseBetweenInverse(motion.SLAMOrientationAdjustment, easyGoalInBaseFrame) - executionID, err := ms.MoveOnMap( - context.Background(), - motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: easyGoalInSLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: extra, - }, - ) - test.That(t, err, test.ShouldNotBeNil) - logger.Debug(err.Error()) - test.That(t, strings.Contains(err.Error(), "starting collision between SLAM map and "), test.ShouldBeTrue) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - }) - - t.Run("Subsequent", func(t *testing.T) { - // goal x-position of 1.32m is scaled to be in mm - goal1SLAMFrame := spatialmath.NewPose(r3.Vector{X: 1.32 * 1000, Y: 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 55}) - goal1BaseFrame := spatialmath.Compose(goal1SLAMFrame, motion.SLAMOrientationAdjustment) - goal2SLAMFrame := spatialmath.NewPose(r3.Vector{X: 277, Y: 593}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 150}) - goal2BaseFrame := spatialmath.Compose(goal2SLAMFrame, motion.SLAMOrientationAdjustment) - - kb, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: goal1SLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: map[string]interface{}{"smooth_iter": 5}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPos, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - logger.Debug(spatialmath.PoseToProtobuf(endPos.Pose())) - test.That(t, spatialmath.PoseAlmostEqualEps(endPos.Pose(), goal1BaseFrame, 10), test.ShouldBeTrue) - - // Now, we try to go to the second goal. Since the `CurrentPosition` of our base is at `goal1`, the pose that motion solves for and - // logs should be {x:-1043 y:593} - req = motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: goal2SLAMFrame, - SlamName: slam.Named("test_slam"), - Extra: map[string]interface{}{"smooth_iter": 5}, - } - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err = ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - - timeoutCtx, timeoutFn = context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - err = motion.PollHistoryUntilSuccessOrError(timeoutCtx, ms, time.Millisecond*5, motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }) - test.That(t, err, test.ShouldBeNil) - - endPos, err = kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - logger.Debug(spatialmath.PoseToProtobuf(endPos.Pose())) - test.That(t, spatialmath.PoseAlmostEqualEps(endPos.Pose(), goal2BaseFrame, 5), test.ShouldBeTrue) - - plans, err := ms.PlanHistory(ctx, motion.PlanHistoryReq{ - ComponentName: base.Named("test-base"), - LastPlanOnly: false, - ExecutionID: executionID, - }) - test.That(t, err, test.ShouldBeNil) - - goalPose1 := plans[0].Plan.Path()[0]["test-base"].Pose() - goalPose2 := spatialmath.PoseBetween( - plans[0].Plan.Path()[0]["test-base"].Pose(), - plans[0].Plan.Path()[len(plans[0].Plan.Path())-1]["test-base"].Pose(), - ) - - // We don't actually surface the internal motion planning goal; we report to the user in terms of what the user provided us. - // Thus, we use PlanHistory to get the plan steps of the latest plan. - // The zeroth index of the plan steps is the relative position of goal1 and the pose inverse between the first and last value of - // plan steps gives us the relative pose we solved for goal2. - test.That(t, spatialmath.PoseAlmostEqualEps(goalPose1, goal1BaseFrame, 10), test.ShouldBeTrue) - - // This is the important test. - test.That(t, spatialmath.PoseAlmostEqualEps(goalPose2, spatialmath.PoseBetween(goal1BaseFrame, goal2BaseFrame), 10), test.ShouldBeTrue) - }) - - t.Run("Timeout", func(t *testing.T) { - cfg, err := config.Read(ctx, "../data/real_wheeled_base.json", logger) - test.That(t, err, test.ShouldBeNil) - myRobot, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, myRobot.Close(context.Background()), test.ShouldBeNil) - }() - - injectSlam := createInjectedSlam("test_slam", "pointcloud/octagonspace.pcd", nil) - - realBase, err := base.FromRobot(myRobot, "test-base") - test.That(t, err, test.ShouldBeNil) - - deps := resource.Dependencies{ - injectSlam.Name(): injectSlam, - realBase.Name(): realBase, - } - fsParts := []*referenceframe.FrameSystemPart{ - {FrameConfig: createBaseLink(t)}, - } - - conf := resource.Config{ConvertedAttributes: &Config{}} - ms, err := NewBuiltIn(ctx, deps, conf, logger) - test.That(t, err, test.ShouldBeNil) - defer ms.Close(context.Background()) - - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - ms.(*builtIn).fsService = fsSvc - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 1001, Y: 1001}), - SlamName: slam.Named("test_slam"), - Extra: map[string]interface{}{"timeout": 0.01}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Changes to executions show up in PlanHistory", func(t *testing.T) { - kb, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - easyGoalInBaseFrame := spatialmath.NewPoseFromPoint(r3.Vector{X: 0.277 * 1000, Y: 0.593 * 1000}) - easyGoalInSLAMFrame := spatialmath.PoseBetweenInverse(motion.SLAMOrientationAdjustment, easyGoalInBaseFrame) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: easyGoalInSLAMFrame, - MotionCfg: &motion.MotionConfiguration{ - PlanDeviationMM: 1, - }, - SlamName: slam.Named("test_slam"), - Extra: map[string]interface{}{"smooth_iter": 0}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotBeEmpty) - - // returns the execution just created in the history - ph, err := ms.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph), test.ShouldEqual, 1) - test.That(t, ph[0].Plan.ExecutionID, test.ShouldResemble, executionID) - test.That(t, len(ph[0].StatusHistory), test.ShouldEqual, 1) - test.That(t, ph[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, len(ph[0].Plan.Path()), test.ShouldNotEqual, 0) - - err = ms.StopPlan(ctx, motion.StopPlanReq{ComponentName: kb.Name()}) - test.That(t, err, test.ShouldBeNil) - - ph2, err := ms.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph2), test.ShouldEqual, 1) - test.That(t, ph2[0].Plan.ExecutionID, test.ShouldResemble, executionID) - test.That(t, len(ph2[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, ph2[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateStopped) - test.That(t, ph2[0].StatusHistory[1].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, len(ph2[0].Plan.Path()), test.ShouldNotEqual, 0) - - // Proves that calling StopPlan after the plan has reached a terminal state is idempotent - err = ms.StopPlan(ctx, motion.StopPlanReq{ComponentName: kb.Name()}) - test.That(t, err, test.ShouldBeNil) - ph3, err := ms.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ph3, test.ShouldResemble, ph2) - }) - - t.Run("returns error when within plan dev m of goal with position_only", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"motion_profile": "position_only"}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeError, motion.ErrGoalWithinPlanDeviation) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("pass when within plan dev m of goal without position_only due to theta difference in goal", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotBeEmpty) - }) -} - -func TestMoveCallInputs(t *testing.T) { - t.Parallel() - ctx := context.Background() - - t.Run("MoveOnMap", func(t *testing.T) { - t.Parallel() - t.Run("Returns error when called with an unknown component", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("non existent base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("Resource missing from dependencies. Resource: rdk:component:base/non existent base")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns error when destination is nil", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - SlamName: slam.Named("test_slam"), - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("destination cannot be nil")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error if the base provided is not a base", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: slam.Named("test_slam"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("Resource missing from dependencies. Resource: rdk:service:slam/test_slam")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error if the slamName provided is not SLAM", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: slam.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test-base"), - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("Resource missing from dependencies. Resource: rdk:service:slam/test-base")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when motion configuration has a negative PlanDeviationMM", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{PlanDeviationMM: -1}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("PlanDeviationMM may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when motion configuration has a NaN PlanDeviationMM", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{PlanDeviationMM: math.NaN()}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("PlanDeviationMM may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when the motion configuration has a negative ObstaclePollingFreqHz", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{ObstaclePollingFreqHz: -1}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("ObstaclePollingFreqHz may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when the motion configuration has a NaN ObstaclePollingFreqHz", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{ObstaclePollingFreqHz: math.NaN()}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("ObstaclePollingFreqHz may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when the motion configuration has a negative PositionPollingFreqHz", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{PositionPollingFreqHz: -1}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("PositionPollingFreqHz may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when the motion configuration has a NaN PositionPollingFreqHz", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{PositionPollingFreqHz: math.NaN()}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("PositionPollingFreqHz may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when motion configuration has a negative AngularDegsPerSec", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{AngularDegsPerSec: -1}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("AngularDegsPerSec may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when motion configuration has a NaN AngularDegsPerSec", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{AngularDegsPerSec: math.NaN()}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("AngularDegsPerSec may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when motion configuration has a negative LinearMPerSec", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{LinearMPerSec: -1}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("LinearMPerSec may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("Returns an error when motion configuration has a NaN LinearMPerSec", func(t *testing.T) { - t.Parallel() - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{LinearMPerSec: math.NaN()}, - } - - executionID, err := ms.(*builtIn).MoveOnMap(context.Background(), req) - test.That(t, err, test.ShouldBeError, errors.New("LinearMPerSec may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("collision_buffer_mm validtations", func(t *testing.T) { - t.Run("fail when collision_buffer_mm is not a float", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": "not a float"}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeError, errors.New("could not interpret collision_buffer_mm field as float64")) - test.That(t, executionID, test.ShouldNotBeEmpty) - }) - - t.Run("fail when collision_buffer_mm is negative", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": -1.}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeError, errors.New("collision_buffer_mm can't be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("fail when collisions are predicted within the collision buffer", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": 200.}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, strings.Contains(err.Error(), "starting collision between SLAM map and "), test.ShouldBeTrue) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("pass when collision_buffer_mm is a small positive float", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": 1e-5}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("pass when collision_buffer_mm is a positive float", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": 0.1}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("pass when extra is empty", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("passes validations when extra is nil", func(t *testing.T) { - _, ms := createMoveOnMapEnvironment(ctx, t, "pointcloud/octagonspace.pcd", 40, nil) - defer ms.Close(ctx) - - req := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewZeroPose(), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{}, - } - - timeoutCtx, timeoutFn := context.WithTimeout(ctx, time.Second*5) - defer timeoutFn() - executionID, err := ms.(*builtIn).MoveOnMap(timeoutCtx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - }) - }) - - t.Run("MoveOnGlobe", func(t *testing.T) { - t.Parallel() - // Near antarctica 🐧 - gpsPoint := geo.NewPoint(-70, 40) - dst := geo.NewPoint(gpsPoint.Lat(), gpsPoint.Lng()+1e-4) - // create motion config - extra := map[string]interface{}{ - "motion_profile": "position_only", - "timeout": 5., - "smooth_iter": 5., - } - t.Run("returns error when called with an unknown component", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, _, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: base.Named("non existent base"), - MovementSensorName: injectedMovementSensor.Name(), - Destination: geo.NewPoint(0, 0), - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:component:base/non existent base\" not found")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns error when called with an unknown movement sensor", func(t *testing.T) { - t.Parallel() - _, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: movementsensor.Named("non existent movement sensor"), - Destination: geo.NewPoint(0, 0), - } - executionID, err := ms.MoveOnGlobe(ctx, req) - e := "Resource missing from dependencies. Resource: rdk:component:movement_sensor/non existent movement sensor" - test.That(t, err, test.ShouldBeError, errors.New(e)) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns error when request would require moving more than 5 km", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Destination: geo.NewPoint(0, 0), - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("cannot move more than 5 kilometers")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns error when destination is nil", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("destination cannot be nil")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns error when destination contains NaN", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - } - - dests := []*geo.Point{ - geo.NewPoint(math.NaN(), math.NaN()), - geo.NewPoint(0, math.NaN()), - geo.NewPoint(math.NaN(), 0), - } - - for _, d := range dests { - req.Destination = d - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("destination may not contain NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - } - }) - - t.Run("returns an error if the base provided is not a base", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, _, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: injectedMovementSensor.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:component:movement_sensor/test-gps\" not found")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error if the movement_sensor provided is not a movement_sensor", func(t *testing.T) { - t.Parallel() - _, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: fakeBase.Name(), - Heading: 90, - Destination: dst, - Extra: extra, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("Resource missing from dependencies. Resource: rdk:component:base/test-base")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("errors when motion configuration has a negative PlanDeviationMM", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{PlanDeviationMM: -1}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("PlanDeviationMM may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("errors when motion configuration has a NaN PlanDeviationMM", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{PlanDeviationMM: math.NaN()}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("PlanDeviationMM may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when the motion configuration has a negative ObstaclePollingFreqHz", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{ObstaclePollingFreqHz: -1}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("ObstaclePollingFreqHz may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when the motion configuration has a NaN ObstaclePollingFreqHz", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{ObstaclePollingFreqHz: math.NaN()}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("ObstaclePollingFreqHz may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when the motion configuration has a negative PositionPollingFreqHz", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{PositionPollingFreqHz: -1}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("PositionPollingFreqHz may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when the motion configuration has a NaN PositionPollingFreqHz", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{PositionPollingFreqHz: math.NaN()}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("PositionPollingFreqHz may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when motion configuration has a negative AngularDegsPerSec", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{AngularDegsPerSec: -1}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("AngularDegsPerSec may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when motion configuration has a NaN AngularDegsPerSec", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{AngularDegsPerSec: math.NaN()}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("AngularDegsPerSec may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when motion configuration has a negative LinearMPerSec", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{LinearMPerSec: -1}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("LinearMPerSec may not be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns an error when motion configuration has a NaN LinearMPerSec", func(t *testing.T) { - t.Parallel() - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{LinearMPerSec: math.NaN()}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("LinearMPerSec may not be NaN")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("collision_buffer_mm validtations", func(t *testing.T) { - t.Run("fail when collision_buffer_mm is not a float", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": "not a float"}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("could not interpret collision_buffer_mm field as float64")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("fail when collision_buffer_mm is negative", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": -1.}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeError, errors.New("collision_buffer_mm can't be negative")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("pass when collision_buffer_mm is a small positive float", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": 1e-5}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("pass when collision_buffer_mm is a positive float", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{"collision_buffer_mm": 10.}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("pass when extra is empty", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{}, - Extra: map[string]interface{}{}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - - t.Run("passes validations when extra is nil", func(t *testing.T) { - injectedMovementSensor, _, fakeBase, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.MoveOnGlobeReq{ - ComponentName: fakeBase.Name(), - MovementSensorName: injectedMovementSensor.Name(), - Heading: 90, - Destination: dst, - MotionCfg: &motion.MotionConfiguration{}, - } - executionID, err := ms.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldNotResemble, uuid.Nil) - }) - }) - }) -} - -func TestGetTransientDetections(t *testing.T) { - t.Parallel() - ctx := context.Background() - - _, ms := createMoveOnMapEnvironment( - ctx, t, - "slam/example_cartographer_outputs/viam-office-02-22-3/pointcloud/pointcloud_4.pcd", - 100, spatialmath.NewZeroPose(), - ) - t.Cleanup(func() { ms.Close(ctx) }) - - // construct move request - moveReq := motion.MoveOnMapReq{ - ComponentName: base.Named("test-base"), - Destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 10, Y: 0, Z: 0}), - SlamName: slam.Named("test_slam"), - MotionCfg: &motion.MotionConfiguration{ - PlanDeviationMM: 1, - ObstacleDetectors: []motion.ObstacleDetectorName{ - {VisionServiceName: vision.Named("test-vision"), CameraName: camera.Named("test-camera")}, - }, - }, - } - - planExecutor, err := ms.(*builtIn).newMoveOnMapRequest(ctx, moveReq, nil, 0) - test.That(t, err, test.ShouldBeNil) - - mr, ok := planExecutor.(*moveRequest) - test.That(t, ok, test.ShouldBeTrue) - - injectedVis, ok := ms.(*builtIn).visionServices[vision.Named("test-vision")].(*inject.VisionService) - test.That(t, ok, test.ShouldBeTrue) - - // define injected method on vision service - injectedVis.GetObjectPointCloudsFunc = func(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) { - boxGeom, err := spatialmath.NewBox( - spatialmath.NewPose(r3.Vector{4, 8, 10}, &spatialmath.OrientationVectorDegrees{OZ: 1}), - r3.Vector{2, 3, 5}, - "test-box", - ) - test.That(t, err, test.ShouldBeNil) - detection, err := viz.NewObjectWithLabel(pointcloud.New(), "test-box", boxGeom.ToProtobuf()) - test.That(t, err, test.ShouldBeNil) - return []*viz.Object{detection}, nil - } - - type testCase struct { - name string - f spatialmath.Pose - detectionPose spatialmath.Pose - } - testCases := []testCase{ - { - name: "relative - SLAM/base theta does not matter", - f: spatialmath.NewZeroPose(), - detectionPose: spatialmath.NewPose(r3.Vector{4, 10, -8}, &spatialmath.OrientationVectorDegrees{OY: 1, Theta: -90}), - }, - { - name: "absolute - SLAM theta: 0, base theta: -90 == 270", - f: spatialmath.NewPose(r3.Vector{-4, -10, 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -90}), - detectionPose: spatialmath.NewPose(r3.Vector{6, -14, -8}, &spatialmath.OrientationVectorDegrees{OX: 1, Theta: -90}), - }, - { - name: "absolute - SLAM theta: 90, base theta: 0", - f: spatialmath.NewPose(r3.Vector{-4, -10, 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 0}), - detectionPose: spatialmath.NewPose(r3.Vector{0, 0, -8}, &spatialmath.OrientationVectorDegrees{OY: 1, Theta: -90}), - }, - { - name: "absolute - SLAM theta: 180, base theta: 90", - f: spatialmath.NewPose(r3.Vector{-4, -10, 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 90}), - detectionPose: spatialmath.NewPose(r3.Vector{-14, -6, -8}, &spatialmath.OrientationVectorDegrees{OX: -1, Theta: -90}), - }, - { - name: "absolute - SLAM theta: 270, base theta: 180", - f: spatialmath.NewPose(r3.Vector{-4, -10, 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 180}), - detectionPose: spatialmath.NewPose(r3.Vector{-8, -20, -8}, &spatialmath.OrientationVectorDegrees{OY: -1, Theta: -90}), - }, - } - - testFn := func(t *testing.T, tc testCase) { - t.Helper() - transformedGeoms, err := mr.getTransientDetections(ctx, injectedVis, camera.Named("test-camera"), tc.f) - test.That(t, err, test.ShouldBeNil) - test.That(t, transformedGeoms.Parent(), test.ShouldEqual, referenceframe.World) - test.That(t, len(transformedGeoms.Geometries()), test.ShouldEqual, 1) - test.That(t, spatialmath.PoseAlmostEqual(transformedGeoms.Geometries()[0].Pose(), tc.detectionPose), test.ShouldBeTrue) - } - - for _, tc := range testCases { - c := tc // needed to workaround loop variable not being captured by func literals - t.Run(c.name, func(t *testing.T) { - t.Parallel() - testFn(t, c) - }) - } -} - -func TestStopPlan(t *testing.T) { - ctx := context.Background() - gpsPoint := geo.NewPoint(0, 0) - //nolint:dogsled - _, _, _, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - - req := motion.StopPlanReq{} - err := ms.StopPlan(ctx, req) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(req.ComponentName)) -} - -func TestListPlanStatuses(t *testing.T) { - ctx := context.Background() - gpsPoint := geo.NewPoint(0, 0) - //nolint:dogsled - _, _, _, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - - req := motion.ListPlanStatusesReq{} - // returns no results as no move on globe calls have been made - planStatusesWithIDs, err := ms.ListPlanStatuses(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planStatusesWithIDs), test.ShouldEqual, 0) -} - -func TestPlanHistory(t *testing.T) { - ctx := context.Background() - gpsPoint := geo.NewPoint(0, 0) - //nolint:dogsled - _, _, _, ms := createMoveOnGlobeEnvironment(ctx, t, gpsPoint, nil, 5) - defer ms.Close(ctx) - req := motion.PlanHistoryReq{} - history, err := ms.PlanHistory(ctx, req) - test.That(t, err, test.ShouldResemble, resource.NewNotFoundError(req.ComponentName)) - test.That(t, history, test.ShouldBeNil) -} - -func TestNewValidatedMotionCfg(t *testing.T) { - t.Run("returns expected defaults when given nil cfg for requestTypeMoveOnGlobe", func(t *testing.T) { - vmc, err := newValidatedMotionCfg(nil, requestTypeMoveOnGlobe) - test.That(t, err, test.ShouldBeNil) - test.That(t, vmc, test.ShouldResemble, &validatedMotionConfiguration{ - angularDegsPerSec: defaultAngularDegsPerSec, - linearMPerSec: defaultLinearMPerSec, - obstaclePollingFreqHz: defaultObstaclePollingHz, - positionPollingFreqHz: defaultPositionPollingHz, - planDeviationMM: defaultGlobePlanDeviationM * 1e3, - obstacleDetectors: []motion.ObstacleDetectorName{}, - }) - }) - - t.Run("returns expected defaults when given zero cfg for requestTypeMoveOnGlobe", func(t *testing.T) { - vmc, err := newValidatedMotionCfg(&motion.MotionConfiguration{}, requestTypeMoveOnGlobe) - test.That(t, err, test.ShouldBeNil) - test.That(t, vmc, test.ShouldResemble, &validatedMotionConfiguration{ - angularDegsPerSec: defaultAngularDegsPerSec, - linearMPerSec: defaultLinearMPerSec, - obstaclePollingFreqHz: defaultObstaclePollingHz, - positionPollingFreqHz: defaultPositionPollingHz, - planDeviationMM: defaultGlobePlanDeviationM * 1e3, - obstacleDetectors: []motion.ObstacleDetectorName{}, - }) - }) - - t.Run("returns expected defaults when given zero cfg for requestTypeMoveOnMap", func(t *testing.T) { - vmc, err := newValidatedMotionCfg(&motion.MotionConfiguration{}, requestTypeMoveOnMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, vmc, test.ShouldResemble, &validatedMotionConfiguration{ - angularDegsPerSec: defaultAngularDegsPerSec, - linearMPerSec: defaultLinearMPerSec, - obstaclePollingFreqHz: defaultObstaclePollingHz, - positionPollingFreqHz: defaultPositionPollingHz, - planDeviationMM: defaultSlamPlanDeviationM * 1e3, - obstacleDetectors: []motion.ObstacleDetectorName{}, - }) - }) - - t.Run("allows overriding defaults", func(t *testing.T) { - vmc, err := newValidatedMotionCfg(&motion.MotionConfiguration{ - AngularDegsPerSec: 10., - LinearMPerSec: 20., - PlanDeviationMM: 30., - PositionPollingFreqHz: 40, - ObstaclePollingFreqHz: 50., - ObstacleDetectors: []motion.ObstacleDetectorName{ - { - VisionServiceName: vision.Named("fakeVision"), - CameraName: camera.Named("fakeCamera"), - }, - }, - }, requestTypeMoveOnMap) - test.That(t, err, test.ShouldBeNil) - test.That(t, vmc, test.ShouldResemble, &validatedMotionConfiguration{ - angularDegsPerSec: 10., - linearMPerSec: 20., - planDeviationMM: 30., - positionPollingFreqHz: 40., - obstaclePollingFreqHz: 50., - obstacleDetectors: []motion.ObstacleDetectorName{ - { - VisionServiceName: vision.Named("fakeVision"), - CameraName: camera.Named("fakeCamera"), - }, - }, - }) - }) -} diff --git a/services/motion/builtin/builtin_utils_test.go b/services/motion/builtin/builtin_utils_test.go deleted file mode 100644 index 3f1cea4b439..00000000000 --- a/services/motion/builtin/builtin_utils_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package builtin - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - "go.viam.com/utils" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/base" - baseFake "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/base/kinematicbase" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -func setupMotionServiceFromConfig(t *testing.T, configFilename string) (motion.Service, func()) { - t.Helper() - ctx := context.Background() - logger := logging.NewTestLogger(t) - cfg, err := config.Read(ctx, configFilename, logger) - test.That(t, err, test.ShouldBeNil) - myRobot, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - svc, err := motion.FromRobot(myRobot, "builtin") - test.That(t, err, test.ShouldBeNil) - return svc, func() { - myRobot.Close(context.Background()) - } -} - -func getPointCloudMap(path string) (func() ([]byte, error), error) { - const chunkSizeBytes = 1 * 1024 * 1024 - file, err := os.Open(path) - if err != nil { - return nil, err - } - chunk := make([]byte, chunkSizeBytes) - f := func() ([]byte, error) { - bytesRead, err := file.Read(chunk) - if err != nil { - defer utils.UncheckedErrorFunc(file.Close) - return nil, err - } - return chunk[:bytesRead], err - } - return f, nil -} - -func createInjectedMovementSensor(name string, gpsPoint *geo.Point) *inject.MovementSensor { - injectedMovementSensor := inject.NewMovementSensor(name) - injectedMovementSensor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return gpsPoint, 0, nil - } - injectedMovementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil - } - injectedMovementSensor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{CompassHeadingSupported: true}, nil - } - - return injectedMovementSensor -} - -func createInjectedSlam(name, pcdPath string, origin spatialmath.Pose) *inject.SLAMService { - injectSlam := inject.NewSLAMService(name) - injectSlam.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - return getPointCloudMap(filepath.Clean(artifact.MustPath(pcdPath))) - } - injectSlam.PositionFunc = func(ctx context.Context) (spatialmath.Pose, string, error) { - if origin != nil { - return origin, "", nil - } - return spatialmath.NewZeroPose(), "", nil - } - return injectSlam -} - -func createBaseLink(t *testing.T) *referenceframe.LinkInFrame { - basePose := spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 0}) - baseSphere, err := spatialmath.NewSphere(basePose, 10, "base-sphere") - test.That(t, err, test.ShouldBeNil) - baseLink := referenceframe.NewLinkInFrame( - referenceframe.World, - spatialmath.NewZeroPose(), - "test-base", - baseSphere, - ) - return baseLink -} - -func createFrameSystemService( - ctx context.Context, - deps resource.Dependencies, - fsParts []*referenceframe.FrameSystemPart, - logger logging.Logger, -) (framesystem.Service, error) { - fsSvc, err := framesystem.New(ctx, deps, logger) - if err != nil { - return nil, err - } - conf := resource.Config{ - ConvertedAttributes: &framesystem.Config{Parts: fsParts}, - } - if err := fsSvc.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - deps[fsSvc.Name()] = fsSvc - - return fsSvc, nil -} - -func createMoveOnGlobeEnvironment(ctx context.Context, t *testing.T, origin *geo.Point, noise spatialmath.Pose, sleepTime int) ( - *inject.MovementSensor, framesystem.Service, kinematicbase.KinematicBase, motion.Service, -) { - logger := logging.NewTestLogger(t) - - // create fake base - baseCfg := resource.Config{ - Name: "test-base", - API: base.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{R: 40}}, - } - fakeBase, err := baseFake.NewBase(ctx, nil, baseCfg, logger) - test.That(t, err, test.ShouldBeNil) - - // create base link - baseLink := createBaseLink(t) - // create MovementSensor link - movementSensorLink := referenceframe.NewLinkInFrame( - baseLink.Name(), - spatialmath.NewPoseFromPoint(r3.Vector{X: -10, Y: 0, Z: 0}), - "test-gps", - nil, - ) - - // create a fake kinematic base - kinematicsOptions := kinematicbase.NewKinematicBaseOptions() - kinematicsOptions.PlanDeviationThresholdMM = 1 // can afford to do this for tests - kb, err := kinematicbase.WrapWithFakePTGKinematics( - ctx, - fakeBase.(*baseFake.Base), - logger, - referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewZeroPose()), - kinematicsOptions, - noise, - sleepTime, - ) - test.That(t, err, test.ShouldBeNil) - - // create injected MovementSensor - dynamicMovementSensor := inject.NewMovementSensor("test-gps") - dynamicMovementSensor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - poseInFrame, err := kb.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - heading := poseInFrame.Pose().Orientation().OrientationVectorDegrees().Theta - distance := poseInFrame.Pose().Point().Norm() - pt := origin.PointAtDistanceAndBearing(distance*1e-6, heading) - return pt, 0, nil - } - dynamicMovementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil - } - dynamicMovementSensor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{CompassHeadingSupported: true}, nil - } - - // create injected vision service - injectedVisionSvc := inject.NewVisionService("injectedVisionSvc") - - cameraGeom, err := spatialmath.NewBox( - spatialmath.NewZeroPose(), - r3.Vector{X: 1, Y: 1, Z: 1}, "camera", - ) - test.That(t, err, test.ShouldBeNil) - - injectedCamera := inject.NewCamera("injectedCamera") - cameraLink := referenceframe.NewLinkInFrame( - baseLink.Name(), - spatialmath.NewPose(r3.Vector{X: 1}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 90}), - "injectedCamera", - cameraGeom, - ) - - // create the frame system - fsParts := []*referenceframe.FrameSystemPart{ - {FrameConfig: movementSensorLink}, - {FrameConfig: baseLink}, - {FrameConfig: cameraLink}, - } - deps := resource.Dependencies{ - fakeBase.Name(): kb, - dynamicMovementSensor.Name(): dynamicMovementSensor, - injectedVisionSvc.Name(): injectedVisionSvc, - injectedCamera.Name(): injectedCamera, - } - - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - - conf := resource.Config{ConvertedAttributes: &Config{}} - ms, err := NewBuiltIn(ctx, deps, conf, logger) - test.That(t, err, test.ShouldBeNil) - - return dynamicMovementSensor, fsSvc, kb, ms -} - -func createMoveOnMapEnvironment( - ctx context.Context, - t *testing.T, - pcdPath string, - geomSize float64, - origin spatialmath.Pose, -) (kinematicbase.KinematicBase, motion.Service) { - if origin == nil { - origin = spatialmath.NewZeroPose() - } - injectSlam := createInjectedSlam("test_slam", pcdPath, origin) - injectVis := inject.NewVisionService("test-vision") - injectCam := inject.NewCamera("test-camera") - - baseLink := createBaseLink(t) - - cfg := resource.Config{ - Name: "test-base", - API: base.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{R: geomSize}}, - } - logger := logging.NewTestLogger(t) - fakeBase, err := baseFake.NewBase(ctx, nil, cfg, logger) - test.That(t, err, test.ShouldBeNil) - kinematicsOptions := kinematicbase.NewKinematicBaseOptions() - kinematicsOptions.PlanDeviationThresholdMM = 1 // can afford to do this for tests - kb, err := kinematicbase.WrapWithFakePTGKinematics( - ctx, - fakeBase.(*baseFake.Base), - logger, - referenceframe.NewPoseInFrame(referenceframe.World, origin), - kinematicsOptions, - spatialmath.NewZeroPose(), - 10, - ) - test.That(t, err, test.ShouldBeNil) - - deps := resource.Dependencies{ - fakeBase.Name(): kb, - injectVis.Name(): injectVis, - injectCam.Name(): injectCam, - injectSlam.Name(): injectSlam, - } - conf := resource.Config{ConvertedAttributes: &Config{}} - - ms, err := NewBuiltIn(ctx, deps, conf, logger) - test.That(t, err, test.ShouldBeNil) - - // create the frame system - cameraGeom, err := spatialmath.NewBox( - spatialmath.NewZeroPose(), - r3.Vector{X: 1, Y: 1, Z: 1}, "camera", - ) - test.That(t, err, test.ShouldBeNil) - cameraLink := referenceframe.NewLinkInFrame( - baseLink.Name(), - // we recreate an intel real sense orientation placed along the +Y axis of the base's coordinate frame. - // i.e. the camera is pointed along the axis the base moves forwa - spatialmath.NewPose(r3.Vector{X: 0, Y: 0, Z: 0}, &spatialmath.OrientationVectorDegrees{OY: 1, Theta: -90}), - "test-camera", - cameraGeom, - ) - - fsParts := []*referenceframe.FrameSystemPart{ - {FrameConfig: baseLink}, - {FrameConfig: cameraLink}, - } - - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - ms.(*builtIn).fsService = fsSvc - - return kb, ms -} diff --git a/services/motion/builtin/move_request.go b/services/motion/builtin/move_request.go deleted file mode 100644 index 6a5518529b8..00000000000 --- a/services/motion/builtin/move_request.go +++ /dev/null @@ -1,833 +0,0 @@ -package builtin - -import ( - "bytes" - "context" - "fmt" - "math" - "strconv" - "sync" - "time" - - "github.com/pkg/errors" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/base/kinematicbase" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/motion/builtin/state" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" -) - -const ( - defaultReplanCostFactor = 1.0 - defaultMaxReplans = -1 // Values below zero will replan infinitely - baseStopTimeout = time.Second * 5 -) - -// validatedMotionConfiguration is a copy of the motion.MotionConfiguration type -// which has been validated to conform to the expectations of the builtin -// motion servicl. -type validatedMotionConfiguration struct { - obstacleDetectors []motion.ObstacleDetectorName - positionPollingFreqHz float64 - obstaclePollingFreqHz float64 - planDeviationMM float64 - linearMPerSec float64 - angularDegsPerSec float64 -} - -type requestType uint8 - -const ( - requestTypeUnspecified requestType = iota - requestTypeMoveOnGlobe - requestTypeMoveOnMap -) - -// moveRequest is a structure that contains all the information necessary for to make a move call. -type moveRequest struct { - requestType requestType - // geoPoseOrigin is only set if requestType == requestTypeMoveOnGlobe - geoPoseOrigin *spatialmath.GeoPose - poseOrigin spatialmath.Pose - logger logging.Logger - config *validatedMotionConfiguration - planRequest *motionplan.PlanRequest - seedPlan motionplan.Plan - kinematicBase kinematicbase.KinematicBase - obstacleDetectors map[vision.Service][]resource.Name - replanCostFactor float64 - fsService framesystem.Service - - executeBackgroundWorkers *sync.WaitGroup - responseChan chan moveResponse - // replanners for the move request - // if we ever have to add additional instances we should figure out how to make this more scalable - position, obstacle *replanner -} - -// plan creates a plan using the currentInputs of the robot and the moveRequest's planRequest. -func (mr *moveRequest) Plan(ctx context.Context) (motionplan.Plan, error) { - inputs, err := mr.kinematicBase.CurrentInputs(ctx) - if err != nil { - return nil, err - } - // TODO: this is really hacky and we should figure out a better place to store this information - if len(mr.kinematicBase.Kinematics().DoF()) == 2 { - inputs = inputs[:2] - } - mr.planRequest.StartConfiguration = map[string][]referenceframe.Input{mr.kinematicBase.Kinematics().Name(): inputs} - - // get existing elements of the worldstate - existingGifs, err := mr.planRequest.WorldState.ObstaclesInWorldFrame(mr.planRequest.FrameSystem, mr.planRequest.StartConfiguration) - if err != nil { - return nil, err - } - - // get transient detections - gifs := []*referenceframe.GeometriesInFrame{} - for visSrvc, cameraNames := range mr.obstacleDetectors { - for _, camName := range cameraNames { - transientGifs, err := mr.getTransientDetections(ctx, visSrvc, camName, mr.poseOrigin) - if err != nil { - return nil, err - } - gifs = append(gifs, transientGifs) - } - } - gifs = append(gifs, existingGifs) - - // update worldstate to include transient detections - planRequestCopy := *mr.planRequest - planRequestCopy.WorldState, err = referenceframe.NewWorldState(gifs, nil) - if err != nil { - return nil, err - } - - // TODO(RSDK-5634): this should pass in mr.seedplan and the appropriate replanCostFactor once this bug is found and fixed. - return motionplan.Replan(ctx, &planRequestCopy, nil, 0) -} - -func (mr *moveRequest) Execute(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - defer mr.executeBackgroundWorkers.Wait() - cancelCtx, cancelFn := context.WithCancel(ctx) - defer cancelFn() - - mr.start(cancelCtx, plan) - return mr.listen(cancelCtx) -} - -func (mr *moveRequest) AnchorGeoPose() *spatialmath.GeoPose { - return mr.geoPoseOrigin -} - -// execute attempts to follow a given Plan starting from the index percribed by waypointIndex. -// Note that waypointIndex is an atomic int that is incremented in this function after each waypoint has been successfully reached. -func (mr *moveRequest) execute(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - waypoints, err := plan.Trajectory().GetFrameInputs(mr.kinematicBase.Name().ShortName()) - if err != nil { - return state.ExecuteResponse{}, err - } - - if err := mr.kinematicBase.GoToInputs(ctx, waypoints...); err != nil { - // If there is an error on GoToInputs, stop the component if possible before returning the error - mr.logger.CDebugf(ctx, "calling kinematicBase.Stop due to %s\n", err) - if stopErr := mr.stop(); stopErr != nil { - return state.ExecuteResponse{}, errors.Wrap(err, stopErr.Error()) - } - return state.ExecuteResponse{}, err - } - - // the plan has been fully executed so check to see if where we are at is close enough to the goal. - return mr.deviatedFromPlan(ctx, plan) -} - -// deviatedFromPlan takes a plan and an index of a waypoint on that Plan and returns whether or not it is still -// following the plan as described by the PlanDeviation specified for the moveRequest. -func (mr *moveRequest) deviatedFromPlan(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - errorState, err := mr.kinematicBase.ErrorState(ctx) - if err != nil { - return state.ExecuteResponse{}, err - } - if errorState.Point().Norm() > mr.config.planDeviationMM { - msg := "error state exceeds planDeviationMM; planDeviationMM: %f, errorstate.Point().Norm(): %f, errorstate.Point(): %#v " - reason := fmt.Sprintf(msg, mr.config.planDeviationMM, errorState.Point().Norm(), errorState.Point()) - return state.ExecuteResponse{Replan: true, ReplanReason: reason}, nil - } - return state.ExecuteResponse{}, nil -} - -// getTransientDetections returns a list of geometries as observed by the provided vision service and camera. -// Depending on the caller, the geometries returned are either in their relative position -// with respect to the base or in their absolute position with respect to the world. -func (mr *moveRequest) getTransientDetections( - ctx context.Context, - visSrvc vision.Service, - camName resource.Name, - transformBy spatialmath.Pose, -) (*referenceframe.GeometriesInFrame, error) { - mr.logger.CDebugf(ctx, - "proceeding to get detections from vision service: %s with camera: %s", - visSrvc.Name().ShortName(), - camName.ShortName(), - ) - - detections, err := visSrvc.GetObjectPointClouds(ctx, camName.Name, nil) - if err != nil { - return nil, err - } - - cameraOrigin := referenceframe.NewPoseInFrame(camName.ShortName(), spatialmath.NewZeroPose()) - cameraToBase, err := mr.fsService.TransformPose(ctx, cameraOrigin, mr.kinematicBase.Name().ShortName(), nil) - if err != nil { - mr.logger.CDebugf(ctx, - "we assume the base named: %s is coincident with the camera named: %s due to err: %v", - mr.kinematicBase.Name().ShortName(), camName.ShortName(), err.Error(), - ) - cameraToBase = cameraOrigin - } - - // transformed detections - transformedGeoms := []spatialmath.Geometry{} - for i, detection := range detections { - geometry := detection.Geometry - // update the label of the geometry so we know it is transient - label := camName.ShortName() + "_transientObstacle_" + strconv.Itoa(i) - if geometry.Label() != "" { - label += "_" + geometry.Label() - } - geometry.SetLabel(label) - - // transform the geometry to be relative to the base frame which is +Y forwards - relativeGeom := geometry.Transform(cameraToBase.Pose()) - - // apply any transformation on the geometry defined a priori by the caller - transformedGeom := relativeGeom.Transform(transformBy) - transformedGeoms = append(transformedGeoms, transformedGeom) - } - return referenceframe.NewGeometriesInFrame(referenceframe.World, transformedGeoms), nil -} - -// obstaclesIntersectPlan takes a list of waypoints and an index of a waypoint on that Plan and reports an error indicating -// whether or not any obstacle detectors report geometries in positions which would cause a collision with the executor -// following the Plan. -func (mr *moveRequest) obstaclesIntersectPlan( - ctx context.Context, - plan motionplan.Plan, -) (state.ExecuteResponse, error) { - // if the camera is mounted on something InputEnabled that isn't the base, then that - // input needs to be known in order to properly calculate the pose of the obstacle - // furthermore, if that InputEnabled thing has moved since this moveRequest was initialized - // (due to some other non-motion call for example), then we can't just get current inputs - // we need the original input to place that thing in its original position - // hence, cached CurrentInputs from the start are used i.e. mr.planRequest.StartConfiguration - existingGifs, err := mr.planRequest.WorldState.ObstaclesInWorldFrame( - mr.planRequest.FrameSystem, mr.planRequest.StartConfiguration, - ) - if err != nil { - return state.ExecuteResponse{}, err - } - - // get the current position of the base - currentPosition, err := mr.kinematicBase.CurrentPosition(ctx) - if err != nil { - return state.ExecuteResponse{}, err - } - - for visSrvc, cameraNames := range mr.obstacleDetectors { - for _, camName := range cameraNames { - // Note: detections are initially observed from the camera frame but must be transformed to be in - // world frame. We cannot use the inputs of the base to transform the detections since they are relative. - gifs, err := mr.getTransientDetections(ctx, visSrvc, camName, currentPosition.Pose()) - if err != nil { - return state.ExecuteResponse{}, err - } - if len(gifs.Geometries()) == 0 { - mr.logger.CDebug(ctx, "will not check if obstacles intersect path since nothing was detected") - return state.ExecuteResponse{}, nil - } - - // construct new worldstate - worldState, err := referenceframe.NewWorldState([]*referenceframe.GeometriesInFrame{existingGifs, gifs}, nil) - if err != nil { - return state.ExecuteResponse{}, err - } - - // build representation of frame system's inputs - currentInputs, err := mr.kinematicBase.CurrentInputs(ctx) - if err != nil { - return state.ExecuteResponse{}, err - } - inputMap := referenceframe.StartPositions(mr.planRequest.FrameSystem) - inputMap[mr.kinematicBase.Name().ShortName()] = currentInputs - - // get the current position of the base - currentPosition, err := mr.kinematicBase.CurrentPosition(ctx) - if err != nil { - return state.ExecuteResponse{}, err - } - - // Note: the value of wayPointIndex is subject to change between when this function is first entered - // versus when CheckPlan is actually called. - // We load the wayPointIndex value to ensure that all information is up to date. - - // get the pose difference between where the robot is versus where it ought to be. - errorState, err := mr.kinematicBase.ErrorState(ctx) - if err != nil { - return state.ExecuteResponse{}, err - } - - mr.logger.CDebugf(ctx, "CheckPlan inputs: \n currentPosition: %v\n currentInputs: %v\n errorState: %v\n worldstate: %s", - spatialmath.PoseToProtobuf(currentPosition.Pose()), - currentInputs, - spatialmath.PoseToProtobuf(errorState), - worldState.String(), - ) - - // TODO(pl): This was disabled as part of course correction. It will need to be re-enabled once a method is developed to surface - // course-corrected plans from the kinematic base to the motion service. - // if err := motionplan.CheckPlan( - // mr.kinematicBase.Kinematics(), // frame we wish to check for collisions - // plan, - // waypointIndex, - // worldState, // detected obstacles by this instance of camera + service - // mr.planRequest.FrameSystem, - // currentPosition.Pose(), // currentPosition of robot accounts for errorState - // inputMap, - // errorState, // deviation of robot from plan - // lookAheadDistanceMM, - // mr.planRequest.Logger, - // ); err != nil { - // mr.planRequest.Logger.CInfo(ctx, err.Error()) - // return state.ExecuteResponse{Replan: true, ReplanReason: err.Error()}, nil - // } - } - } - return state.ExecuteResponse{}, nil -} - -func kbOptionsFromCfg(motionCfg *validatedMotionConfiguration, validatedExtra validatedExtra) kinematicbase.Options { - kinematicsOptions := kinematicbase.NewKinematicBaseOptions() - - if motionCfg.linearMPerSec > 0 { - kinematicsOptions.LinearVelocityMMPerSec = motionCfg.linearMPerSec * 1000 - } - - if motionCfg.angularDegsPerSec > 0 { - kinematicsOptions.AngularVelocityDegsPerSec = motionCfg.angularDegsPerSec - } - - if motionCfg.planDeviationMM > 0 { - kinematicsOptions.PlanDeviationThresholdMM = motionCfg.planDeviationMM - } - - if validatedExtra.motionProfile != "" { - kinematicsOptions.PositionOnlyMode = validatedExtra.motionProfile == motionplan.PositionOnlyMotionProfile - } - - kinematicsOptions.GoalRadiusMM = motionCfg.planDeviationMM - kinematicsOptions.HeadingThresholdDegrees = 8 - return kinematicsOptions -} - -func validateNotNan(f float64, name string) error { - if math.IsNaN(f) { - return errors.Errorf("%s may not be NaN", name) - } - return nil -} - -func validateNotNeg(f float64, name string) error { - if f < 0 { - return errors.Errorf("%s may not be negative", name) - } - return nil -} - -func validateNotNegNorNaN(f float64, name string) error { - if err := validateNotNan(f, name); err != nil { - return err - } - return validateNotNeg(f, name) -} - -func newValidatedMotionCfg(motionCfg *motion.MotionConfiguration, reqType requestType) (*validatedMotionConfiguration, error) { - empty := &validatedMotionConfiguration{} - vmc := &validatedMotionConfiguration{ - angularDegsPerSec: defaultAngularDegsPerSec, - linearMPerSec: defaultLinearMPerSec, - obstaclePollingFreqHz: defaultObstaclePollingHz, - positionPollingFreqHz: defaultPositionPollingHz, - obstacleDetectors: []motion.ObstacleDetectorName{}, - } - - switch reqType { - case requestTypeMoveOnGlobe: - vmc.planDeviationMM = defaultGlobePlanDeviationM * 1e3 - case requestTypeMoveOnMap: - vmc.planDeviationMM = defaultSlamPlanDeviationM * 1e3 - case requestTypeUnspecified: - fallthrough - default: - return empty, fmt.Errorf("invalid moveRequest.requestType: %d", reqType) - } - - if motionCfg == nil { - return vmc, nil - } - - if err := validateNotNegNorNaN(motionCfg.LinearMPerSec, "LinearMPerSec"); err != nil { - return empty, err - } - - if err := validateNotNegNorNaN(motionCfg.AngularDegsPerSec, "AngularDegsPerSec"); err != nil { - return empty, err - } - - if err := validateNotNegNorNaN(motionCfg.PlanDeviationMM, "PlanDeviationMM"); err != nil { - return empty, err - } - - if err := validateNotNegNorNaN(motionCfg.ObstaclePollingFreqHz, "ObstaclePollingFreqHz"); err != nil { - return empty, err - } - - if err := validateNotNegNorNaN(motionCfg.PositionPollingFreqHz, "PositionPollingFreqHz"); err != nil { - return empty, err - } - - if motionCfg.LinearMPerSec != 0 { - vmc.linearMPerSec = motionCfg.LinearMPerSec - } - - if motionCfg.AngularDegsPerSec != 0 { - vmc.angularDegsPerSec = motionCfg.AngularDegsPerSec - } - - if motionCfg.PlanDeviationMM != 0 { - vmc.planDeviationMM = motionCfg.PlanDeviationMM - } - - if motionCfg.ObstaclePollingFreqHz != 0 { - vmc.obstaclePollingFreqHz = motionCfg.ObstaclePollingFreqHz - } - - if motionCfg.PositionPollingFreqHz != 0 { - vmc.positionPollingFreqHz = motionCfg.PositionPollingFreqHz - } - - if motionCfg.ObstacleDetectors != nil { - vmc.obstacleDetectors = motionCfg.ObstacleDetectors - } - - return vmc, nil -} - -func (ms *builtIn) newMoveOnGlobeRequest( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedPlan motionplan.Plan, - replanCount int, -) (state.PlannerExecutor, error) { - valExtra, err := newValidatedExtra(req.Extra) - if err != nil { - return nil, err - } - - if valExtra.maxReplans >= 0 { - if replanCount > valExtra.maxReplans { - return nil, fmt.Errorf("exceeded maximum number of replans: %d", valExtra.maxReplans) - } - } - - motionCfg, err := newValidatedMotionCfg(req.MotionCfg, requestTypeMoveOnGlobe) - if err != nil { - return nil, err - } - // ensure arguments are well behaved - obstacles := req.Obstacles - if obstacles == nil { - obstacles = []*spatialmath.GeoObstacle{} - } - if req.Destination == nil { - return nil, errors.New("destination cannot be nil") - } - - if math.IsNaN(req.Destination.Lat()) || math.IsNaN(req.Destination.Lng()) { - return nil, errors.New("destination may not contain NaN") - } - - // build kinematic options - kinematicsOptions := kbOptionsFromCfg(motionCfg, valExtra) - - // build the localizer from the movement sensor - movementSensor, ok := ms.movementSensors[req.MovementSensorName] - if !ok { - return nil, resource.DependencyNotFoundError(req.MovementSensorName) - } - origin, _, err := movementSensor.Position(ctx, nil) - if err != nil { - return nil, err - } - - heading, err := movementSensor.CompassHeading(ctx, nil) - if err != nil { - return nil, err - } - - // add an offset between the movement sensor and the base if it is applicable - baseOrigin := referenceframe.NewPoseInFrame(req.ComponentName.ShortName(), spatialmath.NewZeroPose()) - movementSensorToBase, err := ms.fsService.TransformPose(ctx, baseOrigin, movementSensor.Name().ShortName(), nil) - if err != nil { - // here we make the assumption the movement sensor is coincident with the base - movementSensorToBase = baseOrigin - } - // Create a localizer from the movement sensor, and collapse reported orientations to 2d - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, movementSensorToBase.Pose())) - - // create a KinematicBase from the componentName - baseComponent, ok := ms.components[req.ComponentName] - if !ok { - return nil, resource.NewNotFoundError(req.ComponentName) - } - b, ok := baseComponent.(base.Base) - if !ok { - return nil, fmt.Errorf("cannot move component of type %T because it is not a Base", baseComponent) - } - - fs, err := ms.fsService.FrameSystem(ctx, nil) - if err != nil { - return nil, err - } - - // Important: GeoPointToPose will create a pose such that incrementing latitude towards north increments +Y, and incrementing - // longitude towards east increments +X. Heading is not taken into account. This pose must therefore be transformed based on the - // orientation of the base such that it is a pose relative to the base's current location. - goalPoseRaw := spatialmath.NewPoseFromPoint(spatialmath.GeoPointToPoint(req.Destination, origin)) - // construct limits - straightlineDistance := goalPoseRaw.Point().Norm() - if straightlineDistance > maxTravelDistanceMM { - return nil, fmt.Errorf("cannot move more than %d kilometers", int(maxTravelDistanceMM*1e-6)) - } - limits := []referenceframe.Limit{ - {Min: -straightlineDistance * 3, Max: straightlineDistance * 3}, - {Min: -straightlineDistance * 3, Max: straightlineDistance * 3}, - {Min: -2 * math.Pi, Max: 2 * math.Pi}, - } // Note: this is only for diff drive, not used for PTGs - ms.logger.CDebugf(ctx, "base limits: %v", limits) - - kb, err := kinematicbase.WrapWithKinematics(ctx, b, ms.logger, localizer, limits, kinematicsOptions) - if err != nil { - return nil, err - } - - geomsRaw := spatialmath.GeoObstaclesToGeometries(obstacles, origin) - - mr, err := ms.createBaseMoveRequest( - ctx, - motionCfg, - ms.logger, - kb, - goalPoseRaw, - fs, - geomsRaw, - valExtra, - ) - if err != nil { - return nil, err - } - mr.seedPlan = seedPlan - mr.replanCostFactor = valExtra.replanCostFactor - mr.requestType = requestTypeMoveOnGlobe - mr.geoPoseOrigin = spatialmath.NewGeoPose(origin, heading) - return mr, nil -} - -// newMoveOnMapRequest instantiates a moveRequest intended to be used in the context of a MoveOnMap call. -func (ms *builtIn) newMoveOnMapRequest( - ctx context.Context, - req motion.MoveOnMapReq, - seedPlan motionplan.Plan, - replanCount int, -) (state.PlannerExecutor, error) { - valExtra, err := newValidatedExtra(req.Extra) - if err != nil { - return nil, err - } - - if valExtra.maxReplans >= 0 { - if replanCount > valExtra.maxReplans { - return nil, fmt.Errorf("exceeded maximum number of replans: %d", valExtra.maxReplans) - } - } - - motionCfg, err := newValidatedMotionCfg(req.MotionCfg, requestTypeMoveOnMap) - if err != nil { - return nil, err - } - - if req.Destination == nil { - return nil, errors.New("destination cannot be nil") - } - - // get the SLAM Service from the slamName - slamSvc, ok := ms.slamServices[req.SlamName] - if !ok { - return nil, resource.DependencyNotFoundError(req.SlamName) - } - - // gets the extents of the SLAM map - limits, err := slam.Limits(ctx, slamSvc, true) - if err != nil { - return nil, err - } - limits = append(limits, referenceframe.Limit{Min: -2 * math.Pi, Max: 2 * math.Pi}) - - // create a KinematicBase from the componentName - component, ok := ms.components[req.ComponentName] - if !ok { - return nil, resource.DependencyNotFoundError(req.ComponentName) - } - b, ok := component.(base.Base) - if !ok { - return nil, fmt.Errorf("cannot move component of type %T because it is not a Base", component) - } - - // build kinematic options - kinematicsOptions := kbOptionsFromCfg(motionCfg, valExtra) - - fs, err := ms.fsService.FrameSystem(ctx, nil) - if err != nil { - return nil, err - } - - // Create a localizer from the movement sensor, and collapse reported orientations to 2d - localizer := motion.TwoDLocalizer(motion.NewSLAMLocalizer(slamSvc)) - kb, err := kinematicbase.WrapWithKinematics(ctx, b, ms.logger, localizer, limits, kinematicsOptions) - if err != nil { - return nil, err - } - - goalPoseAdj := spatialmath.Compose(req.Destination, motion.SLAMOrientationAdjustment) - - // get point cloud data in the form of bytes from pcd - pointCloudData, err := slam.PointCloudMapFull(ctx, slamSvc, true) - if err != nil { - return nil, err - } - // store slam point cloud data in the form of a recursive octree for collision checking - octree, err := pointcloud.ReadPCDToBasicOctree(bytes.NewReader(pointCloudData)) - if err != nil { - return nil, err - } - - req.Obstacles = append(req.Obstacles, octree) - - mr, err := ms.createBaseMoveRequest( - ctx, - motionCfg, - ms.logger, - kb, - goalPoseAdj, - fs, - req.Obstacles, - valExtra, - ) - if err != nil { - return nil, err - } - mr.requestType = requestTypeMoveOnMap - return mr, nil -} - -func (ms *builtIn) createBaseMoveRequest( - ctx context.Context, - motionCfg *validatedMotionConfiguration, - logger logging.Logger, - kb kinematicbase.KinematicBase, - goalPoseInWorld spatialmath.Pose, - fs referenceframe.FrameSystem, - worldObstacles []spatialmath.Geometry, - valExtra validatedExtra, -) (*moveRequest, error) { - // replace original base frame with one that knows how to move itself and allow planning for - kinematicFrame := kb.Kinematics() - if err := fs.ReplaceFrame(kinematicFrame); err != nil { - // If the base frame is not in the frame system, add it to world. This will result in planning for a frame system containing - // only world and the base after the FrameSystemSubset. - err = fs.AddFrame(kinematicFrame, fs.Frame(referenceframe.World)) - if err != nil { - return nil, err - } - } - // We want to disregard anything in the FS whose eventual parent is not the base, because we don't know where it is. - baseOnlyFS, err := fs.FrameSystemSubset(kinematicFrame) - if err != nil { - return nil, err - } - - startPoseIF, err := kb.CurrentPosition(ctx) - if err != nil { - return nil, err - } - startPose := startPoseIF.Pose() - - goal := referenceframe.NewPoseInFrame(referenceframe.World, goalPoseInWorld) - - // Here we determine if we already are at the goal - // If our motion profile is position_only then, we only check against our current & desired position - // Conversely if our motion profile is anything else, then we also need to check again our - // current & desired orientation - if valExtra.motionProfile == motionplan.PositionOnlyMotionProfile { - if spatialmath.PoseAlmostCoincidentEps(goal.Pose(), startPose, motionCfg.planDeviationMM) { - return nil, motion.ErrGoalWithinPlanDeviation - } - } else if spatialmath.OrientationAlmostEqual(goal.Pose().Orientation(), spatialmath.NewZeroPose().Orientation()) && - spatialmath.PoseAlmostCoincidentEps(goal.Pose(), startPose, motionCfg.planDeviationMM) { - return nil, motion.ErrGoalWithinPlanDeviation - } - - gif := referenceframe.NewGeometriesInFrame(referenceframe.World, worldObstacles) - worldState, err := referenceframe.NewWorldState([]*referenceframe.GeometriesInFrame{gif}, nil) - if err != nil { - return nil, err - } - - obstacleDetectors := make(map[vision.Service][]resource.Name) - for _, obstacleDetectorNamePair := range motionCfg.obstacleDetectors { - // get vision service - visionServiceName := obstacleDetectorNamePair.VisionServiceName - visionSvc, ok := ms.visionServices[visionServiceName] - if !ok { - return nil, resource.DependencyNotFoundError(visionServiceName) - } - - // add camera to vision service map - camList, ok := obstacleDetectors[visionSvc] - if !ok { - obstacleDetectors[visionSvc] = []resource.Name{obstacleDetectorNamePair.CameraName} - } else { - camList = append(camList, obstacleDetectorNamePair.CameraName) - obstacleDetectors[visionSvc] = camList - } - } - - currentInputs, _, err := ms.fsService.CurrentInputs(ctx) - if err != nil { - return nil, err - } - - var backgroundWorkers sync.WaitGroup - - // effectively don't poll if the PositionPollingFreqHz is not provided - positionPollingFreq := time.Duration(math.MaxInt64) - if motionCfg.positionPollingFreqHz > 0 { - positionPollingFreq = time.Duration(1000/motionCfg.positionPollingFreqHz) * time.Millisecond - } - - // effectively don't poll if the ObstaclePollingFreqHz is not provided - obstaclePollingFreq := time.Duration(math.MaxInt64) - if motionCfg.obstaclePollingFreqHz > 0 { - obstaclePollingFreq = time.Duration(1000/motionCfg.obstaclePollingFreqHz) * time.Millisecond - } - - mr := &moveRequest{ - config: motionCfg, - logger: ms.logger, - planRequest: &motionplan.PlanRequest{ - Logger: logger, - Goal: goal, - Frame: kinematicFrame, - FrameSystem: baseOnlyFS, - StartConfiguration: currentInputs, - StartPose: startPose, - WorldState: worldState, - Options: valExtra.extra, - }, - poseOrigin: startPose, - kinematicBase: kb, - replanCostFactor: valExtra.replanCostFactor, - obstacleDetectors: obstacleDetectors, - fsService: ms.fsService, - - executeBackgroundWorkers: &backgroundWorkers, - - responseChan: make(chan moveResponse, 1), - } - - // TODO: Change deviatedFromPlan to just query positionPollingFreq on the struct & the same for the obstaclesIntersectPlan - mr.position = newReplanner(positionPollingFreq, mr.deviatedFromPlan) - mr.obstacle = newReplanner(obstaclePollingFreq, mr.obstaclesIntersectPlan) - return mr, nil -} - -type moveResponse struct { - err error - executeResponse state.ExecuteResponse -} - -func (mr moveResponse) String() string { - return fmt.Sprintf("builtin.moveResponse{executeResponse: %#v, err: %v}", mr.executeResponse, mr.err) -} - -func (mr *moveRequest) start(ctx context.Context, plan motionplan.Plan) { - if ctx.Err() != nil { - return - } - mr.executeBackgroundWorkers.Add(1) - goutils.ManagedGo(func() { - mr.position.startPolling(ctx, plan) - }, mr.executeBackgroundWorkers.Done) - - mr.executeBackgroundWorkers.Add(1) - goutils.ManagedGo(func() { - mr.obstacle.startPolling(ctx, plan) - }, mr.executeBackgroundWorkers.Done) - - // spawn function to execute the plan on the robot - mr.executeBackgroundWorkers.Add(1) - goutils.ManagedGo(func() { - executeResp, err := mr.execute(ctx, plan) - resp := moveResponse{executeResponse: executeResp, err: err} - mr.responseChan <- resp - }, mr.executeBackgroundWorkers.Done) -} - -func (mr *moveRequest) listen(ctx context.Context) (state.ExecuteResponse, error) { - select { - case <-ctx.Done(): - mr.logger.CDebugf(ctx, "context err: %s", ctx.Err()) - return state.ExecuteResponse{}, ctx.Err() - - case resp := <-mr.responseChan: - mr.logger.CDebugf(ctx, "execution response: %s", resp) - return resp.executeResponse, resp.err - - case resp := <-mr.position.responseChan: - mr.logger.CDebugf(ctx, "position response: %s", resp) - return resp.executeResponse, resp.err - - case resp := <-mr.obstacle.responseChan: - mr.logger.CDebugf(ctx, "obstacle response: %s", resp) - return resp.executeResponse, resp.err - } -} - -func (mr *moveRequest) stop() error { - stopCtx, cancelFn := context.WithTimeout(context.Background(), baseStopTimeout) - defer cancelFn() - if stopErr := mr.kinematicBase.Stop(stopCtx, nil); stopErr != nil { - mr.logger.Errorf("kinematicBase.Stop returned error %s", stopErr) - return stopErr - } - return nil -} diff --git a/services/motion/builtin/replanner.go b/services/motion/builtin/replanner.go deleted file mode 100644 index d27f5c2d448..00000000000 --- a/services/motion/builtin/replanner.go +++ /dev/null @@ -1,63 +0,0 @@ -package builtin - -import ( - "context" - "fmt" - "time" - - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/services/motion/builtin/state" -) - -// replanResponse is the struct returned by the replanner. -type replanResponse struct { - err error - executeResponse state.ExecuteResponse -} - -// replanFn is an alias for a function that will be polled by a replanner. -type replanFn func(context.Context, motionplan.Plan) (state.ExecuteResponse, error) - -func (rr replanResponse) String() string { - return fmt.Sprintf("builtin.replanResponse{executeResponse: %#v, err: %v}", rr.executeResponse, rr.err) -} - -// replanner bundles everything needed to execute a function at a given interval and return. -type replanner struct { - period time.Duration - responseChan chan replanResponse - - // needReplan is a function that returns a bool describing if a replan is needed, as well as an error - needReplan replanFn -} - -// newReplanner is a constructor for a replanner. -func newReplanner(period time.Duration, fnToPoll replanFn) *replanner { - return &replanner{ - period: period, - needReplan: fnToPoll, - responseChan: make(chan replanResponse, 1), - } -} - -// startPolling executes the replanner's configured function at its configured period -// The caller of this function should read from the replanner's responseChan to know when a replan is requested. -func (r *replanner) startPolling(ctx context.Context, plan motionplan.Plan) { - ticker := time.NewTicker(r.period) - defer ticker.Stop() - - // this check ensures that if the context is cancelled we always return early at the top of the loop - for ctx.Err() == nil { - select { - case <-ctx.Done(): - return - case <-ticker.C: - executeResp, err := r.needReplan(ctx, plan) - if err != nil || executeResp.Replan { - res := replanResponse{executeResponse: executeResp, err: err} - r.responseChan <- res - return - } - } - } -} diff --git a/services/motion/builtin/state/state.go b/services/motion/builtin/state/state.go deleted file mode 100644 index 9091563d579..00000000000 --- a/services/motion/builtin/state/state.go +++ /dev/null @@ -1,674 +0,0 @@ -// Package state provides apis for motion builtin plan executions -// and manages the state of those executions -package state - -import ( - "cmp" - "context" - "fmt" - "slices" - "sync" - "time" - - "github.com/google/uuid" - "github.com/pkg/errors" - "go.viam.com/utils" - "golang.org/x/exp/maps" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" -) - -// PlannerExecutor implements Plan and Execute. -type PlannerExecutor interface { - Plan(ctx context.Context) (motionplan.Plan, error) - Execute(context.Context, motionplan.Plan) (ExecuteResponse, error) - AnchorGeoPose() *spatialmath.GeoPose -} - -// ExecuteResponse is the response from Execute. -type ExecuteResponse struct { - // If true, the Execute function didn't reach the goal & the caller should replan - Replan bool - // Set if Replan is true, describes why replanning was triggered - ReplanReason string -} - -// PlannerExecutorConstructor creates a PlannerExecutor -// if ctx is cancelled then all PlannerExecutor interface -// methods must terminate & return errors -// req is the request that will be used during planning & execution -// seedPlan (nil during the first plan) is the previous plan -// if replanning has occurred -// replanCount is the number of times replanning has occurred, -// zero the first time planning occurs. -// R is a genric type which is able to be used to create a PlannerExecutor. -type PlannerExecutorConstructor[R any] func( - ctx context.Context, - req R, - seedPlan motionplan.Plan, - replanCount int, -) (PlannerExecutor, error) - -type componentState struct { - executionIDHistory []motion.ExecutionID - executionsByID map[motion.ExecutionID]stateExecution -} - -type planMsg struct { - plan motion.PlanWithMetadata - planStatus motion.PlanStatus -} - -type stateUpdateMsg struct { - componentName resource.Name - executionID motion.ExecutionID - planID motion.PlanID - planStatus motion.PlanStatus -} - -// a stateExecution is the struct held in the state that -// holds the history of plans & plan status updates an -// execution has exprienced & the waitGroup & cancelFunc -// required to shut down an execution's goroutine. -type stateExecution struct { - id motion.ExecutionID - componentName resource.Name - waitGroup *sync.WaitGroup - cancelFunc context.CancelFunc - history []motion.PlanWithStatus -} - -func (e *stateExecution) stop() { - e.cancelFunc() - e.waitGroup.Wait() -} - -func (cs componentState) lastExecution() stateExecution { - return cs.executionsByID[cs.lastExecutionID()] -} - -func (cs componentState) lastExecutionID() motion.ExecutionID { - return cs.executionIDHistory[0] -} - -// execution represents the state of a motion planning execution. -// it only ever exists in state.StartExecution function & the go routine created. -type execution[R any] struct { - id motion.ExecutionID - state *State - waitGroup *sync.WaitGroup - cancelCtx context.Context - cancelFunc context.CancelFunc - logger logging.Logger - componentName resource.Name - req R - plannerExecutorConstructor PlannerExecutorConstructor[R] -} - -type planWithExecutor struct { - plan motion.PlanWithMetadata - executor PlannerExecutor -} - -// NewPlan creates a new motion.Plan from an execution & returns an error if one was not able to be created. -func (e *execution[R]) newPlanWithExecutor(ctx context.Context, seedPlan motionplan.Plan, replanCount int) (planWithExecutor, error) { - pe, err := e.plannerExecutorConstructor(e.cancelCtx, e.req, seedPlan, replanCount) - if err != nil { - return planWithExecutor{}, err - } - plan, err := pe.Plan(ctx) - if err != nil { - return planWithExecutor{}, err - } - return planWithExecutor{ - plan: motion.PlanWithMetadata{ - Plan: plan, - ID: uuid.New(), - ExecutionID: e.id, - ComponentName: e.componentName, - AnchorGeoPose: pe.AnchorGeoPose(), - }, - executor: pe, - }, nil -} - -// Start starts an execution with a given plan. -func (e *execution[R]) start(ctx context.Context) error { - var replanCount int - originalPlanWithExecutor, err := e.newPlanWithExecutor(ctx, nil, replanCount) - if err != nil { - return err - } - e.notifyStateNewExecution(e.toStateExecution(), originalPlanWithExecutor.plan, time.Now()) - // We need to add to both the state & execution waitgroups - // B/c both the state & the stateExecution need to know if this - // goroutine have termianted. - // state.Stop() needs to wait for ALL execution goroutines to terminate before - // returning in order to not leak. - // Similarly stateExecution.stop(), which is called by state.StopExecutionByResource - // needs to wait for its 1 execution go routine to termiante before returning. - // As a result, both waitgroups need to be written to. - e.state.waitGroup.Add(1) - e.waitGroup.Add(1) - utils.PanicCapturingGo(func() { - defer e.state.waitGroup.Done() - defer e.waitGroup.Done() - defer e.cancelFunc() - - lastPWE := originalPlanWithExecutor - // Exit conditions of this loop: - // 1. The execution's context was cancelled, which happens if the state's Stop() was called or - // StopExecutionByResource was called for this resource - // 2. the execution succeeded - // 3. the execution failed - // 4. replanning failed - for { - resp, err := lastPWE.executor.Execute(e.cancelCtx, lastPWE.plan.Plan) - - switch { - // stopped - case errors.Is(err, context.Canceled): - e.notifyStatePlanStopped(lastPWE.plan, time.Now()) - return - - // failure - case err != nil: - e.notifyStatePlanFailed(lastPWE.plan, err.Error(), time.Now()) - return - - // success - case !resp.Replan: - e.notifyStatePlanSucceeded(lastPWE.plan, time.Now()) - return - - // replan - default: - replanCount++ - newPWE, err := e.newPlanWithExecutor(e.cancelCtx, lastPWE.plan.Plan, replanCount) - // replan failed - if err != nil { - msg := "failed to replan for execution %s and component: %s, " + - "due to replan reason: %s, tried setting previous plan %s " + - "to failed due to error: %s\n" - e.logger.CWarnf(ctx, msg, e.id, e.componentName, resp.ReplanReason, lastPWE.plan.ID, err.Error()) - - e.notifyStatePlanFailed(lastPWE.plan, err.Error(), time.Now()) - return - } - - e.notifyStateReplan(lastPWE.plan, resp.ReplanReason, newPWE.plan, time.Now()) - lastPWE = newPWE - } - } - }) - - return nil -} - -func (e *execution[R]) toStateExecution() stateExecution { - return stateExecution{ - id: e.id, - componentName: e.componentName, - waitGroup: e.waitGroup, - cancelFunc: e.cancelFunc, - } -} - -func (e *execution[R]) notifyStateNewExecution(execution stateExecution, plan motion.PlanWithMetadata, time time.Time) { - e.state.mu.Lock() - defer e.state.mu.Unlock() - // NOTE: We hold the lock for both updateStateNewExecution & updateStateNewPlan to ensure no readers - // are able to see a state where the execution exists but does not have a plan with a status. - e.state.updateStateNewExecution(execution) - e.state.updateStateNewPlan(planMsg{ - plan: plan, - planStatus: motion.PlanStatus{State: motion.PlanStateInProgress, Timestamp: time}, - }) -} - -func (e *execution[R]) notifyStateReplan(lastPlan motion.PlanWithMetadata, reason string, newPlan motion.PlanWithMetadata, time time.Time) { - e.state.mu.Lock() - defer e.state.mu.Unlock() - // NOTE: We hold the lock for both updateStateNewExecution & updateStateNewPlan to ensure no readers - // are able to see a state where the old plan is failed withou a new plan in progress during replanning - e.state.updateStateStatusUpdate(stateUpdateMsg{ - componentName: e.componentName, - executionID: e.id, - planID: lastPlan.ID, - planStatus: motion.PlanStatus{State: motion.PlanStateFailed, Timestamp: time, Reason: &reason}, - }) - - e.state.updateStateNewPlan(planMsg{ - plan: newPlan, - planStatus: motion.PlanStatus{State: motion.PlanStateInProgress, Timestamp: time}, - }) -} - -func (e *execution[R]) notifyStatePlanFailed(plan motion.PlanWithMetadata, reason string, time time.Time) { - e.state.mu.Lock() - defer e.state.mu.Unlock() - e.state.updateStateStatusUpdate(stateUpdateMsg{ - componentName: e.componentName, - executionID: e.id, - planID: plan.ID, - planStatus: motion.PlanStatus{State: motion.PlanStateFailed, Timestamp: time, Reason: &reason}, - }) -} - -func (e *execution[R]) notifyStatePlanSucceeded(plan motion.PlanWithMetadata, time time.Time) { - e.state.mu.Lock() - defer e.state.mu.Unlock() - e.state.updateStateStatusUpdate(stateUpdateMsg{ - componentName: e.componentName, - executionID: e.id, - planID: plan.ID, - planStatus: motion.PlanStatus{State: motion.PlanStateSucceeded, Timestamp: time}, - }) -} - -func (e *execution[R]) notifyStatePlanStopped(plan motion.PlanWithMetadata, time time.Time) { - e.state.mu.Lock() - defer e.state.mu.Unlock() - e.state.updateStateStatusUpdate(stateUpdateMsg{ - componentName: e.componentName, - executionID: e.id, - planID: plan.ID, - planStatus: motion.PlanStatus{State: motion.PlanStateStopped, Timestamp: time}, - }) -} - -// State is the state of the builtin motion service -// It keeps track of the builtin motion service's executions. -type State struct { - waitGroup *sync.WaitGroup - cancelCtx context.Context - cancelFunc context.CancelFunc - logger logging.Logger - ttl time.Duration - // mu protects the componentStateByComponent - mu sync.RWMutex - componentStateByComponent map[resource.Name]componentState -} - -// NewState creates a new state. -// Takes a [TTL](https://en.wikipedia.org/wiki/Time_to_live) -// and an interval to delete any State data that is older than -// the TTL. -func NewState( - ttl time.Duration, - ttlCheckInterval time.Duration, - logger logging.Logger, -) (*State, error) { - if ttl == 0 { - return nil, errors.New("TTL can't be unset") - } - - if ttlCheckInterval == 0 { - return nil, errors.New("TTLCheckInterval can't be unset") - } - - if logger == nil { - return nil, errors.New("Logger can't be nil") - } - - if ttl < ttlCheckInterval { - return nil, errors.New("TTL can't be lower than the TTLCheckInterval") - } - - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - s := State{ - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - waitGroup: &sync.WaitGroup{}, - componentStateByComponent: make(map[resource.Name]componentState), - ttl: ttl, - logger: logger, - } - s.waitGroup.Add(1) - utils.ManagedGo(func() { - ticker := time.NewTicker(ttlCheckInterval) - defer ticker.Stop() - for { - if cancelCtx.Err() != nil { - return - } - - select { - case <-cancelCtx.Done(): - return - case <-ticker.C: - err := s.purgeOlderThanTTL() - if err != nil { - s.logger.Error(err.Error()) - } - } - } - }, s.waitGroup.Done) - return &s, nil -} - -// StartExecution creates a new execution from a state. -func StartExecution[R any]( - ctx context.Context, - s *State, - componentName resource.Name, - req R, - plannerExecutorConstructor PlannerExecutorConstructor[R], -) (motion.ExecutionID, error) { - if s == nil { - return uuid.Nil, errors.New("state is nil") - } - - if err := s.ValidateNoActiveExecutionID(componentName); err != nil { - return uuid.Nil, err - } - - // the state being cancelled should cause all executions derived from that state to also be cancelled - cancelCtx, cancelFunc := context.WithCancel(s.cancelCtx) - e := execution[R]{ - id: uuid.New(), - state: s, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - waitGroup: &sync.WaitGroup{}, - logger: s.logger, - req: req, - componentName: componentName, - plannerExecutorConstructor: plannerExecutorConstructor, - } - - if err := e.start(ctx); err != nil { - return uuid.Nil, err - } - - return e.id, nil -} - -// Stop stops all executions within the State. -func (s *State) Stop() { - s.cancelFunc() - s.waitGroup.Wait() -} - -// StopExecutionByResource stops the active execution with a given resource name in the State. -func (s *State) StopExecutionByResource(componentName resource.Name) error { - // Read lock held to get the execution - s.mu.RLock() - componentExectionState, exists := s.componentStateByComponent[componentName] - - // return error if component name is not in StateMap - if !exists { - s.mu.RUnlock() - return resource.NewNotFoundError(componentName) - } - - e, exists := componentExectionState.executionsByID[componentExectionState.lastExecutionID()] - if !exists { - s.mu.RUnlock() - return resource.NewNotFoundError(componentName) - } - s.mu.RUnlock() - - // lock released while waiting for the execution to stop as the execution stopping requires writing to the state - // which must take a lock - e.stop() - return nil -} - -// PlanHistory returns the plans with statuses of the resource -// By default returns all plans from the most recent execution of the resoure -// If the ExecutionID is provided, returns the plans of the ExecutionID rather -// than the most recent execution -// If LastPlanOnly is provided then only the last plan is returned for the execution -// with the ExecutionID if it is provided, or the last execution -// for that component otherwise. -func (s *State) PlanHistory(req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - s.mu.RLock() - defer s.mu.RUnlock() - cs, exists := s.componentStateByComponent[req.ComponentName] - if !exists { - return nil, resource.NewNotFoundError(req.ComponentName) - } - - executionID := req.ExecutionID - - // last plan only - if req.LastPlanOnly { - if ex := cs.lastExecution(); executionID == uuid.Nil || executionID == ex.id { - return renderableHistory(ex.history[:1]), nil - } - - // if executionID is provided & doesn't match the last execution for the component - if ex, exists := cs.executionsByID[executionID]; exists { - return renderableHistory(ex.history[:1]), nil - } - return nil, resource.NewNotFoundError(req.ComponentName) - } - - // specific execution id when lastPlanOnly is NOT enabled - if executionID != uuid.Nil { - if ex, exists := cs.executionsByID[executionID]; exists { - return renderableHistory(ex.history), nil - } - return nil, resource.NewNotFoundError(req.ComponentName) - } - - return renderableHistory(cs.lastExecution().history), nil -} - -// visualHistory returns the history struct that has had its plans Offset by. -func renderableHistory(history []motion.PlanWithStatus) []motion.PlanWithStatus { - newHistory := make([]motion.PlanWithStatus, len(history)) - copy(newHistory, history) - for i := range newHistory { - newHistory[i].Plan = newHistory[i].Plan.Renderable() - } - return newHistory -} - -// ListPlanStatuses returns the status of plans created by MoveOnGlobe requests -// that are executing OR are part of an execution which changed it state -// within the a 24HR TTL OR until the robot reinitializes. -// If OnlyActivePlans is provided, only returns plans which are in non terminal states. -func (s *State) ListPlanStatuses(req motion.ListPlanStatusesReq) ([]motion.PlanStatusWithID, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - statuses := []motion.PlanStatusWithID{} - componentNames := maps.Keys(s.componentStateByComponent) - slices.SortFunc(componentNames, func(a, b resource.Name) int { - return cmp.Compare(a.String(), b.String()) - }) - - if req.OnlyActivePlans { - for _, name := range componentNames { - if e, err := s.activeExecution(name); err == nil { - statuses = append(statuses, motion.PlanStatusWithID{ - ExecutionID: e.id, - ComponentName: e.componentName, - PlanID: e.history[0].Plan.ID, - Status: e.history[0].StatusHistory[0], - }) - } - } - return statuses, nil - } - - for _, name := range componentNames { - cs, ok := s.componentStateByComponent[name] - if !ok { - return nil, errors.New("state is corrupted") - } - for _, executionID := range cs.executionIDHistory { - e, exists := cs.executionsByID[executionID] - if !exists { - return nil, errors.New("state is corrupted") - } - for _, pws := range e.history { - statuses = append(statuses, motion.PlanStatusWithID{ - ExecutionID: e.id, - ComponentName: e.componentName, - PlanID: pws.Plan.ID, - Status: pws.StatusHistory[0], - }) - } - } - } - - return statuses, nil -} - -// ValidateNoActiveExecutionID returns an error if there is already an active -// Execution for the resource name within the State. -func (s *State) ValidateNoActiveExecutionID(name resource.Name) error { - if es, err := s.activeExecution(name); err == nil { - return fmt.Errorf("there is already an active executionID: %s", es.id) - } - return nil -} - -func (s *State) updateStateNewExecution(newE stateExecution) { - cs, exists := s.componentStateByComponent[newE.componentName] - - if exists { - _, exists = cs.executionsByID[newE.id] - if exists { - err := fmt.Errorf("unexpected ExecutionID already exists %s", newE.id) - s.logger.Error(err.Error()) - return - } - cs.executionsByID[newE.id] = newE - cs.executionIDHistory = append([]motion.ExecutionID{newE.id}, cs.executionIDHistory...) - s.componentStateByComponent[newE.componentName] = cs - } else { - s.componentStateByComponent[newE.componentName] = componentState{ - executionIDHistory: []motion.ExecutionID{newE.id}, - executionsByID: map[motion.ExecutionID]stateExecution{newE.id: newE}, - } - } -} - -func (s *State) updateStateNewPlan(newPlan planMsg) { - if newPlan.planStatus.State != motion.PlanStateInProgress { - err := errors.New("handleNewPlan received a plan status other than in progress") - s.logger.Error(err.Error()) - return - } - - activeExecutionID := s.componentStateByComponent[newPlan.plan.ComponentName].lastExecutionID() - if newPlan.plan.ExecutionID != activeExecutionID { - e := "got new plan for inactive execution: active executionID %s, planID: %s, component: %s, plan executionID: %s" - err := fmt.Errorf(e, activeExecutionID, newPlan.plan.ID, newPlan.plan.ComponentName, newPlan.plan.ExecutionID) - s.logger.Error(err.Error()) - return - } - execution := s.componentStateByComponent[newPlan.plan.ComponentName].executionsByID[newPlan.plan.ExecutionID] - pws := []motion.PlanWithStatus{{Plan: newPlan.plan, StatusHistory: []motion.PlanStatus{newPlan.planStatus}}} - // prepend to executions.history so that lower indices are newer - execution.history = append(pws, execution.history...) - - s.componentStateByComponent[newPlan.plan.ComponentName].executionsByID[newPlan.plan.ExecutionID] = execution -} - -func (s *State) updateStateStatusUpdate(update stateUpdateMsg) { - switch update.planStatus.State { - // terminal states - case motion.PlanStateSucceeded, motion.PlanStateFailed, motion.PlanStateStopped: - default: - err := fmt.Errorf("unexpected PlanState %v in update %#v", update.planStatus.State, update) - s.logger.Error(err.Error()) - return - } - componentExecutions, exists := s.componentStateByComponent[update.componentName] - if !exists { - err := errors.New("updated component doesn't exist") - s.logger.Error(err.Error()) - return - } - // copy the execution - execution := componentExecutions.executionsByID[update.executionID] - lastPlanWithStatus := execution.history[0] - if lastPlanWithStatus.Plan.ID != update.planID { - err := fmt.Errorf("status update for plan %s is not for last plan: %s", update.planID, lastPlanWithStatus.Plan.ID) - s.logger.Error(err.Error()) - return - } - lastPlanWithStatus.StatusHistory = append([]motion.PlanStatus{update.planStatus}, lastPlanWithStatus.StatusHistory...) - // write updated last plan back to history - execution.history[0] = lastPlanWithStatus - // write the execution with the new history to the component execution state copy - componentExecutions.executionsByID[update.executionID] = execution - // write the component execution state copy back to the state - s.componentStateByComponent[update.componentName] = componentExecutions -} - -func (s *State) activeExecution(name resource.Name) (stateExecution, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - if cs, exists := s.componentStateByComponent[name]; exists { - es := cs.lastExecution() - - if _, exists := motion.TerminalStateSet[es.history[0].StatusHistory[0].State]; exists { - return stateExecution{}, resource.NewNotFoundError(name) - } - return es, nil - } - return stateExecution{}, resource.NewNotFoundError(name) -} - -func (s *State) purgeOlderThanTTL() error { - s.mu.Lock() - defer s.mu.Unlock() - - purgeCutoff := time.Now().Add(-s.ttl) - - for resource, componentState := range s.componentStateByComponent { - keepIndex, err := findKeepIndex(componentState, purgeCutoff) - if err != nil { - return err - } - // If there are no executions to keep, then delete the resource. - if keepIndex == -1 { - delete(s.componentStateByComponent, resource) - continue - } - - executionIdsToKeep := componentState.executionIDHistory[:keepIndex+1] - executionIdsToDelete := componentState.executionIDHistory[keepIndex+1:] - - for _, executionID := range executionIdsToDelete { - delete(componentState.executionsByID, executionID) - } - componentState.executionIDHistory = executionIdsToKeep - s.componentStateByComponent[resource] = componentState - } - return nil -} - -// findKeepIndex returns the index of the executionHistory slice which should be kept -// after purging i.e. are after the purgeCutoff -// returns -1 if none of the executions are after the cutoff i.e. if all need to be purged. -func findKeepIndex(componentState componentState, purgeCutoff time.Time) (int, error) { - // iterate in reverse order (i.e. from oldest execution to newest execution) - for executionIndex := len(componentState.executionIDHistory) - 1; executionIndex >= 0; executionIndex-- { - executionID := componentState.executionIDHistory[executionIndex] - execution, ok := componentState.executionsByID[executionID] - if !ok { - msg := "executionID %s exists at index %d of executionIDHistory but is not present in executionsByID" - return 0, fmt.Errorf(msg, executionID, executionIndex) - } - - mostRecentStatus := execution.history[0].StatusHistory[0] - _, terminated := motion.TerminalStateSet[mostRecentStatus.State] - withinTTL := mostRecentStatus.Timestamp.After(purgeCutoff) - if withinTTL || !terminated { - return executionIndex, nil - } - } - return -1, nil -} diff --git a/services/motion/builtin/state/state_test.go b/services/motion/builtin/state/state_test.go deleted file mode 100644 index 05add9df2e5..00000000000 --- a/services/motion/builtin/state/state_test.go +++ /dev/null @@ -1,1053 +0,0 @@ -package state_test - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/google/uuid" - "go.viam.com/test" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/motion/builtin/state" - "go.viam.com/rdk/spatialmath" -) - -var ( - replanReason = "replan triggered due to location drift" - ttl = time.Hour * 24 - ttlCheckInterval = time.Second -) - -// testPlannerExecutor is a mock PlannerExecutor implementation. -type testPlannerExecutor struct { - planFunc func(context.Context) (motionplan.Plan, error) - executeFunc func(context.Context, motionplan.Plan) (state.ExecuteResponse, error) - anchorGeoPoseFunc func() *spatialmath.GeoPose -} - -// by default Plan successfully returns an empty plan. -func (tpe *testPlannerExecutor) Plan(ctx context.Context) (motionplan.Plan, error) { - if tpe.planFunc != nil { - return tpe.planFunc(ctx) - } - return nil, nil -} - -// by default Execute returns a success response. -func (tpe *testPlannerExecutor) Execute(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if tpe.executeFunc != nil { - return tpe.executeFunc(ctx, plan) - } - return state.ExecuteResponse{}, nil -} - -func (tpe *testPlannerExecutor) AnchorGeoPose() *spatialmath.GeoPose { - if tpe.anchorGeoPoseFunc != nil { - return tpe.anchorGeoPoseFunc() - } - return nil -} - -func TestState(t *testing.T) { - logger := logging.NewTestLogger(t) - myBase := base.Named("mybase") - t.Parallel() - - executionWaitingForCtxCancelledPlanConstructor := func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedplan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - <-ctx.Done() - return state.ExecuteResponse{}, ctx.Err() - }, - }, nil - } - - successPlanConstructor := func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedplan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if err := ctx.Err(); err != nil { - return state.ExecuteResponse{}, err - } - return state.ExecuteResponse{}, nil - }, - }, nil - } - - replanPlanConstructor := func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedplan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if err := ctx.Err(); err != nil { - return state.ExecuteResponse{}, err - } - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - }}, nil - } - - failedExecutionPlanConstructor := func( - ctx context.Context, - _ motion.MoveOnGlobeReq, - _ motionplan.Plan, - _ int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if err := ctx.Err(); err != nil { - return state.ExecuteResponse{}, err - } - return state.ExecuteResponse{}, errors.New("execution failed") - }}, nil - } - - //nolint:unparam - failedPlanningPlanConstructor := func( - ctx context.Context, - _ motion.MoveOnGlobeReq, - _ motionplan.Plan, - _ int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - planFunc: func(context.Context) (motionplan.Plan, error) { - return nil, errors.New("planning failed") - }, - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - t.Fatal("should not be called as planning failed") //nolint:revive - - if err := ctx.Err(); err != nil { - return state.ExecuteResponse{}, err - } - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - }, - }, nil - } - - failedReplanningPlanConstructor := func( - ctx context.Context, - _ motion.MoveOnGlobeReq, - _ motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - // first replan fails during planning - if replanCount == 1 { - return &testPlannerExecutor{ - planFunc: func(ctx context.Context) (motionplan.Plan, error) { - return nil, errors.New("planning failed") - }, - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if err := ctx.Err(); err != nil { - return state.ExecuteResponse{}, err - } - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - }, - }, nil - } - // first plan generates a plan but execution triggers a replan - return &testPlannerExecutor{ - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if err := ctx.Err(); err != nil { - return state.ExecuteResponse{}, err - } - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - }, - }, nil - } - - emptyReq := motion.MoveOnGlobeReq{ComponentName: myBase} - ctx := context.Background() - - t.Run("returns error if TTL is not set", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(0, 0, logger) - test.That(t, err, test.ShouldBeError, errors.New("TTL can't be unset")) - test.That(t, s, test.ShouldBeNil) - }) - - t.Run("returns error if TTLCheckInterval is not set", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(2, 0, logger) - test.That(t, err, test.ShouldBeError, errors.New("TTLCheckInterval can't be unset")) - test.That(t, s, test.ShouldBeNil) - }) - - t.Run("returns error if Logger is nil", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(2, 1, nil) - test.That(t, err, test.ShouldBeError, errors.New("Logger can't be nil")) - test.That(t, s, test.ShouldBeNil) - }) - - t.Run("returns error if TTL < TTLCheckInterval", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(1, 2, logger) - test.That(t, err, test.ShouldBeError, errors.New("TTL can't be lower than the TTLCheckInterval")) - test.That(t, s, test.ShouldBeNil) - }) - - t.Run("creating & stopping a state with no intermediary calls", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - }) - - t.Run("starting a new execution & stopping the state", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - _, err = state.StartExecution(ctx, s, emptyReq.ComponentName, emptyReq, successPlanConstructor) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("starting & stopping an execution & stopping the state", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - - _, err = state.StartExecution(ctx, s, emptyReq.ComponentName, emptyReq, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - - _, err = state.StartExecution(ctx, s, emptyReq.ComponentName, emptyReq, successPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - - _, err = state.StartExecution(ctx, s, emptyReq.ComponentName, emptyReq, replanPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - - _, err = state.StartExecution(ctx, s, emptyReq.ComponentName, emptyReq, failedExecutionPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - - _, err = state.StartExecution(ctx, s, emptyReq.ComponentName, emptyReq, failedPlanningPlanConstructor) - test.That(t, err, test.ShouldBeError, errors.New("planning failed")) - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - - _, err = state.StartExecution(ctx, s, emptyReq.ComponentName, emptyReq, failedReplanningPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("stopping an execution is idempotnet", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - req := motion.MoveOnGlobeReq{ComponentName: myBase} - _, err = state.StartExecution(ctx, s, req.ComponentName, req, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("stopping the state is idempotnet", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - req := motion.MoveOnGlobeReq{ComponentName: myBase} - _, err = state.StartExecution(ctx, s, req.ComponentName, req, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - s.Stop() - s.Stop() - }) - - t.Run("stopping an execution after stopping the state", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - req := motion.MoveOnGlobeReq{ComponentName: myBase} - _, err = state.StartExecution(ctx, s, req.ComponentName, req, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - s.Stop() - - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("querying for an unknown resource returns an unknown resource error", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - req := motion.MoveOnGlobeReq{ComponentName: myBase} - _, err = state.StartExecution(ctx, s, req.ComponentName, req, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - req2 := motion.PlanHistoryReq{} - _, err = s.PlanHistory(req2) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(req2.ComponentName)) - }) - - t.Run("end to end test", func(t *testing.T) { - t.Parallel() - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - - // no plan statuses as no executions have been created - ps, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ps, test.ShouldBeEmpty) - - preExecution := time.Now() - // Failing to plan the first time results in an error - req := motion.MoveOnGlobeReq{ComponentName: myBase} - id, err := state.StartExecution(ctx, s, req.ComponentName, req, failedPlanningPlanConstructor) - test.That(t, err, test.ShouldBeError, errors.New("planning failed")) - test.That(t, id, test.ShouldResemble, uuid.Nil) - - // still no plan statuses as no executions have been created - ps2, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ps2, test.ShouldBeEmpty) - - req = motion.MoveOnGlobeReq{ComponentName: myBase} - executionID1, err := state.StartExecution(ctx, s, req.ComponentName, req, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - cancelCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*500) - defer cancelFn() - // poll until ListPlanStatuses response has length 1 - resPS, succ := pollUntil(cancelCtx, func() (struct { - ps []motion.PlanStatusWithID - err error - }, bool, - ) { - st := struct { - ps []motion.PlanStatusWithID - err error - }{} - ps, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - if err == nil && len(ps) == 1 { - st.ps = ps - st.err = err - return st, true - } - return st, false - }) - - test.That(t, succ, test.ShouldBeTrue) - test.That(t, resPS.err, test.ShouldBeNil) - // we now have a single plan status as an execution has been created - test.That(t, len(resPS.ps), test.ShouldEqual, 1) - test.That(t, resPS.ps[0].ExecutionID, test.ShouldResemble, executionID1) - test.That(t, resPS.ps[0].ComponentName, test.ShouldResemble, req.ComponentName) - test.That(t, resPS.ps[0].PlanID, test.ShouldNotEqual, uuid.Nil) - test.That(t, resPS.ps[0].Status.State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, resPS.ps[0].Status.Reason, test.ShouldBeNil) - test.That(t, resPS.ps[0].Status.Timestamp.After(preExecution), test.ShouldBeTrue) - - id, err = state.StartExecution(ctx, s, req.ComponentName, req, replanPlanConstructor) - test.That(t, err, test.ShouldBeError, fmt.Errorf("there is already an active executionID: %s", executionID1)) - test.That(t, id, test.ShouldResemble, uuid.Nil) - - // Returns results if active plans are requested & there are active plans - ps4, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{OnlyActivePlans: true}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ps4, test.ShouldResemble, resPS.ps) - - // We see that the component has an excution with a single plan & that plan - // is in progress & has had no other statuses. - pws, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(pws), test.ShouldEqual, 1) - // plan id is the same as it was in the list status response - test.That(t, pws[0].Plan.ID, test.ShouldResemble, resPS.ps[0].PlanID) - test.That(t, pws[0].Plan.ExecutionID, test.ShouldEqual, executionID1) - test.That(t, pws[0].Plan.ComponentName, test.ShouldResemble, myBase) - test.That(t, len(pws[0].StatusHistory), test.ShouldEqual, 1) - test.That(t, pws[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, pws[0].StatusHistory[0].Reason, test.ShouldEqual, nil) - test.That(t, pws[0].StatusHistory[0].Timestamp.After(preExecution), test.ShouldBeTrue) - test.That(t, planStatusTimestampsInOrder(pws[0].StatusHistory), test.ShouldBeTrue) - - preStop := time.Now() - // stop the in progress execution - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - - ps5, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps5), test.ShouldEqual, 1) - test.That(t, ps5[0].ExecutionID, test.ShouldResemble, executionID1) - test.That(t, ps5[0].ComponentName, test.ShouldResemble, req.ComponentName) - test.That(t, ps5[0].PlanID, test.ShouldNotEqual, uuid.Nil) - // status now shows that the plan is stopped - test.That(t, ps5[0].Status.State, test.ShouldEqual, motion.PlanStateStopped) - test.That(t, ps5[0].Status.Reason, test.ShouldBeNil) - test.That(t, ps5[0].Status.Timestamp.After(preStop), test.ShouldBeTrue) - - // Returns no results if active plans are requested & there are no active plans - ps6, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{OnlyActivePlans: true}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ps6, test.ShouldBeEmpty) - - // We after stoping execution of the base that the same execution has the same - // plan, but that that plan's status is now stoped. - // The prior status is still in the status history. - pws2, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(pws2), test.ShouldEqual, 1) - test.That(t, pws2[0].Plan, test.ShouldResemble, pws[0].Plan) - test.That(t, len(pws2[0].StatusHistory), test.ShouldEqual, 2) - // previous in progres PlanStatus is now at a higher index - test.That(t, pws2[0].StatusHistory[1], test.ShouldResemble, pws[0].StatusHistory[0]) - // most recent PlanStatus is now that it is stopped - test.That(t, pws2[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateStopped) - test.That(t, pws2[0].StatusHistory[0].Reason, test.ShouldEqual, nil) - test.That(t, planStatusTimestampsInOrder(pws2[0].StatusHistory), test.ShouldBeTrue) - - preExecution2 := time.Now() - ctxReplanning, triggerReplanning := context.WithCancel(context.Background()) - ctxExecutionSuccess, triggerExecutionSuccess := context.WithCancel(context.Background()) - executionID2, err := state.StartExecution(ctx, s, req.ComponentName, req, func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedplan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if replanCount == 0 { - // wait for replanning - <-ctxReplanning.Done() - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - } - <-ctxExecutionSuccess.Done() - return state.ExecuteResponse{}, nil - }, - }, nil - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID2, test.ShouldNotResemble, executionID1) - - // We see after starting a new execution that the old execution is no longer returned and that a new plan has been generated - pws4, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(pws4), test.ShouldEqual, 1) - test.That(t, pws4[0].Plan.ID, test.ShouldNotResemble, pws2[0].Plan.ID) - test.That(t, pws4[0].Plan.ExecutionID, test.ShouldNotResemble, pws2[0].Plan.ExecutionID) - test.That(t, len(pws4[0].StatusHistory), test.ShouldEqual, 1) - test.That(t, pws4[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, pws4[0].StatusHistory[0].Reason, test.ShouldEqual, nil) - test.That(t, pws4[0].StatusHistory[0].Timestamp.After(preExecution2), test.ShouldBeTrue) - test.That(t, planStatusTimestampsInOrder(pws4[0].StatusHistory), test.ShouldBeTrue) - - // trigger replanning once - execution2Replan1 := time.Now() - triggerReplanning() - - // poll until there are 2 plans in the history - resPWS, succ := pollUntil(cancelCtx, func() (pwsRes, bool) { - st := pwsRes{} - pws, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - if err == nil && len(pws) == 2 { - st.pws = pws - st.err = err - return st, true - } - return st, false - }) - - test.That(t, succ, test.ShouldBeTrue) - test.That(t, resPWS.err, test.ShouldBeNil) - test.That(t, len(resPWS.pws), test.ShouldEqual, 2) - // Previous plan is moved to higher index - test.That(t, resPWS.pws[1].Plan, test.ShouldResemble, pws4[0].Plan) - // Current plan is a new plan - test.That(t, resPWS.pws[0].Plan.ID, test.ShouldNotResemble, pws4[0].Plan.ID) - // From the same execution (definition of a replan) - test.That(t, resPWS.pws[0].Plan.ExecutionID, test.ShouldResemble, pws4[0].Plan.ExecutionID) - // new current plan has an in progress status & was created after triggering replanning - test.That(t, len(resPWS.pws[0].StatusHistory), test.ShouldEqual, 1) - test.That(t, resPWS.pws[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, resPWS.pws[0].StatusHistory[0].Reason, test.ShouldEqual, nil) - test.That(t, resPWS.pws[0].StatusHistory[0].Timestamp.After(execution2Replan1), test.ShouldBeTrue) - // previous plan was moved to failed state due to replanning after replanning was triggered - test.That(t, len(resPWS.pws[1].StatusHistory), test.ShouldEqual, 2) - // oldest satus of previous plan is unchanged, just at a higher index - test.That(t, resPWS.pws[1].StatusHistory[1], test.ShouldResemble, pws4[0].StatusHistory[0]) - // last status of the previous plan is failed due to replanning & occurred after replanning was triggered - test.That(t, resPWS.pws[1].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, resPWS.pws[1].StatusHistory[0].Reason, test.ShouldNotBeNil) - test.That(t, *resPWS.pws[1].StatusHistory[0].Reason, test.ShouldResemble, replanReason) - test.That(t, resPWS.pws[1].StatusHistory[0].Timestamp.After(execution2Replan1), test.ShouldBeTrue) - test.That(t, planStatusTimestampsInOrder(resPWS.pws[0].StatusHistory), test.ShouldBeTrue) - test.That(t, planStatusTimestampsInOrder(resPWS.pws[1].StatusHistory), test.ShouldBeTrue) - - // only the last plan is returned if LastPlanOnly is true - pws6, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase, LastPlanOnly: true}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(pws6), test.ShouldEqual, 1) - test.That(t, pws6[0], test.ShouldResemble, resPWS.pws[0]) - - // only the last plan is returned if LastPlanOnly is true - // and the execution id is provided which matches the last execution for the component - pws7, err := s.PlanHistory(motion.PlanHistoryReq{ - ComponentName: myBase, - LastPlanOnly: true, - ExecutionID: pws6[0].Plan.ExecutionID, - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, pws7, test.ShouldResemble, pws6) - - // Succeeded status - preSuccessMsg := time.Now() - triggerExecutionSuccess() - - resPWS2, succ := pollUntil(cancelCtx, func() (pwsRes, bool, - ) { - st := pwsRes{} - pws, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - if err == nil && len(pws[0].StatusHistory) == 2 { - st.pws = pws - st.err = err - return st, true - } - return st, false - }) - // - test.That(t, succ, test.ShouldBeTrue) - test.That(t, resPWS2.err, test.ShouldBeNil) - test.That(t, len(resPWS2.pws), test.ShouldEqual, 2) - // last plan is unchanged - test.That(t, resPWS2.pws[1], test.ShouldResemble, resPWS.pws[1]) - // current plan is the same as it was before - test.That(t, resPWS2.pws[0].Plan, test.ShouldResemble, pws6[0].Plan) - // current plan now has a new status - test.That(t, len(resPWS2.pws[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, resPWS2.pws[0].StatusHistory[1], test.ShouldResemble, pws6[0].StatusHistory[0]) - // new status is succeeded - test.That(t, resPWS2.pws[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateSucceeded) - test.That(t, resPWS2.pws[0].StatusHistory[0].Reason, test.ShouldBeNil) - test.That(t, resPWS2.pws[0].StatusHistory[0].Timestamp.After(preSuccessMsg), test.ShouldBeTrue) - test.That(t, planStatusTimestampsInOrder(resPWS2.pws[0].StatusHistory), test.ShouldBeTrue) - - // maintains success state after calling stop - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - postStopPWS1, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resPWS2.pws, test.ShouldResemble, postStopPWS1) - - // Failed after replanning - preExecution3 := time.Now() - replanFailReason := errors.New("replanning failed") - executionID3, err := state.StartExecution(ctx, s, req.ComponentName, req, func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedplan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - planFunc: func(ctx context.Context) (motionplan.Plan, error) { - // first plan succeeds - if replanCount == 0 { - pbc := motionplan.PathStep{ - req.ComponentName.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewZeroPose()), - } - return motionplan.NewSimplePlan([]motionplan.PathStep{pbc}, nil), nil - } - // first replan succeeds - if replanCount == 1 { - pbc := motionplan.PathStep{ - req.ComponentName.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewZeroPose()), - } - return motionplan.NewSimplePlan([]motionplan.PathStep{pbc, pbc}, nil), nil - } - // second replan fails - return nil, replanFailReason - }, - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if replanCount == 0 { - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - } - if replanCount == 1 { - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - } - t.Log("shouldn't execute as first replanning fails") - t.FailNow() - return state.ExecuteResponse{}, nil - }, - }, nil - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID2, test.ShouldNotResemble, executionID1) - - resPWS3, succ := pollUntil(cancelCtx, func() (pwsRes, bool) { - st := pwsRes{} - pws, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - if err == nil && len(pws) == 2 && len(pws[0].StatusHistory) == 2 { - st.pws = pws - st.err = err - return st, true - } - return st, false - }) - - test.That(t, succ, test.ShouldBeTrue) - test.That(t, resPWS3.err, test.ShouldBeNil) - - test.That(t, len(resPWS3.pws), test.ShouldEqual, 2) - test.That(t, resPWS3.pws[0].Plan.ExecutionID, test.ShouldEqual, executionID3) - test.That(t, resPWS3.pws[1].Plan.ExecutionID, test.ShouldEqual, executionID3) - test.That(t, resPWS3.pws[0].Plan.ID, test.ShouldNotEqual, resPWS2.pws[1].Plan.ID) - test.That(t, len(resPWS3.pws[1].StatusHistory), test.ShouldEqual, 2) - test.That(t, resPWS3.pws[1].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, *resPWS3.pws[1].StatusHistory[0].Reason, test.ShouldResemble, replanReason) - test.That(t, resPWS3.pws[1].StatusHistory[1].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, resPWS3.pws[1].StatusHistory[1].Reason, test.ShouldBeNil) - test.That(t, resPWS3.pws[1].StatusHistory[1].Timestamp.After(preExecution3), test.ShouldBeTrue) - test.That(t, len(resPWS3.pws[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, resPWS3.pws[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, *resPWS3.pws[0].StatusHistory[0].Reason, test.ShouldResemble, replanFailReason.Error()) - test.That(t, resPWS3.pws[0].StatusHistory[1].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, resPWS3.pws[0].StatusHistory[1].Reason, test.ShouldBeNil) - test.That(t, len(resPWS3.pws[0].Plan.Path()), test.ShouldEqual, 2) - test.That(t, len(resPWS3.pws[1].Plan.Path()), test.ShouldEqual, 1) - test.That(t, planStatusTimestampsInOrder(resPWS3.pws[0].StatusHistory), test.ShouldBeTrue) - test.That(t, planStatusTimestampsInOrder(resPWS3.pws[1].StatusHistory), test.ShouldBeTrue) - - // maintains failed state after calling stop - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - postStopPWS2, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - test.That(t, err, test.ShouldBeNil) - test.That(t, resPWS3.pws, test.ShouldResemble, postStopPWS2) - - // Failed at the end of execution - preExecution4 := time.Now() - executionFailReason := errors.New("execution failed") - executionID4, err := state.StartExecution(ctx, s, req.ComponentName, req, func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedplan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if replanCount == 0 { - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - } - return state.ExecuteResponse{}, executionFailReason - }, - }, nil - }) - test.That(t, err, test.ShouldBeNil) - - resPWS4, succ := pollUntil(cancelCtx, func() (pwsRes, bool, - ) { - st := pwsRes{} - pws, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase}) - if err == nil && len(pws) == 2 && len(pws[0].StatusHistory) == 2 { - st.pws = pws - st.err = err - return st, true - } - return st, false - }) - - test.That(t, succ, test.ShouldBeTrue) - test.That(t, resPWS4.err, test.ShouldBeNil) - - test.That(t, len(resPWS4.pws), test.ShouldEqual, 2) - test.That(t, resPWS4.pws[0].Plan.ExecutionID, test.ShouldEqual, executionID4) - test.That(t, resPWS4.pws[1].Plan.ExecutionID, test.ShouldEqual, executionID4) - test.That(t, resPWS4.pws[0].Plan.ID, test.ShouldNotEqual, resPWS3.pws[1].Plan.ID) - test.That(t, len(resPWS4.pws[1].StatusHistory), test.ShouldEqual, 2) - test.That(t, resPWS4.pws[1].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, *resPWS4.pws[1].StatusHistory[0].Reason, test.ShouldResemble, replanReason) - test.That(t, resPWS4.pws[1].StatusHistory[1].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, resPWS4.pws[1].StatusHistory[1].Reason, test.ShouldBeNil) - test.That(t, resPWS4.pws[1].StatusHistory[1].Timestamp.After(preExecution4), test.ShouldBeTrue) - test.That(t, len(resPWS4.pws[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, resPWS4.pws[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, *resPWS4.pws[0].StatusHistory[0].Reason, test.ShouldResemble, executionFailReason.Error()) - test.That(t, resPWS4.pws[0].StatusHistory[1].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, resPWS4.pws[0].StatusHistory[1].Reason, test.ShouldBeNil) - - // providing an executionID lets you look up the plans from a prior execution - pws12, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase, ExecutionID: executionID3}) - test.That(t, err, test.ShouldBeNil) - test.That(t, pws12, test.ShouldResemble, resPWS3.pws) - - // providing an executionID with lastPlanOnly gives you the last plan of that execution - pws13, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase, ExecutionID: executionID3, LastPlanOnly: true}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(pws13), test.ShouldEqual, 1) - test.That(t, pws13[0], test.ShouldResemble, resPWS3.pws[0]) - - // providing an executionID which is not known to the state returns an error - pws14, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: myBase, ExecutionID: uuid.New()}) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(myBase)) - test.That(t, len(pws14), test.ShouldEqual, 0) - - // Returns the last status of all plans that have executed - ps7, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps7), test.ShouldEqual, 7) - test.That(t, ps7[0].ComponentName, test.ShouldResemble, myBase) - test.That(t, ps7[0].ExecutionID, test.ShouldResemble, executionID4) - test.That(t, ps7[0].PlanID, test.ShouldResemble, resPWS4.pws[0].Plan.ID) - test.That(t, ps7[0].Status, test.ShouldResemble, resPWS4.pws[0].StatusHistory[0]) - - test.That(t, ps7[1].ComponentName, test.ShouldResemble, myBase) - test.That(t, ps7[1].ExecutionID, test.ShouldResemble, executionID4) - test.That(t, ps7[1].PlanID, test.ShouldResemble, resPWS4.pws[1].Plan.ID) - test.That(t, ps7[1].Status, test.ShouldResemble, resPWS4.pws[1].StatusHistory[0]) - - test.That(t, ps7[2].ComponentName, test.ShouldResemble, myBase) - test.That(t, ps7[2].ExecutionID, test.ShouldResemble, executionID3) - test.That(t, ps7[2].PlanID, test.ShouldResemble, resPWS3.pws[0].Plan.ID) - test.That(t, ps7[2].Status, test.ShouldResemble, resPWS3.pws[0].StatusHistory[0]) - - test.That(t, ps7[3].ComponentName, test.ShouldResemble, myBase) - test.That(t, ps7[3].ExecutionID, test.ShouldResemble, executionID3) - test.That(t, ps7[3].PlanID, test.ShouldResemble, resPWS3.pws[1].Plan.ID) - test.That(t, ps7[3].Status, test.ShouldResemble, resPWS3.pws[1].StatusHistory[0]) - - test.That(t, ps7[4].ComponentName, test.ShouldResemble, myBase) - test.That(t, ps7[4].ExecutionID, test.ShouldResemble, executionID2) - test.That(t, ps7[4].PlanID, test.ShouldResemble, resPWS2.pws[0].Plan.ID) - test.That(t, ps7[4].Status, test.ShouldResemble, resPWS2.pws[0].StatusHistory[0]) - - test.That(t, ps7[5].ComponentName, test.ShouldResemble, myBase) - test.That(t, ps7[5].ExecutionID, test.ShouldResemble, executionID2) - test.That(t, ps7[5].PlanID, test.ShouldResemble, resPWS2.pws[1].Plan.ID) - test.That(t, ps7[5].Status, test.ShouldResemble, resPWS2.pws[1].StatusHistory[0]) - - test.That(t, ps7[6].ComponentName, test.ShouldResemble, myBase) - test.That(t, ps7[6].ExecutionID, test.ShouldResemble, executionID1) - test.That(t, ps7[6].PlanID, test.ShouldResemble, pws2[0].Plan.ID) - test.That(t, ps7[6].Status, test.ShouldResemble, pws2[0].StatusHistory[0]) - - ps8, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{OnlyActivePlans: true}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ps8, test.ShouldBeEmpty) - }) - - // NOTE: This test is slow b/c it is testing TTL behavior (which is inherently time based) - // the TTL is inteinionally configured to be a very high number to decrease the risk of flakeyness on - // low powered or over utilized hardware. - t.Run("ttl", func(t *testing.T) { - t.Parallel() - ttl := time.Millisecond * 250 - ttlCheckInterval := time.Millisecond * 10 - sleepTTLDuration := ttl * 2 - sleepCheckDuration := ttlCheckInterval * 2 - s, err := state.NewState(ttl, ttlCheckInterval, logger) - test.That(t, err, test.ShouldBeNil) - defer s.Stop() - time.Sleep(sleepCheckDuration) - - // no plan statuses as no executions have been created - ps, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ps, test.ShouldBeEmpty) - - preExecution := time.Now() - - req := motion.MoveOnGlobeReq{ComponentName: myBase} - - // start execution, then stop it to bring it to terminal state - executionID1, err := state.StartExecution(ctx, s, req.ComponentName, req, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - // stop execution, to show that it still shows up within the TTL & is deleted after it - err = s.StopExecutionByResource(myBase) - test.That(t, err, test.ShouldBeNil) - - // start execution, leave it running - executionID2, err := state.StartExecution(ctx, s, req.ComponentName, req, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - // wait till check interval past - time.Sleep(sleepCheckDuration) - - ps1, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps1), test.ShouldEqual, 2) - - // both executions are still around - test.That(t, ps1[0].ExecutionID, test.ShouldResemble, executionID2) - test.That(t, ps1[0].ComponentName, test.ShouldResemble, req.ComponentName) - test.That(t, ps1[0].PlanID, test.ShouldNotEqual, uuid.Nil) - test.That(t, ps1[0].Status.State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, ps1[0].Status.Reason, test.ShouldBeNil) - test.That(t, ps1[0].Status.Timestamp.After(preExecution), test.ShouldBeTrue) - test.That(t, ps1[1].ExecutionID, test.ShouldResemble, executionID1) - test.That(t, ps1[1].ComponentName, test.ShouldResemble, req.ComponentName) - test.That(t, ps1[1].PlanID, test.ShouldNotEqual, uuid.Nil) - test.That(t, ps1[1].Status.State, test.ShouldEqual, motion.PlanStateStopped) - test.That(t, ps1[1].Status.Reason, test.ShouldBeNil) - test.That(t, ps1[1].Status.Timestamp.After(preExecution), test.ShouldBeTrue) - - // by default planHistory returns the most recent plan - ph1, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph1), test.ShouldEqual, 1) - test.That(t, ph1[0].Plan.ID, test.ShouldResemble, ps1[0].PlanID) - test.That(t, ph1[0].Plan.ExecutionID, test.ShouldResemble, executionID2) - test.That(t, len(ph1[0].StatusHistory), test.ShouldEqual, 1) - test.That(t, ph1[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateInProgress) - - // it is possible to retrieve the stopped execution as it is still before the TTL - ph2, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName, ExecutionID: executionID1}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph2), test.ShouldEqual, 1) - test.That(t, ph2[0].Plan.ID, test.ShouldResemble, ps1[1].PlanID) - test.That(t, ph2[0].Plan.ExecutionID, test.ShouldResemble, executionID1) - test.That(t, len(ph2[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, ph2[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateStopped) - test.That(t, ph2[0].StatusHistory[1].State, test.ShouldEqual, motion.PlanStateInProgress) - - time.Sleep(sleepTTLDuration) - - ps3, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - // after the TTL; only the execution in a non terminal state is still around - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps3), test.ShouldEqual, 1) - test.That(t, ps3[0].ExecutionID, test.ShouldResemble, executionID2) - test.That(t, ps3[0].ComponentName, test.ShouldResemble, req.ComponentName) - test.That(t, ps3[0].PlanID, test.ShouldNotEqual, uuid.Nil) - test.That(t, ps3[0].Status.State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, ps3[0].Status.Reason, test.ShouldBeNil) - test.That(t, ps3[0].Status.Timestamp.After(preExecution), test.ShouldBeTrue) - - // should resemble ph1 as the most recent plan is still in progress & hasn't changed state - ph3, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ph3, test.ShouldResemble, ph1) - - // the first execution (stopped) has been forgotten - ph4, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName, ExecutionID: executionID1}) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(req.ComponentName)) - test.That(t, len(ph4), test.ShouldEqual, 0) - - req2 := motion.MoveOnGlobeReq{ComponentName: base.Named("mybase2")} - executionID4, err := state.StartExecution(ctx, s, req2.ComponentName, req2, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - req3 := motion.MoveOnGlobeReq{ComponentName: base.Named("mybase3")} - _, err = state.StartExecution(ctx, s, req3.ComponentName, req3, executionWaitingForCtxCancelledPlanConstructor) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(req3.ComponentName) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(sleepTTLDuration) - - ps4, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps4), test.ShouldEqual, 2) - test.That(t, ps4[0].ExecutionID.String(), test.ShouldResemble, executionID2.String()) - test.That(t, ps4[0].ComponentName, test.ShouldResemble, req.ComponentName) - test.That(t, ps4[0].PlanID, test.ShouldNotEqual, uuid.Nil) - test.That(t, ps4[0].Status.State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, ps4[0].Status.Reason, test.ShouldBeNil) - - test.That(t, ps4[1].ExecutionID.String(), test.ShouldResemble, executionID4.String()) - test.That(t, ps4[1].ComponentName, test.ShouldResemble, req2.ComponentName) - test.That(t, ps4[1].PlanID, test.ShouldNotEqual, uuid.Nil) - test.That(t, ps4[1].Status.State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, ps4[1].Status.Reason, test.ShouldBeNil) - - err = s.StopExecutionByResource(req.ComponentName) - test.That(t, err, test.ShouldBeNil) - - err = s.StopExecutionByResource(req2.ComponentName) - test.That(t, err, test.ShouldBeNil) - - time.Sleep(sleepTTLDuration) - - ps5, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps5), test.ShouldEqual, 0) - - ctxReplanning, triggerReplanning := context.WithCancel(context.Background()) - ctxExecutionSuccess, triggerExecutionSuccess := context.WithCancel(context.Background()) - executionID6, err := state.StartExecution(ctx, s, req.ComponentName, req, func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedPlan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if replanCount == 0 { - // wait for replanning - <-ctxReplanning.Done() - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - } - <-ctxExecutionSuccess.Done() - return state.ExecuteResponse{}, nil - }, - }, nil - }) - test.That(t, err, test.ShouldBeNil) - - // Test replanning - triggerReplanning() - time.Sleep(sleepTTLDuration) - - ph5, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph5), test.ShouldEqual, 2) - test.That(t, ph5[0].Plan.ExecutionID, test.ShouldEqual, executionID6) - test.That(t, ph5[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateInProgress) - test.That(t, ph5[1].Plan.ExecutionID, test.ShouldEqual, executionID6) - test.That(t, ph5[1].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, ph5[1].StatusHistory[0].Reason, test.ShouldNotBeNil) - test.That(t, *ph5[1].StatusHistory[0].Reason, test.ShouldEqual, "replan triggered due to location drift") - - triggerExecutionSuccess() - time.Sleep(sleepTTLDuration) - - _, err = s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(req.ComponentName)) - - ps6, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps6), test.ShouldEqual, 0) - - pbc := motionplan.PathStep{ - req.ComponentName.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewZeroPose()), - } - - // Failed after replanning - replanFailReason := errors.New("replanning failed") - executionID7, err := state.StartExecution(ctx, s, req.ComponentName, req, func( - ctx context.Context, - req motion.MoveOnGlobeReq, - seedPlan motionplan.Plan, - replanCount int, - ) (state.PlannerExecutor, error) { - return &testPlannerExecutor{ - planFunc: func(ctx context.Context) (motionplan.Plan, error) { - // first replan succeeds - if replanCount == 0 || replanCount == 1 { - return motionplan.NewSimplePlan([]motionplan.PathStep{pbc, pbc}, nil), nil - } - - // second replan fails - return motionplan.NewSimplePlan([]motionplan.PathStep{}, nil), replanFailReason - }, - executeFunc: func(ctx context.Context, plan motionplan.Plan) (state.ExecuteResponse, error) { - if replanCount == 0 { - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - } - if replanCount == 1 { - return state.ExecuteResponse{Replan: true, ReplanReason: replanReason}, nil - } - t.Log("shouldn't execute as first replanning fails") - t.FailNow() - return state.ExecuteResponse{}, nil - }, - }, nil - }) - test.That(t, err, test.ShouldBeNil) - time.Sleep(sleepCheckDuration) - ph6, err := s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph6), test.ShouldEqual, 2) - test.That(t, ph6[0].Plan.ExecutionID, test.ShouldResemble, executionID7) - test.That(t, ph6[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, ph6[0].StatusHistory[0].Reason, test.ShouldNotBeNil) - test.That(t, *ph6[0].StatusHistory[0].Reason, test.ShouldResemble, replanFailReason.Error()) - test.That(t, ph6[1].Plan.ExecutionID, test.ShouldResemble, executionID7) - test.That(t, ph6[1].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateFailed) - test.That(t, ph6[1].StatusHistory[0].Reason, test.ShouldNotBeNil) - test.That(t, *ph6[1].StatusHistory[0].Reason, test.ShouldResemble, replanReason) - - ps7, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps7), test.ShouldEqual, 2) - test.That(t, ps7[0].PlanID, test.ShouldEqual, ph6[0].Plan.ID) - test.That(t, ps7[1].PlanID, test.ShouldEqual, ph6[1].Plan.ID) - - time.Sleep(sleepTTLDuration) - _, err = s.PlanHistory(motion.PlanHistoryReq{ComponentName: req.ComponentName}) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(req.ComponentName)) - - ps8, err := s.ListPlanStatuses(motion.ListPlanStatusesReq{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ps8), test.ShouldEqual, 0) - }) -} - -func planStatusTimestampsInOrder(ps []motion.PlanStatus) bool { - if len(ps) == 0 { - return true - } - last := ps[0].Timestamp - for _, p := range ps[1:] { - if p.Timestamp.Equal(last) || p.Timestamp.After(last) { - return false - } - } - return true -} - -type pwsRes struct { - pws []motion.PlanWithStatus - err error -} - -// pollUntil polls the funcion f returns a type T and a success boolean -// pollUntil returns when either the ctx is cancelled or f returns success = true. -// this is needed so the tests can wait until the state has been updated with the results -// of the PlannerExecutor interface methods. -func pollUntil[T any](ctx context.Context, f func() (T, bool)) (T, bool) { - t, b := f() - for { - if err := ctx.Err(); err != nil { - return t, b - } - - t, b = f() - if b { - return t, b - } - } -} diff --git a/services/motion/builtin/state/verify_main_test.go b/services/motion/builtin/state/verify_main_test.go deleted file mode 100644 index f17ea2c353d..00000000000 --- a/services/motion/builtin/state/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package state - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/motion/builtin/verify_main_test.go b/services/motion/builtin/verify_main_test.go deleted file mode 100644 index c8e5bbecc77..00000000000 --- a/services/motion/builtin/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package builtin - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/motion/client.go b/services/motion/client.go deleted file mode 100644 index 68c7750f0d7..00000000000 --- a/services/motion/client.go +++ /dev/null @@ -1,211 +0,0 @@ -package motion - -import ( - "context" - - "github.com/google/uuid" - pb "go.viam.com/api/service/motion/v1" - vprotoutils "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" -) - -// client implements MotionServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.MotionServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Service, error) { - grpcClient := pb.NewMotionServiceClient(conn) - c := &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: grpcClient, - logger: logger, - } - return c, nil -} - -func (c *client) Move( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *pb.Constraints, - extra map[string]interface{}, -) (bool, error) { - ext, err := vprotoutils.StructToStructPb(extra) - if err != nil { - return false, err - } - worldStateMsg, err := worldState.ToProtobuf() - if err != nil { - return false, err - } - resp, err := c.client.Move(ctx, &pb.MoveRequest{ - Name: c.name, - ComponentName: protoutils.ResourceNameToProto(componentName), - Destination: referenceframe.PoseInFrameToProtobuf(destination), - WorldState: worldStateMsg, - Constraints: constraints, - Extra: ext, - }) - if err != nil { - return false, err - } - return resp.Success, nil -} - -func (c *client) MoveOnMap(ctx context.Context, req MoveOnMapReq) (ExecutionID, error) { - protoReq, err := req.toProto(c.name) - if err != nil { - return uuid.Nil, err - } - - resp, err := c.client.MoveOnMap(ctx, protoReq) - if err != nil { - return uuid.Nil, err - } - - executionID, err := uuid.Parse(resp.ExecutionId) - if err != nil { - return uuid.Nil, err - } - - return executionID, nil -} - -func (c *client) MoveOnGlobe( - ctx context.Context, - req MoveOnGlobeReq, -) (ExecutionID, error) { - protoReq, err := req.toProto(c.name) - if err != nil { - return uuid.Nil, err - } - - resp, err := c.client.MoveOnGlobe(ctx, protoReq) - if err != nil { - return uuid.Nil, err - } - - executionID, err := uuid.Parse(resp.ExecutionId) - if err != nil { - return uuid.Nil, err - } - - return executionID, nil -} - -func (c *client) GetPose( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransforms []*referenceframe.LinkInFrame, - extra map[string]interface{}, -) (*referenceframe.PoseInFrame, error) { - ext, err := vprotoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - transforms, err := referenceframe.LinkInFramesToTransformsProtobuf(supplementalTransforms) - if err != nil { - return nil, err - } - resp, err := c.client.GetPose(ctx, &pb.GetPoseRequest{ - Name: c.name, - ComponentName: protoutils.ResourceNameToProto(componentName), - DestinationFrame: destinationFrame, - SupplementalTransforms: transforms, - Extra: ext, - }) - if err != nil { - return nil, err - } - return referenceframe.ProtobufToPoseInFrame(resp.Pose), nil -} - -func (c *client) StopPlan(ctx context.Context, req StopPlanReq) error { - ext, err := vprotoutils.StructToStructPb(req.Extra) - if err != nil { - return err - } - _, err = c.client.StopPlan(ctx, &pb.StopPlanRequest{ - Name: c.name, - ComponentName: protoutils.ResourceNameToProto(req.ComponentName), - Extra: ext, - }) - return err -} - -func (c *client) ListPlanStatuses(ctx context.Context, req ListPlanStatusesReq) ([]PlanStatusWithID, error) { - ext, err := vprotoutils.StructToStructPb(req.Extra) - if err != nil { - return nil, err - } - resp, err := c.client.ListPlanStatuses(ctx, &pb.ListPlanStatusesRequest{ - Name: c.name, - OnlyActivePlans: req.OnlyActivePlans, - Extra: ext, - }) - if err != nil { - return nil, err - } - pswids := make([]PlanStatusWithID, 0, len(resp.PlanStatusesWithIds)) - for _, status := range resp.PlanStatusesWithIds { - pswid, err := planStatusWithIDFromProto(status) - if err != nil { - return nil, err - } - - pswids = append(pswids, pswid) - } - return pswids, err -} - -func (c *client) PlanHistory( - ctx context.Context, - req PlanHistoryReq, -) ([]PlanWithStatus, error) { - protoReq, err := req.toProto(c.name) - if err != nil { - return nil, err - } - resp, err := c.client.GetPlan(ctx, protoReq) - if err != nil { - return nil, err - } - statusHistory := make([]PlanWithStatus, 0, len(resp.ReplanHistory)) - for _, status := range resp.ReplanHistory { - s, err := planWithStatusFromProto(status) - if err != nil { - return nil, err - } - statusHistory = append(statusHistory, s) - } - pws, err := planWithStatusFromProto(resp.CurrentPlanWithStatus) - if err != nil { - return nil, err - } - return append([]PlanWithStatus{pws}, statusHistory...), nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return protoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/services/motion/client_test.go b/services/motion/client_test.go deleted file mode 100644 index efcf5383bda..00000000000 --- a/services/motion/client_test.go +++ /dev/null @@ -1,602 +0,0 @@ -package motion_test - -import ( - "context" - "math" - "net" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - servicepb "go.viam.com/api/service/motion/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/components/movementsensor" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testMotionServiceName = motion.Named("motion1") - testMotionServiceName2 = motion.Named("motion2") -) - -func TestClient(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectMS := &inject.MotionService{} - resources := map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - } - svc, err := resource.NewAPIResourceCollection(motion.API, resources) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[motion.Service](motion.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - - go func() { - test.That(t, rpcServer.Serve(listener1), test.ShouldBeNil) - }() - - defer func() { - test.That(t, rpcServer.Stop(), test.ShouldBeNil) - }() - - zeroPoseInFrame := referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewZeroPose()) - globeDest := geo.NewPoint(0.0, 0.0) - gripperName := gripper.Named("fake") - baseName := base.Named("test-base") - gpsName := movementsensor.Named("test-gps") - slamName := slam.Named("test-slam") - - // failing - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - // working - t.Run("motion client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - - test.That(t, err, test.ShouldBeNil) - - client, err := motion.NewClientFromConn(context.Background(), conn, "", testMotionServiceName, logger) - test.That(t, err, test.ShouldBeNil) - - receivedTransforms := make(map[string]*referenceframe.LinkInFrame) - success := true - injectMS.MoveFunc = func( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *servicepb.Constraints, - extra map[string]interface{}, - ) (bool, error) { - return success, nil - } - injectMS.GetPoseFunc = func( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransforms []*referenceframe.LinkInFrame, - extra map[string]interface{}, - ) (*referenceframe.PoseInFrame, error) { - for _, tf := range supplementalTransforms { - receivedTransforms[tf.Name()] = tf - } - return referenceframe.NewPoseInFrame( - destinationFrame+componentName.Name, spatialmath.NewPoseFromPoint(r3.Vector{X: 1, Y: 2, Z: 3})), nil - } - - // Move - result, err := client.Move(ctx, gripperName, zeroPoseInFrame, nil, nil, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldEqual, success) - - // GetPose - testPose := spatialmath.NewPose( - r3.Vector{X: 1., Y: 2., Z: 3.}, - &spatialmath.R4AA{Theta: math.Pi / 2, RX: 0., RY: 1., RZ: 0.}, - ) - transforms := []*referenceframe.LinkInFrame{ - referenceframe.NewLinkInFrame("arm1", testPose, "frame1", nil), - referenceframe.NewLinkInFrame("frame1", testPose, "frame2", nil), - } - - tfMap := make(map[string]*referenceframe.LinkInFrame) - for _, tf := range transforms { - tfMap[tf.Name()] = tf - } - poseResult, err := client.GetPose(context.Background(), arm.Named("arm1"), "foo", transforms, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, poseResult.Parent(), test.ShouldEqual, "fooarm1") - test.That(t, poseResult.Pose().Point().X, test.ShouldEqual, 1) - test.That(t, poseResult.Pose().Point().Y, test.ShouldEqual, 2) - test.That(t, poseResult.Pose().Point().Z, test.ShouldEqual, 3) - for name, tf := range tfMap { - receivedTf := receivedTransforms[name] - test.That(t, tf.Name(), test.ShouldEqual, receivedTf.Name()) - test.That(t, tf.Parent(), test.ShouldEqual, receivedTf.Parent()) - test.That(t, spatialmath.PoseAlmostEqual(tf.Pose(), receivedTf.Pose()), test.ShouldBeTrue) - } - test.That(t, receivedTransforms, test.ShouldNotBeNil) - - // DoCommand - injectMS.DoCommandFunc = testutils.EchoFunc - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - // broken - t.Run("motion client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", testMotionServiceName, logger) - test.That(t, err, test.ShouldBeNil) - - passedErr := errors.New("fake move error") - injectMS.MoveFunc = func( - ctx context.Context, - componentName resource.Name, - grabPose *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *servicepb.Constraints, - extra map[string]interface{}, - ) (bool, error) { - return false, passedErr - } - passedErr = errors.New("fake GetPose error") - injectMS.GetPoseFunc = func( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransform []*referenceframe.LinkInFrame, - extra map[string]interface{}, - ) (*referenceframe.PoseInFrame, error) { - return nil, passedErr - } - - // Move - resp, err := client2.Move(ctx, gripperName, zeroPoseInFrame, nil, nil, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, passedErr.Error()) - test.That(t, resp, test.ShouldEqual, false) - - // GetPose - _, err = client2.GetPose(context.Background(), arm.Named("arm1"), "foo", nil, map[string]interface{}{}) - test.That(t, err.Error(), test.ShouldContainSubstring, passedErr.Error()) - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("MoveOnMap", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - - test.That(t, err, test.ShouldBeNil) - - client, err := motion.NewClientFromConn(context.Background(), conn, "", testMotionServiceName, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("returns error without calling client since destination is nil", func(t *testing.T) { - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - t.Log("should not be called") - t.FailNow() - return uuid.Nil, errors.New("should not be reached") - } - - req := motion.MoveOnMapReq{ - ComponentName: baseName, - SlamName: slamName, - } - - // nil destination is can't be converted to proto - executionID, err := client.MoveOnMap(ctx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("must provide a destination")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns error if client returns error", func(t *testing.T) { - errExpected := errors.New("some client error") - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - return uuid.Nil, errExpected - } - - req := motion.MoveOnMapReq{ - ComponentName: baseName, - Destination: spatialmath.NewZeroPose(), - SlamName: slamName, - MotionCfg: &motion.MotionConfiguration{}, - } - - executionID, err := client.MoveOnMap(ctx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errExpected.Error()) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("otherwise returns success with an executionID", func(t *testing.T) { - expectedExecutionID := uuid.New() - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - return expectedExecutionID, nil - } - - req := motion.MoveOnMapReq{ - ComponentName: baseName, - Destination: spatialmath.NewZeroPose(), - SlamName: slamName, - MotionCfg: &motion.MotionConfiguration{}, - } - - executionID, err := client.MoveOnMap(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldEqual, expectedExecutionID) - }) - - t.Run("return success with non-nil obstacles", func(t *testing.T) { - expectedExecutionID := uuid.New() - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - test.That(t, len(req.Obstacles), test.ShouldEqual, 1) - equal := spatialmath.GeometriesAlmostEqual(req.Obstacles[0], spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")) - test.That(t, equal, test.ShouldBeTrue) - return expectedExecutionID, nil - } - - req := motion.MoveOnMapReq{ - ComponentName: baseName, - Destination: spatialmath.NewZeroPose(), - SlamName: slamName, - MotionCfg: &motion.MotionConfiguration{}, - Obstacles: []spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")}, - } - - executionID, err := client.MoveOnMap(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldEqual, expectedExecutionID) - }) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("MoveOnGlobe", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - - test.That(t, err, test.ShouldBeNil) - - client, err := motion.NewClientFromConn(context.Background(), conn, "", testMotionServiceName, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("returns error without calling client if params can't be cast to proto", func(t *testing.T) { - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - t.Log("should not be called") - t.FailNow() - return uuid.Nil, errors.New("should not be reached") - } - - req := motion.MoveOnGlobeReq{ - ComponentName: baseName, - Heading: math.NaN(), - MovementSensorName: gpsName, - MotionCfg: &motion.MotionConfiguration{}, - } - // nil destination is can't be converted to proto - executionID, err := client.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("must provide a destination")) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("returns error if client returns error", func(t *testing.T) { - errExpected := errors.New("some client error") - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - return uuid.Nil, errExpected - } - - req := motion.MoveOnGlobeReq{ - ComponentName: baseName, - Destination: globeDest, - Heading: math.NaN(), - MovementSensorName: gpsName, - MotionCfg: &motion.MotionConfiguration{}, - } - executionID, err := client.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errExpected.Error()) - test.That(t, executionID, test.ShouldResemble, uuid.Nil) - }) - - t.Run("otherwise returns success with an executionID", func(t *testing.T) { - expectedExecutionID := uuid.New() - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - return expectedExecutionID, nil - } - - req := motion.MoveOnGlobeReq{ - ComponentName: baseName, - Destination: globeDest, - Heading: math.NaN(), - MovementSensorName: gpsName, - MotionCfg: &motion.MotionConfiguration{}, - } - executionID, err := client.MoveOnGlobe(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, executionID, test.ShouldEqual, expectedExecutionID) - }) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("StopPlan", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - - test.That(t, err, test.ShouldBeNil) - - client, err := motion.NewClientFromConn(context.Background(), conn, "", testMotionServiceName, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("returns error if client returns error", func(t *testing.T) { - errExpected := errors.New("some client error") - injectMS.StopPlanFunc = func( - ctx context.Context, - req motion.StopPlanReq, - ) error { - return errExpected - } - - req := motion.StopPlanReq{ComponentName: base.Named("mybase")} - err := client.StopPlan(ctx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errExpected.Error()) - }) - - t.Run("otherwise returns nil", func(t *testing.T) { - injectMS.StopPlanFunc = func( - ctx context.Context, - req motion.StopPlanReq, - ) error { - return nil - } - - req := motion.StopPlanReq{ComponentName: base.Named("mybase")} - err := client.StopPlan(ctx, req) - test.That(t, err, test.ShouldBeNil) - }) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("ListPlanStatuses", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - client, err := motion.NewClientFromConn(context.Background(), conn, "", testMotionServiceName, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("returns error if client returns error", func(t *testing.T) { - errExpected := errors.New("some client error") - injectMS.ListPlanStatusesFunc = func( - ctx context.Context, - req motion.ListPlanStatusesReq, - ) ([]motion.PlanStatusWithID, error) { - return nil, errExpected - } - req := motion.ListPlanStatusesReq{} - resp, err := client.ListPlanStatuses(ctx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errExpected.Error()) - test.That(t, resp, test.ShouldBeEmpty) - }) - - t.Run("otherwise returns a slice of PlanStautsWithID", func(t *testing.T) { - planID := uuid.New() - - executionID := uuid.New() - - status := motion.PlanStatus{State: motion.PlanStateInProgress, Timestamp: time.Now().UTC(), Reason: nil} - - expectedResp := []motion.PlanStatusWithID{ - {PlanID: planID, ComponentName: base.Named("mybase"), ExecutionID: executionID, Status: status}, - } - - injectMS.ListPlanStatusesFunc = func( - ctx context.Context, - req motion.ListPlanStatusesReq, - ) ([]motion.PlanStatusWithID, error) { - return expectedResp, nil - } - - req := motion.ListPlanStatusesReq{} - resp, err := client.ListPlanStatuses(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, expectedResp) - }) - - t.Run("supports returning multiple PlanStautsWithID", func(t *testing.T) { - planIDA := uuid.New() - - executionIDA := uuid.New() - test.That(t, err, test.ShouldBeNil) - - statusA := motion.PlanStatus{State: motion.PlanStateInProgress, Timestamp: time.Now().UTC(), Reason: nil} - - planIDB := uuid.New() - - executionIDB := uuid.New() - - reason := "failed reason" - statusB := motion.PlanStatus{State: motion.PlanStateInProgress, Timestamp: time.Now().UTC(), Reason: &reason} - - expectedResp := []motion.PlanStatusWithID{ - {PlanID: planIDA, ComponentName: base.Named("mybase"), ExecutionID: executionIDA, Status: statusA}, - {PlanID: planIDB, ComponentName: base.Named("mybase"), ExecutionID: executionIDB, Status: statusB}, - } - - injectMS.ListPlanStatusesFunc = func( - ctx context.Context, - req motion.ListPlanStatusesReq, - ) ([]motion.PlanStatusWithID, error) { - return expectedResp, nil - } - - req := motion.ListPlanStatusesReq{} - resp, err := client.ListPlanStatuses(ctx, req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldResemble, expectedResp) - }) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("PlanHistory", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - client, err := motion.NewClientFromConn(context.Background(), conn, "", testMotionServiceName, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("returns error if client returns error", func(t *testing.T) { - errExpected := errors.New("some client error") - injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return nil, errExpected - } - - req := motion.PlanHistoryReq{ComponentName: base.Named("mybase")} - resp, err := client.PlanHistory(ctx, req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errExpected.Error()) - test.That(t, resp, test.ShouldBeEmpty) - }) - - t.Run("otherwise returns a slice of PlanWithStatus", func(t *testing.T) { - steps := []motionplan.PathStep{{"mybase": zeroPoseInFrame}} - reason := "some reason" - id := uuid.New() - executionID := uuid.New() - - timeA := time.Now().UTC() - timeB := time.Now().UTC() - - plan := motion.PlanWithMetadata{ - ID: id, - ComponentName: base.Named("mybase"), - ExecutionID: executionID, - Plan: motionplan.NewSimplePlan(steps, nil), - } - statusHistory := []motion.PlanStatus{ - {motion.PlanStateFailed, timeB, &reason}, - {motion.PlanStateInProgress, timeA, nil}, - } - expectedResp := []motion.PlanWithStatus{{Plan: plan, StatusHistory: statusHistory}} - injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return expectedResp, nil - } - - req := motion.PlanHistoryReq{ComponentName: base.Named("mybase")} - resp, err := client.PlanHistory(ctx, req) - test.That(t, err, test.ShouldBeNil) - planHistoriesEqual(t, resp, expectedResp) - }) - - t.Run("supports returning a slice of PlanWithStatus with more than one plan", func(t *testing.T) { - steps := []motionplan.PathStep{{"mybase": zeroPoseInFrame}} - reason := "some reason" - - idA := uuid.New() - test.That(t, err, test.ShouldBeNil) - - executionID := uuid.New() - test.That(t, err, test.ShouldBeNil) - - timeAA := time.Now().UTC() - timeAB := time.Now().UTC() - - planA := motion.PlanWithMetadata{ - ID: idA, - ComponentName: base.Named("mybase"), - ExecutionID: executionID, - Plan: motionplan.NewSimplePlan(steps, nil), - } - statusHistoryA := []motion.PlanStatus{ - {motion.PlanStateFailed, timeAB, &reason}, - {motion.PlanStateInProgress, timeAA, nil}, - } - - idB := uuid.New() - test.That(t, err, test.ShouldBeNil) - timeBA := time.Now().UTC() - planB := motion.PlanWithMetadata{ - ID: idB, - ComponentName: base.Named("mybase"), - ExecutionID: executionID, - Plan: motionplan.NewSimplePlan(steps, nil), - } - - statusHistoryB := []motion.PlanStatus{ - {motion.PlanStateInProgress, timeBA, nil}, - } - - expectedResp := []motion.PlanWithStatus{ - {Plan: planB, StatusHistory: statusHistoryB}, - {Plan: planA, StatusHistory: statusHistoryA}, - } - - injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return expectedResp, nil - } - - req := motion.PlanHistoryReq{ComponentName: base.Named("mybase")} - resp, err := client.PlanHistory(ctx, req) - test.That(t, err, test.ShouldBeNil) - planHistoriesEqual(t, resp, expectedResp) - }) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} - -func planHistoriesEqual(t *testing.T, resp, expectedResp []motion.PlanWithStatus) { - t.Helper() - test.That(t, len(resp), test.ShouldEqual, len(expectedResp)) - for i := 0; i < len(resp); i++ { - test.That(t, resp[i].Plan.ID, test.ShouldResemble, expectedResp[i].Plan.ID) - test.That(t, resp[i].Plan.Path(), test.ShouldResemble, expectedResp[i].Plan.Path()) - test.That(t, resp[i].StatusHistory, test.ShouldResemble, expectedResp[i].StatusHistory) - test.That(t, resp[i].Plan.ExecutionID, test.ShouldResemble, expectedResp[i].Plan.ExecutionID) - test.That(t, resp[i].Plan.ComponentName, test.ShouldResemble, expectedResp[i].Plan.ComponentName) - } -} diff --git a/services/motion/data/arm_gantry.json b/services/motion/data/arm_gantry.json deleted file mode 100644 index c94ee23e3a3..00000000000 --- a/services/motion/data/arm_gantry.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "components": [ - { - "name": "arm1", - "type": "arm", - "model": "fake", - "attributes": { - "model-path": "./../../../components/arm/fake/fake_model.json" - }, - "frame": { - "parent": "gantry1", - "translation": { - "x": 0, - "y": 0, - "z": 0 - } - }, - "depends_on": [ - "gantry1" - ] - }, - { - "name": "gantry1", - "type": "gantry", - "model": "fake", - "frame": { - "parent": "world", - "translation": { - "x": 0, - "y": 0, - "z": 0 - } - } - } - ] -} diff --git a/services/motion/data/fake_tomato.json b/services/motion/data/fake_tomato.json deleted file mode 100644 index 1f0b6d5ce4b..00000000000 --- a/services/motion/data/fake_tomato.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "components": [ - { - "name": "c", - "type": "camera", - "model": "fake", - "frame": { - "parent": "gr" - } - }, - { - "name": "gr", - "type": "gripper", - "model": "fake", - "frame": { - "parent": "a" - } - }, - { - "name": "a", - "type": "arm", - "model": "fake", - "attributes": { - "arm-model": "ur5e" - }, - "frame": { - "parent": "ga" - } - }, - { - "name": "ga", - "type": "gantry", - "model": "fake", - "frame": { - "parent": "world" - } - } - ] -} diff --git a/services/motion/data/fake_wheeled_base.json b/services/motion/data/fake_wheeled_base.json deleted file mode 100644 index 66ff02fb22b..00000000000 --- a/services/motion/data/fake_wheeled_base.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "components": [ - { - "name": "test_base", - "type": "base", - "model": "fake", - "attributes": { - "wheel_circumference_mm": 217, - "left": [ - "fake-left" - ], - "right": [ - "fake-right" - ], - "width_mm": 260, - "spin_slip_factor": 1.76 - }, - "depends_on": [], - "frame": { - "parent": "world", - "translation": { - "x": 0, - "y": 0, - "z": 0 - }, - "orientation": { - "type": "ov_degrees", - "value": { - "x": 0, - "y": 0, - "z": 1, - "th": 0 - } - }, - "geometry": { - "r": 20, - "translation": { - "x": 0, - "y": 0, - "z": 0 - } - } - } - }, - { - "name": "fake-left", - "type": "motor", - "model": "fake", - "attributes": { - "pins": { - "dir": "", - "pwm": "" - }, - "board": "", - "max_rpm": 1 - }, - "depends_on": [] - }, - { - "name": "fake-right", - "type": "motor", - "model": "fake", - "attributes": { - "pins": { - "dir": "", - "pwm": "" - }, - "board": "", - "max_rpm": 1 - }, - "depends_on": [] - }, - { - "name": "fake-board", - "type": "board", - "model": "fake", - "attributes": { - "fail_new": false - }, - "depends_on": [] - } - ], - "services": [ - { - "model": "fake", - "name": "test_slam", - "type": "slam" - } - ] -} diff --git a/services/motion/data/gps_base.json b/services/motion/data/gps_base.json deleted file mode 100644 index 819c85f75f9..00000000000 --- a/services/motion/data/gps_base.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "components": [ - { - "name": "test-base", - "type": "base", - "model": "fake", - "attributes": { - "wheel_circumference_mm": 217, - "left": [ - "fake-left" - ], - "right": [ - "fake-right" - ], - "width_mm": 260, - "spin_slip_factor": 1.76 - }, - "depends_on": [], - "frame": { - "parent": "world", - "translation": { - "x": 0, - "y": 0, - "z": 0 - }, - "orientation": { - "type": "ov_degrees", - "value": { - "x": 0, - "y": 0, - "z": 1, - "th": 0 - } - }, - "geometry": { - "r": 20, - "translation": { - "x": 0, - "y": 0, - "z": 0 - } - } - } - }, - { - "name": "fake-left", - "type": "motor", - "model": "fake", - "attributes": { - "pins": { - "dir": "", - "pwm": "" - }, - "board": "", - "max_rpm": 1 - }, - "depends_on": [] - }, - { - "name": "fake-right", - "type": "motor", - "model": "fake", - "attributes": { - "pins": { - "dir": "", - "pwm": "" - }, - "board": "", - "max_rpm": 1 - }, - "depends_on": [] - }, - { - "name": "fake-board", - "type": "board", - "model": "fake", - "attributes": { - "fail_new": false - }, - "depends_on": [] - }, - { - "name": "test-gps", - "type": "movement_sensor", - "model": "fake", - "depends_on": [], - "frame": { - "parent": "test-base", - "translation": { - "x": -10, - "y": 0, - "z": 0 - } - } - } - ] -} diff --git a/services/motion/data/moving_arm.json b/services/motion/data/moving_arm.json deleted file mode 100644 index 13a0f19616e..00000000000 --- a/services/motion/data/moving_arm.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "components": [ - { - "name": "c", - "type": "camera", - "model": "fake", - "frame": { - "parent": "pieceGripper" - } - }, - { - "name": "pieceGripper", - "type": "gripper", - "model": "fake", - "frame": { - "parent": "pieceArm" - } - }, - { - "name": "pieceArm", - "type": "arm", - "model": "fake", - "attributes": { - "arm-model": "ur5e" - }, - "frame": { - "parent": "world" - } - } - ] -} diff --git a/services/motion/data/real_wheeled_base.json b/services/motion/data/real_wheeled_base.json deleted file mode 100644 index 5be4f325c01..00000000000 --- a/services/motion/data/real_wheeled_base.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "components": [ - { - "name": "test-base", - "type": "base", - "model": "wheeled", - "attributes": { - "wheel_circumference_mm": 217, - "left": [ - "fake-left" - ], - "right": [ - "fake-right" - ], - "width_mm": 260, - "spin_slip_factor": 1.76 - }, - "depends_on": [], - "frame": { - "parent": "world", - "translation": { - "x": 0, - "y": 0, - "z": 0 - }, - "orientation": { - "type": "ov_degrees", - "value": { - "x": 0, - "y": 0, - "z": 1, - "th": 0 - } - }, - "geometry": { - "r": 20, - "translation": { - "x": 0, - "y": 0, - "z": 0 - } - } - } - }, - { - "name": "fake-left", - "type": "motor", - "model": "fake", - "attributes": { - "pins": { - "dir": "", - "pwm": "" - }, - "board": "", - "max_rpm": 1 - }, - "depends_on": [] - }, - { - "name": "fake-right", - "type": "motor", - "model": "fake", - "attributes": { - "pins": { - "dir": "", - "pwm": "" - }, - "board": "", - "max_rpm": 1 - }, - "depends_on": [] - }, - { - "name": "fake-board", - "type": "board", - "model": "fake", - "attributes": { - "fail_new": false - }, - "depends_on": [] - } - ], - "services": [ - { - "model": "fake", - "name": "test_slam", - "type": "slam" - } - ] -} diff --git a/services/motion/errors.go b/services/motion/errors.go deleted file mode 100644 index 648cdea1798..00000000000 --- a/services/motion/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package motion - -import "github.com/pkg/errors" - -// ErrGoalWithinPlanDeviation is an error describing when planning fails because there is nothing to be done. -var ErrGoalWithinPlanDeviation = errors.New("no need to move, already within planDeviationMM") diff --git a/services/motion/explore/explore.go b/services/motion/explore/explore.go deleted file mode 100644 index e725042cb4e..00000000000 --- a/services/motion/explore/explore.go +++ /dev/null @@ -1,620 +0,0 @@ -// Package explore implements a motion service for exploration. This motion service model is a temporary model -// that will be incorporated into the rdk:builtin:motion service in the future. -package explore - -import ( - "context" - "fmt" - "math" - "strconv" - "strings" - "sync" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - "github.com/pkg/errors" - servicepb "go.viam.com/api/service/motion/v1" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/base/kinematicbase" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" -) - -var ( - model = resource.DefaultModelFamily.WithModel("explore") - errUnimplemented = errors.New("unimplemented") - // The distance a detected obstacle can be from a base to trigger the Move command to stop. - lookAheadDistanceMM = 500. - // successiveErrorLimit places a limit on the number of errors that can occur in a row when running - // checkForObstacles. - successiveErrorLimit = 5 - // Defines the limit on how far a potential kinematic base move action can be. For explore there is none. - defaultMoveLimitMM = math.Inf(1) - // The timeout for any individual move action on the kinematic base. - defaultExploreTimeout = 100 * time.Second - // The max angle the kinematic base spin action can be. - defaultMaxSpinAngleDegs = 180. -) - -func init() { - resource.RegisterService( - motion.API, - model, - resource.Registration[motion.Service, *Config]{ - Constructor: NewExplore, - WeakDependencies: []resource.Matcher{ - resource.TypeMatcher{Type: resource.APITypeComponentName}, - resource.SubtypeMatcher{Subtype: vision.SubtypeName}, - }, - }, - ) -} - -const ( - exploreOpLabel = "explore-motion-service" -) - -// moveResponse is the message type returned by both channels: obstacle detection and plan execution. -// It contains a bool stating if an obstacle was found or the execution was complete and/or any -// associated error. -type moveResponse struct { - err error - success bool -} - -// inputEnabledActuator is an actuator that interacts with the frame system. -// This allows us to figure out where the actuator currently is and then -// move it. Input units are always in meters or radians. -type inputEnabledActuator interface { - resource.Actuator - referenceframe.InputEnabled -} - -// obstacleDetectorObject provides a map for matching vision services to any and all cameras names they use. -type obstacleDetectorObject map[vision.Service]resource.Name - -// Config describes how to configure the service. As the explore motion service is not being surfaced to users, this is blank. -type Config struct{} - -// Validate here adds a dependency on the internal framesystem service. -func (c *Config) Validate(path string) ([]string, error) { - return []string{framesystem.InternalServiceName.String()}, nil -} - -// NewExplore returns a new explore motion service for the given robot. -func NewExplore(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (motion.Service, error) { - ms := &explore{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - obstacleResponseChan: make(chan moveResponse), - executionResponseChan: make(chan moveResponse), - backgroundWorkers: &sync.WaitGroup{}, - } - - if err := ms.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return ms, nil -} - -// Reconfigure updates the explore motion service when the config has changed. -func (ms *explore) Reconfigure( - ctx context.Context, - deps resource.Dependencies, - conf resource.Config, -) (err error) { - ms.resourceMutex.Lock() - defer ms.resourceMutex.Unlock() - // Iterate over dependencies and store components and services along with the frame service directly - components := make(map[resource.Name]resource.Resource) - services := make(map[resource.Name]resource.Resource) - for name, dep := range deps { - switch dep := dep.(type) { - case framesystem.Service: - ms.fsService = dep - case vision.Service: - services[name] = dep - default: - components[name] = dep - } - } - - ms.components = components - ms.services = services - return nil -} - -type explore struct { - resource.Named - logger logging.Logger - - processCancelFunc context.CancelFunc - obstacleResponseChan chan moveResponse - executionResponseChan chan moveResponse - backgroundWorkers *sync.WaitGroup - - // resourceMutex protects the connect to other resources, the frame service/system and prevent multiple - // Move and/or Reconfigure actions from being performed simultaneously. - resourceMutex sync.Mutex - components map[resource.Name]resource.Resource - services map[resource.Name]resource.Resource - fsService framesystem.Service - frameSystem referenceframe.FrameSystem -} - -func (ms *explore) MoveOnMap(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - return uuid.Nil, errUnimplemented -} - -func (ms *explore) MoveOnGlobe( - ctx context.Context, - req motion.MoveOnGlobeReq, -) (motion.ExecutionID, error) { - return uuid.Nil, errUnimplemented -} - -func (ms *explore) GetPose( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransforms []*referenceframe.LinkInFrame, - extra map[string]interface{}, -) (*referenceframe.PoseInFrame, error) { - return nil, errUnimplemented -} - -func (ms *explore) StopPlan( - ctx context.Context, - req motion.StopPlanReq, -) error { - return errUnimplemented -} - -func (ms *explore) ListPlanStatuses( - ctx context.Context, - req motion.ListPlanStatusesReq, -) ([]motion.PlanStatusWithID, error) { - return nil, errUnimplemented -} - -func (ms *explore) PlanHistory( - ctx context.Context, - req motion.PlanHistoryReq, -) ([]motion.PlanWithStatus, error) { - return nil, errUnimplemented -} - -func (ms *explore) Close(ctx context.Context) error { - ms.resourceMutex.Lock() - defer ms.resourceMutex.Unlock() - - if ms.processCancelFunc != nil { - ms.processCancelFunc() - } - utils.FlushChan(ms.obstacleResponseChan) - utils.FlushChan(ms.executionResponseChan) - ms.backgroundWorkers.Wait() - return nil -} - -// Move takes a goal location and will plan and execute a movement to move a component specified by its name -// to that destination. -func (ms *explore) Move( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *servicepb.Constraints, - extra map[string]interface{}, -) (bool, error) { - ms.resourceMutex.Lock() - defer ms.resourceMutex.Unlock() - - operation.CancelOtherWithLabel(ctx, exploreOpLabel) - - // Parse extras - motionCfg, err := parseMotionConfig(extra) - if err != nil { - return false, err - } - - // obstacleDetectors - obstacleDetectors, err := ms.createObstacleDetectors(motionCfg) - if err != nil { - return false, err - } - - // Create kinematic base - kb, err := ms.createKinematicBase(ctx, componentName, motionCfg) - if err != nil { - return false, err - } - - // Create motionplan plan - plan, err := ms.createMotionPlan(ctx, kb, destination, extra) - if err != nil { - return false, err - } - - // Start background processes - cancelCtx, cancel := context.WithCancel(ctx) - ms.processCancelFunc = cancel - defer ms.processCancelFunc() - - // Start polling for obstacles - ms.backgroundWorkers.Add(1) - goutils.ManagedGo(func() { - ms.checkForObstacles(cancelCtx, obstacleDetectors, kb, plan, motionCfg.ObstaclePollingFreqHz) - }, ms.backgroundWorkers.Done) - - // Start executing plan - ms.backgroundWorkers.Add(1) - goutils.ManagedGo(func() { - ms.executePlan(cancelCtx, kb, plan) - }, ms.backgroundWorkers.Done) - - // this ensures that if the context is cancelled we always return early at the top of the loop - if err := ctx.Err(); err != nil { - return false, err - } - - select { - // if context was cancelled by the calling function, error out - case <-ctx.Done(): - return false, ctx.Err() - - // once execution responds: return the result to the caller - case resp := <-ms.executionResponseChan: - ms.logger.CDebugf(ctx, "execution completed: %v", resp) - if resp.err != nil { - return resp.success, resp.err - } - if resp.success { - return true, nil - } - - // if the checkPartialPlan process hit an error return it, otherwise exit - case resp := <-ms.obstacleResponseChan: - ms.logger.CDebugf(ctx, "obstacle response: %v", resp) - if resp.err != nil { - return resp.success, resp.err - } - if resp.success { - return false, nil - } - } - return false, errors.New("no obstacle was seen during movement to goal and goal was not reached") -} - -// checkForObstacles will continuously monitor the generated transient worldState for obstacles in the given -// motionplan plan. A response will be sent through the channel if an error occurs, the motionplan plan -// completes or an obstacle is detected in the given range. -func (ms *explore) checkForObstacles( - ctx context.Context, - obstacleDetectors []obstacleDetectorObject, - kb kinematicbase.KinematicBase, - plan motionplan.Plan, - obstaclePollingFrequencyHz float64, -) { - ms.logger.Debug("Current Plan") - ms.logger.Debug(plan) - - // Constantly check for obstacles in path at desired obstacle polling frequency - ticker := time.NewTicker(time.Duration(int(1000/obstaclePollingFrequencyHz)) * time.Millisecond) - defer ticker.Stop() - - var errCounterCurrentInputs, errCounterGenerateTransientWorldState int - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - - currentInputs, err := kb.CurrentInputs(ctx) - if err != nil { - ms.logger.Debugf("issue occurred getting current inputs from kinematic base: %v", err) - if errCounterCurrentInputs > successiveErrorLimit { - ms.obstacleResponseChan <- moveResponse{success: false} - return - } - errCounterCurrentInputs++ - } else { - errCounterCurrentInputs = 0 - } - - currentPose := spatialmath.NewPose( - r3.Vector{X: currentInputs[0].Value, Y: currentInputs[1].Value, Z: 0}, - &spatialmath.OrientationVector{OX: 0, OY: 0, OZ: 1, Theta: currentInputs[2].Value}, - ) - - // Look for new transient obstacles and add to worldState - worldState, err := ms.generateTransientWorldState(ctx, obstacleDetectors) - if err != nil { - ms.logger.CDebugf(ctx, "issue occurred generating transient worldState: %v", err) - if errCounterGenerateTransientWorldState > successiveErrorLimit { - ms.obstacleResponseChan <- moveResponse{success: false} - return - } - errCounterGenerateTransientWorldState++ - } else { - errCounterGenerateTransientWorldState = 0 - } - - // TODO: Generalize this fix to work for maps with non-transient obstacles. This current implementation - // relies on the plan being two steps: a start position and a goal position. - // JIRA Ticket: https://viam.atlassian.net/browse/RSDK-5964 - planTraj := plan.Trajectory() - planTraj[0][kb.Name().ShortName()] = currentInputs - ms.logger.Debugf("Current transient worldState: ", worldState.String()) - - // Check plan for transient obstacles - err = motionplan.CheckPlan( - kb.Kinematics(), - plan, - 0, - worldState, - ms.frameSystem, - currentPose, - plan.Trajectory()[0], - spatialmath.NewZeroPose(), - lookAheadDistanceMM, - ms.logger, - ) - if err != nil { - if strings.Contains(err.Error(), "found error between positions") { - ms.logger.CDebug(ctx, "collision found in given range") - ms.obstacleResponseChan <- moveResponse{success: true} - return - } - } - } - } -} - -// executePlan will carry out the desired motionplan plan. -func (ms *explore) executePlan(ctx context.Context, kb kinematicbase.KinematicBase, plan motionplan.Plan) { - steps, err := plan.Trajectory().GetFrameInputs(kb.Name().Name) - if err != nil { - ms.logger.Debugf("error in executePlan: %s", err) - return - } - for _, inputs := range steps { - select { - case <-ctx.Done(): - return - default: - if inputEnabledKb, ok := kb.(inputEnabledActuator); ok { - if err := inputEnabledKb.GoToInputs(ctx, inputs); err != nil { - // If there is an error on GoToInputs, stop the component if possible before returning the error - if stopErr := kb.Stop(ctx, nil); stopErr != nil { - ms.executionResponseChan <- moveResponse{err: err} - return - } - // If the error was simply a cancellation of context return without erroring out - if errors.Is(err, context.Canceled) { - return - } - ms.executionResponseChan <- moveResponse{err: err} - return - } - } else { - ms.executionResponseChan <- moveResponse{err: errors.New("unable to cast kinematic base to inputEnabledActuator")} - return - } - } - } - ms.executionResponseChan <- moveResponse{success: true} -} - -// generateTransientWorldState will create a new worldState with transient obstacles generated by the provided -// obstacleDetectors. -func (ms *explore) generateTransientWorldState( - ctx context.Context, - obstacleDetectors []obstacleDetectorObject, -) (*referenceframe.WorldState, error) { - geometriesInFrame := []*referenceframe.GeometriesInFrame{} - - // Iterate through provided obstacle detectors and their associated vision service and cameras - for _, obstacleDetector := range obstacleDetectors { - for visionService, cameraName := range obstacleDetector { - // Get detections as vision objects - detections, err := visionService.GetObjectPointClouds(ctx, cameraName.Name, nil) - if err != nil && strings.Contains(err.Error(), "does not implement a 3D segmenter") { - ms.logger.CInfof(ctx, "cannot call GetObjectPointClouds on %q as it does not implement a 3D segmenter", - visionService.Name()) - } else if err != nil { - return nil, err - } - - // Extract geometries from vision objects - geometries := []spatialmath.Geometry{} - for i, detection := range detections { - geometry := detection.Geometry - label := cameraName.Name + "_transientObstacle_" + strconv.Itoa(i) - if geometry.Label() != "" { - label += "_" + geometry.Label() - } - geometry.SetLabel(label) - geometries = append(geometries, geometry) - } - geometriesInFrame = append(geometriesInFrame, - referenceframe.NewGeometriesInFrame(cameraName.Name, geometries), - ) - } - } - - // Add geometries to worldState - worldState, err := referenceframe.NewWorldState(geometriesInFrame, nil) - if err != nil { - return nil, err - } - return worldState, nil -} - -// createKinematicBase will instantiate a kinematic base from the provided base name and motionCfg with -// associated kinematic base options. This will be a differential drive kinematic base. -func (ms *explore) createKinematicBase( - ctx context.Context, - baseName resource.Name, - motionCfg motion.MotionConfiguration, -) (kinematicbase.KinematicBase, error) { - // Select the base from the component list using the baseName - component, ok := ms.components[baseName] - if !ok { - return nil, resource.DependencyNotFoundError(baseName) - } - - b, ok := component.(base.Base) - if !ok { - return nil, fmt.Errorf("cannot get component of type %T because it is not a Case", component) - } - - // Generate kinematic base options from motionCfg - kinematicsOptions := kinematicbase.NewKinematicBaseOptions() - kinematicsOptions.NoSkidSteer = true - kinematicsOptions.UsePTGs = false - // PositionOnlyMode needs to be false to allow orientation info to be available in CurrentInputs as - // we need that to correctly place obstacles in world's reference frame. - kinematicsOptions.PositionOnlyMode = false - kinematicsOptions.AngularVelocityDegsPerSec = motionCfg.AngularDegsPerSec - kinematicsOptions.LinearVelocityMMPerSec = motionCfg.LinearMPerSec * 1000 - kinematicsOptions.Timeout = defaultExploreTimeout - kinematicsOptions.MaxSpinAngleDeg = defaultMaxSpinAngleDegs - kinematicsOptions.MaxMoveStraightMM = defaultMoveLimitMM - - // Create new kinematic base (differential drive) - kb, err := kinematicbase.WrapWithKinematics( - ctx, - b, - ms.logger, - nil, - []referenceframe.Limit{ - {Min: -defaultMoveLimitMM, Max: defaultMoveLimitMM}, - {Min: -defaultMoveLimitMM, Max: defaultMoveLimitMM}, - {Min: -2 * math.Pi, Max: 2 * math.Pi}, - }, - kinematicsOptions, - ) - if err != nil { - return nil, err - } - - return kb, nil -} - -// createObstacleDetectors will generate the list of obstacle detectors from the camera and vision services -// names provided in them motionCfg. -func (ms *explore) createObstacleDetectors(motionCfg motion.MotionConfiguration) ([]obstacleDetectorObject, error) { - var obstacleDetectors []obstacleDetectorObject - // Iterate through obstacleDetectorsNames - for _, obstacleDetectorsName := range motionCfg.ObstacleDetectors { - // Select the vision service from the service list using the vision service name in obstacleDetectorsNames - visionServiceResource, ok := ms.services[obstacleDetectorsName.VisionServiceName] - if !ok { - return nil, resource.DependencyNotFoundError(obstacleDetectorsName.VisionServiceName) - } - visionService, ok := visionServiceResource.(vision.Service) - if !ok { - return nil, fmt.Errorf("cannot get service of type %T because it is not a vision service", - visionServiceResource, - ) - } - - // Select the camera from the component list using the camera name in obstacleDetectorsNames - // Note: May need to be converted to a for loop if we accept multiple cameras for each vision service - cameraResource, ok := ms.components[obstacleDetectorsName.CameraName] - if !ok { - return nil, resource.DependencyNotFoundError(obstacleDetectorsName.CameraName) - } - cam, ok := cameraResource.(camera.Camera) - if !ok { - return nil, fmt.Errorf("cannot get component of type %T because it is not a camera", cameraResource) - } - obstacleDetectors = append(obstacleDetectors, obstacleDetectorObject{visionService: cam.Name()}) - } - - return obstacleDetectors, nil -} - -// createMotionPlan will construct a motion plan towards a specified destination using the given kinematic base. -// No position knowledge is assumed so every plan uses the base, and therefore world, origin as the starting location. -func (ms *explore) createMotionPlan( - ctx context.Context, - kb kinematicbase.KinematicBase, - destination *referenceframe.PoseInFrame, - extra map[string]interface{}, -) (motionplan.Plan, error) { - if destination.Pose().Point().Norm() >= defaultMoveLimitMM { - return nil, errors.Errorf("destination %v is above the defined limit of %v", destination.Pose().Point().String(), defaultMoveLimitMM) - } - - worldState, err := referenceframe.NewWorldState(nil, nil) - if err != nil { - return nil, err - } - - ms.frameSystem, err = ms.fsService.FrameSystem(ctx, worldState.Transforms()) - if err != nil { - return nil, err - } - - // replace origin base frame to remove original differential drive kinematic base geometry as it is overwritten by a bounding sphere - // during kinematic base creation - if err := ms.frameSystem.ReplaceFrame(referenceframe.NewZeroStaticFrame(kb.Kinematics().Name() + "_origin")); err != nil { - return nil, err - } - - // replace original base frame with one that knows how to move itself and allow planning for - if err := ms.frameSystem.ReplaceFrame(kb.Kinematics()); err != nil { - return nil, err - } - - inputs := make([]referenceframe.Input, len(kb.Kinematics().DoF())) - - f := kb.Kinematics() - - seedMap := map[string][]referenceframe.Input{f.Name(): inputs} - - goalPose, err := ms.fsService.TransformPose(ctx, destination, ms.frameSystem.World().Name(), nil) - if err != nil { - return nil, err - } - - ms.logger.CDebugf(ctx, "goal position: %v", goalPose.Pose().Point()) - return motionplan.PlanMotion(ctx, &motionplan.PlanRequest{ - Logger: ms.logger, - Goal: goalPose, - Frame: f, - StartConfiguration: seedMap, - FrameSystem: ms.frameSystem, - WorldState: worldState, - ConstraintSpecs: nil, - Options: extra, - }) -} - -// parseMotionConfig extracts the MotionConfiguration from extra's. -func parseMotionConfig(extra map[string]interface{}) (motion.MotionConfiguration, error) { - motionCfgInterface, ok := extra["motionCfg"] - if !ok { - return motion.MotionConfiguration{}, errors.New("no motionCfg provided") - } - - motionCfg, ok := motionCfgInterface.(motion.MotionConfiguration) - if !ok { - return motion.MotionConfiguration{}, errors.New("could not interpret motionCfg field as an MotionConfiguration") - } - return motionCfg, nil -} diff --git a/services/motion/explore/explore_test.go b/services/motion/explore/explore_test.go deleted file mode 100644 index d0e869936ef..00000000000 --- a/services/motion/explore/explore_test.go +++ /dev/null @@ -1,385 +0,0 @@ -package explore - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - goutils "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" -) - -var ( - testBaseName = resource.NewName(base.API, "test_base") - testCameraName1 = resource.NewName(camera.API, "test_camera1") - testCameraName2 = resource.NewName(camera.API, "test_camera2") - testFrameServiceName = resource.NewName(framesystem.API, "test_fs") - defaultCollisionBufferMM = 1e-8 - defaultMotionCfg = motion.MotionConfiguration{ - AngularDegsPerSec: 0.25, - LinearMPerSec: 0.1, - ObstaclePollingFreqHz: 2, - } -) - -type obstacleMetadata struct { - position r3.Vector - data int - label string -} - -func TestExplorePlanMove(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - fakeBase, err := createFakeBase(ctx, logger) - test.That(t, err, test.ShouldBeNil) - - ms, err := createNewExploreMotionService(ctx, logger, fakeBase, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, ms, test.ShouldNotBeNil) - defer ms.Close(ctx) - - msStruct := ms.(*explore) - - // Create kinematic base - kb, err := msStruct.createKinematicBase(ctx, fakeBase.Name(), defaultMotionCfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, kb.Name().Name, test.ShouldEqual, testBaseName.Name) - - cases := []struct { - description string - destination spatialmath.Pose - expectedMotionPlanLength int - }{ - { - description: "destination directly in front of base", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - expectedMotionPlanLength: 2, - }, - { - description: "destination directly behind the base", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: -1000, Z: 0}), - expectedMotionPlanLength: 2, - }, - { - description: "destination off axis of base", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 1000, Y: 1000, Z: 0}), - expectedMotionPlanLength: 2, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - dest := referenceframe.NewPoseInFrame(testBaseName.Name, tt.destination) - plan, err := msStruct.createMotionPlan(ctx, kb, dest, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(plan.Path()), test.ShouldEqual, tt.expectedMotionPlanLength) - }) - } -} - -func TestExploreCheckForObstacles(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Create fake camera - fakeCamera, err := createFakeCamera(ctx, logger, testCameraName1.Name) - test.That(t, err, test.ShouldBeNil) - - // Create fake base - fakeBase, err := createFakeBase(ctx, logger) - test.That(t, err, test.ShouldBeNil) - - // Create explore motion service - ms, err := createNewExploreMotionService(ctx, logger, fakeBase, []camera.Camera{fakeCamera}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ms, test.ShouldNotBeNil) - defer ms.Close(ctx) - - msStruct := ms.(*explore) - - // Create kinematic base - kb, err := msStruct.createKinematicBase(ctx, fakeBase.Name(), defaultMotionCfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, kb.Name().Name, test.ShouldEqual, testBaseName.Name) - - cases := []struct { - description string - destination spatialmath.Pose - obstacle []obstacleMetadata - detection bool - detectionErr error - }{ - { - description: "no obstacles in view", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - detection: false, - }, - { - description: "obstacle in path nearby", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - obstacle: []obstacleMetadata{ - { - position: r3.Vector{X: 0, Y: 300, Z: 0}, - data: 100, - label: "close_obstacle_in_path", - }, - }, - detection: true, - }, - { - description: "obstacle in path farther away", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - obstacle: []obstacleMetadata{ - { - position: r3.Vector{X: 0, Y: 800, Z: 0}, - data: 100, - label: "far_obstacle_in_path", - }, - }, - detection: false, - }, - { - description: "obstacle in path too far", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 9000, Z: 0}), - obstacle: []obstacleMetadata{ - { - position: r3.Vector{X: 0, Y: 1500, Z: 0}, - data: 100, - label: "far_obstacle_not_in_path", - }, - }, - detection: false, - detectionErr: errors.New("found collision between positions"), - }, - { - description: "obstacle off axis in path", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 1000, Y: 1000, Z: 0}), - obstacle: []obstacleMetadata{ - { - position: r3.Vector{X: 500, Y: 500, Z: 0}, - data: 100, - label: "obstacle on diagonal", - }, - }, - detection: true, - }, - { - description: "obstacle off axis not in path", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - obstacle: []obstacleMetadata{ - { - position: r3.Vector{X: 500, Y: 500, Z: 0}, - data: 100, - label: "close_obstacle_not_in_path", - }, - }, - detection: false, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - // Create motionplan plan - dest := referenceframe.NewPoseInFrame(testBaseName.Name, tt.destination) - plan, err := msStruct.createMotionPlan( - ctx, - kb, - dest, - nil, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, kb.Name().Name, test.ShouldEqual, testBaseName.Name) - test.That(t, len(plan.Path()), test.ShouldBeGreaterThan, 0) - - // Create a vision service using provided obstacles and place it in an obstacle DetectorPair object - visionService := createMockVisionService("", tt.obstacle) - - obstacleDetectors := []obstacleDetectorObject{ - {visionService: fakeCamera.Name()}, - } - - // Update and check worldState - worldState, err := msStruct.generateTransientWorldState(ctx, obstacleDetectors) - test.That(t, err, test.ShouldBeNil) - - if len(tt.obstacle) == 0 { - fs, err := msStruct.fsService.FrameSystem(ctx, nil) - test.That(t, err, test.ShouldBeNil) - - obstacles, err := worldState.ObstaclesInWorldFrame(fs, nil) - test.That(t, err, test.ShouldBeNil) - - for _, obs := range tt.obstacle { - collisionDetected, err := geometriesContainsPoint(obstacles.Geometries(), obs.position, defaultCollisionBufferMM) - test.That(t, err, test.ShouldBeNil) - test.That(t, collisionDetected, test.ShouldBeTrue) - } - } - - // Run check obstacles in of plan path - ctxTimeout, cancelFunc := context.WithTimeout(ctx, 1*time.Second) - defer cancelFunc() - - msStruct.backgroundWorkers.Add(1) - goutils.ManagedGo(func() { - msStruct.checkForObstacles(ctxTimeout, obstacleDetectors, kb, plan, defaultMotionCfg.ObstaclePollingFreqHz) - }, msStruct.backgroundWorkers.Done) - - time.Sleep(20 * time.Millisecond) - - if tt.detection { - resp := <-msStruct.obstacleResponseChan - test.That(t, resp.success, test.ShouldEqual, tt.detection) - if tt.detectionErr == nil { - test.That(t, resp.err, test.ShouldBeNil) - } else { - test.That(t, resp.err, test.ShouldNotBeNil) - test.That(t, resp.err.Error(), test.ShouldContainSubstring, tt.detectionErr.Error()) - } - } - }) - } -} - -func TestMultipleObstacleDetectors(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // Create fake cameras - fakeCamera1, err := createFakeCamera(ctx, logger, testCameraName1.Name) - test.That(t, err, test.ShouldBeNil) - - fakeCamera2, err := createFakeCamera(ctx, logger, testCameraName2.Name) - test.That(t, err, test.ShouldBeNil) - - // Create fake base - fakeBase, err := createFakeBase(ctx, logger) - test.That(t, err, test.ShouldBeNil) - - // Create explore motion service - ms, err := createNewExploreMotionService(ctx, logger, fakeBase, []camera.Camera{fakeCamera1, fakeCamera2}) - test.That(t, err, test.ShouldBeNil) - test.That(t, ms, test.ShouldNotBeNil) - defer ms.Close(ctx) - - msStruct := ms.(*explore) - - // Create kinematic base - kb, err := msStruct.createKinematicBase(ctx, fakeBase.Name(), defaultMotionCfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, kb.Name().Name, test.ShouldEqual, testBaseName.Name) - - cases := []struct { - description string - destination spatialmath.Pose - obstacleCamera1 obstacleMetadata - obstacleCamera2 obstacleMetadata - visionServiceCamLink []camera.Camera - }{ - { - description: "two independent vision services both detecting obstacles", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - obstacleCamera1: obstacleMetadata{ - position: r3.Vector{X: 0, Y: 300, Z: 0}, - data: 100, - label: "close_obstacle_in_path", - }, - obstacleCamera2: obstacleMetadata{ - position: r3.Vector{X: 0, Y: 500, Z: 0}, - data: 100, - label: "close_obstacle_in_path", - }, - visionServiceCamLink: []camera.Camera{fakeCamera1, fakeCamera2}, - }, - { - description: "two independent vision services only first detecting obstacle", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - obstacleCamera1: obstacleMetadata{ - position: r3.Vector{X: 0, Y: 300, Z: 0}, - data: 100, - label: "close_obstacle_in_path", - }, - obstacleCamera2: obstacleMetadata{}, - visionServiceCamLink: []camera.Camera{fakeCamera1, fakeCamera2}, - }, - { - description: "two independent vision services only second detecting obstacle", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - obstacleCamera1: obstacleMetadata{}, - obstacleCamera2: obstacleMetadata{ - position: r3.Vector{X: 0, Y: 300, Z: 0}, - data: 100, - label: "close_obstacle_in_path", - }, - visionServiceCamLink: []camera.Camera{fakeCamera1, fakeCamera2}, - }, - { - description: "two vision services depending on same camera", - destination: spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 1000, Z: 0}), - obstacleCamera1: obstacleMetadata{ - position: r3.Vector{X: 0, Y: 300, Z: 0}, - data: 100, - label: "close_obstacle_in_path", - }, - obstacleCamera2: obstacleMetadata{}, - visionServiceCamLink: []camera.Camera{fakeCamera1, fakeCamera2}, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - // Create motionplan plan - dest := referenceframe.NewPoseInFrame(testBaseName.Name, tt.destination) - plan, err := msStruct.createMotionPlan( - ctx, - kb, - dest, - nil, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, kb.Name().Name, test.ShouldEqual, testBaseName.Name) - test.That(t, len(plan.Path()), test.ShouldBeGreaterThan, 0) - - // Create a vision service using provided obstacles and place it in an obstacle DetectorPair object - var obstacleDetectors []obstacleDetectorObject - for i, cam := range tt.visionServiceCamLink { - var obstacles []obstacleMetadata - if cam.Name().Name == testCameraName1.Name { - obstacles = append(obstacles, tt.obstacleCamera1) - } - if cam.Name().Name == testCameraName2.Name { - obstacles = append(obstacles, tt.obstacleCamera2) - } - visionService := createMockVisionService(fmt.Sprint(i), obstacles) - obstacleDetectors = append(obstacleDetectors, obstacleDetectorObject{visionService: cam.Name()}) - } - - // Run check obstacles in of plan path - ctxTimeout, cancelFunc := context.WithTimeout(ctx, 1*time.Second) - defer cancelFunc() - - msStruct.backgroundWorkers.Add(1) - goutils.ManagedGo(func() { - msStruct.checkForObstacles(ctxTimeout, obstacleDetectors, kb, plan, defaultMotionCfg.ObstaclePollingFreqHz) - }, msStruct.backgroundWorkers.Done) - - resp := <-msStruct.obstacleResponseChan - test.That(t, resp.success, test.ShouldBeTrue) - test.That(t, resp.err, test.ShouldBeNil) - }) - } -} diff --git a/services/motion/explore/explore_utils_test.go b/services/motion/explore/explore_utils_test.go deleted file mode 100644 index 3cb02ab94fa..00000000000 --- a/services/motion/explore/explore_utils_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package explore - -import ( - "context" - - "github.com/golang/geo/r3" - - "go.viam.com/rdk/components/base" - baseFake "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/camera" - cameraFake "go.viam.com/rdk/components/camera/fake" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/services/motion" - vSvc "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision" -) - -// createNewExploreMotionService creates a new motion service complete with base, camera (optional) and frame system. -// Note: The required vision service can/is added later as it will not affect the building of the frame system. -func createNewExploreMotionService(ctx context.Context, logger logging.Logger, fakeBase base.Base, cameras []camera.Camera, -) (motion.Service, error) { - var fsParts []*referenceframe.FrameSystemPart - deps := make(resource.Dependencies) - - // create base link - baseLink, err := createBaseLink(testBaseName.Name) - if err != nil { - return nil, err - } - fsParts = append(fsParts, &referenceframe.FrameSystemPart{FrameConfig: baseLink}) - deps[testBaseName] = fakeBase - - // create camera link - for _, cam := range cameras { - cameraLink, err := createCameraLink(cam.Name().Name, fakeBase.Name().Name) - if err != nil { - return nil, err - } - fsParts = append(fsParts, &referenceframe.FrameSystemPart{FrameConfig: cameraLink}) - deps[cam.Name()] = cam - } - - // create frame service - fs, err := createFrameSystemService(ctx, deps, fsParts, logger) - if err != nil { - return nil, err - } - deps[testFrameServiceName] = fs - - // create explore motion service - exploreMotionConf := resource.Config{ConvertedAttributes: &Config{}} - return NewExplore(ctx, deps, exploreMotionConf, logger) -} - -// createFakeBase instantiates a fake base. -func createFakeBase(ctx context.Context, logger logging.Logger) (base.Base, error) { - fakeBaseCfg := resource.Config{ - Name: testBaseName.Name, - API: base.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{X: 300, Y: 200, Z: 100}}, - } - return baseFake.NewBase(ctx, nil, fakeBaseCfg, logger) -} - -// createFakeCamera instantiates a fake camera. -func createFakeCamera(ctx context.Context, logger logging.Logger, name string) (camera.Camera, error) { - fakeCameraCfg := resource.Config{ - Name: name, - API: camera.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{R: 5}}, - ConvertedAttributes: &cameraFake.Config{ - Width: 1000, - Height: 1000, - }, - } - return cameraFake.NewCamera(ctx, resource.Dependencies{}, fakeCameraCfg, logger) -} - -// createMockVisionService instantiates a mock vision service with a custom version of GetObjectPointCloud that returns -// vision objects from a given set of points. -func createMockVisionService(visionSvcNum string, obstacles []obstacleMetadata) vSvc.Service { - mockVisionService := &inject.VisionService{} - - mockVisionService.GetObjectPointCloudsFunc = func(ctx context.Context, cameraName string, extra map[string]interface{}, - ) ([]*vision.Object, error) { - var vObjects []*vision.Object - for _, obs := range obstacles { - obstaclePosition := spatialmath.NewPoseFromPoint(obs.position) - box, err := spatialmath.NewBox(obstaclePosition, r3.Vector{X: 100, Y: 100, Z: 100}, "test-case-2") - if err != nil { - return nil, err - } - - detection, err := vision.NewObjectWithLabel(pointcloud.New(), obs.label+visionSvcNum, box.ToProtobuf()) - if err != nil { - return nil, err - } - vObjects = append(vObjects, detection) - } - return vObjects, nil - } - return mockVisionService -} - -// createFrameSystemService will create a basic frame service from the list of parts. -func createFrameSystemService( - ctx context.Context, - deps resource.Dependencies, - fsParts []*referenceframe.FrameSystemPart, - logger logging.Logger, -) (framesystem.Service, error) { - fsSvc, err := framesystem.New(ctx, deps, logger) - if err != nil { - return nil, err - } - conf := resource.Config{ - ConvertedAttributes: &framesystem.Config{Parts: fsParts}, - } - if err := fsSvc.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return fsSvc, nil -} - -// createBaseLink instantiates the base to world link for the frame system. -func createBaseLink(baseName string) (*referenceframe.LinkInFrame, error) { - basePose := spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 0}) - baseSphere, err := spatialmath.NewSphere(basePose, 10, "base-box") - if err != nil { - return nil, err - } - - baseLink := referenceframe.NewLinkInFrame( - referenceframe.World, - spatialmath.NewZeroPose(), - baseName, - baseSphere, - ) - return baseLink, nil -} - -// createCameraLink instantiates the camera to base link for the frame system. -func createCameraLink(camName, baseFrame string) (*referenceframe.LinkInFrame, error) { - camPose := spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 0, Z: 0}) - camSphere, err := spatialmath.NewSphere(camPose, 5, "cam-sphere") - if err != nil { - return nil, err - } - - camLink := referenceframe.NewLinkInFrame( - baseFrame, - spatialmath.NewZeroPose(), - camName, - camSphere, - ) - return camLink, nil -} - -// geometriesContainsPoint is a helper function to test if a point is in a given geometry. -func geometriesContainsPoint(geometries []spatialmath.Geometry, point r3.Vector, collisionBufferMM float64) (bool, error) { - var collisionDetected bool - for _, geo := range geometries { - pointGeoCfg := spatialmath.GeometryConfig{ - Type: spatialmath.BoxType, - TranslationOffset: point, - } - pointGeo, err := pointGeoCfg.ParseConfig() - if err != nil { - return false, err - } - collides, err := geo.CollidesWith(pointGeo, collisionBufferMM) - if err != nil { - return false, err - } - if collides { - collisionDetected = true - } - } - return collisionDetected, nil -} diff --git a/services/motion/localizer.go b/services/motion/localizer.go deleted file mode 100644 index 17f9e3fb0f5..00000000000 --- a/services/motion/localizer.go +++ /dev/null @@ -1,148 +0,0 @@ -package motion - -import ( - "context" - "math" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/spatialmath" -) - -// SLAMOrientationAdjustment is needed because a SLAM map pose has orientation of OZ=1, Theta=0 when the rover is intended to be pointing -// at the +X axis of the SLAM map. -// However, for a rover's relative planning frame, driving forwards increments +Y. Thus we must adjust where the rover thinks it is. -var SLAMOrientationAdjustment = spatialmath.NewPoseFromOrientation(&spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -90}) - -const poleEpsilon = 0.0001 // If the base is this close to vertical, we cannot plan. - -// Localizer is an interface which both slam and movementsensor can satisfy when wrapped respectively. -type Localizer interface { - CurrentPosition(context.Context) (*referenceframe.PoseInFrame, error) -} - -// slamLocalizer is a struct which only wraps an existing slam service. -type slamLocalizer struct { - slam.Service -} - -// NewSLAMLocalizer creates a new Localizer that relies on a slam service to report Pose. -func NewSLAMLocalizer(slam slam.Service) Localizer { - return &slamLocalizer{Service: slam} -} - -// CurrentPosition returns slam's current position. -func (s *slamLocalizer) CurrentPosition(ctx context.Context) (*referenceframe.PoseInFrame, error) { - pose, _, err := s.Position(ctx) - if err != nil { - return nil, err - } - pose = spatialmath.Compose(pose, SLAMOrientationAdjustment) - - // Slam poses are returned such that theta=0 points along the +X axis - // We must rotate 90 degrees to match the base convention of y = forwards - return referenceframe.NewPoseInFrame(referenceframe.World, pose), err -} - -// movementSensorLocalizer is a struct which only wraps an existing movementsensor. -type movementSensorLocalizer struct { - movementsensor.MovementSensor - origin *geo.Point - calibration spatialmath.Pose -} - -// NewMovementSensorLocalizer creates a Localizer from a MovementSensor. -// An origin point must be specified and the localizer will return Poses relative to this point. -// A calibration pose can also be specified, which will adjust the location after it is calculated relative to the origin. -func NewMovementSensorLocalizer(ms movementsensor.MovementSensor, origin *geo.Point, calibration spatialmath.Pose) Localizer { - return &movementSensorLocalizer{MovementSensor: ms, origin: origin, calibration: calibration} -} - -// CurrentPosition returns a movementsensor's current position. -func (m *movementSensorLocalizer) CurrentPosition(ctx context.Context) (*referenceframe.PoseInFrame, error) { - gp, _, err := m.Position(ctx, nil) - if err != nil { - return nil, err - } - var o spatialmath.Orientation - properties, err := m.Properties(ctx, nil) - if err != nil { - return nil, err - } - switch { - case properties.CompassHeadingSupported: - headingLeft, err := m.CompassHeading(ctx, nil) - if err != nil { - return nil, err - } - // CompassHeading is a left-handed value. Convert to be right-handed. Use math.Mod to ensure that 0 reports 0 rather than 360. - heading := math.Mod(math.Abs(headingLeft-360), 360) - o = &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: heading} - case properties.OrientationSupported: - o, err = m.Orientation(ctx, nil) - if err != nil { - return nil, err - } - default: - return nil, errors.New("could not get orientation from Localizer") - } - - pose := spatialmath.NewPose(spatialmath.GeoPointToPoint(gp, m.origin), o) - return referenceframe.NewPoseInFrame(m.Name().ShortName(), spatialmath.Compose(pose, m.calibration)), nil -} - -// TwoDLocalizer will check the orientation of the pose of a localizer, and ensure that it is normal to the XY plane. -// If it is not, it will be altered such that it is (accounting for e.g. an ourdoor base with one wheel on a rock). If the orientation is -// such that the base is pointed directly up or down (or is upside-down), an error is returned. -// The alteration to ensure normality to the plane is done by transforming the (0,1,0) vector by the provided orientation, and then -// using atan2 on the new x and y values to determine the vector of travel that would be followed. -func TwoDLocalizer(l Localizer) Localizer { - return &yForwards2dLocalizer{l} -} - -type yForwards2dLocalizer struct { - Localizer -} - -func (y *yForwards2dLocalizer) CurrentPosition(ctx context.Context) (*referenceframe.PoseInFrame, error) { - currPos, err := y.Localizer.CurrentPosition(ctx) - if err != nil { - return nil, err - } - newPose, err := projectOrientationTo2dRotation(currPos.Pose()) - if err != nil { - return nil, err - } - newPiF := referenceframe.NewPoseInFrame(currPos.Parent(), newPose) - newPiF.SetName(currPos.Name()) - return newPiF, nil -} - -func projectOrientationTo2dRotation(pose spatialmath.Pose) (spatialmath.Pose, error) { - orient := pose.Orientation().OrientationVectorRadians() // OV is easy to check if we are in plane - if orient.OZ < 0 { - return nil, errors.New("base appears to be upside down, check your movement sensor") - } - if orient.OX == 0 && orient.OY == 0 { - return pose, nil - } - - // This is the vector in which the base would move if it had no changed orientation - adjPt := spatialmath.NewPoseFromPoint(r3.Vector{0, 1, 0}) - // This is the vector the base would follow based on the reported orientation, were it unencumbered by things like gravity or the ground - newAdjPt := spatialmath.Compose(spatialmath.NewPoseFromOrientation(orient), adjPt).Point() - if 1-math.Abs(newAdjPt.Z) < poleEpsilon { - if newAdjPt.Z > 0 { - return nil, errors.New("base appears to be pointing straight up, check your movement sensor") - } - return nil, errors.New("base appears to be pointing straight down, check your movement sensor") - } - // This is the vector across the ground of the above hypothetical vector, projected onto the X-Y plane. - theta := -math.Atan2(newAdjPt.Y, -newAdjPt.X) - return spatialmath.NewPose(pose.Point(), &spatialmath.OrientationVector{OZ: 1, Theta: theta}), nil -} diff --git a/services/motion/localizer_test.go b/services/motion/localizer_test.go deleted file mode 100644 index b11a4588341..00000000000 --- a/services/motion/localizer_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package motion_test - -import ( - "context" - "math" - "testing" - - geo "github.com/kellydunn/golang-geo" - "go.viam.com/test" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -func createInjectedCompassMovementSensor(name string, gpsPoint *geo.Point) *inject.MovementSensor { - injectedMovementSensor := inject.NewMovementSensor(name) - injectedMovementSensor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return gpsPoint, 0, nil - } - injectedMovementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 0, nil - } - injectedMovementSensor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{CompassHeadingSupported: true}, nil - } - - return injectedMovementSensor -} - -func createInjectedOrientationMovementSensor(orient spatialmath.Orientation) *inject.MovementSensor { - injectedMovementSensor := inject.NewMovementSensor("") - injectedMovementSensor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(0, 0), 0, nil - } - injectedMovementSensor.OrientationFunc = func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - return orient, nil - } - injectedMovementSensor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{OrientationSupported: true}, nil - } - - return injectedMovementSensor -} - -func TestLocalizerOrientation(t *testing.T) { - ctx := context.Background() - - origin := geo.NewPoint(-70, 40) - movementSensor := createInjectedCompassMovementSensor("", origin) - localizer := motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose()) - - heading, err := movementSensor.CompassHeading(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, heading, test.ShouldEqual, 0) - - pif, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - // A compass heading of 0 means we are pointing north. - test.That(t, spatialmath.OrientationAlmostEqual( - pif.Pose().Orientation(), - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 0}), - test.ShouldBeTrue, - ) - - // Update compass heading to point northwest - movementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 315, nil - } - - pif, err = localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - // A compass heading of 315 means we are pointing northwest. - test.That(t, spatialmath.OrientationAlmostEqual( - pif.Pose().Orientation(), - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 45}), - test.ShouldBeTrue, - ) - - // Update compass heading to point east - movementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - return 90, nil - } - pif, err = localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - - // A compass heading of 90 means we are pointing east. - test.That(t, spatialmath.OrientationAlmostEqual( - pif.Pose().Orientation(), - &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: -90}), - test.ShouldBeTrue, - ) -} - -func TestCorrectStartPose(t *testing.T) { - t.Parallel() - ctx := context.Background() - origin := geo.NewPoint(0, 0) - t.Run("Test angle from +Y to +X, +Y quadrant", func(t *testing.T) { - t.Parallel() - // -45 - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 1, OY: 1, OZ: 1} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - corrected, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, corrected.Pose().Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, -45.) - }) - t.Run("Test angle from +Y to -X, +Y quadrant", func(t *testing.T) { - t.Parallel() - // 45 - askewOrient := &spatialmath.OrientationVectorDegrees{OX: -1, OY: 1, OZ: 1} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - corrected, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, corrected.Pose().Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 45.) - }) - t.Run("Test angle from +Y to +X, -Y quadrant", func(t *testing.T) { - t.Parallel() - // -135 - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 1, OY: -1, OZ: 1} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - corrected, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, corrected.Pose().Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, -135.) - }) - t.Run("Test angle from +Y to -X, -Y quadrant", func(t *testing.T) { - t.Parallel() - // 135 - askewOrient := &spatialmath.OrientationVectorDegrees{OX: -1, OY: -1, OZ: 1} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - corrected, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, corrected.Pose().Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 135.) - }) - t.Run("Test non-multiple-of-45 angle from +Y to +X, +Y quadrant", func(t *testing.T) { - t.Parallel() - // -30 - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 1, OY: math.Sqrt(3), OZ: 1} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - corrected, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, corrected.Pose().Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, -30.) - }) - t.Run("Test orientation already at OZ=1", func(t *testing.T) { - t.Parallel() - // 127 - askewOrient := &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 127.} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - corrected, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, corrected.Pose().Orientation().OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, 127.) - }) - t.Run("Test upside-down error", func(t *testing.T) { - t.Parallel() - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 1, OY: 1, OZ: -1} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - _, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, "base appears to be upside down, check your movement sensor") - }) - t.Run("Test pointing-straight-up error", func(t *testing.T) { - t.Parallel() - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 0, OY: 1, OZ: 0, Theta: 90} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - _, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, "base appears to be pointing straight up, check your movement sensor") - }) - t.Run("Test pointing-straight-down error", func(t *testing.T) { - t.Parallel() - askewOrient := &spatialmath.OrientationVectorDegrees{OX: 0, OY: 1, OZ: 0, Theta: -90} - movementSensor := createInjectedOrientationMovementSensor(askewOrient) - localizer := motion.TwoDLocalizer(motion.NewMovementSensorLocalizer(movementSensor, origin, spatialmath.NewZeroPose())) - _, err := localizer.CurrentPosition(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, "base appears to be pointing straight down, check your movement sensor") - }) -} diff --git a/services/motion/motion.go b/services/motion/motion.go deleted file mode 100644 index 2dfdf21c9a2..00000000000 --- a/services/motion/motion.go +++ /dev/null @@ -1,412 +0,0 @@ -// Package motion is the service that allows you to plan and execute movements. -package motion - -import ( - "context" - "fmt" - "time" - - "github.com/google/uuid" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - pb "go.viam.com/api/service/motion/v1" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/motionplan" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/spatialmath" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Service]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterMotionServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.MotionService_ServiceDesc, - RPCClient: NewClientFromConn, - }) -} - -// PlanHistoryReq describes the request to PlanHistory(). -type PlanHistoryReq struct { - // ComponentName the returned plans should be associated with. - ComponentName resource.Name - // When true, only the most recent plan will be returned which matches the ComponentName & ExecutionID if one was provided. - LastPlanOnly bool - // Optional, when not uuid.Nil it specifies the ExecutionID of the plans that should be returned. - // Can be used to query plans from executions before the most recent one. - ExecutionID ExecutionID - Extra map[string]interface{} -} - -// MoveOnGlobeReq describes the request to the MoveOnGlobe interface method. -type MoveOnGlobeReq struct { - // ComponentName of the component to move - ComponentName resource.Name - // Goal destination the component should be moved to - Destination *geo.Point - // Heading the component should have a when it reaches the goal. - // Range [0-360] Left Hand Rule (N: 0, E: 90, S: 180, W: 270) - Heading float64 - // Name of the momement sensor which can be used to derive Position & Heading - MovementSensorName resource.Name - // Static obstacles that should be navigated around - Obstacles []*spatialmath.GeoObstacle - // Optional motion configuration - MotionCfg *MotionConfiguration - Extra map[string]interface{} -} - -func (r MoveOnGlobeReq) String() string { - template := "motion.MoveOnGlobeReq{ComponentName: %s, " + - "Destination: %+v, Heading: %f, MovementSensorName: %s, " + - "Obstacles: %v, MotionCfg: %#v, Extra: %s}" - return fmt.Sprintf(template, - r.ComponentName, - r.Destination, - r.Heading, - r.MovementSensorName, - r.Obstacles, - r.MotionCfg, - r.Extra) -} - -// MoveOnMapReq describes a request to MoveOnMap. -type MoveOnMapReq struct { - ComponentName resource.Name - Destination spatialmath.Pose - SlamName resource.Name - MotionCfg *MotionConfiguration - Obstacles []spatialmath.Geometry - Extra map[string]interface{} -} - -func (r MoveOnMapReq) String() string { - return fmt.Sprintf( - "motion.MoveOnMapReq{ComponentName: %s, SlamName: %s, Destination: %+v, "+ - "MotionCfg: %#v, Obstacles: %s, Extra: %s}", - r.ComponentName, - r.SlamName, - spatialmath.PoseToProtobuf(r.Destination), - r.MotionCfg, - r.Obstacles, - r.Extra) -} - -// StopPlanReq describes the request to StopPlan(). -type StopPlanReq struct { - // ComponentName of the plan which should be stopped - ComponentName resource.Name - Extra map[string]interface{} -} - -// ListPlanStatusesReq describes the request to ListPlanStatuses(). -type ListPlanStatusesReq struct { - // If true then only active plans will be returned. - OnlyActivePlans bool - Extra map[string]interface{} -} - -// PlanWithMetadata represents a motion plan with additional metadata used by the motion service. -type PlanWithMetadata struct { - // Unique ID of the plan - ID PlanID - // Name of the component the plan is planning for - ComponentName resource.Name - // Unique ID of the execution - ExecutionID ExecutionID - // The motionplan itself - motionplan.Plan - // The GPS point to anchor visualized plans at - AnchorGeoPose *spatialmath.GeoPose -} - -// PlanState denotes the state a Plan is in. -type PlanState uint8 - -const ( - // PlanStateUnspecified denotes an the Plan is in an unspecified state. This should never happen. - PlanStateUnspecified = iota - - // PlanStateInProgress denotes an the Plan is in an in progress state. It is a temporary state. - PlanStateInProgress - - // PlanStateStopped denotes an the Plan is in a stopped state. It is a terminal state. - PlanStateStopped - - // PlanStateSucceeded denotes an the Plan is in a succeeded state. It is a terminal state. - PlanStateSucceeded - - // PlanStateFailed denotes an the Plan is in a failed state. It is a terminal state. - PlanStateFailed -) - -// TerminalStateSet is a set that defines the PlanState values which are terminal -// i.e. which represent the end of a plan. -var TerminalStateSet = map[PlanState]struct{}{ - PlanStateStopped: {}, - PlanStateSucceeded: {}, - PlanStateFailed: {}, -} - -// PlanID uniquely identifies a Plan. -type PlanID = uuid.UUID - -// ExecutionID uniquely identifies an execution. -type ExecutionID = uuid.UUID - -// PlanStatusWithID describes the state of a given plan at a -// point in time plus the PlanId, ComponentName and ExecutionID -// the status is associated with. -type PlanStatusWithID struct { - PlanID PlanID - ComponentName resource.Name - ExecutionID ExecutionID - Status PlanStatus -} - -// PlanStatus describes the state of a given plan at a -// point in time allong with an optional reason why the PlanStatus -// transitioned to that state. -type PlanStatus struct { - State PlanState - Timestamp time.Time - Reason *string -} - -// PlanWithStatus contains a plan, its current status, and all state changes that came prior -// sorted by ascending timestamp. -type PlanWithStatus struct { - Plan PlanWithMetadata - StatusHistory []PlanStatus -} - -// A Service controls the flow of moving components. -type Service interface { - resource.Resource - Move( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *pb.Constraints, - extra map[string]interface{}, - ) (bool, error) - MoveOnMap( - ctx context.Context, - req MoveOnMapReq, - ) (ExecutionID, error) - MoveOnGlobe( - ctx context.Context, - req MoveOnGlobeReq, - ) (ExecutionID, error) - GetPose( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransforms []*referenceframe.LinkInFrame, - extra map[string]interface{}, - ) (*referenceframe.PoseInFrame, error) - StopPlan( - ctx context.Context, - req StopPlanReq, - ) error - ListPlanStatuses( - ctx context.Context, - req ListPlanStatusesReq, - ) ([]PlanStatusWithID, error) - PlanHistory( - ctx context.Context, - req PlanHistoryReq, - ) ([]PlanWithStatus, error) -} - -// ObstacleDetectorName pairs a vision service name with a camera name. -type ObstacleDetectorName struct { - VisionServiceName resource.Name - CameraName resource.Name -} - -// MotionConfiguration specifies how to configure a call -// -//nolint:revive -type MotionConfiguration struct { - ObstacleDetectors []ObstacleDetectorName - PositionPollingFreqHz float64 - ObstaclePollingFreqHz float64 - PlanDeviationMM float64 - LinearMPerSec float64 - AngularDegsPerSec float64 -} - -// SubtypeName is the name of the type of service. -const SubtypeName = "motion" - -// API is a variable that identifies the motion service resource API. -var API = resource.APINamespaceRDK.WithServiceType(SubtypeName) - -// Named is a helper for getting the named motion service's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named motion service from the given Robot. -func FromRobot(r robot.Robot, name string) (Service, error) { - return robot.ResourceFromRobot[Service](r, Named(name)) -} - -// FromDependencies is a helper for getting the named motion service from a collection of dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Service, error) { - return resource.FromDependencies[Service](deps, Named(name)) -} - -// ToProto converts a PlanWithStatus to a *pb.PlanWithStatus. -func (pws PlanWithStatus) ToProto() *pb.PlanWithStatus { - statusHistory := []*pb.PlanStatus{} - for _, ps := range pws.StatusHistory { - statusHistory = append(statusHistory, ps.ToProto()) - } - - planWithStatusPB := &pb.PlanWithStatus{ - Plan: pws.Plan.ToProto(), - } - - if len(pws.StatusHistory) == 0 { - return planWithStatusPB - } - - planWithStatusPB.Status = statusHistory[0] - planWithStatusPB.StatusHistory = statusHistory[1:] - return planWithStatusPB -} - -// ToProto converts a PlanStatusWithID to a *pb.PlanStatusWithID. -func (ps PlanStatusWithID) ToProto() *pb.PlanStatusWithID { - return &pb.PlanStatusWithID{ - PlanId: ps.PlanID.String(), - ComponentName: rprotoutils.ResourceNameToProto(ps.ComponentName), - ExecutionId: ps.ExecutionID.String(), - Status: ps.Status.ToProto(), - } -} - -// ToProto converts a PlanStatus to a *pb.PlanStatus. -func (ps PlanStatus) ToProto() *pb.PlanStatus { - return &pb.PlanStatus{ - State: ps.State.ToProto(), - Timestamp: timestamppb.New(ps.Timestamp), - Reason: ps.Reason, - } -} - -// ToProto converts a Plan to a *pb.Plan. -func (p PlanWithMetadata) ToProto() *pb.Plan { - steps := []*pb.PlanStep{} - if p.Plan != nil { - for _, s := range p.Path() { - steps = append(steps, s.ToProto()) - } - } - - return &pb.Plan{ - Id: p.ID.String(), - ComponentName: rprotoutils.ResourceNameToProto(p.ComponentName), - ExecutionId: p.ExecutionID.String(), - Steps: steps, - } -} - -// Renderable returns a copy of the struct substituting its Plan for a GeoPlan consisting of smuggled global coordinates -// This will only be done if the AnchorGeoPose field is non-nil, otherwise the original struct will be returned. -func (p PlanWithMetadata) Renderable() PlanWithMetadata { - if p.AnchorGeoPose == nil { - return p - } - return PlanWithMetadata{ - ID: p.ID, - ComponentName: p.ComponentName, - ExecutionID: p.ExecutionID, - Plan: motionplan.NewGeoPlan(p.Plan, p.AnchorGeoPose.Location()), - } -} - -// ToProto converts a PlanState to a pb.PlanState. -func (ps PlanState) ToProto() pb.PlanState { - switch ps { - case PlanStateInProgress: - return pb.PlanState_PLAN_STATE_IN_PROGRESS - case PlanStateStopped: - return pb.PlanState_PLAN_STATE_STOPPED - case PlanStateSucceeded: - return pb.PlanState_PLAN_STATE_SUCCEEDED - case PlanStateFailed: - return pb.PlanState_PLAN_STATE_FAILED - default: - return pb.PlanState_PLAN_STATE_UNSPECIFIED - } -} - -func (ps PlanState) String() string { - switch ps { - case PlanStateInProgress: - return "in progress" - case PlanStateStopped: - return "stopped" - case PlanStateSucceeded: - return "succeeded" - case PlanStateFailed: - return "failed" - case PlanStateUnspecified: - return "unspecified" - default: - return "unknown" - } -} - -// PollHistoryUntilSuccessOrError polls `PlanHistory()` with `req` every `interval` -// until a terminal state is reached. -// An error is returned if the terminal state is Failed, Stopped or an invalid state -// or if the context has an error. -// nil is returned if the terminal state is Succeeded. -func PollHistoryUntilSuccessOrError( - ctx context.Context, - m Service, - interval time.Duration, - req PlanHistoryReq, -) error { - for { - if err := ctx.Err(); err != nil { - return err - } - - ph, err := m.PlanHistory(ctx, req) - if err != nil { - return err - } - - status := ph[0].StatusHistory[0] - - switch status.State { - case PlanStateInProgress: - case PlanStateFailed: - err := errors.New("plan failed") - if reason := status.Reason; reason != nil { - err = errors.Wrap(err, *reason) - } - return err - - case PlanStateStopped: - return errors.New("plan stopped") - - case PlanStateSucceeded: - return nil - - default: - return fmt.Errorf("invalid plan state %d", status.State) - } - - time.Sleep(interval) - } -} diff --git a/services/motion/motion_configuration.go b/services/motion/motion_configuration.go deleted file mode 100644 index 6cb05c5aed3..00000000000 --- a/services/motion/motion_configuration.go +++ /dev/null @@ -1,85 +0,0 @@ -package motion - -import ( - "math" - - pb "go.viam.com/api/service/motion/v1" - - "go.viam.com/rdk/protoutils" -) - -func configurationFromProto(motionCfg *pb.MotionConfiguration) *MotionConfiguration { - obstacleDetectors := []ObstacleDetectorName{} - planDeviationM := 0. - positionPollingHz := 0. - obstaclePollingHz := 0. - linearMPerSec := 0. - angularDegsPerSec := 0. - - if motionCfg != nil { - if motionCfg.ObstacleDetectors != nil { - for _, obstacleDetectorPair := range motionCfg.GetObstacleDetectors() { - obstacleDetectors = append(obstacleDetectors, ObstacleDetectorName{ - VisionServiceName: protoutils.ResourceNameFromProto(obstacleDetectorPair.VisionService), - CameraName: protoutils.ResourceNameFromProto(obstacleDetectorPair.Camera), - }) - } - } - if motionCfg.PositionPollingFrequencyHz != nil { - positionPollingHz = motionCfg.GetPositionPollingFrequencyHz() - } - if motionCfg.ObstaclePollingFrequencyHz != nil { - obstaclePollingHz = motionCfg.GetObstaclePollingFrequencyHz() - } - if motionCfg.PlanDeviationM != nil { - planDeviationM = motionCfg.GetPlanDeviationM() - } - if motionCfg.LinearMPerSec != nil { - linearMPerSec = motionCfg.GetLinearMPerSec() - } - if motionCfg.AngularDegsPerSec != nil { - angularDegsPerSec = motionCfg.GetAngularDegsPerSec() - } - } - - return &MotionConfiguration{ - ObstacleDetectors: obstacleDetectors, - PositionPollingFreqHz: positionPollingHz, - ObstaclePollingFreqHz: obstaclePollingHz, - PlanDeviationMM: 1e3 * planDeviationM, - LinearMPerSec: linearMPerSec, - AngularDegsPerSec: angularDegsPerSec, - } -} - -func (motionCfg MotionConfiguration) toProto() *pb.MotionConfiguration { - proto := &pb.MotionConfiguration{} - if !math.IsNaN(motionCfg.LinearMPerSec) && motionCfg.LinearMPerSec != 0 { - proto.LinearMPerSec = &motionCfg.LinearMPerSec - } - if !math.IsNaN(motionCfg.AngularDegsPerSec) && motionCfg.AngularDegsPerSec != 0 { - proto.AngularDegsPerSec = &motionCfg.AngularDegsPerSec - } - if !math.IsNaN(motionCfg.ObstaclePollingFreqHz) && motionCfg.ObstaclePollingFreqHz > 0 { - proto.ObstaclePollingFrequencyHz = &motionCfg.ObstaclePollingFreqHz - } - if !math.IsNaN(motionCfg.PositionPollingFreqHz) && motionCfg.PositionPollingFreqHz > 0 { - proto.PositionPollingFrequencyHz = &motionCfg.PositionPollingFreqHz - } - if !math.IsNaN(motionCfg.PlanDeviationMM) && motionCfg.PlanDeviationMM >= 0 { - planDeviationM := 1e-3 * motionCfg.PlanDeviationMM - proto.PlanDeviationM = &planDeviationM - } - - if len(motionCfg.ObstacleDetectors) > 0 { - pbObstacleDetector := []*pb.ObstacleDetector{} - for _, obstacleDetectorPair := range motionCfg.ObstacleDetectors { - pbObstacleDetector = append(pbObstacleDetector, &pb.ObstacleDetector{ - VisionService: protoutils.ResourceNameToProto(obstacleDetectorPair.VisionServiceName), - Camera: protoutils.ResourceNameToProto(obstacleDetectorPair.CameraName), - }) - } - proto.ObstacleDetectors = pbObstacleDetector - } - return proto -} diff --git a/services/motion/motion_helpers_test.go b/services/motion/motion_helpers_test.go deleted file mode 100644 index 1ae6afb7b2a..00000000000 --- a/services/motion/motion_helpers_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package motion_test - -import ( - "context" - "testing" - "time" - - "github.com/pkg/errors" - "go.viam.com/test" - - _ "go.viam.com/rdk/components/register" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/testutils/inject" -) - -func TestPollHistoryUntilSuccessOrError(t *testing.T) { - ctx := context.Background() - ms := inject.NewMotionService("my motion") - t.Run("returns error if context is cancelled", func(t *testing.T) { - cancelledCtx, cancelFn := context.WithCancel(context.Background()) - cancelFn() - ms.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - t.Error("should not be called") - t.FailNow() - return nil, nil - } - err := motion.PollHistoryUntilSuccessOrError(cancelledCtx, ms, time.Millisecond, motion.PlanHistoryReq{}) - test.That(t, err, test.ShouldBeError, context.Canceled) - }) - - t.Run("returns error if PlanHistory returns an error", func(t *testing.T) { - errExpected := errors.New("some error") - ms.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return nil, errExpected - } - err := motion.PollHistoryUntilSuccessOrError(ctx, ms, time.Millisecond, motion.PlanHistoryReq{}) - test.That(t, err, test.ShouldBeError, errExpected) - }) - - t.Run("returns an error if PlanHistory returns a most recent plan which is in an invalid state", func(t *testing.T) { - errExpected := errors.New("invalid plan state 0") - ms.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return []motion.PlanWithStatus{{StatusHistory: []motion.PlanStatus{{State: motion.PlanStateUnspecified}}}}, nil - } - err := motion.PollHistoryUntilSuccessOrError(ctx, ms, time.Millisecond, motion.PlanHistoryReq{}) - test.That(t, err, test.ShouldBeError, errExpected) - }) - - t.Run("returns an error if PlanHistory returns a most recent plan which is in Stopped state", func(t *testing.T) { - errExpected := errors.New("plan stopped") - ms.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return []motion.PlanWithStatus{{StatusHistory: []motion.PlanStatus{{State: motion.PlanStateStopped}}}}, nil - } - err := motion.PollHistoryUntilSuccessOrError(ctx, ms, time.Millisecond, motion.PlanHistoryReq{}) - test.That(t, err, test.ShouldBeError, errExpected) - }) - - t.Run("returns an error with reason if PlanHistory returns a most recent plan which is in Failed state", func(t *testing.T) { - reason := "this is the fail reason" - errExpected := errors.Wrap(errors.New("plan failed"), reason) - ms.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return []motion.PlanWithStatus{{StatusHistory: []motion.PlanStatus{{State: motion.PlanStateFailed, Reason: &reason}}}}, nil - } - err := motion.PollHistoryUntilSuccessOrError(ctx, ms, time.Millisecond, motion.PlanHistoryReq{}) - test.That(t, err, test.ShouldBeError, errExpected) - }) - - t.Run("returns nil if PlanHistory returns a most recent plan which is in Succeeded state", func(t *testing.T) { - ms.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return []motion.PlanWithStatus{{StatusHistory: []motion.PlanStatus{{State: motion.PlanStateSucceeded}}}}, nil - } - err := motion.PollHistoryUntilSuccessOrError(ctx, ms, time.Millisecond, motion.PlanHistoryReq{}) - test.That(t, err, test.ShouldBeNil) - }) - - t.Run("returns polls until a terminal state is reached", func(t *testing.T) { - var callCount int - ms.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - callCount++ - switch callCount { - case 1: - return []motion.PlanWithStatus{{StatusHistory: []motion.PlanStatus{{State: motion.PlanStateInProgress}}}}, nil - case 2: - return []motion.PlanWithStatus{{StatusHistory: []motion.PlanStatus{{State: motion.PlanStateSucceeded}}}}, nil - default: - t.Error("should not be called") - t.FailNow() - return nil, errors.New("should not happen") - } - } - err := motion.PollHistoryUntilSuccessOrError(ctx, ms, time.Millisecond, motion.PlanHistoryReq{}) - test.That(t, err, test.ShouldBeNil) - }) -} diff --git a/services/motion/motion_test.go b/services/motion/motion_test.go deleted file mode 100644 index 4253c4a8843..00000000000 --- a/services/motion/motion_test.go +++ /dev/null @@ -1,1451 +0,0 @@ -package motion - -import ( - "fmt" - "math" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/motion/v1" - "go.viam.com/test" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/motionplan" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" -) - -var defaultMotionCfg = MotionConfiguration{ - ObstacleDetectors: []ObstacleDetectorName{}, -} - -func TestPlanWithStatus(t *testing.T) { - planID := uuid.New() - executionID := uuid.New() - - baseName := base.Named("my-base1") - poseA := spatialmath.NewZeroPose() - poseB := spatialmath.NewPose(r3.Vector{X: 100}, spatialmath.NewOrientationVector()) - - timestamp := time.Now().UTC() - timestampb := timestamppb.New(timestamp) - reason := "some reason" - - plan := PlanWithMetadata{ - ID: planID, - ExecutionID: executionID, - ComponentName: baseName, - Plan: motionplan.NewSimplePlan( - []motionplan.PathStep{ - {baseName.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, poseA)}, - {baseName.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, poseB)}, - }, - nil, - ), - } - - protoPlan := &pb.Plan{ - Id: planID.String(), - ExecutionId: executionID.String(), - ComponentName: rprotoutils.ResourceNameToProto(baseName), - Steps: []*pb.PlanStep{ - { - Step: map[string]*pb.ComponentState{ - baseName.ShortName(): {Pose: spatialmath.PoseToProtobuf(poseA)}, - }, - }, - { - Step: map[string]*pb.ComponentState{ - baseName.ShortName(): {Pose: spatialmath.PoseToProtobuf(poseB)}, - }, - }, - }, - } - - t.Run("planWithStatusFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.PlanWithStatus - result PlanWithStatus - err error - } - - testCases := []testCase{ - { - description: "nil pointer returns error", - input: nil, - result: PlanWithStatus{}, - err: errors.New("received nil *pb.PlanWithStatus"), - }, - { - description: "empty plan returns an error", - input: &pb.PlanWithStatus{}, - result: PlanWithStatus{}, - err: errors.New("received nil *pb.Plan"), - }, - { - description: "empty status returns an error", - input: &pb.PlanWithStatus{Plan: PlanWithMetadata{}.ToProto()}, - result: PlanWithStatus{}, - err: errors.New("received nil *pb.PlanStatus"), - }, - { - description: "nil pointers in the status history returns an error", - input: &pb.PlanWithStatus{ - Plan: PlanWithMetadata{}.ToProto(), - Status: PlanStatus{}.ToProto(), - StatusHistory: []*pb.PlanStatus{nil}, - }, - result: PlanWithStatus{}, - err: errors.New("received nil *pb.PlanStatus"), - }, - { - description: "empty *pb.PlanWithStatus status returns an empty PlanWithStatus", - input: &pb.PlanWithStatus{ - Plan: PlanWithMetadata{}.ToProto(), - Status: PlanStatus{}.ToProto(), - }, - result: PlanWithStatus{ - Plan: PlanWithMetadata{}, - StatusHistory: []PlanStatus{{}}, - }, - }, - { - description: "full *pb.PlanWithStatus status returns a full PlanWithStatus", - input: &pb.PlanWithStatus{ - Plan: protoPlan, - Status: &pb.PlanStatus{ - State: pb.PlanState_PLAN_STATE_FAILED, - Timestamp: timestampb, - Reason: &reason, - }, - StatusHistory: []*pb.PlanStatus{ - { - State: pb.PlanState_PLAN_STATE_IN_PROGRESS, - Timestamp: timestampb, - }, - }, - }, - result: PlanWithStatus{ - Plan: plan, - StatusHistory: []PlanStatus{ - {State: PlanStateFailed, Timestamp: timestamp, Reason: &reason}, - {State: PlanStateInProgress, Timestamp: timestamp}, - }, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := planWithStatusFromProto(tc.input) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("ToProto()", func(t *testing.T) { - type testCase struct { - description string - input PlanWithStatus - result *pb.PlanWithStatus - } - - testCases := []testCase{ - { - description: "an empty PlanWithStatus returns an empty *pb.PlanWithStatus", - input: PlanWithStatus{}, - result: &pb.PlanWithStatus{Plan: PlanWithMetadata{}.ToProto()}, - }, - { - description: "full PlanWithStatus without status history returns a full *pb.PlanWithStatus", - input: PlanWithStatus{ - Plan: plan, - StatusHistory: []PlanStatus{ - {State: PlanStateInProgress, Timestamp: timestamp}, - }, - }, - result: &pb.PlanWithStatus{ - Plan: protoPlan, - Status: &pb.PlanStatus{ - State: pb.PlanState_PLAN_STATE_IN_PROGRESS, - Timestamp: timestampb, - }, - }, - }, - { - description: "full PlanWithStatus with status history returns a full *pb.PlanWithStatus", - input: PlanWithStatus{ - Plan: plan, - StatusHistory: []PlanStatus{ - {State: PlanStateFailed, Timestamp: timestamp, Reason: &reason}, - {State: PlanStateInProgress, Timestamp: timestamp}, - }, - }, - result: &pb.PlanWithStatus{ - Plan: protoPlan, - Status: &pb.PlanStatus{ - State: pb.PlanState_PLAN_STATE_FAILED, - Timestamp: timestampb, - Reason: &reason, - }, - StatusHistory: []*pb.PlanStatus{ - { - State: pb.PlanState_PLAN_STATE_IN_PROGRESS, - Timestamp: timestampb, - }, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res := tc.input.ToProto() - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func TestPlanState(t *testing.T) { - t.Run("planStateFromProto", func(t *testing.T) { - type testCase struct { - input pb.PlanState - expected PlanState - } - - testCases := []testCase{ - {input: pb.PlanState_PLAN_STATE_IN_PROGRESS, expected: PlanStateInProgress}, - {input: pb.PlanState_PLAN_STATE_STOPPED, expected: PlanStateStopped}, - {input: pb.PlanState_PLAN_STATE_SUCCEEDED, expected: PlanStateSucceeded}, - {input: pb.PlanState_PLAN_STATE_FAILED, expected: PlanStateFailed}, - {input: pb.PlanState_PLAN_STATE_UNSPECIFIED, expected: PlanStateUnspecified}, - {input: 50, expected: PlanStateUnspecified}, - } - for _, tc := range testCases { - test.That(t, planStateFromProto(tc.input), test.ShouldEqual, tc.expected) - } - }) - - t.Run("ToProto()", func(t *testing.T) { - type testCase struct { - input PlanState - expected pb.PlanState - } - - testCases := []testCase{ - {input: PlanStateInProgress, expected: pb.PlanState_PLAN_STATE_IN_PROGRESS}, - {input: PlanStateStopped, expected: pb.PlanState_PLAN_STATE_STOPPED}, - {input: PlanStateSucceeded, expected: pb.PlanState_PLAN_STATE_SUCCEEDED}, - {input: PlanStateFailed, expected: pb.PlanState_PLAN_STATE_FAILED}, - {input: PlanStateUnspecified, expected: pb.PlanState_PLAN_STATE_UNSPECIFIED}, - {input: 60, expected: pb.PlanState_PLAN_STATE_UNSPECIFIED}, - } - - for _, tc := range testCases { - test.That(t, tc.input.ToProto(), test.ShouldEqual, tc.expected) - } - }) - - t.Run("String()", func(t *testing.T) { - type testCase struct { - input PlanState - expected string - } - - testCases := []testCase{ - {input: PlanStateInProgress, expected: "in progress"}, - {input: PlanStateStopped, expected: "stopped"}, - {input: PlanStateSucceeded, expected: "succeeded"}, - {input: PlanStateFailed, expected: "failed"}, - {input: PlanStateUnspecified, expected: "unspecified"}, - {input: 60, expected: "unknown"}, - } - - for _, tc := range testCases { - test.That(t, tc.input.String(), test.ShouldEqual, tc.expected) - } - }) -} - -func TestPlanStatusWithID(t *testing.T) { - t.Run("planStatusWithIDFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.PlanStatusWithID - result PlanStatusWithID - err error - } - - id := uuid.New() - - mybase := base.Named("mybase") - timestamp := time.Now().UTC() - timestampb := timestamppb.New(timestamp) - reason := "some reason" - - testCases := []testCase{ - { - description: "nil pointer returns error", - input: nil, - result: PlanStatusWithID{}, - err: errors.New("received nil *pb.PlanStatusWithID"), - }, - { - description: "non uuid PlanID returns error", - input: &pb.PlanStatusWithID{PlanId: "not a uuid"}, - result: PlanStatusWithID{}, - err: errors.New("invalid UUID length: 10"), - }, - { - description: "non uuid ExecutionID returns error", - input: &pb.PlanStatusWithID{PlanId: id.String(), ExecutionId: "not a uuid"}, - result: PlanStatusWithID{}, - err: errors.New("invalid UUID length: 10"), - }, - { - description: "nil status returns error", - input: &pb.PlanStatusWithID{PlanId: id.String(), ExecutionId: id.String()}, - result: PlanStatusWithID{}, - err: errors.New("received nil *pb.PlanStatus"), - }, - { - description: "no component name returns error", - input: &pb.PlanStatusWithID{PlanId: id.String(), ExecutionId: id.String(), Status: &pb.PlanStatus{}}, - result: PlanStatusWithID{}, - err: errors.New("received nil *commonpb.ResourceName"), - }, - { - description: "success case with a failed plan status & reason", - input: &pb.PlanStatusWithID{ - ComponentName: rprotoutils.ResourceNameToProto(mybase), - ExecutionId: id.String(), - PlanId: id.String(), - Status: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_FAILED, Timestamp: timestampb, Reason: &reason}, - }, - result: PlanStatusWithID{ - ComponentName: mybase, - ExecutionID: id, - PlanID: id, - Status: PlanStatus{State: PlanStateFailed, Timestamp: timestamp, Reason: &reason}, - }, - }, - { - description: "success case with a in progress plan status", - input: &pb.PlanStatusWithID{ - ComponentName: rprotoutils.ResourceNameToProto(mybase), - ExecutionId: id.String(), - PlanId: id.String(), - Status: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_IN_PROGRESS, Timestamp: timestampb}, - }, - result: PlanStatusWithID{ - ComponentName: mybase, - ExecutionID: id, - PlanID: id, - Status: PlanStatus{State: PlanStateInProgress, Timestamp: timestamp}, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := planStatusWithIDFromProto(tc.input) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("ToProto()", func(t *testing.T) { - type testCase struct { - description string - input PlanStatusWithID - result *pb.PlanStatusWithID - } - - id := uuid.New() - - mybase := base.Named("mybase") - timestamp := time.Now().UTC() - timestampb := timestamppb.New(timestamp) - reason := "some reason" - - testCases := []testCase{ - { - description: "an empty PlanStatusWithID returns an empty *pb.PlanStatusWithID", - input: PlanStatusWithID{}, - result: &pb.PlanStatusWithID{ - PlanId: uuid.Nil.String(), - ExecutionId: uuid.Nil.String(), - ComponentName: rprotoutils.ResourceNameToProto(resource.Name{}), - Status: PlanStatus{}.ToProto(), - }, - }, - { - description: "a full PlanStatusWithID with a failed state & reason returns a full *pb.PlanStatusWithID", - input: PlanStatusWithID{ - ComponentName: mybase, - ExecutionID: id, - PlanID: id, - Status: PlanStatus{State: PlanStateFailed, Timestamp: timestamp, Reason: &reason}, - }, - result: &pb.PlanStatusWithID{ - ComponentName: rprotoutils.ResourceNameToProto(mybase), - ExecutionId: id.String(), - PlanId: id.String(), - Status: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_FAILED, Timestamp: timestampb, Reason: &reason}, - }, - }, - { - description: "a full PlanStatusWithID with an in progres state & nil reason returns a full *pb.PlanStatusWithID", - input: PlanStatusWithID{ - ComponentName: mybase, - ExecutionID: id, - PlanID: id, - Status: PlanStatus{State: PlanStateInProgress, Timestamp: timestamp}, - }, - result: &pb.PlanStatusWithID{ - ComponentName: rprotoutils.ResourceNameToProto(mybase), - ExecutionId: id.String(), - PlanId: id.String(), - Status: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_IN_PROGRESS, Timestamp: timestampb}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res := tc.input.ToProto() - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func TestPlanStatus(t *testing.T) { - t.Run("planStatusFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.PlanStatus - result PlanStatus - err error - } - - timestamp := time.Now().UTC() - timestampb := timestamppb.New(timestamp) - reason := "some reason" - - testCases := []testCase{ - { - description: "nil pointer returns error", - input: nil, - result: PlanStatus{}, - err: errors.New("received nil *pb.PlanStatus"), - }, - { - description: "success case with a failed plan state & reason", - input: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_FAILED, Timestamp: timestampb, Reason: &reason}, - result: PlanStatus{State: PlanStateFailed, Timestamp: timestamp, Reason: &reason}, - }, - { - description: "success case with a stopped plan state", - input: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_STOPPED, Timestamp: timestampb}, - result: PlanStatus{State: PlanStateStopped, Timestamp: timestamp}, - }, - { - description: "success case with a succeeded plan state", - input: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_SUCCEEDED, Timestamp: timestampb}, - result: PlanStatus{State: PlanStateSucceeded, Timestamp: timestamp}, - }, - { - description: "success case with an unspecified plan state", - input: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_UNSPECIFIED, Timestamp: timestampb}, - result: PlanStatus{State: PlanStateUnspecified, Timestamp: timestamp}, - }, - { - description: "success case with a in progress plan status", - input: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_IN_PROGRESS, Timestamp: timestampb}, - result: PlanStatus{State: PlanStateInProgress, Timestamp: timestamp}, - }, - } - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := planStatusFromProto(tc.input) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("ToProto()", func(t *testing.T) { - type testCase struct { - description string - input PlanStatus - result *pb.PlanStatus - } - - timestamp := time.Now().UTC() - timestampb := timestamppb.New(timestamp) - reason := "some reason" - - testCases := []testCase{ - { - description: "an empty PlanStatus returns an empty *pb.PlanStatus", - input: PlanStatus{}, - result: &pb.PlanStatus{Timestamp: timestamppb.New(time.Time{})}, - }, - { - description: "success case with a failed plan state & reason", - input: PlanStatus{State: PlanStateFailed, Timestamp: timestamp, Reason: &reason}, - result: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_FAILED, Timestamp: timestampb, Reason: &reason}, - }, - { - description: "success case with a stopped plan state", - input: PlanStatus{State: PlanStateStopped, Timestamp: timestamp}, - result: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_STOPPED, Timestamp: timestampb}, - }, - { - description: "success case with a succeeded plan state", - input: PlanStatus{State: PlanStateSucceeded, Timestamp: timestamp}, - result: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_SUCCEEDED, Timestamp: timestampb}, - }, - { - description: "success case with an unspecified plan state", - input: PlanStatus{State: PlanStateUnspecified, Timestamp: timestamp}, - result: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_UNSPECIFIED, Timestamp: timestampb}, - }, - { - description: "success case with a in progress plan status", - input: PlanStatus{State: PlanStateInProgress, Timestamp: timestamp}, - result: &pb.PlanStatus{State: pb.PlanState_PLAN_STATE_IN_PROGRESS, Timestamp: timestampb}, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res := tc.input.ToProto() - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func TestPlan(t *testing.T) { - planID := uuid.New() - executionID := uuid.New() - - baseName := base.Named("my-base1") - poseA := spatialmath.NewZeroPose() - poseB := spatialmath.NewPose(r3.Vector{X: 100}, spatialmath.NewOrientationVector()) - - protoAB := &pb.Plan{ - Id: planID.String(), - ExecutionId: executionID.String(), - ComponentName: rprotoutils.ResourceNameToProto(baseName), - Steps: []*pb.PlanStep{ - { - Step: map[string]*pb.ComponentState{ - baseName.ShortName(): {Pose: spatialmath.PoseToProtobuf(poseA)}, - }, - }, - { - Step: map[string]*pb.ComponentState{ - baseName.ShortName(): {Pose: spatialmath.PoseToProtobuf(poseB)}, - }, - }, - }, - } - planAB := PlanWithMetadata{ - ID: planID, - ExecutionID: executionID, - ComponentName: baseName, - Plan: motionplan.NewSimplePlan( - []motionplan.PathStep{ - {baseName.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, poseA)}, - {baseName.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, poseB)}, - }, - nil, - ), - } - - t.Run("planFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.Plan - result PlanWithMetadata - err error - } - - testCases := []testCase{ - { - description: "nil pointer returns error", - input: nil, - result: PlanWithMetadata{}, - err: errors.New("received nil *pb.Plan"), - }, - { - description: "empty PlanID in *pb.Plan{} returns an error", - input: &pb.Plan{}, - result: PlanWithMetadata{}, - err: errors.New("invalid UUID length: 0"), - }, - { - description: "empty ExecutionID in *pb.Plan{} returns an error", - input: &pb.Plan{Id: planID.String()}, - result: PlanWithMetadata{}, - err: errors.New("invalid UUID length: 0"), - }, - { - description: "empty ComponentName in *pb.Plan{} returns an error", - input: &pb.Plan{Id: planID.String(), ExecutionId: executionID.String()}, - result: PlanWithMetadata{}, - err: errors.New("received nil *pb.ResourceName"), - }, - { - description: "a nil *pb.PlanStep{} returns an error", - input: &pb.Plan{ - Id: planID.String(), - ExecutionId: executionID.String(), - ComponentName: rprotoutils.ResourceNameToProto(resource.Name{}), - Steps: []*pb.PlanStep{nil}, - }, - result: PlanWithMetadata{}, - err: errors.New("received nil *pb.PlanStep"), - }, - { - description: "success case for empty steps", - input: &pb.Plan{ - Id: planID.String(), - ExecutionId: executionID.String(), - ComponentName: rprotoutils.ResourceNameToProto(resource.Name{}), - }, - result: PlanWithMetadata{ - ID: planID, - ExecutionID: executionID, - ComponentName: resource.Name{}, - }, - }, - { - description: "success case for full steps", - input: protoAB, - result: planAB, - }, - } - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := planFromProto(tc.input) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("ToProto()", func(t *testing.T) { - type testCase struct { - description string - input PlanWithMetadata - result *pb.Plan - } - - testCases := []testCase{ - { - description: "an empty Plan returns an empty *pb.Plan", - input: PlanWithMetadata{}, - result: &pb.Plan{ - Id: uuid.Nil.String(), - ComponentName: rprotoutils.ResourceNameToProto(resource.Name{}), - ExecutionId: uuid.Nil.String(), - }, - }, - { - description: "full Plan returns full *pb.Plan", - input: planAB, - result: protoAB, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res := tc.input.ToProto() - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func TestConfiguration(t *testing.T) { - visionCameraPairs := [][]resource.Name{ - {vision.Named("vision service 1"), camera.Named("camera 1")}, - {vision.Named("vision service 2"), camera.Named("camera 2")}, - } - obstacleDetectorsPB := []*pb.ObstacleDetector{} - obstacleDetectors := []ObstacleDetectorName{} - for _, pair := range visionCameraPairs { - obstacleDetectors = append(obstacleDetectors, ObstacleDetectorName{ - VisionServiceName: pair[0], - CameraName: pair[1], - }) - obstacleDetectorsPB = append(obstacleDetectorsPB, &pb.ObstacleDetector{ - VisionService: rprotoutils.ResourceNameToProto(pair[0]), - Camera: rprotoutils.ResourceNameToProto(pair[1]), - }) - } - - t.Run("configurationFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.MotionConfiguration - result *MotionConfiguration - } - linearMPerSec := 1. - angularDegsPerSec := 2. - planDeviationMM := 3000. - planDeviationM := planDeviationMM / 1000 - positionPollingFreqHz := 4. - obstaclePollingFreqHz := 5. - - testCases := []testCase{ - { - description: "when passed a nil pointer returns default MotionConfiguration struct", - input: nil, - result: &defaultMotionCfg, - }, - { - description: "when passed an empty struct returns default MotionConfiguration struct", - input: &pb.MotionConfiguration{}, - result: &defaultMotionCfg, - }, - { - description: "when passed a full struct returns a full struct", - input: &pb.MotionConfiguration{ - ObstacleDetectors: obstacleDetectorsPB, - LinearMPerSec: &linearMPerSec, - AngularDegsPerSec: &angularDegsPerSec, - PlanDeviationM: &planDeviationM, - PositionPollingFrequencyHz: &positionPollingFreqHz, - ObstaclePollingFrequencyHz: &obstaclePollingFreqHz, - }, - result: &MotionConfiguration{ - ObstacleDetectors: obstacleDetectors, - LinearMPerSec: linearMPerSec, - AngularDegsPerSec: angularDegsPerSec, - PlanDeviationMM: planDeviationMM, - PositionPollingFreqHz: positionPollingFreqHz, - ObstaclePollingFreqHz: obstaclePollingFreqHz, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res := configurationFromProto(tc.input) - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("toProto", func(t *testing.T) { - type testCase struct { - description string - input *MotionConfiguration - result *pb.MotionConfiguration - } - - linearMPerSec := 1. - angularDegsPerSec := 2. - planDeviationMM := 3000. - planDeviationM := planDeviationMM / 1000 - positionPollingFreqHz := 4. - obstaclePollingFreqHz := 5. - zero := 0. - - testCases := []testCase{ - { - description: "when passed an empty struct returns mostly empty struct", - input: &MotionConfiguration{}, - result: &pb.MotionConfiguration{PlanDeviationM: &zero}, - }, - { - description: "when passed a full struct returns a full struct", - input: &MotionConfiguration{ - ObstacleDetectors: obstacleDetectors, - LinearMPerSec: linearMPerSec, - AngularDegsPerSec: angularDegsPerSec, - PlanDeviationMM: planDeviationMM, - PositionPollingFreqHz: positionPollingFreqHz, - ObstaclePollingFreqHz: obstaclePollingFreqHz, - }, - result: &pb.MotionConfiguration{ - ObstacleDetectors: obstacleDetectorsPB, - LinearMPerSec: &linearMPerSec, - AngularDegsPerSec: &angularDegsPerSec, - PlanDeviationM: &planDeviationM, - PositionPollingFrequencyHz: &positionPollingFreqHz, - ObstaclePollingFrequencyHz: &obstaclePollingFreqHz, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res := tc.input.toProto() - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func TestMoveOnGlobeReq(t *testing.T) { - name := "somename" - dst := geo.NewPoint(1, 2) - - t.Run("String()", func(t *testing.T) { - s := "motion.MoveOnGlobeReq{ComponentName: " + - "rdk:component:base/my-base, Destination: " + - "&{lat:1 lng:2}, Heading: 0.500000, MovementSensorName: " + - "rdk:component:movement_sensor/my-movementsensor, " + - "Obstacles: [], MotionCfg: &motion.MotionConfiguration{" + - "ObstacleDetectors:[]motion.ObstacleDetectorName{" + - "motion.ObstacleDetectorName{VisionServiceName:resource.Name{" + - "API:resource.API{Type:resource.APIType{Namespace:\"rdk\", Name:\"service\"}, " + - "SubtypeName:\"vision\"}, Remote:\"\", Name:\"vision service 1\"}, " + - "CameraName:resource.Name{API:resource.API{Type:resource.APIType{" + - "Namespace:\"rdk\", Name:\"component\"}, SubtypeName:\"camera\"}, " + - "Remote:\"\", Name:\"camera 1\"}}, motion.ObstacleDetectorName{" + - "VisionServiceName:resource.Name{API:resource.API{Type:resource.APIType{" + - "Namespace:\"rdk\", Name:\"service\"}, SubtypeName:\"vision\"}, " + - "Remote:\"\", Name:\"vision service 2\"}, CameraName:resource.Name{" + - "API:resource.API{Type:resource.APIType{Namespace:\"rdk\", " + - "Name:\"component\"}, SubtypeName:\"camera\"}, Remote:\"\", " + - "Name:\"camera 2\"}}}, PositionPollingFreqHz:4, ObstaclePollingFreqHz:5, " + - "PlanDeviationMM:3, LinearMPerSec:1, AngularDegsPerSec:2}, Extra: map[]}" - test.That(t, validMoveOnGlobeRequest().String(), test.ShouldResemble, s) - }) - - t.Run("toProto", func(t *testing.T) { - t.Run("error due to nil destination", func(t *testing.T) { - mogReq := validMoveOnGlobeRequest() - mogReq.Destination = nil - _, err := mogReq.toProto(name) - test.That(t, err, test.ShouldBeError, errors.New("must provide a destination")) - }) - - t.Run("sets heading to nil if set to NaN", func(t *testing.T) { - mogReq := validMoveOnGlobeRequest() - mogReq.Heading = math.NaN() - req, err := mogReq.toProto(name) - test.That(t, err, test.ShouldBeNil) - test.That(t, req.Heading, test.ShouldBeNil) - }) - - t.Run("success", func(t *testing.T) { - mogReq := validMoveOnGlobeRequest() - req, err := mogReq.toProto(name) - - test.That(t, err, test.ShouldBeNil) - test.That(t, req.Name, test.ShouldResemble, "somename") - test.That(t, req.ComponentName.Name, test.ShouldResemble, "my-base") - test.That(t, req.Destination.Latitude, test.ShouldAlmostEqual, dst.Lat()) - test.That(t, req.Destination.Longitude, test.ShouldAlmostEqual, dst.Lng()) - test.That(t, req.Heading, test.ShouldNotBeNil) - test.That(t, *req.Heading, test.ShouldAlmostEqual, 0.5) - test.That(t, req.MovementSensorName.Name, test.ShouldResemble, "my-movementsensor") - test.That(t, req.Obstacles, test.ShouldBeEmpty) - test.That(t, req.MotionConfiguration, test.ShouldResemble, mogReq.MotionCfg.toProto()) - test.That(t, req.Extra.AsMap(), test.ShouldBeEmpty) - }) - - t.Run("allows nil motion config", func(t *testing.T) { - mogReq := validMoveOnGlobeRequest() - mogReq.MotionCfg = nil - req, err := mogReq.toProto(name) - test.That(t, err, test.ShouldBeNil) - test.That(t, req.Name, test.ShouldResemble, "somename") - test.That(t, req.ComponentName.Name, test.ShouldResemble, "my-base") - test.That(t, req.Destination.Latitude, test.ShouldAlmostEqual, dst.Lat()) - test.That(t, req.Destination.Longitude, test.ShouldAlmostEqual, dst.Lng()) - test.That(t, req.Heading, test.ShouldNotBeNil) - test.That(t, *req.Heading, test.ShouldAlmostEqual, 0.5) - test.That(t, req.MovementSensorName.Name, test.ShouldResemble, "my-movementsensor") - test.That(t, req.Obstacles, test.ShouldBeEmpty) - test.That(t, req.MotionConfiguration, test.ShouldBeNil) - test.That(t, req.Extra.AsMap(), test.ShouldBeEmpty) - }) - }) - - visionCameraPairs := [][]resource.Name{ - {vision.Named("vision service 1"), camera.Named("camera 1")}, - {vision.Named("vision service 2"), camera.Named("camera 2")}, - } - obstacleDetectorsPB := []*pb.ObstacleDetector{} - obstacleDetectors := []ObstacleDetectorName{} - for _, pair := range visionCameraPairs { - obstacleDetectors = append(obstacleDetectors, ObstacleDetectorName{ - VisionServiceName: pair[0], - CameraName: pair[1], - }) - obstacleDetectorsPB = append(obstacleDetectorsPB, &pb.ObstacleDetector{ - VisionService: rprotoutils.ResourceNameToProto(pair[0]), - Camera: rprotoutils.ResourceNameToProto(pair[1]), - }) - } - - t.Run("moveOnGlobeRequestFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.MoveOnGlobeRequest - result MoveOnGlobeReq - err error - } - - heading := 1. - linearMPerSec := 1. - angularDegsPerSec := 2. - planDeviationMM := 3000. - planDeviationM := planDeviationMM / 1000 - positionPollingFreqHz := 4. - obstaclePollingFreqHz := 5. - - mybase := base.Named("my-base") - - testCases := []testCase{ - { - description: "an nil *pb.MoveOnGlobeRequest returns an error", - input: nil, - result: MoveOnGlobeReq{}, - err: errors.New("received nil *pb.MoveOnGlobeRequest"), - }, - { - description: "an empty destination returns an error", - input: &pb.MoveOnGlobeRequest{}, - result: MoveOnGlobeReq{}, - err: errors.New("must provide a destination"), - }, - { - description: "an empty compnent name returns an error", - input: &pb.MoveOnGlobeRequest{ - Destination: &commonpb.GeoPoint{Latitude: 1, Longitude: 2}, - }, - result: MoveOnGlobeReq{}, - err: errors.New("received nil *commonpb.ResourceName"), - }, - { - description: "an empty movement sensor name returns an error", - input: &pb.MoveOnGlobeRequest{ - Destination: &commonpb.GeoPoint{Latitude: 1, Longitude: 2}, - ComponentName: rprotoutils.ResourceNameToProto(mybase), - }, - result: MoveOnGlobeReq{}, - err: errors.New("received nil *commonpb.ResourceName"), - }, - { - description: "an empty *pb.MoveOnGlobeRequest returns an empty MoveOnGlobeReq", - input: &pb.MoveOnGlobeRequest{ - Heading: &heading, - Destination: &commonpb.GeoPoint{Latitude: 1, Longitude: 2}, - ComponentName: rprotoutils.ResourceNameToProto(mybase), - MovementSensorName: rprotoutils.ResourceNameToProto(movementsensor.Named("my-movementsensor")), - }, - result: MoveOnGlobeReq{ - Heading: heading, - Destination: geo.NewPoint(1, 2), - ComponentName: mybase, - MovementSensorName: movementsensor.Named("my-movementsensor"), - Obstacles: []*spatialmath.GeoObstacle{}, - MotionCfg: &defaultMotionCfg, - Extra: map[string]interface{}{}, - }, - }, - { - description: "a full *pb.MoveOnGlobeRequest returns a full MoveOnGlobeReq", - input: &pb.MoveOnGlobeRequest{ - Heading: &heading, - Destination: &commonpb.GeoPoint{Latitude: 1, Longitude: 2}, - ComponentName: rprotoutils.ResourceNameToProto(mybase), - MovementSensorName: rprotoutils.ResourceNameToProto(movementsensor.Named("my-movementsensor")), - Obstacles: []*commonpb.GeoObstacle{}, - MotionConfiguration: &pb.MotionConfiguration{ - ObstacleDetectors: obstacleDetectorsPB, - LinearMPerSec: &linearMPerSec, - AngularDegsPerSec: &angularDegsPerSec, - PlanDeviationM: &planDeviationM, - PositionPollingFrequencyHz: &positionPollingFreqHz, - ObstaclePollingFrequencyHz: &obstaclePollingFreqHz, - }, - }, - result: MoveOnGlobeReq{ - Heading: heading, - Destination: dst, - ComponentName: mybase, - MovementSensorName: movementsensor.Named("my-movementsensor"), - Obstacles: []*spatialmath.GeoObstacle{}, - MotionCfg: &MotionConfiguration{ - ObstacleDetectors: obstacleDetectors, - LinearMPerSec: linearMPerSec, - AngularDegsPerSec: angularDegsPerSec, - PlanDeviationMM: planDeviationMM, - PositionPollingFreqHz: positionPollingFreqHz, - ObstaclePollingFreqHz: obstaclePollingFreqHz, - }, - Extra: map[string]interface{}{}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := moveOnGlobeRequestFromProto(tc.input) - - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - - t.Run("nil heading is converted into a NaN heading", func(t *testing.T) { - input := &pb.MoveOnGlobeRequest{ - Destination: &commonpb.GeoPoint{Latitude: 1, Longitude: 2}, - ComponentName: rprotoutils.ResourceNameToProto(mybase), - MovementSensorName: rprotoutils.ResourceNameToProto(movementsensor.Named("my-movementsensor")), - } - res, err := moveOnGlobeRequestFromProto(input) - test.That(t, err, test.ShouldBeNil) - test.That(t, math.IsNaN(res.Heading), test.ShouldBeTrue) - }) - }) -} - -func TestMoveOnMapReq(t *testing.T) { - visionCameraPairs := [][]resource.Name{ - {vision.Named("vision service 1"), camera.Named("camera 1")}, - {vision.Named("vision service 2"), camera.Named("camera 2")}, - } - obstacleDetectors := []ObstacleDetectorName{} - for _, pair := range visionCameraPairs { - obstacleDetectors = append(obstacleDetectors, ObstacleDetectorName{ - VisionServiceName: pair[0], - CameraName: pair[1], - }) - } - myBase := base.Named("mybase") - mySlam := slam.Named(("mySlam")) - motionCfg := &MotionConfiguration{ - ObstacleDetectors: obstacleDetectors, - LinearMPerSec: 1, - AngularDegsPerSec: 2, - PlanDeviationMM: 3, - PositionPollingFreqHz: 4, - ObstaclePollingFreqHz: 5, - } - - validMoveOnMapReq := MoveOnMapReq{ - ComponentName: myBase, - Destination: spatialmath.NewZeroPose(), - SlamName: mySlam, - MotionCfg: motionCfg, - Obstacles: []spatialmath.Geometry{}, - Extra: map[string]interface{}{}, - } - - validPbMoveOnMapRequest := &pb.MoveOnMapRequest{ - Name: "bloop", - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - ComponentName: rprotoutils.ResourceNameToProto(myBase), - SlamServiceName: rprotoutils.ResourceNameToProto(mySlam), - MotionConfiguration: motionCfg.toProto(), - Extra: &structpb.Struct{}, - } - - t.Run("String()", func(t *testing.T) { - s := fmt.Sprintf( - "motion.MoveOnMapReq{ComponentName: %s, SlamName: %s, Destination: %+v, "+ - "MotionCfg: %#v, Obstacles: %s, Extra: %s}", - validMoveOnMapReq.ComponentName, - validMoveOnMapReq.SlamName, - spatialmath.PoseToProtobuf(validMoveOnMapReq.Destination), - validMoveOnMapReq.MotionCfg, - validMoveOnMapReq.Obstacles, - validMoveOnMapReq.Extra) - test.That(t, validMoveOnMapReq.String(), test.ShouldEqual, s) - }) - - t.Run("toProto", func(t *testing.T) { - type testCase struct { - description string - input MoveOnMapReq - name string - result *pb.MoveOnMapRequest - err error - } - - testCases := []testCase{ - { - description: "empty struct fails due to nil destination", - input: MoveOnMapReq{}, - name: "bloop", - result: nil, - err: errors.New("must provide a destination"), - }, - { - description: "success", - input: validMoveOnMapReq, - name: "bloop", - result: validPbMoveOnMapRequest, - err: nil, - }, - { - description: "allows nil motion cfg", - input: MoveOnMapReq{ - ComponentName: myBase, - Destination: spatialmath.NewZeroPose(), - SlamName: mySlam, - }, - name: "bloop", - - result: &pb.MoveOnMapRequest{ - Name: "bloop", - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - ComponentName: rprotoutils.ResourceNameToProto(myBase), - SlamServiceName: rprotoutils.ResourceNameToProto(mySlam), - Extra: &structpb.Struct{}, - }, - err: nil, - }, - { - description: "allows non-nil obstacles", - input: MoveOnMapReq{ - ComponentName: myBase, - Destination: spatialmath.NewZeroPose(), - SlamName: mySlam, - Obstacles: []spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")}, - }, - name: "bloop", - - result: &pb.MoveOnMapRequest{ - Name: "bloop", - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - ComponentName: rprotoutils.ResourceNameToProto(myBase), - SlamServiceName: rprotoutils.ResourceNameToProto(mySlam), - Obstacles: spatialmath.NewGeometriesToProto([]spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")}), - Extra: &structpb.Struct{}, - }, - err: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := tc.input.toProto(tc.name) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("moveOnMapRequestFromProto", func(t *testing.T) { - type testCase struct { - description string - - input *pb.MoveOnMapRequest - result MoveOnMapReq - err error - } - - testCases := []testCase{ - { - description: "nil request fails", - input: nil, - result: MoveOnMapReq{}, - err: errors.New("received nil *pb.MoveOnMapRequest"), - }, - { - description: "nil destination causes failure", - - input: &pb.MoveOnMapRequest{}, - result: MoveOnMapReq{}, - err: errors.New("received nil *commonpb.Pose for destination"), - }, - { - description: "nil componentName causes failure", - - input: &pb.MoveOnMapRequest{ - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - }, - result: MoveOnMapReq{}, - err: errors.New("received nil *commonpb.ResourceName for component name"), - }, - { - description: "nil SlamName causes failure", - - input: &pb.MoveOnMapRequest{ - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - ComponentName: rprotoutils.ResourceNameToProto(myBase), - }, - result: MoveOnMapReq{}, - err: errors.New("received nil *commonpb.ResourceName for SlamService name"), - }, - { - description: "success", - input: validPbMoveOnMapRequest, - result: validMoveOnMapReq, - err: nil, - }, - { - description: "success - allow nil motionCfg", - - input: &pb.MoveOnMapRequest{ - Destination: spatialmath.PoseToProtobuf(spatialmath.NewPoseFromPoint(r3.Vector{2700, 0, 0})), - ComponentName: rprotoutils.ResourceNameToProto(myBase), - SlamServiceName: rprotoutils.ResourceNameToProto(mySlam), - }, - result: MoveOnMapReq{ - ComponentName: myBase, - Destination: spatialmath.NewPoseFromPoint(r3.Vector{2700, 0, 0}), - SlamName: mySlam, - MotionCfg: &MotionConfiguration{ - ObstacleDetectors: []ObstacleDetectorName{}, - PositionPollingFreqHz: 0, - ObstaclePollingFreqHz: 0, - PlanDeviationMM: 0, - LinearMPerSec: 0, - AngularDegsPerSec: 0, - }, - Obstacles: []spatialmath.Geometry{}, - Extra: map[string]interface{}{}, - }, - err: nil, - }, - { - description: "success - allow non-nil obstacles", - input: &pb.MoveOnMapRequest{ - Destination: spatialmath.PoseToProtobuf(spatialmath.NewPoseFromPoint(r3.Vector{2700, 0, 0})), - ComponentName: rprotoutils.ResourceNameToProto(myBase), - SlamServiceName: rprotoutils.ResourceNameToProto(mySlam), - Obstacles: spatialmath.NewGeometriesToProto([]spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")}), - }, - result: MoveOnMapReq{ - ComponentName: myBase, - Destination: spatialmath.NewPoseFromPoint(r3.Vector{2700, 0, 0}), - SlamName: mySlam, - MotionCfg: &MotionConfiguration{ - ObstacleDetectors: []ObstacleDetectorName{}, - PositionPollingFreqHz: 0, - ObstaclePollingFreqHz: 0, - PlanDeviationMM: 0, - LinearMPerSec: 0, - AngularDegsPerSec: 0, - }, - Obstacles: []spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")}, - Extra: map[string]interface{}{}, - }, - err: nil, - }, - { - description: "fail - inconvertible geometry", - input: &pb.MoveOnMapRequest{ - Destination: spatialmath.PoseToProtobuf(spatialmath.NewPoseFromPoint(r3.Vector{2700, 0, 0})), - ComponentName: rprotoutils.ResourceNameToProto(myBase), - SlamServiceName: rprotoutils.ResourceNameToProto(mySlam), - Obstacles: []*commonpb.Geometry{{GeometryType: nil}}, - }, - result: MoveOnMapReq{}, - err: errors.New("cannot convert obstacles into geometries: cannot have nil pose for geometry"), - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := moveOnMapRequestFromProto(tc.input) - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func TestPlanHistoryReq(t *testing.T) { - t.Run("toProto", func(t *testing.T) { - type testCase struct { - description string - input PlanHistoryReq - name string - result *pb.GetPlanRequest - err error - } - - executionID := uuid.New() - mybase := base.Named("mybase") - executionIDStr := executionID.String() - - testCases := []testCase{ - { - description: "empty struct returns an empty struct", - input: PlanHistoryReq{}, - name: "some name", - result: &pb.GetPlanRequest{ - Name: "some name", - ComponentName: rprotoutils.ResourceNameToProto(resource.Name{}), - Extra: &structpb.Struct{}, - }, - }, - { - description: "full struct returns a full struct", - input: PlanHistoryReq{ - ComponentName: mybase, - ExecutionID: executionID, - LastPlanOnly: true, - }, - name: "some name", - result: &pb.GetPlanRequest{ - Name: "some name", - ComponentName: rprotoutils.ResourceNameToProto(mybase), - ExecutionId: &executionIDStr, - LastPlanOnly: true, - Extra: &structpb.Struct{}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := tc.input.toProto(tc.name) - - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) - - t.Run("getPlanRequestFromProto", func(t *testing.T) { - type testCase struct { - description string - input *pb.GetPlanRequest - result PlanHistoryReq - err error - } - - executionID := uuid.New() - mybase := base.Named("mybase") - executionIDStr := executionID.String() - - testCases := []testCase{ - { - description: "returns an error if component name is nil", - input: &pb.GetPlanRequest{}, - result: PlanHistoryReq{}, - err: errors.New("received nil *commonpb.ResourceName"), - }, - { - description: "empty struct returns an empty struct", - input: &pb.GetPlanRequest{ - ComponentName: rprotoutils.ResourceNameToProto(resource.Name{}), - }, - result: PlanHistoryReq{Extra: map[string]interface{}{}}, - }, - { - description: "full struct returns a full struct", - input: &pb.GetPlanRequest{ - Name: "some name", - ComponentName: rprotoutils.ResourceNameToProto(mybase), - ExecutionId: &executionIDStr, - LastPlanOnly: true, - Extra: &structpb.Struct{}, - }, - result: PlanHistoryReq{ - ComponentName: mybase, - ExecutionID: executionID, - LastPlanOnly: true, - Extra: map[string]interface{}{}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - res, err := getPlanRequestFromProto(tc.input) - - if tc.err != nil { - test.That(t, err, test.ShouldBeError, tc.err) - } else { - test.That(t, err, test.ShouldBeNil) - } - test.That(t, res, test.ShouldResemble, tc.result) - }) - } - }) -} - -func validMoveOnGlobeRequest() MoveOnGlobeReq { - dst := geo.NewPoint(1, 2) - visionCameraPairs := [][]resource.Name{ - {vision.Named("vision service 1"), camera.Named("camera 1")}, - {vision.Named("vision service 2"), camera.Named("camera 2")}, - } - obstacleDetectors := []ObstacleDetectorName{} - for _, pair := range visionCameraPairs { - obstacleDetectors = append(obstacleDetectors, ObstacleDetectorName{ - VisionServiceName: pair[0], - CameraName: pair[1], - }) - } - return MoveOnGlobeReq{ - ComponentName: base.Named("my-base"), - Destination: dst, - Heading: 0.5, - MovementSensorName: movementsensor.Named("my-movementsensor"), - Obstacles: nil, - MotionCfg: &MotionConfiguration{ - ObstacleDetectors: obstacleDetectors, - LinearMPerSec: 1, - AngularDegsPerSec: 2, - PlanDeviationMM: 3, - PositionPollingFreqHz: 4, - ObstaclePollingFreqHz: 5, - }, - Extra: nil, - } -} diff --git a/services/motion/pbhelpers.go b/services/motion/pbhelpers.go deleted file mode 100644 index 366eab3b410..00000000000 --- a/services/motion/pbhelpers.go +++ /dev/null @@ -1,336 +0,0 @@ -package motion - -import ( - "math" - - "github.com/google/uuid" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/motion/v1" - vprotoutils "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/motionplan" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/spatialmath" -) - -// planWithStatusFromProto converts a *pb.PlanWithStatus to a PlanWithStatus. -func planWithStatusFromProto(pws *pb.PlanWithStatus) (PlanWithStatus, error) { - if pws == nil { - return PlanWithStatus{}, errors.New("received nil *pb.PlanWithStatus") - } - - plan, err := planFromProto(pws.Plan) - if err != nil { - return PlanWithStatus{}, err - } - - status, err := planStatusFromProto(pws.Status) - if err != nil { - return PlanWithStatus{}, err - } - statusHistory := []PlanStatus{} - statusHistory = append(statusHistory, status) - for _, s := range pws.StatusHistory { - ps, err := planStatusFromProto(s) - if err != nil { - return PlanWithStatus{}, err - } - statusHistory = append(statusHistory, ps) - } - - return PlanWithStatus{ - Plan: plan, - StatusHistory: statusHistory, - }, nil -} - -// planStatusFromProto converts a *pb.PlanStatus to a PlanStatus. -func planStatusFromProto(ps *pb.PlanStatus) (PlanStatus, error) { - if ps == nil { - return PlanStatus{}, errors.New("received nil *pb.PlanStatus") - } - - return PlanStatus{ - State: planStateFromProto(ps.State), - Reason: ps.Reason, - Timestamp: ps.Timestamp.AsTime(), - }, nil -} - -// planStatusWithIDFromProto converts a *pb.PlanStatus to a PlanStatus. -func planStatusWithIDFromProto(ps *pb.PlanStatusWithID) (PlanStatusWithID, error) { - if ps == nil { - return PlanStatusWithID{}, errors.New("received nil *pb.PlanStatusWithID") - } - - planID, err := uuid.Parse(ps.PlanId) - if err != nil { - return PlanStatusWithID{}, err - } - - executionID, err := uuid.Parse(ps.ExecutionId) - if err != nil { - return PlanStatusWithID{}, err - } - - status, err := planStatusFromProto(ps.Status) - if err != nil { - return PlanStatusWithID{}, err - } - - if ps.ComponentName == nil { - return PlanStatusWithID{}, errors.New("received nil *commonpb.ResourceName") - } - - return PlanStatusWithID{ - PlanID: planID, - ComponentName: rprotoutils.ResourceNameFromProto(ps.ComponentName), - ExecutionID: executionID, - Status: status, - }, nil -} - -// planFromProto converts a *pb.Plan to a Plan. -func planFromProto(p *pb.Plan) (PlanWithMetadata, error) { - if p == nil { - return PlanWithMetadata{}, errors.New("received nil *pb.Plan") - } - - id, err := uuid.Parse(p.Id) - if err != nil { - return PlanWithMetadata{}, err - } - - executionID, err := uuid.Parse(p.ExecutionId) - if err != nil { - return PlanWithMetadata{}, err - } - - if p.ComponentName == nil { - return PlanWithMetadata{}, errors.New("received nil *pb.ResourceName") - } - - plan := PlanWithMetadata{ - ID: id, - ComponentName: rprotoutils.ResourceNameFromProto(p.ComponentName), - ExecutionID: executionID, - } - - if len(p.Steps) == 0 { - return plan, nil - } - - steps := motionplan.Path{} - for _, s := range p.Steps { - step, err := motionplan.PathStepFromProto(s) - if err != nil { - return PlanWithMetadata{}, err - } - steps = append(steps, step) - } - plan.Plan = motionplan.NewSimplePlan(steps, nil) - return plan, nil -} - -// planStateFromProto converts a pb.PlanState to a PlanState. -func planStateFromProto(ps pb.PlanState) PlanState { - switch ps { - case pb.PlanState_PLAN_STATE_IN_PROGRESS: - return PlanStateInProgress - case pb.PlanState_PLAN_STATE_STOPPED: - return PlanStateStopped - case pb.PlanState_PLAN_STATE_SUCCEEDED: - return PlanStateSucceeded - case pb.PlanState_PLAN_STATE_FAILED: - return PlanStateFailed - case pb.PlanState_PLAN_STATE_UNSPECIFIED: - return PlanStateUnspecified - default: - return PlanStateUnspecified - } -} - -// toProto converts a MoveOnGlobeRequest to a *pb.MoveOnGlobeRequest. -func (r MoveOnGlobeReq) toProto(name string) (*pb.MoveOnGlobeRequest, error) { - ext, err := vprotoutils.StructToStructPb(r.Extra) - if err != nil { - return nil, err - } - - if r.Destination == nil { - return nil, errors.New("must provide a destination") - } - - req := &pb.MoveOnGlobeRequest{ - Name: name, - ComponentName: rprotoutils.ResourceNameToProto(r.ComponentName), - Destination: &commonpb.GeoPoint{Latitude: r.Destination.Lat(), Longitude: r.Destination.Lng()}, - MovementSensorName: rprotoutils.ResourceNameToProto(r.MovementSensorName), - Extra: ext, - } - - if !math.IsNaN(r.Heading) { - req.Heading = &r.Heading - } - - if r.MotionCfg != nil { - req.MotionConfiguration = r.MotionCfg.toProto() - } - - if len(r.Obstacles) > 0 { - obstaclesProto := make([]*commonpb.GeoObstacle, 0, len(r.Obstacles)) - for _, obstacle := range r.Obstacles { - obstaclesProto = append(obstaclesProto, spatialmath.GeoObstacleToProtobuf(obstacle)) - } - req.Obstacles = obstaclesProto - } - return req, nil -} - -func moveOnGlobeRequestFromProto(req *pb.MoveOnGlobeRequest) (MoveOnGlobeReq, error) { - if req == nil { - return MoveOnGlobeReq{}, errors.New("received nil *pb.MoveOnGlobeRequest") - } - - if req.Destination == nil { - return MoveOnGlobeReq{}, errors.New("must provide a destination") - } - - // Optionals - heading := math.NaN() - if req.Heading != nil { - heading = req.GetHeading() - } - obstaclesProto := req.GetObstacles() - obstacles := make([]*spatialmath.GeoObstacle, 0, len(obstaclesProto)) - for _, eachProtoObst := range obstaclesProto { - convObst, err := spatialmath.GeoObstacleFromProtobuf(eachProtoObst) - if err != nil { - return MoveOnGlobeReq{}, err - } - obstacles = append(obstacles, convObst) - } - protoComponentName := req.GetComponentName() - if protoComponentName == nil { - return MoveOnGlobeReq{}, errors.New("received nil *commonpb.ResourceName") - } - componentName := rprotoutils.ResourceNameFromProto(protoComponentName) - destination := geo.NewPoint(req.GetDestination().GetLatitude(), req.GetDestination().GetLongitude()) - protoMovementSensorName := req.GetMovementSensorName() - if protoMovementSensorName == nil { - return MoveOnGlobeReq{}, errors.New("received nil *commonpb.ResourceName") - } - movementSensorName := rprotoutils.ResourceNameFromProto(protoMovementSensorName) - motionCfg := configurationFromProto(req.MotionConfiguration) - - return MoveOnGlobeReq{ - ComponentName: componentName, - Destination: destination, - Heading: heading, - MovementSensorName: movementSensorName, - Obstacles: obstacles, - MotionCfg: motionCfg, - Extra: req.Extra.AsMap(), - }, nil -} - -func (req PlanHistoryReq) toProto(name string) (*pb.GetPlanRequest, error) { - ext, err := vprotoutils.StructToStructPb(req.Extra) - if err != nil { - return nil, err - } - - var executionIDPtr *string - if req.ExecutionID != uuid.Nil { - executionID := req.ExecutionID.String() - executionIDPtr = &executionID - } - return &pb.GetPlanRequest{ - Name: name, - ComponentName: rprotoutils.ResourceNameToProto(req.ComponentName), - LastPlanOnly: req.LastPlanOnly, - Extra: ext, - ExecutionId: executionIDPtr, - }, nil -} - -func getPlanRequestFromProto(req *pb.GetPlanRequest) (PlanHistoryReq, error) { - if req.GetComponentName() == nil { - return PlanHistoryReq{}, errors.New("received nil *commonpb.ResourceName") - } - - executionID := uuid.Nil - if executionIDStr := req.GetExecutionId(); executionIDStr != "" { - id, err := uuid.Parse(executionIDStr) - if err != nil { - return PlanHistoryReq{}, err - } - executionID = id - } - - return PlanHistoryReq{ - ComponentName: rprotoutils.ResourceNameFromProto(req.GetComponentName()), - LastPlanOnly: req.GetLastPlanOnly(), - ExecutionID: executionID, - Extra: req.Extra.AsMap(), - }, nil -} - -func moveOnMapRequestFromProto(req *pb.MoveOnMapRequest) (MoveOnMapReq, error) { - if req == nil { - return MoveOnMapReq{}, errors.New("received nil *pb.MoveOnMapRequest") - } - if req.GetDestination() == nil { - return MoveOnMapReq{}, errors.New("received nil *commonpb.Pose for destination") - } - protoComponentName := req.GetComponentName() - if protoComponentName == nil { - return MoveOnMapReq{}, errors.New("received nil *commonpb.ResourceName for component name") - } - protoSlamServiceName := req.GetSlamServiceName() - if protoSlamServiceName == nil { - return MoveOnMapReq{}, errors.New("received nil *commonpb.ResourceName for SlamService name") - } - geoms := []spatialmath.Geometry{} - if obs := req.GetObstacles(); len(obs) > 0 { - convertedGeom, err := spatialmath.NewGeometriesFromProto(obs) - if err != nil { - return MoveOnMapReq{}, errors.Wrap(err, "cannot convert obstacles into geometries") - } - geoms = convertedGeom - } - return MoveOnMapReq{ - ComponentName: rprotoutils.ResourceNameFromProto(protoComponentName), - Destination: spatialmath.NewPoseFromProtobuf(req.GetDestination()), - SlamName: rprotoutils.ResourceNameFromProto(protoSlamServiceName), - MotionCfg: configurationFromProto(req.MotionConfiguration), - Obstacles: geoms, - Extra: req.Extra.AsMap(), - }, nil -} - -func (r MoveOnMapReq) toProto(name string) (*pb.MoveOnMapRequest, error) { - ext, err := vprotoutils.StructToStructPb(r.Extra) - if err != nil { - return nil, err - } - if r.Destination == nil { - return nil, errors.New("must provide a destination") - } - req := &pb.MoveOnMapRequest{ - Name: name, - ComponentName: rprotoutils.ResourceNameToProto(r.ComponentName), - Destination: spatialmath.PoseToProtobuf(r.Destination), - SlamServiceName: rprotoutils.ResourceNameToProto(r.SlamName), - Obstacles: spatialmath.NewGeometriesToProto(r.Obstacles), - Extra: ext, - } - - if r.MotionCfg != nil { - req.MotionConfiguration = r.MotionCfg.toProto() - } - - return req, nil -} diff --git a/services/motion/register/register.go b/services/motion/register/register.go deleted file mode 100644 index 8f2b9b324a3..00000000000 --- a/services/motion/register/register.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package register registers all relevant motion services and API specific functions. -package register - -import ( - // for motion models. - _ "go.viam.com/rdk/services/motion/builtin" -) diff --git a/services/motion/server.go b/services/motion/server.go deleted file mode 100644 index 0df06d71443..00000000000 --- a/services/motion/server.go +++ /dev/null @@ -1,173 +0,0 @@ -package motion - -import ( - "context" - - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/motion/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" -) - -// serviceServer implements the MotionService from motion.proto. -type serviceServer struct { - pb.UnimplementedMotionServiceServer - coll resource.APIResourceCollection[Service] -} - -// NewRPCServiceServer constructs a motion gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Service]) interface{} { - return &serviceServer{coll: coll} -} - -func (server *serviceServer) Move(ctx context.Context, req *pb.MoveRequest) (*pb.MoveResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - worldState, err := referenceframe.WorldStateFromProtobuf(req.GetWorldState()) - if err != nil { - return nil, err - } - success, err := svc.Move( - ctx, - protoutils.ResourceNameFromProto(req.GetComponentName()), - referenceframe.ProtobufToPoseInFrame(req.GetDestination()), - worldState, - req.GetConstraints(), - req.Extra.AsMap(), - ) - return &pb.MoveResponse{Success: success}, err -} - -func (server *serviceServer) MoveOnMap(ctx context.Context, req *pb.MoveOnMapRequest) (*pb.MoveOnMapResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - r, err := moveOnMapRequestFromProto(req) - if err != nil { - return nil, err - } - - id, err := svc.MoveOnMap(ctx, r) - if err != nil { - return nil, err - } - - return &pb.MoveOnMapResponse{ExecutionId: id.String()}, nil -} - -func (server *serviceServer) MoveOnGlobe(ctx context.Context, req *pb.MoveOnGlobeRequest) (*pb.MoveOnGlobeResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - r, err := moveOnGlobeRequestFromProto(req) - if err != nil { - return nil, err - } - - id, err := svc.MoveOnGlobe(ctx, r) - if err != nil { - return nil, err - } - - return &pb.MoveOnGlobeResponse{ExecutionId: id.String()}, nil -} - -func (server *serviceServer) GetPose(ctx context.Context, req *pb.GetPoseRequest) (*pb.GetPoseResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - if req.ComponentName == nil { - return nil, errors.New("must provide component name") - } - transforms, err := referenceframe.LinkInFramesFromTransformsProtobuf(req.GetSupplementalTransforms()) - if err != nil { - return nil, err - } - pose, err := svc.GetPose(ctx, protoutils.ResourceNameFromProto(req.ComponentName), req.DestinationFrame, transforms, req.Extra.AsMap()) - if err != nil { - return nil, err - } - return &pb.GetPoseResponse{Pose: referenceframe.PoseInFrameToProtobuf(pose)}, nil -} - -func (server *serviceServer) StopPlan(ctx context.Context, req *pb.StopPlanRequest) (*pb.StopPlanResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - componentName := protoutils.ResourceNameFromProto(req.GetComponentName()) - r := StopPlanReq{ComponentName: componentName, Extra: req.Extra.AsMap()} - err = svc.StopPlan(ctx, r) - if err != nil { - return nil, err - } - - return &pb.StopPlanResponse{}, nil -} - -func (server *serviceServer) ListPlanStatuses(ctx context.Context, req *pb.ListPlanStatusesRequest) (*pb.ListPlanStatusesResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - r := ListPlanStatusesReq{OnlyActivePlans: req.GetOnlyActivePlans(), Extra: req.Extra.AsMap()} - statuses, err := svc.ListPlanStatuses(ctx, r) - if err != nil { - return nil, err - } - - protoStatuses := make([]*pb.PlanStatusWithID, 0, len(statuses)) - for _, status := range statuses { - protoStatuses = append(protoStatuses, status.ToProto()) - } - - return &pb.ListPlanStatusesResponse{PlanStatusesWithIds: protoStatuses}, nil -} - -func (server *serviceServer) GetPlan(ctx context.Context, req *pb.GetPlanRequest) (*pb.GetPlanResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - r, err := getPlanRequestFromProto(req) - if err != nil { - return nil, err - } - - planHistory, err := svc.PlanHistory(ctx, r) - if err != nil { - return nil, err - } - - cpws := planHistory[0].ToProto() - - history := []*pb.PlanWithStatus{} - for _, plan := range planHistory[1:] { - history = append(history, plan.ToProto()) - } - - return &pb.GetPlanResponse{CurrentPlanWithStatus: cpws, ReplanHistory: history}, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, svc, req) -} diff --git a/services/motion/server_test.go b/services/motion/server_test.go deleted file mode 100644 index bd8e44c6ae4..00000000000 --- a/services/motion/server_test.go +++ /dev/null @@ -1,778 +0,0 @@ -package motion_test - -import ( - "context" - "errors" - "math" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - geo "github.com/kellydunn/golang-geo" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/motion/v1" - "go.viam.com/test" - vprotoutils "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func newServer(resources map[resource.Name]motion.Service) (pb.MotionServiceServer, error) { - coll, err := resource.NewAPIResourceCollection(motion.API, resources) - if err != nil { - return nil, err - } - return motion.NewRPCServiceServer(coll).(pb.MotionServiceServer), nil -} - -func TestServerMove(t *testing.T) { - grabRequest := &pb.MoveRequest{ - Name: testMotionServiceName.ShortName(), - ComponentName: protoutils.ResourceNameToProto(gripper.Named("fake")), - Destination: referenceframe.PoseInFrameToProtobuf(referenceframe.NewPoseInFrame("", spatialmath.NewZeroPose())), - } - - resources := map[resource.Name]motion.Service{} - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - _, err = server.Move(context.Background(), grabRequest) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:motion/motion1\" not found")) - - // error - injectMS := &inject.MotionService{} - resources = map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - } - server, err = newServer(resources) - test.That(t, err, test.ShouldBeNil) - passedErr := errors.New("fake move error") - injectMS.MoveFunc = func( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *pb.Constraints, - extra map[string]interface{}, - ) (bool, error) { - return false, passedErr - } - - _, err = server.Move(context.Background(), grabRequest) - test.That(t, err, test.ShouldBeError, passedErr) - - // returns response - successfulMoveFunc := func( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *pb.Constraints, - extra map[string]interface{}, - ) (bool, error) { - return true, nil - } - injectMS.MoveFunc = successfulMoveFunc - resp, err := server.Move(context.Background(), grabRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.GetSuccess(), test.ShouldBeTrue) - - // Multiple Servies names Valid - injectMS = &inject.MotionService{} - resources = map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - testMotionServiceName2: injectMS, - } - server, _ = newServer(resources) - injectMS.MoveFunc = successfulMoveFunc - resp, err = server.Move(context.Background(), grabRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.GetSuccess(), test.ShouldBeTrue) - grabRequest2 := &pb.MoveRequest{ - Name: testMotionServiceName2.ShortName(), - ComponentName: protoutils.ResourceNameToProto(gripper.Named("fake")), - Destination: referenceframe.PoseInFrameToProtobuf(referenceframe.NewPoseInFrame("", spatialmath.NewZeroPose())), - } - resp, err = server.Move(context.Background(), grabRequest2) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.GetSuccess(), test.ShouldBeTrue) -} - -func TestServerMoveOnGlobe(t *testing.T) { - injectMS := &inject.MotionService{} - resources := map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - } - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - t.Run("returns error without calling MoveOnGlobe if req.Name doesn't map to a resource", func(t *testing.T) { - moveOnGlobeRequest := &pb.MoveOnGlobeRequest{ - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - Destination: &commonpb.GeoPoint{Latitude: 0.0, Longitude: 0.0}, - MovementSensorName: protoutils.ResourceNameToProto(movementsensor.Named("test-gps")), - } - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - t.Log("should not be called") - t.FailNow() - return uuid.Nil, errors.New("should not be called") - } - - moveOnGlobeResponse, err := server.MoveOnGlobe(context.Background(), moveOnGlobeRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:motion/\" not found")) - test.That(t, moveOnGlobeResponse, test.ShouldBeNil) - }) - - t.Run("returns error if destination is nil without calling MoveOnGlobe", func(t *testing.T) { - moveOnGlobeRequest := &pb.MoveOnGlobeRequest{ - Name: testMotionServiceName.ShortName(), - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - MovementSensorName: protoutils.ResourceNameToProto(movementsensor.Named("test-gps")), - } - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - t.Log("should not be called") - t.FailNow() - return uuid.Nil, errors.New("should not be called") - } - - moveOnGlobeResponse, err := server.MoveOnGlobe(context.Background(), moveOnGlobeRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("must provide a destination")) - test.That(t, moveOnGlobeResponse, test.ShouldBeNil) - }) - - validMoveOnGlobeRequest := &pb.MoveOnGlobeRequest{ - Name: testMotionServiceName.ShortName(), - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - Destination: &commonpb.GeoPoint{Latitude: 0.0, Longitude: 0.0}, - MovementSensorName: protoutils.ResourceNameToProto(movementsensor.Named("test-gps")), - } - - t.Run("returns error when MoveOnGlobe returns an error", func(t *testing.T) { - notYetImplementedErr := errors.New("Not yet implemented") - - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - return uuid.Nil, notYetImplementedErr - } - moveOnGlobeResponse, err := server.MoveOnGlobe(context.Background(), validMoveOnGlobeRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, notYetImplementedErr.Error()) - test.That(t, moveOnGlobeResponse, test.ShouldBeNil) - }) - - t.Run("sets heading to NaN if nil in request", func(t *testing.T) { - firstExecutionID := uuid.New() - secondExecutionID := uuid.New() - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - test.That(t, math.IsNaN(req.Heading), test.ShouldBeTrue) - return firstExecutionID, nil - } - moveOnGlobeResponse, err := server.MoveOnGlobe(context.Background(), validMoveOnGlobeRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, moveOnGlobeResponse.ExecutionId, test.ShouldEqual, firstExecutionID.String()) - - reqHeading := 6. - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - test.That(t, req.Heading, test.ShouldAlmostEqual, reqHeading) - return secondExecutionID, nil - } - - validMoveOnGlobeRequest.Heading = &reqHeading - moveOnGlobeResponse, err = server.MoveOnGlobe(context.Background(), validMoveOnGlobeRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, moveOnGlobeResponse.ExecutionId, test.ShouldEqual, secondExecutionID.String()) - }) - - t.Run("returns success when MoveOnGlobe returns success", func(t *testing.T) { - expectedComponentName := base.Named("test-base") - expectedMovSensorName := movementsensor.Named("test-gps") - reqHeading := 3. - - boxDims := r3.Vector{X: 5, Y: 50, Z: 10} - - geometries1, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{X: 50, Y: 0, Z: 0}), - boxDims, - "wall") - test.That(t, err, test.ShouldBeNil) - - geometries2, err := spatialmath.NewBox( - spatialmath.NewPoseFromPoint(r3.Vector{X: 0, Y: 70, Z: 0}), - boxDims, - "other wall") - test.That(t, err, test.ShouldBeNil) - - geoObstacle1 := spatialmath.NewGeoObstacle(geo.NewPoint(70, 40), []spatialmath.Geometry{geometries1}) - geoObstacle2 := spatialmath.NewGeoObstacle(geo.NewPoint(-70, 40), []spatialmath.Geometry{geometries2}) - obs := []*commonpb.GeoObstacle{ - spatialmath.GeoObstacleToProtobuf(geoObstacle1), - spatialmath.GeoObstacleToProtobuf(geoObstacle2), - } - angularDegsPerSec := 1. - linearMPerSec := 2. - planDeviationM := 3. - obstaclePollingFrequencyHz := 4. - positionPollingFrequencyHz := 5. - obstacleDetectorsPB := []*pb.ObstacleDetector{ - { - VisionService: protoutils.ResourceNameToProto(vision.Named("vision service 1")), - Camera: protoutils.ResourceNameToProto(camera.Named("camera 1")), - }, - { - VisionService: protoutils.ResourceNameToProto(vision.Named("vision service 2")), - Camera: protoutils.ResourceNameToProto(camera.Named("camera 2")), - }, - } - - moveOnGlobeRequest := &pb.MoveOnGlobeRequest{ - Name: testMotionServiceName.ShortName(), - Heading: &reqHeading, - ComponentName: protoutils.ResourceNameToProto(expectedComponentName), - Destination: &commonpb.GeoPoint{Latitude: 1.0, Longitude: 2.0}, - MovementSensorName: protoutils.ResourceNameToProto(expectedMovSensorName), - Obstacles: obs, - MotionConfiguration: &pb.MotionConfiguration{ - AngularDegsPerSec: &angularDegsPerSec, - LinearMPerSec: &linearMPerSec, - PlanDeviationM: &planDeviationM, - ObstaclePollingFrequencyHz: &obstaclePollingFrequencyHz, - PositionPollingFrequencyHz: &positionPollingFrequencyHz, - ObstacleDetectors: obstacleDetectorsPB, - }, - } - - firstExecutionID := uuid.New() - injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - test.That(t, req.ComponentName, test.ShouldResemble, expectedComponentName) - test.That(t, req.Destination, test.ShouldNotBeNil) - test.That(t, req.Destination, test.ShouldResemble, geo.NewPoint(1, 2)) - test.That(t, req.Heading, test.ShouldResemble, reqHeading) - test.That(t, req.MovementSensorName, test.ShouldResemble, expectedMovSensorName) - test.That(t, len(req.Obstacles), test.ShouldEqual, 2) - test.That(t, req.Obstacles[0], test.ShouldResemble, geoObstacle1) - test.That(t, req.Obstacles[1], test.ShouldResemble, geoObstacle2) - test.That(t, req.MotionCfg.AngularDegsPerSec, test.ShouldAlmostEqual, angularDegsPerSec) - test.That(t, req.MotionCfg.LinearMPerSec, test.ShouldAlmostEqual, linearMPerSec) - test.That(t, req.MotionCfg.PlanDeviationMM, test.ShouldAlmostEqual, planDeviationM*1000) - test.That(t, req.MotionCfg.ObstaclePollingFreqHz, test.ShouldAlmostEqual, obstaclePollingFrequencyHz) - test.That(t, req.MotionCfg.PositionPollingFreqHz, test.ShouldAlmostEqual, positionPollingFrequencyHz) - test.That(t, len(req.MotionCfg.ObstacleDetectors), test.ShouldAlmostEqual, 2) - test.That(t, req.MotionCfg.ObstacleDetectors[0].VisionServiceName, test.ShouldResemble, vision.Named("vision service 1")) - test.That(t, req.MotionCfg.ObstacleDetectors[0].CameraName, test.ShouldResemble, camera.Named("camera 1")) - test.That(t, req.MotionCfg.ObstacleDetectors[1].VisionServiceName, test.ShouldResemble, vision.Named("vision service 2")) - test.That(t, req.MotionCfg.ObstacleDetectors[1].CameraName, test.ShouldResemble, camera.Named("camera 2")) - return firstExecutionID, nil - } - moveOnGlobeResponse, err := server.MoveOnGlobe(context.Background(), moveOnGlobeRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, moveOnGlobeResponse.ExecutionId, test.ShouldEqual, firstExecutionID.String()) - }) -} - -func TestServerMoveOnMap(t *testing.T) { - injectMS := &inject.MotionService{} - resources := map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - } - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - - t.Run("returns error without calling MoveOnMap if req.Name doesn't map to a resource", func(t *testing.T) { - moveOnMapRequest := &pb.MoveOnMapRequest{ - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - SlamServiceName: protoutils.ResourceNameToProto(slam.Named("test-slam")), - } - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - t.Log("should not be called") - t.FailNow() - return uuid.Nil, errors.New("should not be called") - } - - moveOnMapResponse, err := server.MoveOnMap(context.Background(), moveOnMapRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:motion/\" not found")) - test.That(t, moveOnMapResponse, test.ShouldBeNil) - }) - - t.Run("returns error if destination is nil without calling MoveOnMap", func(t *testing.T) { - moveOnMapRequest := &pb.MoveOnMapRequest{ - Name: testMotionServiceName.ShortName(), - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - Destination: nil, - SlamServiceName: protoutils.ResourceNameToProto(slam.Named("test-slam")), - } - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - t.Log("should not be called") - t.FailNow() - return uuid.Nil, errors.New("should not be called") - } - moveOnMapResponse, err := server.MoveOnMap(context.Background(), moveOnMapRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("received nil *commonpb.Pose for destination")) - test.That(t, moveOnMapResponse, test.ShouldBeNil) - }) - - validMoveOnMapRequest := &pb.MoveOnMapRequest{ - Name: testMotionServiceName.ShortName(), - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - SlamServiceName: protoutils.ResourceNameToProto(slam.Named("test-slam")), - } - - t.Run("returns error when MoveOnMap returns an error", func(t *testing.T) { - notYetImplementedErr := errors.New("Not yet implemented") - - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - return uuid.Nil, notYetImplementedErr - } - moveOnMapResponse, err := server.MoveOnMap(context.Background(), validMoveOnMapRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, notYetImplementedErr) - test.That(t, moveOnMapResponse, test.ShouldBeNil) - }) - - t.Run("returns success when MoveOnMap returns success", func(t *testing.T) { - expectedComponentName := base.Named("test-base") - expectedSlamName := slam.Named("test-slam") - expectedDestination := spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()) - - angularDegsPerSec := 1. - linearMPerSec := 2. - planDeviationM := 3. - obstaclePollingFrequencyHz := 4. - positionPollingFrequencyHz := 5. - obstacleDetectorsPB := []*pb.ObstacleDetector{ - { - VisionService: protoutils.ResourceNameToProto(vision.Named("vision service 1")), - Camera: protoutils.ResourceNameToProto(camera.Named("camera 1")), - }, - { - VisionService: protoutils.ResourceNameToProto(vision.Named("vision service 2")), - Camera: protoutils.ResourceNameToProto(camera.Named("camera 2")), - }, - } - - moveOnMapRequest := &pb.MoveOnMapRequest{ - Name: testMotionServiceName.ShortName(), - - ComponentName: protoutils.ResourceNameToProto(expectedComponentName), - Destination: expectedDestination, - SlamServiceName: protoutils.ResourceNameToProto(expectedSlamName), - - MotionConfiguration: &pb.MotionConfiguration{ - AngularDegsPerSec: &angularDegsPerSec, - LinearMPerSec: &linearMPerSec, - PlanDeviationM: &planDeviationM, - ObstaclePollingFrequencyHz: &obstaclePollingFrequencyHz, - PositionPollingFrequencyHz: &positionPollingFrequencyHz, - ObstacleDetectors: obstacleDetectorsPB, - }, - } - - firstExecutionID := uuid.New() - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - test.That(t, req.ComponentName, test.ShouldResemble, expectedComponentName) - test.That(t, req.Destination, test.ShouldNotBeNil) - test.That(t, - spatialmath.PoseAlmostEqualEps(req.Destination, spatialmath.NewPoseFromProtobuf(expectedDestination), 1e-5), - test.ShouldBeTrue, - ) - test.That(t, req.SlamName, test.ShouldResemble, expectedSlamName) - test.That(t, req.MotionCfg.AngularDegsPerSec, test.ShouldAlmostEqual, angularDegsPerSec) - test.That(t, req.MotionCfg.LinearMPerSec, test.ShouldAlmostEqual, linearMPerSec) - test.That(t, req.MotionCfg.PlanDeviationMM, test.ShouldAlmostEqual, planDeviationM*1000) - test.That(t, req.MotionCfg.ObstaclePollingFreqHz, test.ShouldAlmostEqual, obstaclePollingFrequencyHz) - test.That(t, req.MotionCfg.PositionPollingFreqHz, test.ShouldAlmostEqual, positionPollingFrequencyHz) - test.That(t, len(req.MotionCfg.ObstacleDetectors), test.ShouldAlmostEqual, 2) - test.That(t, req.MotionCfg.ObstacleDetectors[0].VisionServiceName, test.ShouldResemble, vision.Named("vision service 1")) - test.That(t, req.MotionCfg.ObstacleDetectors[0].CameraName, test.ShouldResemble, camera.Named("camera 1")) - test.That(t, req.MotionCfg.ObstacleDetectors[1].VisionServiceName, test.ShouldResemble, vision.Named("vision service 2")) - test.That(t, req.MotionCfg.ObstacleDetectors[1].CameraName, test.ShouldResemble, camera.Named("camera 2")) - return firstExecutionID, nil - } - moveOnMapResponse, err := server.MoveOnMap(context.Background(), moveOnMapRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, moveOnMapResponse.ExecutionId, test.ShouldEqual, firstExecutionID.String()) - }) - - t.Run("non-nil obstacles passes", func(t *testing.T) { - moveOnMapReq := &pb.MoveOnMapRequest{ - Name: testMotionServiceName.ShortName(), - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - SlamServiceName: protoutils.ResourceNameToProto(slam.Named("test-slam")), - Obstacles: spatialmath.NewGeometriesToProto([]spatialmath.Geometry{spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")}), - } - - firstExecutionID := uuid.New() - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - test.That(t, len(req.Obstacles), test.ShouldEqual, 1) - equal := spatialmath.GeometriesAlmostEqual(req.Obstacles[0], spatialmath.NewPoint(r3.Vector{2, 2, 2}, "pt")) - test.That(t, equal, test.ShouldBeTrue) - return firstExecutionID, nil - } - - moveOnMapResponse, err := server.MoveOnMap(context.Background(), moveOnMapReq) - test.That(t, err, test.ShouldBeNil) - test.That(t, moveOnMapResponse.ExecutionId, test.ShouldEqual, firstExecutionID.String()) - }) - - t.Run("fails with inconvertible geometry", func(t *testing.T) { - moveOnMapReq := &pb.MoveOnMapRequest{ - Name: testMotionServiceName.ShortName(), - ComponentName: protoutils.ResourceNameToProto(base.Named("test-base")), - Destination: spatialmath.PoseToProtobuf(spatialmath.NewZeroPose()), - SlamServiceName: protoutils.ResourceNameToProto(slam.Named("test-slam")), - Obstacles: []*commonpb.Geometry{{GeometryType: nil}}, - } - - injectMS.MoveOnMapFunc = func(ctx context.Context, req motion.MoveOnMapReq) (motion.ExecutionID, error) { - t.Log("should not be called") - t.FailNow() - return uuid.Nil, errors.New("should not be called") - } - - moveOnMapResponse, err := server.MoveOnMap(context.Background(), moveOnMapReq) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("cannot convert obstacles into geometries: cannot have nil pose for geometry")) - test.That(t, moveOnMapResponse, test.ShouldBeNil) - }) -} - -func TestServerStopPlan(t *testing.T) { - injectMS := &inject.MotionService{} - resources := map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - } - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - - expectedComponentName := base.Named("test-base") - - validStopPlanRequest := &pb.StopPlanRequest{ - ComponentName: protoutils.ResourceNameToProto(expectedComponentName), - Name: testMotionServiceName.ShortName(), - } - - t.Run("returns error without calling StopPlan if req.Name doesn't map to a resource", func(t *testing.T) { - stopPlanRequest := &pb.StopPlanRequest{ - ComponentName: protoutils.ResourceNameToProto(expectedComponentName), - } - - injectMS.StopPlanFunc = func( - ctx context.Context, - req motion.StopPlanReq, - ) error { - t.Log("should not be called") - t.FailNow() - return errors.New("should not be called") - } - - stopPlanResponse, err := server.StopPlan(context.Background(), stopPlanRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:motion/\" not found")) - test.That(t, stopPlanResponse, test.ShouldBeNil) - }) - - t.Run("returns error if StopPlan returns an error", func(t *testing.T) { - errExpected := errors.New("stop error") - injectMS.StopPlanFunc = func( - ctx context.Context, - req motion.StopPlanReq, - ) error { - return errExpected - } - - stopPlanResponse, err := server.StopPlan(context.Background(), validStopPlanRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errExpected) - test.That(t, stopPlanResponse, test.ShouldBeNil) - }) - - t.Run("otherwise returns a success response", func(t *testing.T) { - injectMS.StopPlanFunc = func( - ctx context.Context, - req motion.StopPlanReq, - ) error { - test.That(t, req.ComponentName, test.ShouldResemble, expectedComponentName) - return nil - } - - stopPlanResponse, err := server.StopPlan(context.Background(), validStopPlanRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, stopPlanResponse, test.ShouldResemble, &pb.StopPlanResponse{}) - }) -} - -func TestServerListPlanStatuses(t *testing.T) { - injectMS := &inject.MotionService{} - resources := map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - } - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - - validListPlanStatusesRequest := &pb.ListPlanStatusesRequest{ - Name: testMotionServiceName.ShortName(), - } - - t.Run("returns error without calling ListPlanStatuses if req.Name doesn't map to a resource", func(t *testing.T) { - listPlanStatusesRequest := &pb.ListPlanStatusesRequest{} - injectMS.ListPlanStatusesFunc = func( - ctx context.Context, - req motion.ListPlanStatusesReq, - ) ([]motion.PlanStatusWithID, error) { - t.Log("should not be called") - t.FailNow() - return nil, errors.New("should not be called") - } - - listPlanStatusesResponse, err := server.ListPlanStatuses(context.Background(), listPlanStatusesRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:motion/\" not found")) - test.That(t, listPlanStatusesResponse, test.ShouldBeNil) - }) - - t.Run("returns error if ListPlanStatuses returns an error", func(t *testing.T) { - errExpected := errors.New("ListPlanStatuses error") - injectMS.ListPlanStatusesFunc = func( - ctx context.Context, - req motion.ListPlanStatusesReq, - ) ([]motion.PlanStatusWithID, error) { - return nil, errExpected - } - - listPlanStatusesResponse, err := server.ListPlanStatuses(context.Background(), validListPlanStatusesRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errExpected) - test.That(t, listPlanStatusesResponse, test.ShouldBeNil) - }) - - t.Run("otherwise returns a success response", func(t *testing.T) { - executionID := uuid.New() - planID1 := uuid.New() - planID2 := uuid.New() - planID3 := uuid.New() - planID4 := uuid.New() - - expectedComponentName := base.Named("test-base") - failedReason := "some reason for failure" - - status1 := motion.PlanStatus{State: motion.PlanStateFailed, Timestamp: time.Now(), Reason: &failedReason} - status2 := motion.PlanStatus{State: motion.PlanStateSucceeded, Timestamp: time.Now()} - status3 := motion.PlanStatus{State: motion.PlanStateStopped, Timestamp: time.Now()} - status4 := motion.PlanStatus{State: motion.PlanStateInProgress, Timestamp: time.Now()} - - pswid1 := motion.PlanStatusWithID{ - PlanID: planID1, - ExecutionID: executionID, - ComponentName: expectedComponentName, - Status: status1, - } - pswid2 := motion.PlanStatusWithID{ - PlanID: planID2, - ExecutionID: executionID, - ComponentName: expectedComponentName, - Status: status2, - } - pswid3 := motion.PlanStatusWithID{ - PlanID: planID3, - ExecutionID: executionID, - ComponentName: expectedComponentName, - Status: status3, - } - pswid4 := motion.PlanStatusWithID{ - PlanID: planID4, - ExecutionID: executionID, - ComponentName: expectedComponentName, - Status: status4, - } - - injectMS.ListPlanStatusesFunc = func( - ctx context.Context, - req motion.ListPlanStatusesReq, - ) ([]motion.PlanStatusWithID, error) { - return []motion.PlanStatusWithID{ - pswid1, - pswid2, - pswid3, - pswid4, - }, nil - } - - listPlanStatusesResponse, err := server.ListPlanStatuses(context.Background(), validListPlanStatusesRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(listPlanStatusesResponse.PlanStatusesWithIds), test.ShouldEqual, 4) - test.That(t, listPlanStatusesResponse.PlanStatusesWithIds[0], test.ShouldResemble, pswid1.ToProto()) - test.That(t, listPlanStatusesResponse.PlanStatusesWithIds[1], test.ShouldResemble, pswid2.ToProto()) - test.That(t, listPlanStatusesResponse.PlanStatusesWithIds[2], test.ShouldResemble, pswid3.ToProto()) - test.That(t, listPlanStatusesResponse.PlanStatusesWithIds[3], test.ShouldResemble, pswid4.ToProto()) - }) -} - -func TestServerGetPlan(t *testing.T) { - injectMS := &inject.MotionService{} - resources := map[resource.Name]motion.Service{ - testMotionServiceName: injectMS, - } - server, err := newServer(resources) - test.That(t, err, test.ShouldBeNil) - - expectedComponentName := base.Named("test-base") - uuidID := uuid.New() - id := uuidID.String() - - validGetPlanRequest := &pb.GetPlanRequest{ - ComponentName: protoutils.ResourceNameToProto(expectedComponentName), - Name: testMotionServiceName.ShortName(), - LastPlanOnly: false, - ExecutionId: &id, - } - - t.Run("returns error without calling GetPlan if req.Name doesn't map to a resource", func(t *testing.T) { - getPlanRequest := &pb.GetPlanRequest{} - - injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - t.Log("should not be called") - t.FailNow() - return nil, errors.New("should not be called") - } - - getPlanResponse, err := server.GetPlan(context.Background(), getPlanRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:motion/\" not found")) - test.That(t, getPlanResponse, test.ShouldBeNil) - }) - - t.Run("returns error if GetPlan returns an error", func(t *testing.T) { - errExpected := errors.New("stop error") - injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - return nil, errExpected - } - - getPlanResponse, err := server.GetPlan(context.Background(), validGetPlanRequest) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, errExpected) - test.That(t, getPlanResponse, test.ShouldBeNil) - }) - - t.Run("otherwise returns a success response", func(t *testing.T) { - executionID := uuid.New() - planID1 := uuid.New() - planID2 := uuid.New() - - base1 := base.Named("base1") - steps := []motionplan.PathStep{{base1.ShortName(): referenceframe.NewPoseInFrame(referenceframe.World, spatialmath.NewZeroPose())}} - - plan1 := motion.PlanWithMetadata{ - ID: planID1, - ComponentName: base1, - ExecutionID: executionID, - Plan: motionplan.NewSimplePlan(steps, nil), - } - - plan2 := motion.PlanWithMetadata{ - ID: planID2, - ComponentName: base1, - ExecutionID: executionID, - Plan: motionplan.NewSimplePlan(steps, nil), - } - - time1A := time.Now() - time1B := time.Now() - - statusHistory1 := []motion.PlanStatus{ - { - State: motion.PlanStateSucceeded, - Timestamp: time1B, - Reason: nil, - }, - { - State: motion.PlanStateInProgress, - Timestamp: time1A, - Reason: nil, - }, - } - - time2A := time.Now() - time2B := time.Now() - - reason := "some failed reason" - statusHistory2 := []motion.PlanStatus{ - { - State: motion.PlanStateFailed, - Timestamp: time2B, - Reason: &reason, - }, - { - State: motion.PlanStateInProgress, - Timestamp: time2A, - Reason: nil, - }, - } - - planWithStatus2 := motion.PlanWithStatus{Plan: plan2, StatusHistory: statusHistory2} - planWithStatus1 := motion.PlanWithStatus{Plan: plan1, StatusHistory: statusHistory1} - - injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - test.That(t, req.ComponentName, test.ShouldResemble, expectedComponentName) - test.That(t, req.LastPlanOnly, test.ShouldResemble, validGetPlanRequest.LastPlanOnly) - test.That(t, req.ExecutionID.String(), test.ShouldResemble, *validGetPlanRequest.ExecutionId) - return []motion.PlanWithStatus{ - planWithStatus2, - planWithStatus1, - }, nil - } - - getPlanResponse, err := server.GetPlan(context.Background(), validGetPlanRequest) - test.That(t, err, test.ShouldBeNil) - - expectedResponse := &pb.GetPlanResponse{ - CurrentPlanWithStatus: &pb.PlanWithStatus{ - Plan: plan2.ToProto(), - Status: statusHistory2[0].ToProto(), - StatusHistory: []*pb.PlanStatus{statusHistory2[1].ToProto()}, - }, - ReplanHistory: []*pb.PlanWithStatus{planWithStatus1.ToProto()}, - } - test.That(t, getPlanResponse, test.ShouldResemble, expectedResponse) - }) -} - -func TestServerDoCommand(t *testing.T) { - resourceMap := map[resource.Name]motion.Service{ - testMotionServiceName: &inject.MotionService{ - DoCommandFunc: testutils.EchoFunc, - }, - } - server, err := newServer(resourceMap) - test.That(t, err, test.ShouldBeNil) - - cmd, err := vprotoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - doCommandRequest := &commonpb.DoCommandRequest{ - Name: testMotionServiceName.ShortName(), - Command: cmd, - } - doCommandResponse, err := server.DoCommand(context.Background(), doCommandRequest) - test.That(t, err, test.ShouldBeNil) - - // Assert that do command response is an echoed request. - respMap := doCommandResponse.Result.AsMap() - test.That(t, respMap["command"], test.ShouldResemble, "test") - test.That(t, respMap["data"], test.ShouldResemble, 500.0) -} diff --git a/services/motion/verify_main_test.go b/services/motion/verify_main_test.go deleted file mode 100644 index 51505cadadd..00000000000 --- a/services/motion/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package motion - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/navigation/builtin/builtin.go b/services/navigation/builtin/builtin.go deleted file mode 100644 index 233749a8042..00000000000 --- a/services/navigation/builtin/builtin.go +++ /dev/null @@ -1,838 +0,0 @@ -// Package builtin implements a navigation service. -package builtin - -import ( - "context" - "fmt" - "math" - "slices" - "strconv" - "sync" - "sync/atomic" - "time" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/services/motion" - "go.viam.com/rdk/services/motion/explore" - "go.viam.com/rdk/services/navigation" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - rdkutils "go.viam.com/rdk/utils" -) - -// available modes for each MapType. -var ( - availableModesByMapType = map[navigation.MapType][]navigation.Mode{ - navigation.NoMap: {navigation.ModeManual, navigation.ModeExplore}, - navigation.GPSMap: {navigation.ModeManual, navigation.ModeWaypoint, navigation.ModeExplore}, - } - - errNegativeDegPerSec = errors.New("degs_per_sec must be non-negative if set") - errNegativeMetersPerSec = errors.New("meters_per_sec must be non-negative if set") - errNegativePositionPollingFrequencyHz = errors.New("position_polling_frequency_hz must be non-negative if set") - errNegativeObstaclePollingFrequencyHz = errors.New("obstacle_polling_frequency_hz must be non-negative if set") - errNegativePlanDeviationM = errors.New("plan_deviation_m must be non-negative if set") - errNegativeReplanCostFactor = errors.New("replan_cost_factor must be non-negative if set") -) - -const ( - // default configuration for Store parameter. - defaultStoreType = navigation.StoreTypeMemory - - // default map type is GPS. - defaultMapType = navigation.GPSMap - - // desired speeds to maintain for the base. - defaultLinearMPerSec = 0.3 - defaultAngularDegsPerSec = 20. - - // how far off the path must the robot be to trigger replanning. - defaultPlanDeviationM = 2.6 - - // the allowable quality change between the new plan and the remainder - // of the original plan. - defaultReplanCostFactor = 1. - - // frequency measured in hertz. - defaultObstaclePollingHz = 1. - defaultPositionPollingHz = 1. - - // frequency in milliseconds. - planHistoryPollFrequency = time.Millisecond * 50 -) - -func init() { - resource.RegisterService(navigation.API, resource.DefaultServiceModel, resource.Registration[navigation.Service, *Config]{ - Constructor: NewBuiltIn, - }) -} - -// ObstacleDetectorNameConfig is the protobuf version of ObstacleDetectorName. -type ObstacleDetectorNameConfig struct { - VisionServiceName string `json:"vision_service"` - CameraName string `json:"camera"` -} - -// Config describes how to configure the service. -type Config struct { - Store navigation.StoreConfig `json:"store"` - BaseName string `json:"base"` - MapType string `json:"map_type"` - MovementSensorName string `json:"movement_sensor"` - MotionServiceName string `json:"motion_service"` - ObstacleDetectors []*ObstacleDetectorNameConfig `json:"obstacle_detectors"` - - // DegPerSec and MetersPerSec are targets and not hard limits on speed - DegPerSec float64 `json:"degs_per_sec,omitempty"` - MetersPerSec float64 `json:"meters_per_sec,omitempty"` - - Obstacles []*spatialmath.GeoObstacleConfig `json:"obstacles,omitempty"` - PositionPollingFrequencyHz float64 `json:"position_polling_frequency_hz,omitempty"` - ObstaclePollingFrequencyHz float64 `json:"obstacle_polling_frequency_hz,omitempty"` - PlanDeviationM float64 `json:"plan_deviation_m,omitempty"` - ReplanCostFactor float64 `json:"replan_cost_factor,omitempty"` - LogFilePath string `json:"log_file_path"` -} - -type executionWaypoint struct { - executionID motion.ExecutionID - waypoint navigation.Waypoint -} - -var emptyExecutionWaypoint = executionWaypoint{} - -// Validate creates the list of implicit dependencies. -func (conf *Config) Validate(path string) ([]string, error) { - var deps []string - - // Add base dependencies - if conf.BaseName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "base") - } - deps = append(deps, conf.BaseName) - - // Add movement sensor dependencies - if conf.MovementSensorName != "" { - deps = append(deps, conf.MovementSensorName) - } - - // Add motion service dependencies - if conf.MotionServiceName != "" { - deps = append(deps, resource.NewName(motion.API, conf.MotionServiceName).String()) - } else { - deps = append(deps, resource.NewName(motion.API, resource.DefaultServiceName).String()) - } - - // Ensure map_type is valid and a movement sensor is available if MapType is GPS (or default) - mapType, err := navigation.StringToMapType(conf.MapType) - if err != nil { - return nil, err - } - if mapType == navigation.GPSMap && conf.MovementSensorName == "" { - return nil, resource.NewConfigValidationFieldRequiredError(path, "movement_sensor") - } - - for _, obstacleDetectorPair := range conf.ObstacleDetectors { - if obstacleDetectorPair.VisionServiceName == "" || obstacleDetectorPair.CameraName == "" { - return nil, resource.NewConfigValidationError(path, errors.New("an obstacle detector is missing either a camera or vision service")) - } - deps = append(deps, resource.NewName(vision.API, obstacleDetectorPair.VisionServiceName).String()) - deps = append(deps, resource.NewName(camera.API, obstacleDetectorPair.CameraName).String()) - } - - // Ensure store is valid - if err := conf.Store.Validate(path); err != nil { - return nil, err - } - - // Ensure inputs are non-negative - if conf.DegPerSec < 0 { - return nil, errNegativeDegPerSec - } - if conf.MetersPerSec < 0 { - return nil, errNegativeMetersPerSec - } - if conf.PositionPollingFrequencyHz < 0 { - return nil, errNegativePositionPollingFrequencyHz - } - if conf.ObstaclePollingFrequencyHz < 0 { - return nil, errNegativeObstaclePollingFrequencyHz - } - if conf.PlanDeviationM < 0 { - return nil, errNegativePlanDeviationM - } - if conf.ReplanCostFactor < 0 { - return nil, errNegativeReplanCostFactor - } - - // Ensure obstacles have no translation - for _, obs := range conf.Obstacles { - for _, geoms := range obs.Geometries { - if !geoms.TranslationOffset.ApproxEqual(r3.Vector{}) { - return nil, errors.New("geometries specified through the navigation are not allowed to have a translation") - } - } - } - - // add framesystem service as dependency to be used by builtin and explore motion service - deps = append(deps, framesystem.InternalServiceName.String()) - - return deps, nil -} - -// NewBuiltIn returns a new navigation service for the given robot. -func NewBuiltIn( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (navigation.Service, error) { - navSvc := &builtIn{ - Named: conf.ResourceName().AsNamed(), - logger: logger, - } - if err := navSvc.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - - return navSvc, nil -} - -type builtIn struct { - resource.Named - activeExecutionWaypoint atomic.Value - actionMu sync.RWMutex - mu sync.RWMutex - store navigation.NavStore - storeType string - mode navigation.Mode - mapType navigation.MapType - - fsService framesystem.Service - base base.Base - movementSensor movementsensor.MovementSensor - visionServicesByName map[resource.Name]vision.Service - motionService motion.Service - // exploreMotionService will be removed once the motion explore model is integrated into motion builtin - exploreMotionService motion.Service - obstacles []*spatialmath.GeoObstacle - - motionCfg *motion.MotionConfiguration - replanCostFactor float64 - - logger logging.Logger - wholeServiceCancelFunc func() - currentWaypointCancelFunc func() - waypointInProgress *navigation.Waypoint - activeBackgroundWorkers sync.WaitGroup -} - -func (svc *builtIn) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - svc.actionMu.Lock() - defer svc.actionMu.Unlock() - - svc.stopActiveMode() - - // Set framesystem service - for name, dep := range deps { - if name == framesystem.InternalServiceName { - fsService, ok := dep.(framesystem.Service) - if !ok { - return errors.New("frame system service is invalid type") - } - svc.fsService = fsService - break - } - } - - svcConfig, err := resource.NativeConfig[*Config](conf) - if err != nil { - return err - } - - // Set optional variables - metersPerSec := defaultLinearMPerSec - if svcConfig.MetersPerSec != 0 { - metersPerSec = svcConfig.MetersPerSec - } - degPerSec := defaultAngularDegsPerSec - if svcConfig.DegPerSec != 0 { - degPerSec = svcConfig.DegPerSec - } - positionPollingFrequencyHz := defaultPositionPollingHz - if svcConfig.PositionPollingFrequencyHz != 0 { - positionPollingFrequencyHz = svcConfig.PositionPollingFrequencyHz - } - obstaclePollingFrequencyHz := defaultObstaclePollingHz - if svcConfig.ObstaclePollingFrequencyHz != 0 { - obstaclePollingFrequencyHz = svcConfig.ObstaclePollingFrequencyHz - } - planDeviationM := defaultPlanDeviationM - if svcConfig.PlanDeviationM != 0 { - planDeviationM = svcConfig.PlanDeviationM - } - replanCostFactor := defaultReplanCostFactor - if svcConfig.ReplanCostFactor != 0 { - replanCostFactor = svcConfig.ReplanCostFactor - } - - motionServiceName := resource.DefaultServiceName - if svcConfig.MotionServiceName != "" { - motionServiceName = svcConfig.MotionServiceName - } - mapType := defaultMapType - if svcConfig.MapType != "" { - mapType, err = navigation.StringToMapType(svcConfig.MapType) - if err != nil { - return err - } - } - - storeCfg := navigation.StoreConfig{Type: defaultStoreType} - if svcConfig.Store.Type != navigation.StoreTypeUnset { - storeCfg = svcConfig.Store - } - - svc.mu.Lock() - defer svc.mu.Unlock() - - // Parse logger file from the configuration if given - if svcConfig.LogFilePath != "" { - logger, err := rdkutils.NewFilePathDebugLogger(svcConfig.LogFilePath, "navigation") - if err != nil { - return err - } - svc.logger = logger - } - - // Parse base from the configuration - baseComponent, err := base.FromDependencies(deps, svcConfig.BaseName) - if err != nil { - return err - } - - // Parse motion services from the configuration - motionSvc, err := motion.FromDependencies(deps, motionServiceName) - if err != nil { - return err - } - - var obstacleDetectorNamePairs []motion.ObstacleDetectorName - visionServicesByName := make(map[resource.Name]vision.Service) - for _, pbObstacleDetectorPair := range svcConfig.ObstacleDetectors { - visionSvc, err := vision.FromDependencies(deps, pbObstacleDetectorPair.VisionServiceName) - if err != nil { - return err - } - camera, err := camera.FromDependencies(deps, pbObstacleDetectorPair.CameraName) - if err != nil { - return err - } - obstacleDetectorNamePairs = append(obstacleDetectorNamePairs, motion.ObstacleDetectorName{ - VisionServiceName: visionSvc.Name(), CameraName: camera.Name(), - }) - visionServicesByName[visionSvc.Name()] = visionSvc - } - - // Parse movement sensor from the configuration if map type is GPS - if mapType == navigation.GPSMap { - movementSensor, err := movementsensor.FromDependencies(deps, svcConfig.MovementSensorName) - if err != nil { - return err - } - svc.movementSensor = movementSensor - } - - // Reconfigure the store if necessary - if svc.storeType != string(storeCfg.Type) { - newStore, err := navigation.NewStoreFromConfig(ctx, svcConfig.Store) - if err != nil { - return err - } - svc.store = newStore - svc.storeType = string(storeCfg.Type) - } - - // Parse obstacles from the configuration - newObstacles, err := spatialmath.GeoObstaclesFromConfigs(svcConfig.Obstacles) - if err != nil { - return err - } - - // Create explore motion service - // Note: this service will disappear after the explore motion model is integrated into builtIn - exploreMotionConf := resource.Config{ConvertedAttributes: &explore.Config{}} - svc.exploreMotionService, err = explore.NewExplore(ctx, deps, exploreMotionConf, svc.logger) - if err != nil { - return err - } - - svc.mode = navigation.ModeManual - svc.base = baseComponent - svc.mapType = mapType - svc.motionService = motionSvc - svc.obstacles = newObstacles - svc.replanCostFactor = replanCostFactor - svc.visionServicesByName = visionServicesByName - svc.motionCfg = &motion.MotionConfiguration{ - ObstacleDetectors: obstacleDetectorNamePairs, - LinearMPerSec: metersPerSec, - AngularDegsPerSec: degPerSec, - PlanDeviationMM: 1e3 * planDeviationM, - PositionPollingFreqHz: positionPollingFrequencyHz, - ObstaclePollingFreqHz: obstaclePollingFrequencyHz, - } - - return nil -} - -func (svc *builtIn) Mode(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - svc.mu.RLock() - defer svc.mu.RUnlock() - return svc.mode, nil -} - -func (svc *builtIn) SetMode(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - svc.actionMu.Lock() - defer svc.actionMu.Unlock() - - svc.mu.RLock() - svc.logger.CInfof(ctx, "SetMode called: transitioning from %s to %s", svc.mode, mode) - if svc.mode == mode { - svc.mu.RUnlock() - return nil - } - svc.mu.RUnlock() - - // stop passed active sessions - svc.stopActiveMode() - - // switch modes - svc.mu.Lock() - defer svc.mu.Unlock() - cancelCtx, cancelFunc := context.WithCancel(context.Background()) - svc.wholeServiceCancelFunc = cancelFunc - svc.mode = mode - - if !slices.Contains(availableModesByMapType[svc.mapType], svc.mode) { - return errors.Errorf("%v mode is unavailable for map type %v", svc.mode.String(), svc.mapType.String()) - } - - switch svc.mode { - case navigation.ModeManual: - // do nothing - case navigation.ModeWaypoint: - svc.startWaypointMode(cancelCtx, extra) - case navigation.ModeExplore: - if len(svc.motionCfg.ObstacleDetectors) == 0 { - return errors.New("explore mode requires at least one vision service") - } - svc.startExploreMode(cancelCtx) - } - - return nil -} - -func (svc *builtIn) Location(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - svc.mu.RLock() - defer svc.mu.RUnlock() - - if svc.movementSensor == nil { - return nil, errors.New("no way to get location") - } - loc, _, err := svc.movementSensor.Position(ctx, extra) - if err != nil { - return nil, err - } - compassHeading, err := svc.movementSensor.CompassHeading(ctx, extra) - if err != nil { - return nil, err - } - geoPose := spatialmath.NewGeoPose(loc, compassHeading) - return geoPose, err -} - -func (svc *builtIn) Waypoints(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) { - wps, err := svc.store.Waypoints(ctx) - if err != nil { - return nil, err - } - wpsCopy := make([]navigation.Waypoint, 0, len(wps)) - wpsCopy = append(wpsCopy, wps...) - return wpsCopy, nil -} - -func (svc *builtIn) AddWaypoint(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - svc.logger.CInfof(ctx, "AddWaypoint called with %#v", *point) - _, err := svc.store.AddWaypoint(ctx, point) - return err -} - -func (svc *builtIn) RemoveWaypoint(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - svc.mu.Lock() - defer svc.mu.Unlock() - svc.logger.CInfof(ctx, "RemoveWaypoint called with waypointID: %s", id) - if svc.waypointInProgress != nil && svc.waypointInProgress.ID == id { - if svc.currentWaypointCancelFunc != nil { - svc.currentWaypointCancelFunc() - } - svc.waypointInProgress = nil - } - return svc.store.RemoveWaypoint(ctx, id) -} - -func (svc *builtIn) waypointReached(ctx context.Context) error { - if ctx.Err() != nil { - return ctx.Err() - } - - svc.mu.RLock() - wp := svc.waypointInProgress - svc.mu.RUnlock() - - if wp == nil { - return errors.New("can't mark waypoint reached since there is none in progress") - } - return svc.store.WaypointVisited(ctx, wp.ID) -} - -func (svc *builtIn) Close(ctx context.Context) error { - svc.actionMu.Lock() - defer svc.actionMu.Unlock() - - svc.stopActiveMode() - if err := svc.exploreMotionService.Close(ctx); err != nil { - return err - } - return svc.store.Close(ctx) -} - -func (svc *builtIn) moveToWaypoint(ctx context.Context, wp navigation.Waypoint, extra map[string]interface{}) error { - req := motion.MoveOnGlobeReq{ - ComponentName: svc.base.Name(), - Destination: wp.ToPoint(), - Heading: math.NaN(), - MovementSensorName: svc.movementSensor.Name(), - Obstacles: svc.obstacles, - MotionCfg: svc.motionCfg, - Extra: extra, - } - cancelCtx, cancelFn := context.WithCancel(ctx) - defer cancelFn() - executionID, err := svc.motionService.MoveOnGlobe(cancelCtx, req) - if errors.Is(err, motion.ErrGoalWithinPlanDeviation) { - // make an exception for the error that is raised when motion is not possible because already at goal. - return svc.waypointReached(cancelCtx) - } else if err != nil { - return err - } - - executionWaypoint := executionWaypoint{executionID: executionID, waypoint: wp} - if old := svc.activeExecutionWaypoint.Swap(executionWaypoint); old != nil && old != emptyExecutionWaypoint { - msg := "unexpected race condition in moveOnGlobeSync, expected " + - "replaced waypoint & execution id to be nil or %#v; instead was %s" - svc.logger.CErrorf(ctx, msg, emptyExecutionWaypoint, old) - } - // call StopPlan upon exiting moveOnGlobeSync - // is a NoOp if execution has already terminted - defer func() { - timeoutCtx, timeoutCancelFn := context.WithTimeout(context.Background(), time.Second*5) - defer timeoutCancelFn() - err := svc.motionService.StopPlan(timeoutCtx, motion.StopPlanReq{ComponentName: req.ComponentName}) - if err != nil { - svc.logger.CError(ctx, "hit error trying to stop plan %s", err) - } - - if old := svc.activeExecutionWaypoint.Swap(emptyExecutionWaypoint); old != executionWaypoint { - msg := "unexpected race condition in moveOnGlobeSync, expected " + - "replaced waypoint & execution id to equal %s, was actually %s" - svc.logger.CErrorf(ctx, msg, executionWaypoint, old) - } - }() - - err = motion.PollHistoryUntilSuccessOrError(cancelCtx, svc.motionService, planHistoryPollFrequency, - motion.PlanHistoryReq{ - ComponentName: req.ComponentName, - ExecutionID: executionID, - LastPlanOnly: true, - }, - ) - if err != nil { - return err - } - - return svc.waypointReached(cancelCtx) -} - -func (svc *builtIn) startWaypointMode(ctx context.Context, extra map[string]interface{}) { - if extra == nil { - extra = map[string]interface{}{} - } - - extra["motion_profile"] = "position_only" - - svc.activeBackgroundWorkers.Add(1) - utils.ManagedGo(func() { - // do not exit loop - even if there are no waypoints remaining - for { - if ctx.Err() != nil { - return - } - - wp, err := svc.store.NextWaypoint(ctx) - if err != nil { - time.Sleep(planHistoryPollFrequency) - continue - } - svc.mu.Lock() - svc.waypointInProgress = &wp - cancelCtx, cancelFunc := context.WithCancel(ctx) - svc.currentWaypointCancelFunc = cancelFunc - svc.mu.Unlock() - - svc.logger.CInfof(ctx, "navigating to waypoint: %+v", wp) - if err := svc.moveToWaypoint(cancelCtx, wp, extra); err != nil { - if svc.waypointIsDeleted() { - svc.logger.CInfof(ctx, "skipping waypoint %+v since it was deleted", wp) - continue - } - svc.logger.CWarnf(ctx, "retrying navigation to waypoint %+v since it errored out: %s", wp, err) - continue - } - svc.logger.CInfof(ctx, "reached waypoint: %+v", wp) - } - }, svc.activeBackgroundWorkers.Done) -} - -func (svc *builtIn) stopActiveMode() { - if svc.wholeServiceCancelFunc != nil { - svc.wholeServiceCancelFunc() - } - svc.activeBackgroundWorkers.Wait() -} - -func (svc *builtIn) waypointIsDeleted() bool { - svc.mu.RLock() - defer svc.mu.RUnlock() - return svc.waypointInProgress == nil -} - -func (svc *builtIn) Obstacles(ctx context.Context, extra map[string]interface{}) ([]*spatialmath.GeoObstacle, error) { - svc.mu.RLock() - defer svc.mu.RUnlock() - - // get static geoObstacles - geoObstacles := svc.obstacles - - for _, detector := range svc.motionCfg.ObstacleDetectors { - // get the vision service - visSvc, ok := svc.visionServicesByName[detector.VisionServiceName] - if !ok { - return nil, fmt.Errorf("vision service with name: %s not found", detector.VisionServiceName) - } - - svc.logger.CDebugf( - ctx, - "proceeding to get detections from vision service: %s with camera: %s", - detector.VisionServiceName.ShortName(), - detector.CameraName.ShortName(), - ) - - // get the detections - detections, err := visSvc.GetObjectPointClouds(ctx, detector.CameraName.Name, nil) - if err != nil { - return nil, err - } - - // determine transform from camera to movement sensor - movementsensorOrigin := referenceframe.NewPoseInFrame(svc.movementSensor.Name().ShortName(), spatialmath.NewZeroPose()) - cameraToMovementsensor, err := svc.fsService.TransformPose(ctx, movementsensorOrigin, detector.CameraName.ShortName(), nil) - if err != nil { - // here we make the assumption the movementsensor is coincident with the camera - svc.logger.CDebugf( - ctx, - "we assume the movementsensor named: %s is coincident with the camera named: %s due to err: %v", - svc.movementSensor.Name().ShortName(), detector.CameraName.ShortName(), err.Error(), - ) - cameraToMovementsensor = movementsensorOrigin - } - svc.logger.CDebugf(ctx, "cameraToMovementsensor Pose: %v", spatialmath.PoseToProtobuf(cameraToMovementsensor.Pose())) - - // determine transform from base to movement sensor - baseToMovementSensor, err := svc.fsService.TransformPose(ctx, movementsensorOrigin, svc.base.Name().ShortName(), nil) - if err != nil { - // here we make the assumption the movementsensor is coincident with the base - svc.logger.CDebugf( - ctx, - "we assume the movementsensor named: %s is coincident with the base named: %s due to err: %v", - svc.movementSensor.Name().ShortName(), svc.base.Name().ShortName(), err.Error(), - ) - baseToMovementSensor = movementsensorOrigin - } - svc.logger.CDebugf(ctx, "baseToMovementSensor Pose: %v", spatialmath.PoseToProtobuf(baseToMovementSensor.Pose())) - - // determine transform from base to camera - cameraOrigin := referenceframe.NewPoseInFrame(detector.CameraName.ShortName(), spatialmath.NewZeroPose()) - baseToCamera, err := svc.fsService.TransformPose(ctx, cameraOrigin, svc.base.Name().ShortName(), nil) - if err != nil { - // here we make the assumption the base is coincident with the camera - svc.logger.CDebugf( - ctx, - "we assume the base named: %s is coincident with the camera named: %s due to err: %v", - svc.base.Name().ShortName(), detector.CameraName.ShortName(), err.Error(), - ) - baseToCamera = cameraOrigin - } - svc.logger.CDebugf(ctx, "baseToCamera Pose: %v", spatialmath.PoseToProtobuf(baseToCamera.Pose())) - - // get current geo position of robot - gp, _, err := svc.movementSensor.Position(ctx, nil) - if err != nil { - return nil, err - } - - // instantiate a localizer and use it to get our current position - localizer := motion.NewMovementSensorLocalizer(svc.movementSensor, gp, spatialmath.NewZeroPose()) - currentPIF, err := localizer.CurrentPosition(ctx) - if err != nil { - return nil, err - } - - // convert orientation of currentPIF to be left handed - localizerHeading := math.Mod(math.Abs(currentPIF.Pose().Orientation().OrientationVectorDegrees().Theta-360), 360) - - // ensure baseToMovementSensor orientation is positive - localizerBaseThetaDiff := math.Mod(math.Abs(baseToMovementSensor.Pose().Orientation().OrientationVectorDegrees().Theta+360), 360) - - baseHeading := math.Mod(localizerHeading+localizerBaseThetaDiff, 360) - - // convert geo position into GeoPose - robotGeoPose := spatialmath.NewGeoPose(gp, baseHeading) - svc.logger.CDebugf(ctx, "robotGeoPose Location: %v, Heading: %v", *robotGeoPose.Location(), robotGeoPose.Heading()) - - // iterate through all detections and construct a geoObstacle to append - for i, detection := range detections { - svc.logger.CInfof( - ctx, - "detection %d pose with respect to camera frame: %v", - i, spatialmath.PoseToProtobuf(detection.Geometry.Pose()), - ) - // the position of the detection in the camera coordinate frame if it were at the movementsensor's location - desiredPoint := detection.Geometry.Pose().Point().Sub(cameraToMovementsensor.Pose().Point()) - - desiredPose := spatialmath.NewPose( - desiredPoint, - detection.Geometry.Pose().Orientation(), - ) - - transformBy := spatialmath.PoseBetweenInverse(detection.Geometry.Pose(), desiredPose) - - // get the manipulated geometry - manipulatedGeom := detection.Geometry.Transform(transformBy) - svc.logger.CDebugf( - ctx, - "detection %d pose from movementsensor's position with camera frame coordinate axes: %v ", - i, spatialmath.PoseToProtobuf(manipulatedGeom.Pose()), - ) - - // fix axes of geometry's pose such that it is in the cooordinate system of the base - manipulatedGeom = manipulatedGeom.Transform(spatialmath.NewPoseFromOrientation(baseToCamera.Pose().Orientation())) - svc.logger.CDebugf( - ctx, - "detection %d pose from movementsensor's position with base frame coordinate axes: %v ", - i, spatialmath.PoseToProtobuf(manipulatedGeom.Pose()), - ) - - // get the geometry's lat & lng along with its heading with respect to north as a left handed value - obstacleGeoPose := spatialmath.PoseToGeoPose(robotGeoPose, manipulatedGeom.Pose()) - svc.logger.CDebugf( - ctx, - "obstacleGeoPose Location: %v, Heading: %v", - *obstacleGeoPose.Location(), obstacleGeoPose.Heading(), - ) - - // prefix the label of the geometry so we know it is transient and add extra info - label := "transient_" + strconv.Itoa(i) + "_" + detector.CameraName.Name - if detection.Geometry.Label() != "" { - label += "_" + detection.Geometry.Label() - } - detection.Geometry.SetLabel(label) - - // determine the desired geometry pose - desiredPose = spatialmath.NewPoseFromOrientation(detection.Geometry.Pose().Orientation()) - - // calculate what we need to transform by - transformBy = spatialmath.PoseBetweenInverse(detection.Geometry.Pose(), desiredPose) - - // set the geometry's pose to desiredPose - manipulatedGeom = detection.Geometry.Transform(transformBy) - - // create the geo obstacle - obstacle := spatialmath.NewGeoObstacle(obstacleGeoPose.Location(), []spatialmath.Geometry{manipulatedGeom}) - - // add manipulatedGeom to list of geoObstacles we return - geoObstacles = append(geoObstacles, obstacle) - } - } - - return geoObstacles, nil -} - -func (svc *builtIn) Paths(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) { - svc.mu.RLock() - defer svc.mu.RUnlock() - - rawExecutionWaypoint := svc.activeExecutionWaypoint.Load() - // If there is no execution, return empty paths - if rawExecutionWaypoint == nil || rawExecutionWaypoint == emptyExecutionWaypoint { - return []*navigation.Path{}, nil - } - - ewp, ok := rawExecutionWaypoint.(executionWaypoint) - if !ok { - return nil, errors.New("execution corrupt") - } - - ph, err := svc.motionService.PlanHistory(ctx, motion.PlanHistoryReq{ - ComponentName: svc.base.Name(), - ExecutionID: ewp.executionID, - LastPlanOnly: true, - }) - if err != nil { - return nil, err - } - - path := ph[0].Plan.Path() - geoPoints := make([]*geo.Point, 0, len(path)) - poses, err := path.GetFramePoses(svc.base.Name().ShortName()) - if err != nil { - return nil, err - } - for _, p := range poses { - geoPoints = append(geoPoints, geo.NewPoint(p.Point().Y, p.Point().X)) - } - navPath, err := navigation.NewPath(ewp.waypoint.ID, geoPoints) - if err != nil { - return nil, err - } - return []*navigation.Path{navPath}, nil -} - -func (svc *builtIn) Properties(ctx context.Context) (navigation.Properties, error) { - svc.mu.RLock() - defer svc.mu.RUnlock() - - prop := navigation.Properties{ - MapType: svc.mapType, - } - return prop, nil -} diff --git a/services/navigation/builtin/builtin_test.go b/services/navigation/builtin/builtin_test.go deleted file mode 100644 index 2c9bed8b9cd..00000000000 --- a/services/navigation/builtin/builtin_test.go +++ /dev/null @@ -1,1918 +0,0 @@ -package builtin - -import ( - "context" - "errors" - "math" - "sync" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/google/uuid" - geo "github.com/kellydunn/golang-geo" - "go.uber.org/atomic" - "go.viam.com/test" - "go.viam.com/utils" - - "go.viam.com/rdk/components/base" - baseFake "go.viam.com/rdk/components/base/fake" - "go.viam.com/rdk/components/camera" - _ "go.viam.com/rdk/components/camera/fake" - "go.viam.com/rdk/components/movementsensor" - _ "go.viam.com/rdk/components/movementsensor/fake" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/motionplan" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/services/motion" - _ "go.viam.com/rdk/services/motion/builtin" - "go.viam.com/rdk/services/navigation" - "go.viam.com/rdk/services/vision" - _ "go.viam.com/rdk/services/vision/colordetector" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - viz "go.viam.com/rdk/vision" -) - -type startWaypointState struct { - ns navigation.Service - injectMS *inject.MotionService - base base.Base - movementSensor *inject.MovementSensor - closeFunc func() - sync.RWMutex - pws []motion.PlanWithStatus - mogrs []motion.MoveOnGlobeReq - sprs []motion.StopPlanReq -} - -func setupNavigationServiceFromConfig(t *testing.T, configFilename string) (navigation.Service, func()) { - t.Helper() - ctx := context.Background() - logger := logging.NewTestLogger(t) - cfg, err := config.Read(ctx, configFilename, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, cfg.Ensure(false, logger), test.ShouldBeNil) - myRobot, err := robotimpl.New(ctx, cfg, logger) - test.That(t, err, test.ShouldBeNil) - svc, err := navigation.FromRobot(myRobot, "test_navigation") - test.That(t, err, test.ShouldBeNil) - return svc, func() { - myRobot.Close(context.Background()) - } -} - -func TestValidateConfig(t *testing.T) { - path := "" - - cases := []struct { - description string - cfg Config - numDeps int - expectedErr error - }{ - { - description: "valid config default map type (GPS) give", - cfg: Config{ - BaseName: "base", - MovementSensorName: "localizer", - }, - numDeps: 4, - expectedErr: nil, - }, - { - description: "valid config for map_type none given", - cfg: Config{ - BaseName: "base", - MapType: "None", - }, - numDeps: 3, - expectedErr: nil, - }, - { - description: "valid config for map_type GPS given", - cfg: Config{ - BaseName: "base", - MapType: "GPS", - MovementSensorName: "localizer", - }, - numDeps: 4, - expectedErr: nil, - }, - { - description: "invalid config no base", - cfg: Config{}, - numDeps: 0, - expectedErr: resource.NewConfigValidationFieldRequiredError(path, "base"), - }, - { - description: "invalid config no movement_sensor given for map type GPS", - cfg: Config{ - BaseName: "base", - MapType: "GPS", - }, - numDeps: 0, - expectedErr: resource.NewConfigValidationFieldRequiredError(path, "movement_sensor"), - }, - { - description: "invalid config negative degs_per_sec", - cfg: Config{ - BaseName: "base", - MovementSensorName: "localizer", - DegPerSec: -1, - }, - numDeps: 0, - expectedErr: errNegativeDegPerSec, - }, - { - description: "invalid config negative meters_per_sec", - cfg: Config{ - BaseName: "base", - MovementSensorName: "localizer", - MetersPerSec: -1, - }, - numDeps: 0, - expectedErr: errNegativeMetersPerSec, - }, - { - description: "invalid config negative position_polling_frequency_hz", - cfg: Config{ - BaseName: "base", - MovementSensorName: "localizer", - PositionPollingFrequencyHz: -1, - }, - numDeps: 0, - expectedErr: errNegativePositionPollingFrequencyHz, - }, - { - description: "invalid config negative obstacle_polling_frequency_hz", - cfg: Config{ - BaseName: "base", - MovementSensorName: "localizer", - ObstaclePollingFrequencyHz: -1, - }, - numDeps: 0, - expectedErr: errNegativeObstaclePollingFrequencyHz, - }, - { - description: "invalid config negative plan_deviation_m", - cfg: Config{ - BaseName: "base", - MovementSensorName: "localizer", - PlanDeviationM: -1, - }, - numDeps: 0, - expectedErr: errNegativePlanDeviationM, - }, - { - description: "invalid config negative replan_cost_factor", - cfg: Config{ - BaseName: "base", - MovementSensorName: "localizer", - ReplanCostFactor: -1, - }, - numDeps: 0, - expectedErr: errNegativeReplanCostFactor, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - deps, err := tt.cfg.Validate(path) - if tt.expectedErr == nil { - test.That(t, err, test.ShouldBeNil) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, tt.expectedErr.Error()) - } - test.That(t, len(deps), test.ShouldEqual, tt.numDeps) - }) - } -} - -func TestNew(t *testing.T) { - ctx := context.Background() - - svc, closeNavSvc := setupNavigationServiceFromConfig(t, "../data/nav_no_map_cfg_minimal.json") - - t.Run("checking defaults have been set", func(t *testing.T) { - svcStruct := svc.(*builtIn) - - test.That(t, svcStruct.base.Name().Name, test.ShouldEqual, "test_base") - test.That(t, svcStruct.motionService.Name().Name, test.ShouldEqual, "builtin") - - test.That(t, svcStruct.mapType, test.ShouldEqual, navigation.NoMap) - test.That(t, svcStruct.mode, test.ShouldEqual, navigation.ModeManual) - test.That(t, svcStruct.replanCostFactor, test.ShouldEqual, defaultReplanCostFactor) - - test.That(t, svcStruct.storeType, test.ShouldEqual, string(navigation.StoreTypeMemory)) - test.That(t, svcStruct.store, test.ShouldResemble, navigation.NewMemoryNavigationStore()) - - test.That(t, svcStruct.motionCfg.ObstacleDetectors, test.ShouldBeNil) - test.That(t, svcStruct.motionCfg.AngularDegsPerSec, test.ShouldEqual, defaultAngularDegsPerSec) - test.That(t, svcStruct.motionCfg.LinearMPerSec, test.ShouldEqual, defaultLinearMPerSec) - test.That(t, svcStruct.motionCfg.PositionPollingFreqHz, test.ShouldEqual, defaultPositionPollingHz) - test.That(t, svcStruct.motionCfg.ObstaclePollingFreqHz, test.ShouldEqual, defaultObstaclePollingHz) - test.That(t, svcStruct.motionCfg.PlanDeviationMM, test.ShouldEqual, defaultPlanDeviationM*1e3) - }) - - t.Run("setting parameters for None map_type", func(t *testing.T) { - cfg := &Config{ - BaseName: "base", - MapType: "None", - MovementSensorName: "movement_sensor", - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): inject.NewBase("new_base"), - resource.NewName(motion.API, "builtin"): inject.NewMotionService("new_motion"), - resource.NewName(movementsensor.API, "movement_sensor"): inject.NewMovementSensor("movement_sensor"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeNil) - svcStruct := svc.(*builtIn) - - test.That(t, svcStruct.mapType, test.ShouldEqual, navigation.NoMap) - test.That(t, svcStruct.base.Name().Name, test.ShouldEqual, "new_base") - test.That(t, svcStruct.motionService.Name().Name, test.ShouldEqual, "new_motion") - test.That(t, svcStruct.movementSensor, test.ShouldBeNil) - }) - - t.Run("setting parameters for GPS map_type", func(t *testing.T) { - cfg := &Config{ - BaseName: "base", - MapType: "GPS", - MovementSensorName: "movement_sensor", - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): inject.NewBase("new_base"), - resource.NewName(motion.API, "builtin"): inject.NewMotionService("new_motion"), - resource.NewName(movementsensor.API, "movement_sensor"): inject.NewMovementSensor("movement_sensor"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeNil) - svcStruct := svc.(*builtIn) - - test.That(t, svcStruct.mapType, test.ShouldEqual, navigation.GPSMap) - test.That(t, svcStruct.base.Name().Name, test.ShouldEqual, "new_base") - test.That(t, svcStruct.motionService.Name().Name, test.ShouldEqual, "new_motion") - test.That(t, svcStruct.movementSensor.Name().Name, test.ShouldEqual, cfg.MovementSensorName) - }) - - t.Run("setting motion parameters", func(t *testing.T) { - cfg := &Config{ - BaseName: "base", - MapType: "None", - DegPerSec: 1, - MetersPerSec: 2, - PositionPollingFrequencyHz: 3, - ObstaclePollingFrequencyHz: 4, - PlanDeviationM: 5, - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - VisionServiceName: "vision", - CameraName: "camera", - }, - }, - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): &inject.Base{}, - resource.NewName(camera.API, "camera"): inject.NewCamera("camera"), - resource.NewName(motion.API, "builtin"): inject.NewMotionService("motion"), - resource.NewName(vision.API, "vision"): inject.NewVisionService("vision"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeNil) - svcStruct := svc.(*builtIn) - - test.That(t, len(svcStruct.motionCfg.ObstacleDetectors), test.ShouldEqual, 1) - test.That(t, svcStruct.motionCfg.ObstacleDetectors[0].VisionServiceName.Name, test.ShouldEqual, "vision") - test.That(t, svcStruct.motionCfg.ObstacleDetectors[0].CameraName.Name, test.ShouldEqual, "camera") - - test.That(t, svcStruct.motionCfg.AngularDegsPerSec, test.ShouldEqual, cfg.DegPerSec) - test.That(t, svcStruct.motionCfg.LinearMPerSec, test.ShouldEqual, cfg.MetersPerSec) - test.That(t, svcStruct.motionCfg.PositionPollingFreqHz, test.ShouldEqual, cfg.PositionPollingFrequencyHz) - test.That(t, svcStruct.motionCfg.ObstaclePollingFreqHz, test.ShouldEqual, cfg.ObstaclePollingFrequencyHz) - test.That(t, svcStruct.motionCfg.PlanDeviationMM, test.ShouldEqual, cfg.PlanDeviationM*1e3) - }) - - t.Run("setting additional parameters", func(t *testing.T) { - cfg := &Config{ - BaseName: "base", - MapType: "None", - ReplanCostFactor: 1, - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - VisionServiceName: "vision", - CameraName: "camera", - }, - }, - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): &inject.Base{}, - resource.NewName(camera.API, "camera"): inject.NewCamera("camera"), - resource.NewName(motion.API, "builtin"): inject.NewMotionService("motion"), - resource.NewName(vision.API, "vision"): inject.NewVisionService("vision"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeNil) - svcStruct := svc.(*builtIn) - - test.That(t, svcStruct.motionCfg.ObstacleDetectors[0].VisionServiceName.Name, test.ShouldEqual, "vision") - test.That(t, svcStruct.motionCfg.ObstacleDetectors[0].CameraName.Name, test.ShouldEqual, "camera") - test.That(t, svcStruct.replanCostFactor, test.ShouldEqual, cfg.ReplanCostFactor) - }) - - t.Run("base missing from deps", func(t *testing.T) { - expectedErr := resource.DependencyNotFoundError(base.Named("")) - cfg := &Config{} - deps := resource.Dependencies{} - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeError, expectedErr) - }) - - t.Run("motion missing from deps", func(t *testing.T) { - expectedErr := resource.DependencyNotFoundError(motion.Named("builtin")) - cfg := &Config{ - BaseName: "base", - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): &inject.Base{}, - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeError, expectedErr) - }) - - t.Run("movement sensor missing from deps", func(t *testing.T) { - expectedErr := resource.DependencyNotFoundError(movementsensor.Named("")) - cfg := &Config{ - BaseName: "base", - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): &inject.Base{}, - resource.NewName(motion.API, "builtin"): inject.NewMotionService("motion"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeError, expectedErr) - }) - - t.Run("vision missing from deps", func(t *testing.T) { - expectedErr := resource.DependencyNotFoundError(vision.Named("")) - cfg := &Config{ - BaseName: "base", - MovementSensorName: "movement_sensor", - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - CameraName: "camera", - }, - }, - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): &inject.Base{}, - resource.NewName(motion.API, "builtin"): inject.NewMotionService("motion"), - resource.NewName(camera.API, "camera"): inject.NewCamera("camera"), - resource.NewName(movementsensor.API, "movement_sensor"): inject.NewMovementSensor("movement_sensor"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeError, expectedErr) - }) - - t.Run("camera missing from deps", func(t *testing.T) { - expectedErr := resource.DependencyNotFoundError(camera.Named("")) - cfg := &Config{ - BaseName: "base", - MovementSensorName: "movement_sensor", - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - VisionServiceName: "vision", - }, - }, - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): &inject.Base{}, - resource.NewName(motion.API, "builtin"): inject.NewMotionService("motion"), - resource.NewName(vision.API, "vision"): inject.NewVisionService("vision"), - resource.NewName(movementsensor.API, "movement_sensor"): inject.NewMovementSensor("movement_sensor"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeError, expectedErr) - }) - - t.Run("necessary for MoveOnGlobe", func(t *testing.T) { - cfg := &Config{ - BaseName: "base", - MovementSensorName: "movement_sensor", - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - VisionServiceName: "vision", - CameraName: "camera", - }, - }, - } - deps := resource.Dependencies{ - resource.NewName(base.API, "base"): &inject.Base{}, - resource.NewName(motion.API, "builtin"): inject.NewMotionService("motion"), - resource.NewName(vision.API, "vision"): inject.NewVisionService("vision"), - resource.NewName(camera.API, "camera"): inject.NewCamera("camera"), - resource.NewName(movementsensor.API, "movement_sensor"): inject.NewMovementSensor("movement_sensor"), - } - - err := svc.Reconfigure(ctx, deps, resource.Config{ConvertedAttributes: cfg}) - test.That(t, err, test.ShouldBeNil) - svcStruct := svc.(*builtIn) - - test.That(t, svcStruct.motionCfg.ObstacleDetectors[0].VisionServiceName.Name, test.ShouldEqual, "vision") - test.That(t, svcStruct.motionCfg.ObstacleDetectors[0].CameraName.Name, test.ShouldEqual, "camera") - }) - - closeNavSvc() -} - -func TestSetMode(t *testing.T) { - ctx := context.Background() - - cases := []struct { - description string - cfg string - mapType navigation.MapType - mode navigation.Mode - expectedErr error - }{ - { - description: "setting mode to manual when map_type is None", - cfg: "../data/nav_no_map_cfg.json", - mapType: navigation.NoMap, - mode: navigation.ModeManual, - expectedErr: nil, - }, - { - description: "setting mode to waypoint when map_type is None", - cfg: "../data/nav_no_map_cfg.json", - mapType: navigation.NoMap, - mode: navigation.ModeWaypoint, - expectedErr: errors.New("Waypoint mode is unavailable for map type None"), - }, - { - description: "setting mode to explore when map_type is None", - cfg: "../data/nav_no_map_cfg.json", - mapType: navigation.NoMap, - mode: navigation.ModeExplore, - expectedErr: nil, - }, - { - description: "setting mode to explore when map_type is None and no vision service is configured", - cfg: "../data/nav_no_map_cfg_minimal.json", - mapType: navigation.GPSMap, - mode: navigation.ModeExplore, - expectedErr: errors.New("explore mode requires at least one vision service"), - }, - { - description: "setting mode to manual when map_type is GPS", - cfg: "../data/nav_cfg.json", - mapType: navigation.GPSMap, - mode: navigation.ModeManual, - expectedErr: nil, - }, - { - description: "setting mode to waypoint when map_type is GPS", - cfg: "../data/nav_cfg.json", - mapType: navigation.GPSMap, - mode: navigation.ModeWaypoint, - expectedErr: nil, - }, - { - description: "setting mode to explore when map_type is GPS", - cfg: "../data/nav_cfg.json", - mapType: navigation.GPSMap, - mode: navigation.ModeExplore, - expectedErr: nil, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - ns, teardown := setupNavigationServiceFromConfig(t, tt.cfg) - defer teardown() - - navMode, err := ns.Mode(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, navMode, test.ShouldEqual, navigation.ModeManual) - - err = ns.SetMode(ctx, tt.mode, nil) - if tt.expectedErr == nil { - test.That(t, err, test.ShouldEqual, tt.expectedErr) - } else { - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, tt.expectedErr.Error()) - } - }) - } -} - -func TestNavSetup(t *testing.T) { - ns, teardown := setupNavigationServiceFromConfig(t, "../data/nav_cfg.json") - defer teardown() - ctx := context.Background() - - navMode, err := ns.Mode(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, navMode, test.ShouldEqual, navigation.ModeManual) - - err = ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - navMode, err = ns.Mode(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, navMode, test.ShouldEqual, navigation.ModeWaypoint) - - // Prevent race - err = ns.SetMode(ctx, navigation.ModeManual, nil) - test.That(t, err, test.ShouldBeNil) - - geoPose, err := ns.Location(ctx, nil) - test.That(t, err, test.ShouldBeNil) - expectedGeoPose := spatialmath.NewGeoPose(geo.NewPoint(40.7, -73.98), 25.) - test.That(t, geoPose, test.ShouldResemble, expectedGeoPose) - - wayPt, err := ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, wayPt, test.ShouldBeEmpty) - - pt := geo.NewPoint(0, 0) - err = ns.AddWaypoint(ctx, pt, nil) - test.That(t, err, test.ShouldBeNil) - - wayPt, err = ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(wayPt), test.ShouldEqual, 1) - - id := wayPt[0].ID - err = ns.RemoveWaypoint(ctx, id, nil) - test.That(t, err, test.ShouldBeNil) - wayPt, err = ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(wayPt), test.ShouldEqual, 0) - - // Calling RemoveWaypoint on an already removed waypoint doesn't return an error - err = ns.RemoveWaypoint(ctx, id, nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(ns.(*builtIn).motionCfg.ObstacleDetectors), test.ShouldEqual, 1) - - paths, err := ns.Paths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, paths, test.ShouldBeEmpty) -} - -func setupStartWaypoint(ctx context.Context, t *testing.T, logger logging.Logger) startWaypointState { - fakeBase, err := baseFake.NewBase(ctx, nil, resource.Config{ - Name: "test_base", - API: base.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{R: 100}}, - }, logger) - test.That(t, err, test.ShouldBeNil) - - injectMovementSensor := inject.NewMovementSensor("test_movement") - visionService := inject.NewVisionService("vision") - camera := inject.NewCamera("camera") - config := resource.Config{ - ConvertedAttributes: &Config{ - Store: navigation.StoreConfig{ - Type: navigation.StoreTypeMemory, - }, - BaseName: "test_base", - MovementSensorName: "test_movement", - MotionServiceName: "test_motion", - DegPerSec: 1, - MetersPerSec: 1, - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - VisionServiceName: "vision", - CameraName: "camera", - }, - }, - }, - } - injectMS := inject.NewMotionService("test_motion") - deps := resource.Dependencies{ - injectMS.Name(): injectMS, - fakeBase.Name(): fakeBase, - injectMovementSensor.Name(): injectMovementSensor, - visionService.Name(): visionService, - camera.Name(): camera, - } - ns, err := NewBuiltIn(ctx, deps, config, logger) - test.That(t, err, test.ShouldBeNil) - return startWaypointState{ - ns: ns, - injectMS: injectMS, - base: fakeBase, - movementSensor: injectMovementSensor, - closeFunc: func() { test.That(t, ns.Close(context.Background()), test.ShouldBeNil) }, - } -} - -func setupStartWaypointExplore(ctx context.Context, t *testing.T, logger logging.Logger) startWaypointState { - fsSvc, err := framesystem.New(ctx, nil, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, fsSvc, test.ShouldNotBeNil) - fakeBase, err := baseFake.NewBase(ctx, nil, resource.Config{ - Name: "test_base", - API: base.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{R: 100}}, - }, logger) - test.That(t, err, test.ShouldBeNil) - - injectMovementSensor := inject.NewMovementSensor("test_movement") - visionService := inject.NewVisionService("vision") - camera := inject.NewCamera("camera") - config := resource.Config{ - ConvertedAttributes: &Config{ - Store: navigation.StoreConfig{ - Type: navigation.StoreTypeMemory, - }, - BaseName: "test_base", - MovementSensorName: "test_movement", - MotionServiceName: "test_motion", - DegPerSec: 1, - MetersPerSec: 1, - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - VisionServiceName: "vision", - CameraName: "camera", - }, - }, - }, - } - injectMS := inject.NewMotionService("test_motion") - deps := resource.Dependencies{ - injectMS.Name(): injectMS, - fakeBase.Name(): fakeBase, - injectMovementSensor.Name(): injectMovementSensor, - visionService.Name(): visionService, - camera.Name(): camera, - // to placate the explore struct to not panic - fsSvc.Name(): fsSvc, - } - ns, err := NewBuiltIn(ctx, deps, config, logger) - test.That(t, err, test.ShouldBeNil) - return startWaypointState{ - ns: ns, - injectMS: injectMS, - base: fakeBase, - movementSensor: injectMovementSensor, - closeFunc: func() { test.That(t, ns.Close(context.Background()), test.ShouldBeNil) }, - } -} - -func TestPaths(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - t.Run("Paths reflects the paths of all components which have in progress MoveOnGlobe calls", func(t *testing.T) { - expectedLng := 1. - expectedLat := 2. - var wg sync.WaitGroup - wg.Wait() - s := setupStartWaypoint(ctx, t, logger) - defer s.closeFunc() - - planHistoryCalledCtx, planHistoryCalledCancelFn := context.WithCancel(ctx) - planSucceededCtx, planSucceededCancelFn := context.WithCancel(ctx) - defer planSucceededCancelFn() - // we expect 2 executions to be generated - executionID := uuid.New() - // MoveOnGlobe will behave as if it created a new plan & queue up a goroutine which will then behave as if the plan succeeded - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - s.Lock() - defer s.Unlock() - if s.mogrs == nil { - s.mogrs = []motion.MoveOnGlobeReq{} - } - s.mogrs = append(s.mogrs, req) - s.pws = []motion.PlanWithStatus{ - { - Plan: motion.PlanWithMetadata{ - ExecutionID: executionID, - Plan: motionplan.NewSimplePlan( - []motionplan.PathStep{{s.base.Name().ShortName(): referenceframe.NewPoseInFrame( - referenceframe.World, - spatialmath.NewPose(r3.Vector{X: expectedLng, Y: expectedLat}, nil), - )}}, - nil, - ), - }, - StatusHistory: []motion.PlanStatus{ - {State: motion.PlanStateInProgress}, - }, - }, - } - logger.Infof("before cancel, len: %d", len(s.pws)) - wg.Add(1) - logger.Infof("after cancel, len: %d", len(s.pws)) - utils.ManagedGo(func() { - <-planSucceededCtx.Done() - s.Lock() - defer s.Unlock() - for i, p := range s.pws { - if p.Plan.ExecutionID == executionID { - succeeded := []motion.PlanStatus{{State: motion.PlanStateSucceeded}} - p.StatusHistory = append(succeeded, p.StatusHistory...) - s.pws[i] = p - return - } - } - t.Error("MoveOnGlobe called unexpectedly") - }, wg.Done) - return executionID, nil - } - - s.injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - s.RLock() - defer s.RUnlock() - planHistoryCalledCancelFn() - logger.Infof("PlanHistory called, len: %d", len(s.pws)) - defer logger.Infof("PlanHistory done, len: %d", len(s.pws)) - history := make([]motion.PlanWithStatus, len(s.pws)) - copy(history, s.pws) - return history, nil - } - - s.injectMS.StopPlanFunc = func(ctx context.Context, req motion.StopPlanReq) error { - if err := ctx.Err(); err != nil { - return err - } - s.Lock() - defer s.Unlock() - if s.sprs == nil { - s.sprs = []motion.StopPlanReq{} - } - s.sprs = append(s.sprs, req) - return nil - } - - paths, err := s.ns.Paths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, paths, test.ShouldBeEmpty) - - pt1 := geo.NewPoint(1, 0) - err = s.ns.AddWaypoint(ctx, pt1, nil) - test.That(t, err, test.ShouldBeNil) - - paths, err = s.ns.Paths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, paths, test.ShouldBeEmpty) - - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - - // poll till waypoints is of length 0 - // query PlanHistory & confirm that you get back UUID 2 - timeoutCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*500) - defer cancelFn() - select { - case <-timeoutCtx.Done(): - t.Error("test timed out") - t.FailNow() - case <-planHistoryCalledCtx.Done(): - paths, err := s.ns.Paths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(paths), test.ShouldEqual, 1) - test.That(t, len(paths[0].DestinationWaypointID()), test.ShouldBeGreaterThan, 0) - test.That(t, len(paths[0].GeoPoints()), test.ShouldEqual, 1) - test.That(t, paths[0].GeoPoints()[0].Lat(), test.ShouldAlmostEqual, expectedLat) - test.That(t, paths[0].GeoPoints()[0].Lng(), test.ShouldAlmostEqual, expectedLng) - // trigger plan success - planSucceededCancelFn() - for { - if timeoutCtx.Err() != nil { - t.Error("test timed out") - t.FailNow() - } - // wait until nav detects that the plan succeeded & removes its path from the Paths method response - paths, err := s.ns.Paths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - if len(paths) == 0 { - break - } - time.Sleep(time.Millisecond * 50) - } - } - }) -} - -func TestStartWaypoint(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - t.Run("Reach waypoints successfully", func(t *testing.T) { - s := setupStartWaypoint(ctx, t, logger) - defer s.closeFunc() - - // we expect 2 executions to be generated - executionIDs := []uuid.UUID{ - uuid.New(), - uuid.New(), - } - counter := atomic.NewInt32(-1) - var wg sync.WaitGroup - defer wg.Wait() - // MoveOnGlobe will behave as if it created a new plan & queue up a goroutine which will then behave as if the plan succeeded - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - s.Lock() - defer s.Unlock() - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - count := counter.Inc() - if count > 1 { - t.Error("MoveOnGlobe should not be called more than twice") - t.FailNow() - } - executionID := executionIDs[count] - if s.mogrs == nil { - s.mogrs = []motion.MoveOnGlobeReq{} - } - s.mogrs = append(s.mogrs, req) - s.pws = []motion.PlanWithStatus{ - { - Plan: motion.PlanWithMetadata{ - ExecutionID: executionID, - }, - StatusHistory: []motion.PlanStatus{ - {State: motion.PlanStateInProgress}, - }, - }, - } - wg.Add(1) - utils.ManagedGo(func() { - s.Lock() - defer s.Unlock() - for i, p := range s.pws { - if p.Plan.ExecutionID == executionID { - succeeded := []motion.PlanStatus{{State: motion.PlanStateSucceeded}} - p.StatusHistory = append(succeeded, p.StatusHistory...) - s.pws[i] = p - return - } - } - t.Error("MoveOnGlobe called unexpectedly") - }, wg.Done) - return executionID, nil - } - - s.injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - s.RLock() - defer s.RUnlock() - if err := ctx.Err(); err != nil { - return nil, err - } - history := make([]motion.PlanWithStatus, len(s.pws)) - copy(history, s.pws) - if len(history) == 0 { - return nil, errors.New("no plan") - } - return history, nil - } - - s.injectMS.StopPlanFunc = func(ctx context.Context, req motion.StopPlanReq) error { - s.Lock() - defer s.Unlock() - if err := ctx.Err(); err != nil { - return err - } - if s.sprs == nil { - s.sprs = []motion.StopPlanReq{} - } - s.sprs = append(s.sprs, req) - return nil - } - - pt1 := geo.NewPoint(1, 0) - err := s.ns.AddWaypoint(ctx, pt1, nil) - test.That(t, err, test.ShouldBeNil) - - pt2 := geo.NewPoint(3, 1) - err = s.ns.AddWaypoint(ctx, pt2, nil) - test.That(t, err, test.ShouldBeNil) - - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(wps), test.ShouldEqual, 2) - - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - - expectedMotionCfg := &motion.MotionConfiguration{ - PositionPollingFreqHz: 1, - ObstaclePollingFreqHz: 1, - PlanDeviationMM: 2600, - LinearMPerSec: 1, - AngularDegsPerSec: 1, - ObstacleDetectors: []motion.ObstacleDetectorName{ - { - VisionServiceName: vision.Named("vision"), - CameraName: camera.Named("camera"), - }, - }, - } - // poll till waypoints is of length 0 - // query PlanHistory & confirm that you get back UUID 2 - timeoutCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*500) - defer cancelFn() - for { - if timeoutCtx.Err() != nil { - t.Error("test timed out") - t.FailNow() - } - // once all waypoints have been consumed - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - if len(wps) == 0 { - // wait until success has completed - wg.Wait() - s.RLock() - // wait until StopPlan has been called twice - if len(s.sprs) == 2 { - // MoveOnGlobe was called twice, once for each waypoint - test.That(t, len(s.mogrs), test.ShouldEqual, 2) - test.That(t, s.mogrs[0].ComponentName, test.ShouldResemble, s.base.Name()) - test.That(t, math.IsNaN(s.mogrs[0].Heading), test.ShouldBeTrue) - test.That(t, s.mogrs[0].MovementSensorName, test.ShouldResemble, s.movementSensor.Name()) - - test.That(t, s.mogrs[0].Extra, test.ShouldResemble, map[string]interface{}{ - "motion_profile": "position_only", - }) - test.That(t, s.mogrs[0].MotionCfg, test.ShouldResemble, expectedMotionCfg) - test.That(t, s.mogrs[0].Obstacles, test.ShouldBeNil) - // waypoint 1 - test.That(t, s.mogrs[0].Destination, test.ShouldResemble, pt1) - - test.That(t, s.mogrs[1].ComponentName, test.ShouldResemble, s.base.Name()) - test.That(t, math.IsNaN(s.mogrs[1].Heading), test.ShouldBeTrue) - test.That(t, s.mogrs[1].MovementSensorName, test.ShouldResemble, s.movementSensor.Name()) - test.That(t, s.mogrs[1].Extra, test.ShouldResemble, map[string]interface{}{ - "motion_profile": "position_only", - }) - test.That(t, s.mogrs[1].MotionCfg, test.ShouldResemble, expectedMotionCfg) - test.That(t, s.mogrs[1].Obstacles, test.ShouldBeNil) - // waypoint 2 - test.That(t, s.mogrs[1].Destination, test.ShouldResemble, pt2) - - // PlanStop called twice, once for each waypoint - test.That(t, s.sprs[0].ComponentName, test.ShouldResemble, s.base.Name()) - test.That(t, s.sprs[1].ComponentName, test.ShouldResemble, s.base.Name()) - - // Motion reports that the last execution succeeded - ph, err := s.injectMS.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: s.base.Name()}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph), test.ShouldEqual, 1) - test.That(t, ph[0].Plan.ExecutionID, test.ShouldResemble, executionIDs[1]) - test.That(t, len(ph[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, ph[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateSucceeded) - - // paths should be empty after all MoveOnGlobe calls have terminated - paths, err := s.ns.Paths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, paths, test.ShouldBeEmpty) - s.RUnlock() - break - } - s.RUnlock() - } - } - }) - - t.Run("SetMode's extra field is passed to MoveOnGlobe, with the default "+ - "motion profile of position_only", func(t *testing.T) { - s := setupStartWaypoint(ctx, t, logger) - defer s.closeFunc() - - executionID := uuid.New() - mogCalled := make(chan struct{}, 1) - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - - s.Lock() - if s.mogrs == nil { - s.mogrs = []motion.MoveOnGlobeReq{} - } - s.mogrs = append(s.mogrs, req) - s.Unlock() - mogCalled <- struct{}{} - return executionID, nil - } - - // PlanHistory always reports execution is in progress - s.injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - return []motion.PlanWithStatus{ - { - Plan: motion.PlanWithMetadata{ - ExecutionID: executionID, - }, - StatusHistory: []motion.PlanStatus{ - {State: motion.PlanStateInProgress}, - }, - }, - }, nil - } - - s.injectMS.StopPlanFunc = func(ctx context.Context, req motion.StopPlanReq) error { - if err := ctx.Err(); err != nil { - return err - } - return nil - } - - pt1 := geo.NewPoint(1, 0) - err := s.ns.AddWaypoint(ctx, pt1, nil) - test.That(t, err, test.ShouldBeNil) - - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(wps), test.ShouldEqual, 1) - - // SetMode with nil extra - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - <-mogCalled - - err = s.ns.SetMode(ctx, navigation.ModeManual, nil) - test.That(t, err, test.ShouldBeNil) - - // SetMode with empty map - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - <-mogCalled - - err = s.ns.SetMode(ctx, navigation.ModeManual, nil) - test.That(t, err, test.ShouldBeNil) - - // SetMode with motion_profile set - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, map[string]interface{}{"some_other": "config_param"}) - test.That(t, err, test.ShouldBeNil) - <-mogCalled - - // poll till s.mogrs has length 3 - timeoutCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*500) - defer cancelFn() - for { - if timeoutCtx.Err() != nil { - t.Error("test timed out") - t.FailNow() - } - s.RLock() - if len(s.mogrs) == 3 { - // MoveOnGlobe was called twice, once for each waypoint - test.That(t, s.mogrs[0].Extra, test.ShouldResemble, map[string]interface{}{ - "motion_profile": "position_only", - }) - test.That(t, s.mogrs[1].Extra, test.ShouldResemble, map[string]interface{}{ - "motion_profile": "position_only", - }) - test.That(t, s.mogrs[2].Extra, test.ShouldResemble, map[string]interface{}{ - "motion_profile": "position_only", - "some_other": "config_param", - }) - s.RUnlock() - break - } - s.RUnlock() - } - }) - - t.Run("motion errors result in retrying the current waypoint", func(t *testing.T) { - s := setupStartWaypoint(ctx, t, logger) - defer s.closeFunc() - - mogCounter := atomic.NewInt32(-1) - planHistoryCounter := atomic.NewInt32(0) - var wg sync.WaitGroup - executionIDs := []uuid.UUID{ - uuid.New(), - uuid.New(), - uuid.New(), - } - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - s.Lock() - defer s.Unlock() - if s.mogrs == nil { - s.mogrs = []motion.MoveOnGlobeReq{} - } - s.mogrs = append(s.mogrs, req) - - // first call returns motion error - // second call returns context cancelled - // third call returns success - count := mogCounter.Inc() - switch count { - case 0: - return uuid.Nil, errors.New("motion error") - case 1: - return uuid.Nil, context.Canceled - case 2, 3, 4: - executionID := executionIDs[count-2] - s.pws = []motion.PlanWithStatus{ - { - Plan: motion.PlanWithMetadata{ - ExecutionID: executionID, - }, - StatusHistory: []motion.PlanStatus{ - {State: motion.PlanStateInProgress}, - }, - }, - } - wg.Add(1) - utils.ManagedGo(func() { - s.Lock() - defer s.Unlock() - for i, p := range s.pws { - if p.Plan.ExecutionID == executionID { - succeeded := []motion.PlanStatus{{State: motion.PlanStateSucceeded}} - p.StatusHistory = append(succeeded, p.StatusHistory...) - s.pws[i] = p - return - } - } - }, wg.Done) - return executionID, nil - default: - t.Error("unexpected call to MOG") - t.Fail() - return uuid.Nil, errors.New("unexpected call to MOG") - } - } - - s.injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - switch planHistoryCounter.Inc() { - case 1: - return nil, errors.New("motion error") - case 2: - return nil, context.Canceled - default: - s.RLock() - defer s.RUnlock() - history := make([]motion.PlanWithStatus, len(s.pws)) - copy(history, s.pws) - return history, nil - } - } - - s.injectMS.StopPlanFunc = func(ctx context.Context, req motion.StopPlanReq) error { - if err := ctx.Err(); err != nil { - return err - } - s.Lock() - defer s.Unlock() - if s.sprs == nil { - s.sprs = []motion.StopPlanReq{} - } - s.sprs = append(s.sprs, req) - return nil - } - - pt1 := geo.NewPoint(1, 0) - err := s.ns.AddWaypoint(ctx, pt1, nil) - test.That(t, err, test.ShouldBeNil) - - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - - timeoutCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*500) - defer cancelFn() - for { - if timeoutCtx.Err() != nil { - t.Error("test timed out") - t.FailNow() - } - // once all waypoints have been consumed - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - if len(wps) == 0 { - s.RLock() - // wait until StopPlan has been called twice - if len(s.sprs) == 3 { - test.That(t, len(s.mogrs), test.ShouldEqual, 5) - test.That(t, s.mogrs[0].Destination, test.ShouldResemble, pt1) - test.That(t, s.mogrs[1].Destination, test.ShouldResemble, pt1) - test.That(t, s.mogrs[2].Destination, test.ShouldResemble, pt1) - test.That(t, s.mogrs[3].Destination, test.ShouldResemble, pt1) - test.That(t, s.mogrs[4].Destination, test.ShouldResemble, pt1) - - // Motion reports that the last execution succeeded - ph, err := s.injectMS.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: s.base.Name()}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph), test.ShouldEqual, 1) - test.That(t, ph[0].Plan.ExecutionID, test.ShouldResemble, executionIDs[2]) - test.That(t, len(ph[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, ph[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateSucceeded) - s.RUnlock() - break - } - s.RUnlock() - } - } - }) - - sManual := setupStartWaypoint(ctx, t, logger) - sExplore := setupStartWaypointExplore(ctx, t, logger) - // Calling SetMode cancels current and future MoveOnGlobe calls - cases := []struct { - description string - mode navigation.Mode - s *startWaypointState - }{ - { - description: "Calling SetMode manual cancels context of current and future motion calls", - mode: navigation.ModeManual, - s: &sManual, - }, - { - description: "Calling SetMode explore cancels context of current and future motion calls", - mode: navigation.ModeExplore, - s: &sExplore, - }, - } - - for _, tt := range cases { - t.Run(tt.description, func(t *testing.T) { - s := tt.s - defer s.closeFunc() - fsSvc, err := framesystem.New(ctx, nil, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, fsSvc, test.ShouldNotBeNil) - - executionID := uuid.New() - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - s.Lock() - defer s.Unlock() - if s.mogrs == nil { - s.mogrs = []motion.MoveOnGlobeReq{} - } - s.mogrs = append(s.mogrs, req) - s.pws = []motion.PlanWithStatus{ - { - Plan: motion.PlanWithMetadata{ - ExecutionID: executionID, - }, - StatusHistory: []motion.PlanStatus{ - {State: motion.PlanStateInProgress}, - }, - }, - } - return executionID, nil - } - - counter := atomic.NewInt32(0) - modeFlag := make(chan struct{}, 1) - s.injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - s.RLock() - defer s.RUnlock() - history := make([]motion.PlanWithStatus, len(s.pws)) - copy(history, s.pws) - count := counter.Inc() - if count == 2 { - modeFlag <- struct{}{} - } - return history, nil - } - - s.injectMS.StopPlanFunc = func(ctx context.Context, req motion.StopPlanReq) error { - if err := ctx.Err(); err != nil { - return err - } - s.Lock() - defer s.Unlock() - if s.sprs == nil { - s.sprs = []motion.StopPlanReq{} - } - s.sprs = append(s.sprs, req) - for i, p := range s.pws { - if p.Plan.ExecutionID == executionID { - stopped := []motion.PlanStatus{{State: motion.PlanStateStopped}} - p.StatusHistory = append(stopped, p.StatusHistory...) - s.pws[i] = p - return nil - } - } - return nil - } - - // Set manual mode to ensure waypoint loop from prior test exits - points := []*geo.Point{geo.NewPoint(1, 2), geo.NewPoint(2, 3)} - for _, pt := range points { - err := s.ns.AddWaypoint(ctx, pt, nil) - test.That(t, err, test.ShouldBeNil) - } - wpBefore, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(wpBefore), test.ShouldEqual, 2) - test.That(t, wpBefore[0].ToPoint(), test.ShouldResemble, points[0]) - test.That(t, wpBefore[1].ToPoint(), test.ShouldResemble, points[1]) - - // start navigation - set ModeWaypoint first to ensure navigation starts up - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - - // wait until MoveOnGlobe has been called & are polling the history - <-modeFlag - - // Change the mode --> cancels the context - err = s.ns.SetMode(ctx, tt.mode, nil) - test.That(t, err, test.ShouldBeNil) - - // observe that no waypoints are deleted - wpAfter, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, wpAfter, test.ShouldResemble, wpBefore) - - // check the last state of the execution - ph, err := s.injectMS.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: s.base.Name()}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph), test.ShouldEqual, 1) - test.That(t, len(ph[0].StatusHistory), test.ShouldEqual, 2) - // The history reports that the terminal state of the execution is stopped - test.That(t, ph[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateStopped) - }) - } - - t.Run("motion error returned when within planDeviation results in visiting waypoint", func(t *testing.T) { - s := setupStartWaypoint(ctx, t, logger) - defer s.closeFunc() - - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - return uuid.Nil, motion.ErrGoalWithinPlanDeviation - } - - cancelCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*200) - defer cancelFn() - - err := s.ns.AddWaypoint(cancelCtx, geo.NewPoint(1, 2), nil) - test.That(t, err, test.ShouldBeNil) - wps, err := s.ns.Waypoints(cancelCtx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(wps), test.ShouldEqual, 1) - err = s.ns.SetMode(cancelCtx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - - for { - if cancelCtx.Err() != nil { - t.Error("test timed out") - t.FailNow() - } - wps, err := s.ns.Waypoints(cancelCtx, nil) - test.That(t, err, test.ShouldBeNil) - if len(wps) == 0 { - break - } - } - }) - - t.Run("Calling RemoveWaypoint on the waypoint in progress cancels current MoveOnGlobe call", func(t *testing.T) { - s := setupStartWaypoint(ctx, t, logger) - defer s.closeFunc() - - // we expect 2 executions to be generated - executionIDs := []uuid.UUID{ - uuid.New(), - uuid.New(), - } - counter := atomic.NewInt32(-1) - var wg sync.WaitGroup - defer wg.Wait() - // MoveOnGlobe will behave as if it created a new plan & queue up a goroutine which will then behave as if the plan succeeded - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - executionID := executionIDs[(counter.Inc())] - s.Lock() - defer s.Unlock() - if s.mogrs == nil { - s.mogrs = []motion.MoveOnGlobeReq{} - } - s.mogrs = append(s.mogrs, req) - s.pws = []motion.PlanWithStatus{ - { - Plan: motion.PlanWithMetadata{ - ExecutionID: executionID, - }, - StatusHistory: []motion.PlanStatus{ - {State: motion.PlanStateInProgress}, - }, - }, - } - // only succeed for the second execution - if executionID == executionIDs[1] { - wg.Add(1) - utils.ManagedGo(func() { - s.Lock() - defer s.Unlock() - for i, p := range s.pws { - if p.Plan.ExecutionID == executionID { - succeeded := []motion.PlanStatus{{State: motion.PlanStateSucceeded}} - p.StatusHistory = append(succeeded, p.StatusHistory...) - s.pws[i] = p - return - } - } - t.Error("MoveOnGlobe called unexpectedly") - }, wg.Done) - } - return executionID, nil - } - - planHistoryCalledCtx, planHistoryCalledCancelFn := context.WithTimeout(ctx, time.Millisecond*500) - defer planHistoryCalledCancelFn() - s.injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - s.RLock() - defer s.RUnlock() - history := make([]motion.PlanWithStatus, len(s.pws)) - copy(history, s.pws) - planHistoryCalledCancelFn() - return history, nil - } - - s.injectMS.StopPlanFunc = func(ctx context.Context, req motion.StopPlanReq) error { - if err := ctx.Err(); err != nil { - return err - } - s.Lock() - defer s.Unlock() - if s.sprs == nil { - s.sprs = []motion.StopPlanReq{} - } - s.sprs = append(s.sprs, req) - return nil - } - - // Set manual mode to ensure waypoint loop from prior test exits - points := []*geo.Point{geo.NewPoint(1, 2), geo.NewPoint(2, 3)} - for _, pt := range points { - err := s.ns.AddWaypoint(ctx, pt, nil) - test.That(t, err, test.ShouldBeNil) - } - - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - wp1 := wps[0] - // start navigation - set ModeManual first to ensure navigation starts up - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - - // wait for plan history to be called, indicating that MoveOnGlobe is in progress - <-planHistoryCalledCtx.Done() - - err = s.ns.RemoveWaypoint(ctx, wp1.ID, nil) - test.That(t, err, test.ShouldBeNil) - - // Reach the second waypoint - timeoutCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*1000) - defer cancelFn() - for { - if timeoutCtx.Err() != nil { - t.Error("test timed out") - t.FailNow() - } - // once all waypoints have been consumed - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - if len(wps) == 0 { - s.RLock() - // MoveOnGlobe was called twice, once for each waypoint - test.That(t, len(s.mogrs), test.ShouldEqual, 2) - // waypoint 1 - test.That(t, s.mogrs[0].Destination, test.ShouldResemble, points[0]) - // waypoint 2 - test.That(t, s.mogrs[1].Destination, test.ShouldResemble, points[1]) - // Motion reports that the last execution succeeded - s.RUnlock() - - ph, err := s.injectMS.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: s.base.Name()}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph), test.ShouldEqual, 1) - test.That(t, ph[0].Plan.ExecutionID, test.ShouldResemble, executionIDs[1]) - test.That(t, len(ph[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, ph[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateSucceeded) - break - } - } - }) - - t.Run("Calling RemoveWaypoint on a waypoint that is not in progress does not cancel MoveOnGlobe", func(t *testing.T) { - s := setupStartWaypoint(ctx, t, logger) - defer s.closeFunc() - - // we expect 2 executions to be generated - executionIDs := []uuid.UUID{ - uuid.New(), - uuid.New(), - } - counter := atomic.NewInt32(-1) - var wg sync.WaitGroup - defer wg.Wait() - pauseMOGSuccess := make(chan struct{}) - resumeMOGSuccess := make(chan struct{}) - // MoveOnGlobe will behave as if it created a new plan & queue up a goroutine which will then behave as if the plan succeeded - s.injectMS.MoveOnGlobeFunc = func(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if err := ctx.Err(); err != nil { - return uuid.Nil, err - } - executionID := executionIDs[(counter.Inc())] - s.Lock() - defer s.Unlock() - if s.mogrs == nil { - s.mogrs = []motion.MoveOnGlobeReq{} - } - s.mogrs = append(s.mogrs, req) - s.pws = []motion.PlanWithStatus{ - { - Plan: motion.PlanWithMetadata{ - ExecutionID: executionID, - }, - StatusHistory: []motion.PlanStatus{ - {State: motion.PlanStateInProgress}, - }, - }, - } - wg.Add(1) - utils.ManagedGo(func() { - pauseMOGSuccess <- struct{}{} - <-resumeMOGSuccess - s.Lock() - defer s.Unlock() - for i, p := range s.pws { - if p.Plan.ExecutionID == executionID { - succeeded := []motion.PlanStatus{{State: motion.PlanStateSucceeded}} - p.StatusHistory = append(succeeded, p.StatusHistory...) - s.pws[i] = p - return - } - } - t.Error("MoveOnGlobe called unexpectedly") - }, wg.Done) - return executionID, nil - } - - s.injectMS.PlanHistoryFunc = func(ctx context.Context, req motion.PlanHistoryReq) ([]motion.PlanWithStatus, error) { - if err := ctx.Err(); err != nil { - return nil, err - } - s.RLock() - defer s.RUnlock() - history := make([]motion.PlanWithStatus, len(s.pws)) - copy(history, s.pws) - return history, nil - } - - s.injectMS.StopPlanFunc = func(ctx context.Context, req motion.StopPlanReq) error { - if err := ctx.Err(); err != nil { - return err - } - s.Lock() - defer s.Unlock() - if s.sprs == nil { - s.sprs = []motion.StopPlanReq{} - } - s.sprs = append(s.sprs, req) - return nil - } - - // Set manual mode to ensure waypoint loop from prior test exits - points := []*geo.Point{geo.NewPoint(1, 2), geo.NewPoint(2, 3)} - for _, pt := range points { - err := s.ns.AddWaypoint(ctx, pt, nil) - test.That(t, err, test.ShouldBeNil) - } - - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - wp2 := wps[1] - // start navigation - set ModeManual first to ensure navigation starts up - err = s.ns.SetMode(ctx, navigation.ModeWaypoint, nil) - test.That(t, err, test.ShouldBeNil) - <-pauseMOGSuccess - - err = s.ns.RemoveWaypoint(ctx, wp2.ID, nil) - test.That(t, err, test.ShouldBeNil) - resumeMOGSuccess <- struct{}{} - - // Reach the second waypoint - timeoutCtx, cancelFn := context.WithTimeout(ctx, time.Millisecond*1000) - defer cancelFn() - for { - if timeoutCtx.Err() != nil { - t.Error("test timed out") - t.FailNow() - } - // once all waypoints have been consumed - wps, err := s.ns.Waypoints(ctx, nil) - test.That(t, err, test.ShouldBeNil) - if len(wps) == 0 { - s.RLock() - // MoveOnGlobe was called once with waypoint 1 - test.That(t, len(s.mogrs), test.ShouldEqual, 1) - // waypoint 1 - test.That(t, s.mogrs[0].Destination, test.ShouldResemble, points[0]) - // Motion reports that the last execution succeeded - s.RUnlock() - - ph, err := s.injectMS.PlanHistory(ctx, motion.PlanHistoryReq{ComponentName: s.base.Name()}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(ph), test.ShouldEqual, 1) - test.That(t, ph[0].Plan.ExecutionID, test.ShouldResemble, executionIDs[0]) - test.That(t, len(ph[0].StatusHistory), test.ShouldEqual, 2) - test.That(t, ph[0].StatusHistory[0].State, test.ShouldEqual, motion.PlanStateSucceeded) - break - } - } - }) -} - -func TestValidateGeometry(t *testing.T) { - cfg := Config{ - BaseName: "base", - MapType: "GPS", - MovementSensorName: "localizer", - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - { - VisionServiceName: "vision", - CameraName: "camera", - }, - }, - } - - createBox := func(translation r3.Vector) Config { - boxPose := spatialmath.NewPoseFromPoint(translation) - geometries, err := spatialmath.NewBox(boxPose, r3.Vector{X: 10, Y: 10, Z: 10}, "") - test.That(t, err, test.ShouldBeNil) - - geoObstacle := spatialmath.NewGeoObstacle(geo.NewPoint(0, 0), []spatialmath.Geometry{geometries}) - geoObstacleCfg, err := spatialmath.NewGeoObstacleConfig(geoObstacle) - test.That(t, err, test.ShouldBeNil) - - cfg.Obstacles = []*spatialmath.GeoObstacleConfig{geoObstacleCfg} - - return cfg - } - - t.Run("fail case", func(t *testing.T) { - cfg = createBox(r3.Vector{X: 10, Y: 10, Z: 10}) - _, err := cfg.Validate("") - expectedErr := "geometries specified through the navigation are not allowed to have a translation" - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldEqual, expectedErr) - }) - - t.Run("success case", func(t *testing.T) { - cfg = createBox(r3.Vector{}) - _, err := cfg.Validate("") - test.That(t, err, test.ShouldBeNil) - }) -} - -func TestGetObstacles(t *testing.T) { - ctx := context.Background() - logger := logging.NewTestLogger(t) - - // create injected/fake components and services - fakeBase, err := baseFake.NewBase( - ctx, - nil, - resource.Config{ - Name: "test_base", - API: base.API, - Frame: &referenceframe.LinkConfig{Geometry: &spatialmath.GeometryConfig{R: 100}}, - }, - logger, - ) - test.That(t, err, test.ShouldBeNil) - - injectMS := inject.NewMotionService("test_motion") - injectedVis := inject.NewVisionService("test_vision") - injectMovementSensor := inject.NewMovementSensor("test_movement") - injectedCam := inject.NewCamera("test_camera") - - // set the dependencies for the navigation service - deps := resource.Dependencies{ - fakeBase.Name(): fakeBase, - injectMovementSensor.Name(): injectMovementSensor, - injectedCam.Name(): injectedCam, - } - - // create static geo obstacle - sphereGeom, err := spatialmath.NewSphere(spatialmath.NewZeroPose(), 1.0, "test-sphere") - test.That(t, err, test.ShouldBeNil) - sphereGob := spatialmath.NewGeoObstacle(geo.NewPoint(1, 1), []spatialmath.Geometry{sphereGeom}) - gobCfg, err := spatialmath.NewGeoObstacleConfig(sphereGob) - test.That(t, err, test.ShouldBeNil) - - // construct the navigation service - ns, err := NewBuiltIn( - ctx, - resource.Dependencies{ - injectMS.Name(): injectMS, - fakeBase.Name(): fakeBase, - injectMovementSensor.Name(): injectMovementSensor, - injectedVis.Name(): injectedVis, - injectedCam.Name(): injectedCam, - }, - resource.Config{ - ConvertedAttributes: &Config{ - Store: navigation.StoreConfig{ - Type: navigation.StoreTypeMemory, - }, - BaseName: "test_base", - MovementSensorName: "test_movement", - MotionServiceName: "test_motion", - DegPerSec: 1, - MetersPerSec: 1, - MapType: "", - Obstacles: []*spatialmath.GeoObstacleConfig{gobCfg}, - ObstacleDetectors: []*ObstacleDetectorNameConfig{ - {VisionServiceName: injectedVis.Name().Name, CameraName: injectedCam.Name().Name}, - }, - }, - }, - logger, - ) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, ns.Close(context.Background()), test.ShouldBeNil) - }() - - // create links for framesystem - baseLink := createBaseLink(t) - movementSensorLink := referenceframe.NewLinkInFrame( - baseLink.Name(), - spatialmath.NewPose(r3.Vector{-5, 7, 0}, &spatialmath.OrientationVectorDegrees{OZ: 1, Theta: 90}), - "test_movement", - nil, - ) - cameraGeom, err := spatialmath.NewBox( - spatialmath.NewZeroPose(), - r3.Vector{1, 1, 1}, "camera", - ) - test.That(t, err, test.ShouldBeNil) - cameraLink := referenceframe.NewLinkInFrame( - baseLink.Name(), - spatialmath.NewPose(r3.Vector{6, -3, 0}, &spatialmath.OrientationVectorDegrees{OX: 1, Theta: -90}), - "test_camera", - cameraGeom, - ) - - // construct the framesystem - fsParts := []*referenceframe.FrameSystemPart{ - {FrameConfig: movementSensorLink}, - {FrameConfig: baseLink}, - {FrameConfig: cameraLink}, - } - fsSvc, err := createFrameSystemService(ctx, deps, fsParts, logger) - test.That(t, err, test.ShouldBeNil) - - // set the framesystem service for the navigation service - ns.(*builtIn).fsService = fsSvc - - // set injectMovementSensor functions - injectMovementSensor.PositionFunc = func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - return geo.NewPoint(1, 1), 0, nil - } - injectMovementSensor.PropertiesFunc = func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - return &movementsensor.Properties{ - CompassHeadingSupported: true, - }, nil - } - injectMovementSensor.CompassHeadingFunc = func(ctx context.Context, extra map[string]interface{}) (float64, error) { - // this is a left-handed value - return 315, nil - } - - // set injectedVis functions - injectedVis.GetObjectPointCloudsFunc = func(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) { - boxGeom, err := spatialmath.NewBox( - spatialmath.NewPose(r3.Vector{-10, 0, 11}, &spatialmath.OrientationVectorDegrees{OZ: -1, OX: 1}), - r3.Vector{5, 5, 1}, - "test-box", - ) - test.That(t, err, test.ShouldBeNil) - - detection, err := viz.NewObjectWithLabel(pointcloud.New(), "test-box", boxGeom.ToProtobuf()) - test.That(t, err, test.ShouldBeNil) - return []*viz.Object{detection}, nil - } - - manipulatedBoxGeom, err := spatialmath.NewBox( - spatialmath.NewPose( - r3.Vector{0, 0, 0}, - &spatialmath.OrientationVectorDegrees{OZ: -1, OX: 1}, - ), - r3.Vector{5, 5, 1}, - "transient_0_test_camera_test-box", - ) - test.That(t, err, test.ShouldBeNil) - - dets, err := ns.Obstacles(ctx, nil) - - test.That(t, err, test.ShouldBeNil) - test.That(t, len(dets), test.ShouldEqual, 2) - test.That(t, dets[0], test.ShouldResemble, sphereGob) - test.That(t, dets[1].Location(), test.ShouldResemble, geo.NewPoint(0.9999998600983906, 1.0000001399229705)) - test.That(t, len(dets[1].Geometries()), test.ShouldEqual, 1) - test.That(t, spatialmath.GeometriesAlmostEqual(dets[1].Geometries()[0], manipulatedBoxGeom), test.ShouldBeTrue) - test.That(t, dets[1].Geometries()[0].Label(), test.ShouldEqual, manipulatedBoxGeom.Label()) -} - -func TestProperties(t *testing.T) { - ctx := context.Background() - - t.Run("no map case", func(t *testing.T) { - svc := builtIn{ - mapType: navigation.NoMap, - } - - prop, err := svc.Properties(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.MapType, test.ShouldEqual, svc.mapType) - }) - - t.Run("gps map case", func(t *testing.T) { - svc := builtIn{ - mapType: navigation.GPSMap, - } - - prop, err := svc.Properties(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.MapType, test.ShouldEqual, svc.mapType) - }) -} - -func createBaseLink(t *testing.T) *referenceframe.LinkInFrame { - baseBox, err := spatialmath.NewBox(spatialmath.NewZeroPose(), r3.Vector{20, 20, 20}, "base-box") - test.That(t, err, test.ShouldBeNil) - baseLink := referenceframe.NewLinkInFrame( - referenceframe.World, - spatialmath.NewZeroPose(), - "test_base", - baseBox, - ) - return baseLink -} - -func createFrameSystemService( - ctx context.Context, - deps resource.Dependencies, - fsParts []*referenceframe.FrameSystemPart, - logger logging.Logger, -) (framesystem.Service, error) { - fsSvc, err := framesystem.New(ctx, deps, logger) - if err != nil { - return nil, err - } - conf := resource.Config{ - ConvertedAttributes: &framesystem.Config{Parts: fsParts}, - } - if err := fsSvc.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - deps[fsSvc.Name()] = fsSvc - - return fsSvc, nil -} diff --git a/services/navigation/builtin/explore.go b/services/navigation/builtin/explore.go deleted file mode 100644 index ce8cd208489..00000000000 --- a/services/navigation/builtin/explore.go +++ /dev/null @@ -1,46 +0,0 @@ -package builtin - -import ( - "context" - "math" - "math/rand" - - "github.com/golang/geo/r3" - "go.viam.com/utils" - - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/spatialmath" -) - -const defaultDistanceMM = 50 * 1000 - -func (svc *builtIn) startExploreMode(ctx context.Context) { - svc.logger.CDebug(ctx, "startExploreMode called") - - svc.activeBackgroundWorkers.Add(1) - - utils.ManagedGo(func() { - // Send motionCfg parameters through extra until motionCfg can be added to Move() - extra := map[string]interface{}{"motionCfg": *svc.motionCfg} - - for { - if ctx.Err() != nil { - return - } - - // Choose a new random point using a normal distribution centered on the position directly the robot - randAngle := rand.NormFloat64() + math.Pi - destination := referenceframe.NewPoseInFrame(svc.base.Name().Name, spatialmath.NewPose( - r3.Vector{ - X: math.Sin(randAngle), - Y: math.Cos(randAngle), - Z: 0., - }.Normalize().Mul(defaultDistanceMM), spatialmath.NewOrientationVector())) - - _, err := svc.exploreMotionService.Move(ctx, svc.base.Name(), destination, nil, nil, extra) - if err != nil { - svc.logger.CDebugf(ctx, "error occurred when moving to point %v: %v", destination, err) - } - } - }, svc.activeBackgroundWorkers.Done) -} diff --git a/services/navigation/builtin/explore_test.go b/services/navigation/builtin/explore_test.go deleted file mode 100644 index 8b179845acc..00000000000 --- a/services/navigation/builtin/explore_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package builtin - -import ( - "context" - "testing" - "time" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - v1 "go.viam.com/api/service/motion/v1" - "go.viam.com/test" - - frame "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/testutils/inject" -) - -func TestExploreMode(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - ns, teardown := setupNavigationServiceFromConfig(t, "../data/nav_no_map_cfg.json") - - var points []r3.Vector - mockExploreMotionService := &inject.MotionService{} - mockExploreMotionService.MoveFunc = func(ctx context.Context, componentName resource.Name, - destination *frame.PoseInFrame, worldState *frame.WorldState, constraints *v1.Constraints, - extra map[string]interface{}, - ) (bool, error) { - points = append(points, destination.Pose().Point()) - return false, errors.New("expected error") - } - - nsStruct := ns.(*builtIn) - nsStruct.exploreMotionService = mockExploreMotionService - - ctxTimeout, cancelFunc := context.WithTimeout(cancelCtx, 50*time.Millisecond) - defer cancelFunc() - nsStruct.startExploreMode(ctxTimeout) - <-ctxTimeout.Done() - teardown() - test.That(t, len(points), test.ShouldBeGreaterThan, 2) -} diff --git a/services/navigation/client.go b/services/navigation/client.go deleted file mode 100644 index 29a71ee91b2..00000000000 --- a/services/navigation/client.go +++ /dev/null @@ -1,225 +0,0 @@ -package navigation - -import ( - "context" - - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson/primitive" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/navigation/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// client implements NavigationServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.NavigationServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Service, error) { - grpcClient := pb.NewNavigationServiceClient(conn) - c := &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: grpcClient, - logger: logger, - } - return c, nil -} - -func (c *client) Mode(ctx context.Context, extra map[string]interface{}) (Mode, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return 0, err - } - resp, err := c.client.GetMode(ctx, &pb.GetModeRequest{Name: c.name, Extra: ext}) - if err != nil { - return 0, err - } - pbMode := resp.GetMode() - switch pbMode { - case pb.Mode_MODE_MANUAL: - return ModeManual, nil - case pb.Mode_MODE_WAYPOINT: - return ModeWaypoint, nil - case pb.Mode_MODE_EXPLORE: - return ModeExplore, nil - case pb.Mode_MODE_UNSPECIFIED: - fallthrough - default: - return 0, errors.New("mode error") - } -} - -func (c *client) SetMode(ctx context.Context, mode Mode, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - var pbMode pb.Mode - switch mode { - case ModeManual: - pbMode = pb.Mode_MODE_MANUAL - case ModeWaypoint: - pbMode = pb.Mode_MODE_WAYPOINT - case ModeExplore: - pbMode = pb.Mode_MODE_EXPLORE - default: - pbMode = pb.Mode_MODE_UNSPECIFIED - } - _, err = c.client.SetMode(ctx, &pb.SetModeRequest{Name: c.name, Mode: pbMode, Extra: ext}) - if err != nil { - return err - } - return nil -} - -func (c *client) Location(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetLocation(ctx, &pb.GetLocationRequest{Name: c.name, Extra: ext}) - if err != nil { - return nil, err - } - geoPose := spatialmath.NewGeoPose( - geo.NewPoint(resp.GetLocation().GetLatitude(), resp.GetLocation().GetLongitude()), - resp.GetCompassHeading(), - ) - return geoPose, nil -} - -func (c *client) Waypoints(ctx context.Context, extra map[string]interface{}) ([]Waypoint, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetWaypoints(ctx, &pb.GetWaypointsRequest{Name: c.name, Extra: ext}) - if err != nil { - return nil, err - } - waypoints := resp.GetWaypoints() - result := make([]Waypoint, 0, len(waypoints)) - for _, wpt := range waypoints { - id, err := primitive.ObjectIDFromHex(wpt.GetId()) - if err != nil { - return nil, err - } - loc := wpt.GetLocation() - result = append(result, Waypoint{ - ID: id, - Lat: loc.GetLatitude(), - Long: loc.GetLongitude(), - }) - } - return result, nil -} - -func (c *client) AddWaypoint(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - loc := &commonpb.GeoPoint{ - Latitude: point.Lat(), - Longitude: point.Lng(), - } - req := &pb.AddWaypointRequest{ - Name: c.name, - Location: loc, - Extra: ext, - } - _, err = c.client.AddWaypoint(ctx, req) - if err != nil { - return err - } - return nil -} - -func (c *client) RemoveWaypoint(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return err - } - req := &pb.RemoveWaypointRequest{Name: c.name, Id: id.Hex(), Extra: ext} - _, err = c.client.RemoveWaypoint(ctx, req) - if err != nil { - return err - } - return nil -} - -func (c *client) Obstacles(ctx context.Context, extra map[string]interface{}) ([]*spatialmath.GeoObstacle, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - req := &pb.GetObstaclesRequest{Name: c.name, Extra: ext} - resp, err := c.client.GetObstacles(ctx, req) - if err != nil { - return nil, err - } - protoObs := resp.GetObstacles() - geos := []*spatialmath.GeoObstacle{} - for _, o := range protoObs { - obstacle, err := spatialmath.GeoObstacleFromProtobuf(o) - if err != nil { - return nil, err - } - geos = append(geos, obstacle) - } - return geos, nil -} - -func (c *client) Paths(ctx context.Context, extra map[string]interface{}) ([]*Path, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - req := &pb.GetPathsRequest{Name: c.name, Extra: ext} - resp, err := c.client.GetPaths(ctx, req) - if err != nil { - return nil, err - } - return ProtoSliceToPaths(resp.GetPaths()) -} - -func (c *client) Properties(ctx context.Context) (Properties, error) { - resp, err := c.client.GetProperties(ctx, &pb.GetPropertiesRequest{Name: c.name}) - if err != nil { - return Properties{}, errors.Wrapf(err, "failure to get properties") - } - - mapType, err := protobufToMapType(resp.MapType) - if err != nil { - return Properties{}, err - } - - prop := Properties{ - MapType: mapType, - } - return prop, nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/services/navigation/client_test.go b/services/navigation/client_test.go deleted file mode 100644 index bb311d87126..00000000000 --- a/services/navigation/client_test.go +++ /dev/null @@ -1,330 +0,0 @@ -package navigation_test - -import ( - "context" - "math" - "net" - "testing" - - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson/primitive" - servicepb "go.viam.com/api/service/navigation/v1" - "go.viam.com/test" - "go.viam.com/utils/rpc" - "google.golang.org/grpc" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/navigation" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var ( - testSvcName1 = navigation.Named("nav1") - testSvcName2 = navigation.Named("nav2") -) - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - listener2, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - workingServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - failingServer := grpc.NewServer() - - var extraOptions map[string]interface{} - workingNavigationService := &inject.NavigationService{} - failingNavigationService := &inject.NavigationService{} - - modeTested := false - workingNavigationService.ModeFunc = func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - extraOptions = extra - if !modeTested { - modeTested = true - return navigation.ModeManual, nil - } - return navigation.ModeWaypoint, nil - } - var receivedMode navigation.Mode - workingNavigationService.SetModeFunc = func(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - extraOptions = extra - receivedMode = mode - return nil - } - expectedLoc := geo.NewPoint(80, 1) - expectedCompassHeading := 90. - expectedGeoPose := spatialmath.NewGeoPose(expectedLoc, expectedCompassHeading) - workingNavigationService.LocationFunc = func(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - extraOptions = extra - - return expectedGeoPose, nil - } - waypoints := []navigation.Waypoint{ - { - ID: primitive.NewObjectID(), - Order: 0, - Lat: 40, - Long: 20, - }, - } - workingNavigationService.WaypointsFunc = func(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) { - extraOptions = extra - return waypoints, nil - } - var receivedPoint *geo.Point - workingNavigationService.AddWaypointFunc = func(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - extraOptions = extra - receivedPoint = point - return nil - } - var receivedID primitive.ObjectID - workingNavigationService.RemoveWaypointFunc = func(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - extraOptions = extra - receivedID = id - return nil - } - - var receivedPaths []*navigation.Path - workingNavigationService.PathsFunc = func(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) { - path, err := navigation.NewPath(primitive.NewObjectID(), []*geo.Point{geo.NewPoint(0, 0)}) - test.That(t, err, test.ShouldBeNil) - receivedPaths = []*navigation.Path{path} - return receivedPaths, nil - } - - receivedProp := navigation.Properties{MapType: navigation.NoMap} - workingNavigationService.PropertiesFunc = func(ctx context.Context) (navigation.Properties, error) { - return receivedProp, nil - } - - failingNavigationService.ModeFunc = func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - return navigation.ModeManual, errors.New("failure to retrieve mode") - } - var receivedFailingMode navigation.Mode - failingNavigationService.SetModeFunc = func(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - receivedFailingMode = mode - return errors.New("failure to set mode") - } - failingNavigationService.LocationFunc = func(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - return nil, errors.New("failure to retrieve location") - } - failingNavigationService.WaypointsFunc = func(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) { - return nil, errors.New("failure to retrieve waypoints") - } - var receivedFailingPoint *geo.Point - failingNavigationService.AddWaypointFunc = func(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - receivedFailingPoint = point - return errors.New("failure to add waypoint") - } - var receivedFailingID primitive.ObjectID - failingNavigationService.RemoveWaypointFunc = func(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - receivedFailingID = id - return errors.New("failure to remove waypoint") - } - failingNavigationService.PathsFunc = func(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) { - return nil, errors.New("unimplemented") - } - - failingNavigationService.PropertiesFunc = func(ctx context.Context) (navigation.Properties, error) { - return navigation.Properties{}, errors.New("unimplemented") - } - - workingSvc, err := resource.NewAPIResourceCollection(navigation.API, map[resource.Name]navigation.Service{ - testSvcName1: workingNavigationService, - }) - test.That(t, err, test.ShouldBeNil) - failingSvc, err := resource.NewAPIResourceCollection(navigation.API, map[resource.Name]navigation.Service{ - testSvcName1: failingNavigationService, - }) - test.That(t, err, test.ShouldBeNil) - - resourceAPI, ok, err := resource.LookupAPIRegistration[navigation.Service](navigation.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - resourceAPI.RegisterRPCService(context.Background(), workingServer, workingSvc) - failingServer.RegisterService(&servicepb.NavigationService_ServiceDesc, navigation.NewRPCServiceServer(failingSvc)) - - go workingServer.Serve(listener1) - defer workingServer.Stop() - - t.Run("context canceled", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingNavClient, err := navigation.NewClientFromConn(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - t.Run("client tests for working navigation service", func(t *testing.T) { - // test mode - extra := map[string]interface{}{"foo": "Mode"} - mode, err := workingNavClient.Mode(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, mode, test.ShouldEqual, navigation.ModeManual) - test.That(t, extraOptions, test.ShouldResemble, extra) - mode, err = workingNavClient.Mode(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, mode, test.ShouldEqual, navigation.ModeWaypoint) - test.That(t, extraOptions, test.ShouldResemble, map[string]interface{}{}) - - // test set mode - extra = map[string]interface{}{"foo": "SetMode"} - err = workingNavClient.SetMode(context.Background(), navigation.ModeManual, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, receivedMode, test.ShouldEqual, navigation.ModeManual) - test.That(t, extraOptions, test.ShouldResemble, extra) - - err = workingNavClient.SetMode(context.Background(), navigation.ModeWaypoint, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, receivedMode, test.ShouldEqual, navigation.ModeWaypoint) - test.That(t, extraOptions, test.ShouldResemble, extra) - - err = workingNavClient.SetMode(context.Background(), navigation.ModeExplore, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, receivedMode, test.ShouldEqual, navigation.ModeExplore) - test.That(t, extraOptions, test.ShouldResemble, extra) - - err = workingNavClient.SetMode(context.Background(), 99, extra) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "MODE_UNSPECIFIED") - - // test add waypoint - point := geo.NewPoint(90, 1) - extra = map[string]interface{}{"foo": "AddWaypoint"} - err = workingNavClient.AddWaypoint(context.Background(), point, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, receivedPoint, test.ShouldResemble, point) - test.That(t, extraOptions, test.ShouldResemble, extra) - - // test Paths - ctx := context.Background() - paths, err := workingNavClient.Paths(ctx, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, paths, test.ShouldResemble, receivedPaths) - - // test Properties - prop, err := workingNavClient.Properties(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.MapType, test.ShouldEqual, receivedProp.MapType) - - // test do command - workingNavigationService.DoCommandFunc = testutils.EchoFunc - resp, err := workingNavClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client tests for working navigation service", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingDialedClient, err := navigation.NewClientFromConn(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - // test location - extra := map[string]interface{}{"foo": "Location"} - geoPose, err := workingDialedClient.Location(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, geoPose, test.ShouldResemble, expectedGeoPose) - test.That(t, extraOptions, test.ShouldResemble, extra) - - // test remove waypoint - wptID := primitive.NewObjectID() - extra = map[string]interface{}{"foo": "RemoveWaypoint"} - err = workingDialedClient.RemoveWaypoint(context.Background(), wptID, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, wptID, test.ShouldEqual, receivedID) - test.That(t, extraOptions, test.ShouldResemble, extra) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client test 2 for working navigation service", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - dialedClient, err := resourceAPI.RPCClient(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - // test waypoints - extra := map[string]interface{}{"foo": "Waypoints"} - receivedWpts, err := dialedClient.Waypoints(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, receivedWpts, test.ShouldResemble, waypoints) - test.That(t, extraOptions, test.ShouldResemble, extra) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - go failingServer.Serve(listener2) - defer failingServer.Stop() - - t.Run("client tests for failing navigation service", func(t *testing.T) { - conn, err = viamgrpc.Dial(context.Background(), listener2.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - failingNavClient, err := navigation.NewClientFromConn(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - // test mode - _, err = failingNavClient.Mode(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - - // test set mode - err = failingNavClient.SetMode(context.Background(), navigation.ModeWaypoint, map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, receivedFailingMode, test.ShouldEqual, navigation.ModeWaypoint) - err = failingNavClient.SetMode(context.Background(), navigation.Mode(math.MaxUint8), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - - // test add waypoint - point := geo.NewPoint(90, 1) - err = failingNavClient.AddWaypoint(context.Background(), point, map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, receivedFailingPoint, test.ShouldResemble, point) - - // test Paths - paths, err := failingNavClient.Paths(context.Background(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, paths, test.ShouldBeNil) - - // test Properties - prop, err := failingNavClient.Properties(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, prop, test.ShouldResemble, navigation.Properties{}) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("dialed client test for failing navigation service", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener2.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - dialedClient, err := resourceAPI.RPCClient(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - // test waypoints - _, err = dialedClient.Waypoints(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - - // test location - geoPose, err := dialedClient.Location(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, geoPose, test.ShouldBeNil) - - // test remove waypoint - wptID := primitive.NewObjectID() - err = dialedClient.RemoveWaypoint(context.Background(), wptID, map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, wptID, test.ShouldEqual, receivedFailingID) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/navigation/data/nav_cfg.json b/services/navigation/data/nav_cfg.json deleted file mode 100644 index 4c6ce0de08a..00000000000 --- a/services/navigation/data/nav_cfg.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "components": - [{ - "name": "test_camera", - "type": "camera", - "model": "fake" - }, - { - "model": "fake", - "name": "test_base", - "type": "base" - }, - { - "name": "test_movement", - "type": "movement_sensor", - "model": "fake" - }], - "services": - [{ - "name": "blue_square", - "type": "vision", - "model": "color_detector", - "attributes": { - "segment_size_px": 100, - "detect_color": "#1C4599", - "hue_tolerance_pct": 0.07, - "value_cutoff_pct": 0.15 - } - }, - { - "name":"test_navigation", - "type": "navigation", - "attributes": { - "base":"test_base", - "movement_sensor":"test_movement", - "obstacle_detectors": [{ - "vision_service": "blue_square", - "camera": "test_camera" - }], - "obstacles": - [{ - "geometries": - [{ - "label":"aLabel", - "orientation":{ - "type":"ov_degrees", - "value":{ - "X":1, - "Y":0, - "Z":0, - "Th": 90 - } - }, - "x":10, - "y":10, - "z":10 - }], - "location":{ - "latitude": 1, - "longitude": 1 - } - }], - "store":{ - "type":"memory" - } - } - }] -} diff --git a/services/navigation/data/nav_no_map_cfg.json b/services/navigation/data/nav_no_map_cfg.json deleted file mode 100644 index 975e94f343d..00000000000 --- a/services/navigation/data/nav_no_map_cfg.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "components": - [{ - "model": "fake", - "name": "test_base", - "type": "base" - }, - { - "model": "fake", - "name": "test_camera", - "type": "camera" - }], - "services": - [{ - "name": "test_vision", - "type": "vision", - "model": "color_detector", - "attributes": { - "segment_size_px": 100, - "detect_color": "#1C4599", - "hue_tolerance_pct": 0.07, - "value_cutoff_pct": 0.15 - } - }, - { - "name":"test_navigation", - "type": "navigation", - "attributes":{ - "base":"test_base", - "map_type": "None", - "obstacle_detectors": [{ - "vision_service": "test_vision", - "camera": "test_camera" - }] - } - }] -} diff --git a/services/navigation/data/nav_no_map_cfg_minimal.json b/services/navigation/data/nav_no_map_cfg_minimal.json deleted file mode 100644 index f0fea9e423a..00000000000 --- a/services/navigation/data/nav_no_map_cfg_minimal.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "components": - [{ - "model": "fake", - "name": "test_base", - "type": "base" - }], - "services": - [{ - "name":"test_navigation", - "type": "navigation", - "attributes":{ - "base":"test_base", - "map_type": "None" - } - }] -} diff --git a/services/navigation/navigation.go b/services/navigation/navigation.go deleted file mode 100644 index 94be97e5dab..00000000000 --- a/services/navigation/navigation.go +++ /dev/null @@ -1,140 +0,0 @@ -// Package navigation is the service that allows you to navigate along waypoints. -package navigation - -import ( - "context" - - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson/primitive" - servicepb "go.viam.com/api/service/navigation/v1" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/spatialmath" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Service]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: servicepb.RegisterNavigationServiceHandlerFromEndpoint, - RPCServiceDesc: &servicepb.NavigationService_ServiceDesc, - RPCClient: NewClientFromConn, - }) -} - -// Mode describes what mode to operate the service in. -type Mode uint8 - -// MapType describes what map the navigation service is operating on. -type MapType uint8 - -// The set of known modes. -const ( - ModeManual = Mode(iota) - ModeWaypoint - ModeExplore - - NoMap = MapType(iota) - GPSMap -) - -func (m Mode) String() string { - switch m { - case ModeManual: - return "Manual" - case ModeWaypoint: - return "Waypoint" - case ModeExplore: - return "Explore" - default: - return "UNKNOWN" - } -} - -func (m MapType) String() string { - switch m { - case NoMap: - return "None" - case GPSMap: - return "GPS" - default: - return "UNKNOWN" - } -} - -// StringToMapType converts an input string into one of the valid map type if possible. -func StringToMapType(mapTypeName string) (MapType, error) { - switch mapTypeName { - case "None": - return NoMap, nil - case "GPS", "": - return GPSMap, nil - } - return 0, errors.Errorf("invalid map_type '%v' given", mapTypeName) -} - -// Properties returns information regarding the current navigation service. This includes the map type -// being ingested and used by the navigation service. -type Properties struct { - MapType MapType -} - -// A Service controls the navigation for a robot. -type Service interface { - resource.Resource - Mode(ctx context.Context, extra map[string]interface{}) (Mode, error) - SetMode(ctx context.Context, mode Mode, extra map[string]interface{}) error - Location(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) - - // Waypoint - Waypoints(ctx context.Context, extra map[string]interface{}) ([]Waypoint, error) - AddWaypoint(ctx context.Context, point *geo.Point, extra map[string]interface{}) error - RemoveWaypoint(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error - - Obstacles(ctx context.Context, extra map[string]interface{}) ([]*spatialmath.GeoObstacle, error) - - Paths(ctx context.Context, extra map[string]interface{}) ([]*Path, error) - - Properties(ctx context.Context) (Properties, error) -} - -// SubtypeName is the name of the type of service. -const SubtypeName = "navigation" - -// API is a variable that identifies the navigation service resource API. -var API = resource.APINamespaceRDK.WithServiceType(SubtypeName) - -// Named is a helper for getting the named navigation service's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named navigation service from the given Robot. -func FromRobot(r robot.Robot, name string) (Service, error) { - return robot.ResourceFromRobot[Service](r, Named(name)) -} - -func mapTypeToProtobuf(mapType MapType) servicepb.MapType { - switch mapType { - case NoMap: - return servicepb.MapType_MAP_TYPE_NONE - case GPSMap: - return servicepb.MapType_MAP_TYPE_GPS - default: - return servicepb.MapType_MAP_TYPE_UNSPECIFIED - } -} - -func protobufToMapType(mapType servicepb.MapType) (MapType, error) { - switch mapType { - case servicepb.MapType_MAP_TYPE_NONE: - return NoMap, nil - case servicepb.MapType_MAP_TYPE_GPS: - return GPSMap, nil - case servicepb.MapType_MAP_TYPE_UNSPECIFIED: - fallthrough - default: - return 0, errors.New("map type unspecified") - } -} diff --git a/services/navigation/path.go b/services/navigation/path.go deleted file mode 100644 index 59da4c85d9a..00000000000 --- a/services/navigation/path.go +++ /dev/null @@ -1,100 +0,0 @@ -// Package navigation is the service that allows you to navigate along waypoints. -package navigation - -import ( - "errors" - - geo "github.com/kellydunn/golang-geo" - "go.mongodb.org/mongo-driver/bson/primitive" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/navigation/v1" -) - -var errNilPath = errors.New("cannot convert nil path") - -// Path describes a series of geo points the robot will travel through. -type Path struct { - destinationWaypointID primitive.ObjectID - geoPoints []*geo.Point -} - -// NewPath constructs a Path from a slice of geo.Points and ID. -func NewPath(id primitive.ObjectID, geoPoints []*geo.Point) (*Path, error) { - if len(geoPoints) == 0 { - return nil, errors.New("cannot instantiate path with no geoPoints") - } - return &Path{ - destinationWaypointID: id, - geoPoints: geoPoints, - }, nil -} - -// DestinationWaypointID returns the ID of the Path. -func (p *Path) DestinationWaypointID() primitive.ObjectID { - return p.destinationWaypointID -} - -// GeoPoints returns the slice of geo.Points the Path is comprised of. -func (p *Path) GeoPoints() []*geo.Point { - return p.geoPoints -} - -// PathSliceToProto converts a slice of Path into an equivalent Protobuf message. -func PathSliceToProto(paths []*Path) ([]*pb.Path, error) { - var pbPaths []*pb.Path - for _, path := range paths { - pbPath, err := PathToProto(path) - if err != nil { - return nil, err - } - pbPaths = append(pbPaths, pbPath) - } - return pbPaths, nil -} - -// PathToProto converts the Path struct into an equivalent Protobuf message. -func PathToProto(path *Path) (*pb.Path, error) { - if path == nil { - return nil, errNilPath - } - var pbGeoPoints []*commonpb.GeoPoint - for _, pt := range path.geoPoints { - pbGeoPoints = append(pbGeoPoints, &commonpb.GeoPoint{ - Latitude: pt.Lat(), Longitude: pt.Lng(), - }) - } - return &pb.Path{ - DestinationWaypointId: path.destinationWaypointID.Hex(), - Geopoints: pbGeoPoints, - }, nil -} - -// ProtoSliceToPaths converts a slice of Path Protobuf messages into an equivalent struct. -func ProtoSliceToPaths(pbPaths []*pb.Path) ([]*Path, error) { - var paths []*Path - for _, pbPath := range pbPaths { - path, err := ProtoToPath(pbPath) - if err != nil { - return nil, err - } - paths = append(paths, path) - } - return paths, nil -} - -// ProtoToPath converts the Path Protobuf message into an equivalent struct. -func ProtoToPath(path *pb.Path) (*Path, error) { - if path == nil { - return nil, errNilPath - } - geoPoints := []*geo.Point{} - for _, pt := range path.GetGeopoints() { - geoPoints = append(geoPoints, geo.NewPoint(pt.GetLatitude(), pt.GetLongitude())) - } - id, err := primitive.ObjectIDFromHex(path.GetDestinationWaypointId()) - if err != nil { - return nil, err - } - - return NewPath(id, geoPoints) -} diff --git a/services/navigation/path_test.go b/services/navigation/path_test.go deleted file mode 100644 index cee01b4951e..00000000000 --- a/services/navigation/path_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package navigation_test - -import ( - "errors" - "testing" - - geo "github.com/kellydunn/golang-geo" - "go.mongodb.org/mongo-driver/bson/primitive" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/navigation/v1" - "go.viam.com/test" - - "go.viam.com/rdk/services/navigation" -) - -func TestPaths(t *testing.T) { - t.Parallel() - t.Run("convert to and from proto", func(t *testing.T) { - t.Parallel() - id := primitive.NewObjectID() - path, err := navigation.NewPath(id, []*geo.Point{geo.NewPoint(0, 0)}) - test.That(t, err, test.ShouldBeNil) - - // create valid pb.path - pbPath := &pb.Path{ - DestinationWaypointId: id.Hex(), - Geopoints: []*commonpb.GeoPoint{{Latitude: 0, Longitude: 0}}, - } - - // test converting path to pb.path - convertedPath, err := navigation.PathToProto(path) - test.That(t, err, test.ShouldBeNil) - test.That(t, convertedPath, test.ShouldResemble, pbPath) - - // test converting pb.path to path - convertedProtoPath, err := navigation.ProtoToPath(pbPath) - test.That(t, err, test.ShouldBeNil) - test.That(t, convertedProtoPath, test.ShouldResemble, path) - }) - - t.Run("creating invalid path", func(t *testing.T) { - t.Parallel() - shouldBeNil, err := navigation.NewPath(primitive.NewObjectID(), nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, shouldBeNil, test.ShouldBeNil) - }) - - t.Run("converting slice of nil pbPath to slice of path", func(t *testing.T) { - t.Parallel() - nilSlice := []*pb.Path{nil} - _, err := navigation.ProtoSliceToPaths(nilSlice) - test.That(t, err, test.ShouldBeError, errors.New("cannot convert nil path")) - }) - - t.Run("converting slice of pb path with nil geoPoints", func(t *testing.T) { - t.Parallel() - id := primitive.NewObjectID() - malformedPath := []*pb.Path{ - { - DestinationWaypointId: id.Hex(), - Geopoints: nil, - }, - } - _, err := navigation.ProtoSliceToPaths(malformedPath) - test.That(t, err, test.ShouldBeError, errors.New("cannot instantiate path with no geoPoints")) - }) - - t.Run("invalid DestinationWaypointId", func(t *testing.T) { - t.Parallel() - malformedPath := []*pb.Path{ - { - DestinationWaypointId: "malformed", - Geopoints: nil, - }, - } - _, err := navigation.ProtoSliceToPaths(malformedPath) - test.That(t, err, test.ShouldBeError, errors.New("the provided hex string is not a valid ObjectID")) - }) - - t.Run("converting slice of pb path with nil id", func(t *testing.T) { - t.Parallel() - malformedPath := []*pb.Path{ - { - DestinationWaypointId: "", - Geopoints: []*commonpb.GeoPoint{{Latitude: 0, Longitude: 0}}, - }, - } - _, err := navigation.ProtoSliceToPaths(malformedPath) - test.That(t, err, test.ShouldBeError, errors.New("the provided hex string is not a valid ObjectID")) - }) -} diff --git a/services/navigation/register/register.go b/services/navigation/register/register.go deleted file mode 100644 index 7f649c38756..00000000000 --- a/services/navigation/register/register.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package register registers all relevant navigation models and API specific functions. -package register - -import ( - // for navigation models. - _ "go.viam.com/rdk/services/navigation/builtin" -) diff --git a/services/navigation/server.go b/services/navigation/server.go deleted file mode 100644 index a4f075b5c37..00000000000 --- a/services/navigation/server.go +++ /dev/null @@ -1,199 +0,0 @@ -package navigation - -import ( - "context" - - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson/primitive" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/navigation/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// serviceServer implements the contract from navigation.proto. -type serviceServer struct { - pb.UnimplementedNavigationServiceServer - coll resource.APIResourceCollection[Service] -} - -// NewRPCServiceServer constructs a navigation gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Service]) interface{} { - return &serviceServer{coll: coll} -} - -func (server *serviceServer) GetMode(ctx context.Context, req *pb.GetModeRequest) (*pb.GetModeResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - mode, err := svc.Mode(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoMode := pb.Mode_MODE_UNSPECIFIED - switch mode { - case ModeManual: - protoMode = pb.Mode_MODE_MANUAL - case ModeWaypoint: - protoMode = pb.Mode_MODE_WAYPOINT - case ModeExplore: - protoMode = pb.Mode_MODE_EXPLORE - } - return &pb.GetModeResponse{ - Mode: protoMode, - }, nil -} - -func (server *serviceServer) SetMode(ctx context.Context, req *pb.SetModeRequest) (*pb.SetModeResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - switch req.Mode { - case pb.Mode_MODE_MANUAL: - if err := svc.SetMode(ctx, ModeManual, req.Extra.AsMap()); err != nil { - return nil, err - } - case pb.Mode_MODE_WAYPOINT: - if err := svc.SetMode(ctx, ModeWaypoint, req.Extra.AsMap()); err != nil { - return nil, err - } - case pb.Mode_MODE_EXPLORE: - if err := svc.SetMode(ctx, ModeExplore, req.Extra.AsMap()); err != nil { - return nil, err - } - case pb.Mode_MODE_UNSPECIFIED: - fallthrough - default: - return nil, errors.Errorf("unknown mode %q", req.Mode.String()) - } - return &pb.SetModeResponse{}, nil -} - -func (server *serviceServer) GetLocation(ctx context.Context, req *pb.GetLocationRequest) (*pb.GetLocationResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - geoPose, err := svc.Location(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - - return &pb.GetLocationResponse{ - Location: &commonpb.GeoPoint{Latitude: geoPose.Location().Lat(), Longitude: geoPose.Location().Lng()}, - CompassHeading: geoPose.Heading(), - }, nil -} - -func (server *serviceServer) GetWaypoints(ctx context.Context, req *pb.GetWaypointsRequest) (*pb.GetWaypointsResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - waypoints, err := svc.Waypoints(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoWaypoints := make([]*pb.Waypoint, 0, len(waypoints)) - for _, wp := range waypoints { - protoWaypoints = append(protoWaypoints, &pb.Waypoint{ - Id: wp.ID.Hex(), - Location: &commonpb.GeoPoint{Latitude: wp.Lat, Longitude: wp.Long}, - }) - } - return &pb.GetWaypointsResponse{ - Waypoints: protoWaypoints, - }, nil -} - -func (server *serviceServer) AddWaypoint(ctx context.Context, req *pb.AddWaypointRequest) (*pb.AddWaypointResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - point := geo.NewPoint(req.Location.Latitude, req.Location.Longitude) - if err = svc.AddWaypoint(ctx, point, req.Extra.AsMap()); err != nil { - return nil, err - } - return &pb.AddWaypointResponse{}, nil -} - -func (server *serviceServer) RemoveWaypoint(ctx context.Context, req *pb.RemoveWaypointRequest) (*pb.RemoveWaypointResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - id, err := primitive.ObjectIDFromHex(req.Id) - if err != nil { - return nil, err - } - if err = svc.RemoveWaypoint(ctx, id, req.Extra.AsMap()); err != nil { - return nil, err - } - return &pb.RemoveWaypointResponse{}, nil -} - -func (server *serviceServer) GetObstacles(ctx context.Context, req *pb.GetObstaclesRequest) (*pb.GetObstaclesResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - obstacles, err := svc.Obstacles(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoObs := []*commonpb.GeoObstacle{} - for _, obstacle := range obstacles { - protoObs = append(protoObs, spatialmath.GeoObstacleToProtobuf(obstacle)) - } - return &pb.GetObstaclesResponse{Obstacles: protoObs}, nil -} - -func (server *serviceServer) GetPaths(ctx context.Context, req *pb.GetPathsRequest) (*pb.GetPathsResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - paths, err := svc.Paths(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - pbPaths, err := PathSliceToProto(paths) - if err != nil { - return nil, err - } - return &pb.GetPathsResponse{Paths: pbPaths}, nil -} - -func (server *serviceServer) GetProperties(ctx context.Context, req *pb.GetPropertiesRequest) (*pb.GetPropertiesResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - prop, err := svc.Properties(ctx) - if err != nil { - return nil, err - } - - return &pb.GetPropertiesResponse{ - MapType: mapTypeToProtobuf(prop.MapType), - }, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand( - ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, svc, req) -} diff --git a/services/navigation/server_test.go b/services/navigation/server_test.go deleted file mode 100644 index d359a323e56..00000000000 --- a/services/navigation/server_test.go +++ /dev/null @@ -1,456 +0,0 @@ -package navigation_test - -import ( - "context" - "errors" - "math" - "testing" - - geo "github.com/kellydunn/golang-geo" - "go.mongodb.org/mongo-driver/bson/primitive" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/navigation/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/navigation" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func createWaypoints() ([]navigation.Waypoint, []*pb.Waypoint) { - waypoints := []navigation.Waypoint{ - { - ID: primitive.NewObjectID(), - Visited: true, - Order: 0, - Lat: 40, - Long: 20, - }, - { - ID: primitive.NewObjectID(), - Visited: true, - Order: 1, - Lat: 50, - Long: 30, - }, - { - ID: primitive.NewObjectID(), - Visited: false, - Order: 2, - Lat: 60, - Long: 40, - }, - } - protoWaypoints := make([]*pb.Waypoint, 0, len(waypoints)) - for _, wp := range waypoints { - protoWaypoints = append(protoWaypoints, &pb.Waypoint{ - Id: wp.ID.Hex(), - Location: &commonpb.GeoPoint{ - Latitude: wp.Lat, - Longitude: wp.Long, - }, - }) - } - return waypoints, protoWaypoints -} - -func TestServer(t *testing.T) { - injectSvc := &inject.NavigationService{} - resourceMap := map[resource.Name]navigation.Service{ - testSvcName1: injectSvc, - testSvcName2: injectSvc, - } - injectAPISvc, err := resource.NewAPIResourceCollection(navigation.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - navServer := navigation.NewRPCServiceServer(injectAPISvc).(pb.NavigationServiceServer) - - var extraOptions map[string]interface{} - t.Run("working mode function", func(t *testing.T) { - // manual mode - injectSvc.ModeFunc = func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - extraOptions = extra - return navigation.ModeManual, nil - } - - extra := map[string]interface{}{"foo": "Mode"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := &pb.GetModeRequest{Name: testSvcName1.ShortName(), Extra: ext} - resp, err := navServer.GetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Mode, test.ShouldEqual, pb.Mode_MODE_MANUAL) - test.That(t, extraOptions, test.ShouldResemble, extra) - - // waypoint mode - injectSvc.ModeFunc = func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - return navigation.ModeWaypoint, nil - } - req = &pb.GetModeRequest{Name: testSvcName1.ShortName()} - resp, err = navServer.GetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Mode, test.ShouldEqual, pb.Mode_MODE_WAYPOINT) - - // return unspecified mode when returned mode unrecognized - injectSvc.ModeFunc = func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - return navigation.Mode(math.MaxUint8), nil - } - req = &pb.GetModeRequest{Name: testSvcName1.ShortName()} - resp, err = navServer.GetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Mode, test.ShouldEqual, pb.Mode_MODE_UNSPECIFIED) - }) - - t.Run("failing mode function", func(t *testing.T) { - injectSvc.ModeFunc = func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - return 0, errors.New("mode failed") - } - req := &pb.GetModeRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetMode(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("working set mode function", func(t *testing.T) { - var currentMode navigation.Mode - injectSvc.SetModeFunc = func(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - extraOptions = extra - currentMode = mode - return nil - } - extra := map[string]interface{}{"foo": "SetMode"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - // set manual mode - req := &pb.SetModeRequest{ - Name: testSvcName1.ShortName(), - Mode: pb.Mode_MODE_MANUAL, - Extra: ext, - } - resp, err := navServer.SetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, currentMode, test.ShouldEqual, navigation.ModeManual) - test.That(t, extraOptions, test.ShouldResemble, extra) - - // set waypoint mode - req = &pb.SetModeRequest{ - Name: testSvcName1.ShortName(), - Mode: pb.Mode_MODE_WAYPOINT, - } - resp, err = navServer.SetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, currentMode, test.ShouldEqual, navigation.ModeWaypoint) - - // set explore mode - req = &pb.SetModeRequest{ - Name: testSvcName1.ShortName(), - Mode: pb.Mode_MODE_EXPLORE, - } - resp, err = navServer.SetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, currentMode, test.ShouldEqual, navigation.ModeExplore) - - // set unknown mode - req = &pb.SetModeRequest{ - Name: testSvcName1.ShortName(), - Mode: 99, - } - resp, err = navServer.SetMode(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "unknown mode") - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("failing set mode function", func(t *testing.T) { - // internal set mode failure - injectSvc.SetModeFunc = func(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - return errors.New("failed to set mode") - } - req := &pb.SetModeRequest{ - Name: testSvcName1.ShortName(), - Mode: pb.Mode_MODE_MANUAL, - } - resp, err := navServer.SetMode(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - - // unspecified mode passed - injectSvc.SetModeFunc = func(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - return nil - } - req = &pb.SetModeRequest{ - Name: testSvcName1.ShortName(), - Mode: pb.Mode_MODE_UNSPECIFIED, - } - resp, err = navServer.SetMode(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("working location function", func(t *testing.T) { - loc := geo.NewPoint(90, 1) - expectedCompassHeading := 90. - expectedGeoPose := spatialmath.NewGeoPose(loc, expectedCompassHeading) - injectSvc.LocationFunc = func(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - extraOptions = extra - return expectedGeoPose, nil - } - extra := map[string]interface{}{"foo": "Location"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := &pb.GetLocationRequest{Name: testSvcName1.ShortName(), Extra: ext} - resp, err := navServer.GetLocation(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - protoLoc := resp.GetLocation() - test.That(t, protoLoc.GetLatitude(), test.ShouldEqual, loc.Lat()) - test.That(t, protoLoc.GetLongitude(), test.ShouldEqual, loc.Lng()) - test.That(t, extraOptions, test.ShouldResemble, extra) - test.That(t, resp.GetCompassHeading(), test.ShouldEqual, 90.) - }) - - t.Run("failing location function", func(t *testing.T) { - injectSvc.LocationFunc = func(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - return nil, errors.New("location retrieval failed") - } - req := &pb.GetLocationRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetLocation(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("working waypoints function", func(t *testing.T) { - waypoints, expectedResp := createWaypoints() - injectSvc.WaypointsFunc = func(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) { - extraOptions = extra - return waypoints, nil - } - extra := map[string]interface{}{"foo": "Waypoints"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := &pb.GetWaypointsRequest{Name: testSvcName1.ShortName(), Extra: ext} - resp, err := navServer.GetWaypoints(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.GetWaypoints(), test.ShouldResemble, expectedResp) - test.That(t, extraOptions, test.ShouldResemble, extra) - }) - - t.Run("failing waypoints function", func(t *testing.T) { - injectSvc.WaypointsFunc = func(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) { - return nil, errors.New("waypoints retrieval failed") - } - req := &pb.GetWaypointsRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetWaypoints(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("working add waypoint", func(t *testing.T) { - var receivedPoint geo.Point - injectSvc.AddWaypointFunc = func(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - extraOptions = extra - receivedPoint = *point - return nil - } - extra := map[string]interface{}{"foo": "AddWaypoint"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := &pb.AddWaypointRequest{ - Name: testSvcName1.ShortName(), - Location: &commonpb.GeoPoint{ - Latitude: 90, - Longitude: 0, - }, - Extra: ext, - } - expectedLatitude := req.GetLocation().GetLatitude() - expectedLongitude := req.GetLocation().GetLongitude() - resp, err := navServer.AddWaypoint(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, receivedPoint.Lat(), test.ShouldEqual, expectedLatitude) - test.That(t, receivedPoint.Lng(), test.ShouldEqual, expectedLongitude) - test.That(t, extraOptions, test.ShouldResemble, extra) - }) - - t.Run("failing add waypoint", func(t *testing.T) { - addWaypointCalled := false - injectSvc.AddWaypointFunc = func(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - addWaypointCalled = true - return errors.New("failed to add waypoint") - } - req := &pb.AddWaypointRequest{ - Name: testSvcName1.ShortName(), - Location: &commonpb.GeoPoint{ - Latitude: 90, - Longitude: 0, - }, - } - resp, err := navServer.AddWaypoint(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - test.That(t, addWaypointCalled, test.ShouldBeTrue) - }) - - t.Run("working remove waypoint", func(t *testing.T) { - var receivedID primitive.ObjectID - injectSvc.RemoveWaypointFunc = func(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - extraOptions = extra - receivedID = id - return nil - } - extra := map[string]interface{}{"foo": "Sync"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - objectID := primitive.NewObjectID() - req := &pb.RemoveWaypointRequest{ - Name: testSvcName1.ShortName(), - Id: objectID.Hex(), - Extra: ext, - } - resp, err := navServer.RemoveWaypoint(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp, test.ShouldNotBeNil) - test.That(t, receivedID, test.ShouldEqual, objectID) - test.That(t, extraOptions, test.ShouldResemble, extra) - }) - - t.Run("failing remove waypoint", func(t *testing.T) { - // fail on bad hex - injectSvc.RemoveWaypointFunc = func(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - return nil - } - req := &pb.RemoveWaypointRequest{ - Name: testSvcName1.ShortName(), - Id: "not a hex", - } - resp, err := navServer.RemoveWaypoint(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - - // fail on failing function - injectSvc.RemoveWaypointFunc = func(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - return errors.New("failed to remove waypoint") - } - req = &pb.RemoveWaypointRequest{ - Name: testSvcName1.ShortName(), - Id: primitive.NewObjectID().Hex(), - } - resp, err = navServer.RemoveWaypoint(context.Background(), req) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("working Paths", func(t *testing.T) { - path, err := navigation.NewPath(primitive.NewObjectID(), []*geo.Point{geo.NewPoint(0, 0)}) - test.That(t, err, test.ShouldBeNil) - expectedOutput := []*navigation.Path{path} - injectSvc.PathsFunc = func(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) { - return expectedOutput, nil - } - req := &pb.GetPathsRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetPaths(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - convertedPbPath, err := navigation.ProtoSliceToPaths(resp.Paths) - test.That(t, err, test.ShouldBeNil) - test.That(t, convertedPbPath, test.ShouldResemble, expectedOutput) - }) - t.Run("failing Paths", func(t *testing.T) { - expectedErr := errors.New("unimplemented") - injectSvc.PathsFunc = func(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) { - return nil, expectedErr - } - req := &pb.GetPathsRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetPaths(context.Background(), req) - test.That(t, err, test.ShouldResemble, expectedErr) - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("working Properties", func(t *testing.T) { - prop := navigation.Properties{ - MapType: navigation.NoMap, - } - injectSvc.PropertiesFunc = func(ctx context.Context) (navigation.Properties, error) { - return prop, nil - } - req := &pb.GetPropertiesRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetProperties(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.MapType, test.ShouldEqual, pb.MapType_MAP_TYPE_NONE) - }) - t.Run("failing Properties", func(t *testing.T) { - expectedErr := errors.New("unimplemented") - injectSvc.PropertiesFunc = func(ctx context.Context) (navigation.Properties, error) { - return navigation.Properties{}, expectedErr - } - req := &pb.GetPropertiesRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetProperties(context.Background(), req) - test.That(t, err, test.ShouldBeError, expectedErr) - test.That(t, resp, test.ShouldBeNil) - }) - - injectAPISvc, _ = resource.NewAPIResourceCollection(navigation.API, map[resource.Name]navigation.Service{}) - navServer = navigation.NewRPCServiceServer(injectAPISvc).(pb.NavigationServiceServer) - t.Run("failing on nonexistent server", func(t *testing.T) { - req := &pb.GetModeRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetMode(context.Background(), req) - test.That(t, resp, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(testSvcName1)) - }) - t.Run("multiple services valid", func(t *testing.T) { - injectSvc = &inject.NavigationService{} - resourceMap = map[resource.Name]navigation.Service{ - testSvcName1: injectSvc, - testSvcName2: injectSvc, - } - injectAPISvc, err = resource.NewAPIResourceCollection(navigation.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - navServer = navigation.NewRPCServiceServer(injectAPISvc).(pb.NavigationServiceServer) - injectSvc.ModeFunc = func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - return navigation.ModeManual, nil - } - req := &pb.GetModeRequest{Name: testSvcName1.ShortName()} - resp, err := navServer.GetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Mode, test.ShouldEqual, pb.Mode_MODE_MANUAL) - req = &pb.GetModeRequest{Name: testSvcName2.ShortName()} - resp, err = navServer.GetMode(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp.Mode, test.ShouldEqual, pb.Mode_MODE_MANUAL) - }) -} - -func TestServerDoCommand(t *testing.T) { - resourceMap := map[resource.Name]navigation.Service{ - testSvcName1: &inject.NavigationService{ - DoCommandFunc: testutils.EchoFunc, - }, - } - injectAPISvc, err := resource.NewAPIResourceCollection(navigation.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - server := navigation.NewRPCServiceServer(injectAPISvc).(pb.NavigationServiceServer) - - cmd, err := protoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - doCommandRequest := &commonpb.DoCommandRequest{ - Name: testSvcName1.ShortName(), - Command: cmd, - } - doCommandResponse, err := server.DoCommand(context.Background(), doCommandRequest) - test.That(t, err, test.ShouldBeNil) - - // Assert that do command response is an echoed request. - respMap := doCommandResponse.Result.AsMap() - test.That(t, respMap["command"], test.ShouldResemble, "test") - test.That(t, respMap["data"], test.ShouldResemble, 500.0) -} diff --git a/services/navigation/store.go b/services/navigation/store.go deleted file mode 100644 index 7a3a02c6644..00000000000 --- a/services/navigation/store.go +++ /dev/null @@ -1,309 +0,0 @@ -package navigation - -import ( - "context" - "math" - "sync" - "time" - - geo "github.com/kellydunn/golang-geo" - "github.com/pkg/errors" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/mongo/readpref" - "go.uber.org/multierr" - mongoutils "go.viam.com/utils/mongo" -) - -var errNoMoreWaypoints = errors.New("no more waypoints") - -// NavStore handles the waypoints for a navigation service. -type NavStore interface { - Waypoints(ctx context.Context) ([]Waypoint, error) - AddWaypoint(ctx context.Context, point *geo.Point) (Waypoint, error) - RemoveWaypoint(ctx context.Context, id primitive.ObjectID) error - NextWaypoint(ctx context.Context) (Waypoint, error) - WaypointVisited(ctx context.Context, id primitive.ObjectID) error - Close(ctx context.Context) error -} - -type storeType string - -const ( - // StoreTypeUnset represents when a store type was not set. - StoreTypeUnset = "" - // StoreTypeMemory is the constant for the memory store type. - StoreTypeMemory = "memory" - // StoreTypeMongoDB is the constant for the mongodb store type. - StoreTypeMongoDB = "mongodb" -) - -// StoreConfig describes how to configure data storage. -type StoreConfig struct { - Type storeType `json:"type"` - Config map[string]interface{} `json:"config"` -} - -// Validate ensures all parts of the config are valid. -func (config *StoreConfig) Validate(path string) error { - switch config.Type { - case StoreTypeMemory, StoreTypeMongoDB, StoreTypeUnset: - default: - return errors.Errorf("unknown store type %q", config.Type) - } - return nil -} - -// NewStoreFromConfig builds a NavStore from the provided StoreConfig and returns it. -func NewStoreFromConfig(ctx context.Context, conf StoreConfig) (NavStore, error) { - switch conf.Type { - case StoreTypeMemory, StoreTypeUnset: - return NewMemoryNavigationStore(), nil - case StoreTypeMongoDB: - return NewMongoDBNavigationStore(ctx, conf.Config) - default: - return nil, errors.Errorf("unknown store type %q", conf.Type) - } -} - -// A Waypoint designates a location within a path to navigate to. -type Waypoint struct { - ID primitive.ObjectID `bson:"_id"` - Visited bool `bson:"visited"` - Order int `bson:"order"` - Lat float64 `bson:"latitude"` - Long float64 `bson:"longitude"` -} - -// ToPoint converts the waypoint to a geo.Point. -func (wp *Waypoint) ToPoint() *geo.Point { - return geo.NewPoint(wp.Lat, wp.Long) -} - -// LatLongApproxEqual returns true if the lat / long of the waypoint is within a small epsilon of the parameter. -func (wp *Waypoint) LatLongApproxEqual(wp2 Waypoint) bool { - const epsilon = 1e-16 - return math.Abs(wp.Lat-wp2.Lat) < epsilon && math.Abs(wp.Long-wp2.Long) < epsilon -} - -// NewMemoryNavigationStore returns and empty MemoryNavigationStore. -func NewMemoryNavigationStore() *MemoryNavigationStore { - return &MemoryNavigationStore{} -} - -// MemoryNavigationStore holds the waypoints for the navigation service. -type MemoryNavigationStore struct { - mu sync.RWMutex - waypoints []*Waypoint -} - -// Waypoints returns a copy of all of the waypoints in the MemoryNavigationStore. -func (store *MemoryNavigationStore) Waypoints(ctx context.Context) ([]Waypoint, error) { - if ctx.Err() != nil { - return nil, ctx.Err() - } - store.mu.RLock() - defer store.mu.RUnlock() - wps := make([]Waypoint, 0, len(store.waypoints)) - for _, wp := range store.waypoints { - if wp.Visited { - continue - } - wpCopy := *wp - wps = append(wps, wpCopy) - } - return wps, nil -} - -// AddWaypoint adds a waypoint to the MemoryNavigationStore. -func (store *MemoryNavigationStore) AddWaypoint(ctx context.Context, point *geo.Point) (Waypoint, error) { - if ctx.Err() != nil { - return Waypoint{}, ctx.Err() - } - store.mu.Lock() - defer store.mu.Unlock() - newPoint := Waypoint{ - ID: primitive.NewObjectID(), - Lat: point.Lat(), - Long: point.Lng(), - } - store.waypoints = append(store.waypoints, &newPoint) - return newPoint, nil -} - -// RemoveWaypoint removes a waypoint from the MemoryNavigationStore. -func (store *MemoryNavigationStore) RemoveWaypoint(ctx context.Context, id primitive.ObjectID) error { - if ctx.Err() != nil { - return ctx.Err() - } - store.mu.Lock() - defer store.mu.Unlock() - // the math.Max is to avoid a panic if the store is already empty - // when RemoveWaypoint is called. - newCapacity := int(math.Max(float64(len(store.waypoints)-1), 0)) - newWps := make([]*Waypoint, 0, newCapacity) - for _, wp := range store.waypoints { - if wp.ID == id { - continue - } - newWps = append(newWps, wp) - } - store.waypoints = newWps - return nil -} - -// NextWaypoint gets the next waypoint that has not been visited. -func (store *MemoryNavigationStore) NextWaypoint(ctx context.Context) (Waypoint, error) { - if ctx.Err() != nil { - return Waypoint{}, ctx.Err() - } - store.mu.RLock() - defer store.mu.RUnlock() - for _, wp := range store.waypoints { - if !wp.Visited { - return *wp, nil - } - } - return Waypoint{}, errNoMoreWaypoints -} - -// WaypointVisited sets that a waypoint has been visited. -func (store *MemoryNavigationStore) WaypointVisited(ctx context.Context, id primitive.ObjectID) error { - if ctx.Err() != nil { - return ctx.Err() - } - store.mu.Lock() - defer store.mu.Unlock() - for _, wp := range store.waypoints { - if wp.ID != id { - continue - } - wp.Visited = true - } - return nil -} - -// Close does nothing. -func (store *MemoryNavigationStore) Close(ctx context.Context) error { - return nil -} - -// Database and collection names used by the MongoDBNavigationStore. -var ( - defaultMongoDBURI = "mongodb://127.0.0.1:27017" - MongoDBNavStoreDBName = "navigation" - MongoDBNavStoreWaypointsCollName = "waypoints" - mongoDBNavStoreIndexes = []mongo.IndexModel{ - { - Keys: bson.D{ - {"order", -1}, - {"_id", 1}, - }, - }, - } -) - -// NewMongoDBNavigationStore creates a new navigation store using MongoDB. -func NewMongoDBNavigationStore(ctx context.Context, config map[string]interface{}) (*MongoDBNavigationStore, error) { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - uri, ok := config["uri"].(string) - if !ok { - uri = defaultMongoDBURI - } - - mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(uri)) - if err != nil { - return nil, err - } - if err := mongoClient.Ping(ctx, readpref.Primary()); err != nil { - return nil, multierr.Combine(err, mongoClient.Disconnect(ctx)) - } - - waypoints := mongoClient.Database(MongoDBNavStoreDBName).Collection(MongoDBNavStoreWaypointsCollName) - if err := mongoutils.EnsureIndexes(ctx, waypoints, mongoDBNavStoreIndexes...); err != nil { - return nil, err - } - - return &MongoDBNavigationStore{ - mongoClient: mongoClient, - waypointsColl: waypoints, - }, nil -} - -// MongoDBNavigationStore holds the mongodb client and waypoints collection. -type MongoDBNavigationStore struct { - mongoClient *mongo.Client - waypointsColl *mongo.Collection -} - -// Close closes the connection with the mongodb client. -func (store *MongoDBNavigationStore) Close(ctx context.Context) error { - return store.mongoClient.Disconnect(ctx) -} - -// Waypoints returns a copy of all the waypoints in the MongoDBNavigationStore. -func (store *MongoDBNavigationStore) Waypoints(ctx context.Context) ([]Waypoint, error) { - filter := bson.D{{"visited", false}} - cursor, err := store.waypointsColl.Find( - ctx, - filter, - options.Find().SetSort(bson.D{{"order", -1}, {"_id", 1}}), - ) - if err != nil { - return nil, err - } - - var all []Waypoint - if err := cursor.All(ctx, &all); err != nil { - return nil, err - } - return all, nil -} - -// AddWaypoint adds a waypoint to the MongoDBNavigationStore. -func (store *MongoDBNavigationStore) AddWaypoint(ctx context.Context, point *geo.Point) (Waypoint, error) { - newPoint := Waypoint{ - ID: primitive.NewObjectID(), - Lat: point.Lat(), - Long: point.Lng(), - } - if _, err := store.waypointsColl.InsertOne(ctx, newPoint); err != nil { - return Waypoint{}, err - } - return newPoint, nil -} - -// RemoveWaypoint removes a waypoint from the MongoDBNavigationStore. -func (store *MongoDBNavigationStore) RemoveWaypoint(ctx context.Context, id primitive.ObjectID) error { - _, err := store.waypointsColl.DeleteOne(ctx, bson.D{{"_id", id}}) - return err -} - -// NextWaypoint gets the next waypoint that has not been visited. -func (store *MongoDBNavigationStore) NextWaypoint(ctx context.Context) (Waypoint, error) { - filter := bson.D{{"visited", false}} - result := store.waypointsColl.FindOne( - ctx, - filter, - options.FindOne().SetSort(bson.D{{"order", -1}, {"_id", 1}}), - ) - var wp Waypoint - if err := result.Decode(&wp); err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - return Waypoint{}, errNoMoreWaypoints - } - return Waypoint{}, err - } - - return wp, nil -} - -// WaypointVisited sets that a waypoint has been visited. -func (store *MongoDBNavigationStore) WaypointVisited(ctx context.Context, id primitive.ObjectID) error { - _, err := store.waypointsColl.UpdateOne(ctx, bson.D{{"_id", id}}, bson.D{{"$set", bson.D{{"visited", true}}}}) - return err -} diff --git a/services/navigation/verify_main_test.go b/services/navigation/verify_main_test.go deleted file mode 100644 index 61552caa147..00000000000 --- a/services/navigation/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package navigation - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/register/all.go b/services/register/all.go index d3f38f47afc..0e9293b71d5 100644 --- a/services/register/all.go +++ b/services/register/all.go @@ -3,10 +3,6 @@ package register import ( // register services. - _ "go.viam.com/rdk/services/baseremotecontrol/register" - _ "go.viam.com/rdk/services/datamanager/register" _ "go.viam.com/rdk/services/generic/register" - _ "go.viam.com/rdk/services/sensors/register" _ "go.viam.com/rdk/services/shell/register" - _ "go.viam.com/rdk/services/slam/register" ) diff --git a/services/register/all_cgo.go b/services/register/all_cgo.go deleted file mode 100644 index e4794c395c2..00000000000 --- a/services/register/all_cgo.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !no_cgo - -package register - -import ( - // blank import registration pattern. - _ "go.viam.com/rdk/services/motion/register" - _ "go.viam.com/rdk/services/navigation/register" -) diff --git a/services/register/all_cgo_droid.go b/services/register/all_cgo_droid.go index d0e9219e0dd..7c0f2cb43d6 100644 --- a/services/register/all_cgo_droid.go +++ b/services/register/all_cgo_droid.go @@ -5,5 +5,4 @@ package register import ( // register services. _ "go.viam.com/rdk/services/mlmodel/register" - _ "go.viam.com/rdk/services/vision/register" ) diff --git a/services/sensors/builtin/builtin.go b/services/sensors/builtin/builtin.go deleted file mode 100644 index b0d04034188..00000000000 --- a/services/sensors/builtin/builtin.go +++ /dev/null @@ -1,105 +0,0 @@ -// Package builtin implements the default sensors service. -package builtin - -import ( - "context" - "sync" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/sensors" -) - -func init() { - resource.RegisterDefaultService( - sensors.API, - resource.DefaultServiceModel, - resource.Registration[sensors.Service, resource.NoNativeConfig]{ - Constructor: NewBuiltIn, - WeakDependencies: []resource.Matcher{resource.InterfaceMatcher{Interface: new(resource.Sensor)}}, - }, - ) -} - -// NewBuiltIn returns a new default sensor service for the given robot. -func NewBuiltIn( - ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger, -) (sensors.Service, error) { - s := &builtIn{ - Named: conf.ResourceName().AsNamed(), - sensors: map[resource.Name]sensor.Sensor{}, - logger: logger, - } - if err := s.Reconfigure(ctx, deps, conf); err != nil { - return nil, err - } - return s, nil -} - -type builtIn struct { - resource.Named - resource.TriviallyCloseable - mu sync.RWMutex - sensors map[resource.Name]sensor.Sensor - logger logging.Logger -} - -// Sensors returns all sensors in the robot. -func (s *builtIn) Sensors(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - names := make([]resource.Name, 0, len(s.sensors)) - for name := range s.sensors { - names = append(names, name) - } - return names, nil -} - -// Readings returns the readings of the resources specified. -func (s *builtIn) Readings(ctx context.Context, sensorNames []resource.Name, extra map[string]interface{}) ([]sensors.Readings, error) { - s.mu.RLock() - // make a copy of sensors and then unlock - sensorsMap := make(map[resource.Name]sensor.Sensor, len(s.sensors)) - for name, sensor := range s.sensors { - sensorsMap[name] = sensor - } - s.mu.RUnlock() - - // dedupe sensorNames - deduped := make(map[resource.Name]struct{}, len(sensorNames)) - for _, val := range sensorNames { - deduped[val] = struct{}{} - } - - readings := make([]sensors.Readings, 0, len(deduped)) - for name := range deduped { - sensor, ok := sensorsMap[name] - if !ok { - return nil, errors.Errorf("resource %q not a registered sensor", name) - } - reading, err := sensor.Readings(ctx, extra) - if err != nil { - return nil, errors.Wrapf(err, "failed to get reading from %q", name) - } - readings = append(readings, sensors.Readings{Name: name, Readings: reading}) - } - return readings, nil -} - -func (s *builtIn) Reconfigure(ctx context.Context, deps resource.Dependencies, _ resource.Config) error { - s.mu.Lock() - defer s.mu.Unlock() - - sensors := map[resource.Name]sensor.Sensor{} - for n, r := range deps { - if sensor, ok := r.(sensor.Sensor); ok { - sensors[n] = sensor - } - } - s.sensors = sensors - return nil -} diff --git a/services/sensors/builtin/builtin_test.go b/services/sensors/builtin/builtin_test.go deleted file mode 100644 index f434aa07a6b..00000000000 --- a/services/sensors/builtin/builtin_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package builtin_test - -import ( - "context" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/sensors/builtin" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func TestNew(t *testing.T) { - logger := logging.NewTestLogger(t) - t.Run("no error", func(t *testing.T) { - deps := make(resource.Dependencies) - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - test.That(t, svc, test.ShouldNotBeNil) - }) -} - -func TestGetSensors(t *testing.T) { - logger := logging.NewTestLogger(t) - sensorNames := []resource.Name{movementsensor.Named("imu"), movementsensor.Named("gps")} - deps := make(resource.Dependencies) - - t.Run("no sensors", func(t *testing.T) { - resourceMap := map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): testutils.NewUnimplementedResource(movementsensor.Named("resource")), - movementsensor.Named("gps"): testutils.NewUnimplementedResource(movementsensor.Named("resource")), - } - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - names, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, names, test.ShouldBeEmpty) - }) - - t.Run("one sensor", func(t *testing.T) { - resourceMap := map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): &inject.Sensor{}, - movementsensor.Named("gps"): testutils.NewUnimplementedResource(movementsensor.Named("resource")), - } - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - sNames1, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That( - t, - testutils.NewResourceNameSet(sNames1...), - test.ShouldResemble, - testutils.NewResourceNameSet(movementsensor.Named("imu")), - ) - }) - - t.Run("many sensors", func(t *testing.T) { - resourceMap := map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): &inject.Sensor{}, - movementsensor.Named("gps"): &inject.Sensor{}, - } - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - sNames1, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, testutils.NewResourceNameSet(sNames1...), test.ShouldResemble, testutils.NewResourceNameSet(sensorNames...)) - }) -} - -func TestReadings(t *testing.T) { - logger := logging.NewTestLogger(t) - sensorNames := []resource.Name{movementsensor.Named("imu"), movementsensor.Named("gps"), movementsensor.Named("gps2")} - deps := make(resource.Dependencies) - - t.Run("no sensors", func(t *testing.T) { - resourceMap := map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): testutils.NewUnimplementedResource(movementsensor.Named("resource")), - movementsensor.Named("gps"): testutils.NewUnimplementedResource(movementsensor.Named("resource")), - movementsensor.Named("gps2"): testutils.NewUnimplementedResource(movementsensor.Named("resource")), - } - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - _, err = svc.Readings(context.Background(), []resource.Name{movementsensor.Named("imu")}, map[string]interface{}{}) - test.That(t, err.Error(), test.ShouldContainSubstring, "not a registered sensor") - }) - - t.Run("failing sensor", func(t *testing.T) { - injectSensor := &inject.Sensor{} - passedErr := errors.New("can't get readings") - injectSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return nil, passedErr - } - failMap := map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): injectSensor, - movementsensor.Named("gps"): injectSensor, - movementsensor.Named("gps2"): injectSensor, - } - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), failMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - _, err = svc.Readings(context.Background(), []resource.Name{movementsensor.Named("imu")}, map[string]interface{}{}) - test.That(t, err, test.ShouldBeError, errors.Wrapf(passedErr, "failed to get reading from %q", movementsensor.Named("imu"))) - }) - - t.Run("many sensors", func(t *testing.T) { - readings1 := map[string]interface{}{"a": 1.1, "b": 2.2} - injectSensor := &inject.Sensor{} - injectSensor.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return readings1, nil - } - readings2 := map[string]interface{}{"a": 2.2, "b": 3.3} - injectSensor2 := &inject.Sensor{} - injectSensor2.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return readings2, nil - } - injectSensor3 := &inject.Sensor{} - passedErr := errors.New("can't read") - injectSensor3.ReadingsFunc = func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - return nil, passedErr - } - expected := map[resource.Name]interface{}{ - movementsensor.Named("imu"): readings1, - movementsensor.Named("gps"): readings2, - } - resourceMap := map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): injectSensor, - movementsensor.Named("gps"): injectSensor2, movementsensor.Named("gps2"): injectSensor3, - } - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - _, err = svc.Readings(context.Background(), []resource.Name{movementsensor.Named("imu2")}, map[string]interface{}{}) - test.That(t, err.Error(), test.ShouldContainSubstring, "not a registered sensor") - - readings, err := svc.Readings(context.Background(), []resource.Name{movementsensor.Named("imu")}, map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(readings), test.ShouldEqual, 1) - reading := readings[0] - test.That(t, reading.Name, test.ShouldResemble, movementsensor.Named("imu")) - test.That(t, reading.Readings, test.ShouldResemble, readings1) - - readings, err = svc.Readings( - context.Background(), - []resource.Name{movementsensor.Named("imu"), movementsensor.Named("imu"), movementsensor.Named("imu")}, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(readings), test.ShouldEqual, 1) - reading = readings[0] - test.That(t, reading.Name, test.ShouldResemble, movementsensor.Named("imu")) - test.That(t, reading.Readings, test.ShouldResemble, readings1) - - readings, err = svc.Readings( - context.Background(), - []resource.Name{movementsensor.Named("imu"), movementsensor.Named("gps")}, - map[string]interface{}{}, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(readings), test.ShouldEqual, 2) - test.That(t, readings[0].Readings, test.ShouldResemble, expected[readings[0].Name]) - test.That(t, readings[1].Readings, test.ShouldResemble, expected[readings[1].Name]) - - _, err = svc.Readings(context.Background(), sensorNames, map[string]interface{}{}) - test.That(t, err, test.ShouldBeError, errors.Wrapf(passedErr, "failed to get reading from %q", movementsensor.Named("gps2"))) - }) -} - -func TestReconfigure(t *testing.T) { - logger := logging.NewTestLogger(t) - sensorNames := []resource.Name{movementsensor.Named("imu"), movementsensor.Named("gps")} - resourceMap := map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): &inject.Sensor{}, - movementsensor.Named("gps"): &inject.Sensor{}, - } - deps := make(resource.Dependencies) - - t.Run("update with no sensors", func(t *testing.T) { - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - sNames1, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, testutils.NewResourceNameSet(sNames1...), test.ShouldResemble, testutils.NewResourceNameSet(sensorNames...)) - - err = svc.Reconfigure( - context.Background(), - map[resource.Name]resource.Resource{ - movementsensor.Named("imu"): testutils.NewUnimplementedResource(generic.Named("not sensor")), - }, - resource.Config{}, - ) - test.That(t, err, test.ShouldBeNil) - - sNames1, err = svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, sNames1, test.ShouldBeEmpty) - }) - - t.Run("update with one sensor", func(t *testing.T) { - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - sNames1, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, testutils.NewResourceNameSet(sNames1...), test.ShouldResemble, testutils.NewResourceNameSet(sensorNames...)) - - err = svc.Reconfigure( - context.Background(), - map[resource.Name]resource.Resource{movementsensor.Named("imu"): &inject.Sensor{}}, - resource.Config{}, - ) - test.That(t, err, test.ShouldBeNil) - - sNames1, err = svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That( - t, - testutils.NewResourceNameSet(sNames1...), - test.ShouldResemble, - testutils.NewResourceNameSet(movementsensor.Named("imu")), - ) - }) - - t.Run("update with same sensors", func(t *testing.T) { - svc, err := builtin.NewBuiltIn(context.Background(), deps, resource.Config{}, logger) - test.That(t, err, test.ShouldBeNil) - err = svc.Reconfigure(context.Background(), resourceMap, resource.Config{}) - test.That(t, err, test.ShouldBeNil) - - sNames1, err := svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, testutils.NewResourceNameSet(sNames1...), test.ShouldResemble, testutils.NewResourceNameSet(sensorNames...)) - - err = svc.Reconfigure( - context.Background(), - map[resource.Name]resource.Resource{movementsensor.Named("imu"): &inject.Sensor{}, movementsensor.Named("gps"): &inject.Sensor{}}, - resource.Config{}, - ) - test.That(t, err, test.ShouldBeNil) - - sNames1, err = svc.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, testutils.NewResourceNameSet(sNames1...), test.ShouldResemble, testutils.NewResourceNameSet(sensorNames...)) - }) -} diff --git a/services/sensors/client.go b/services/sensors/client.go deleted file mode 100644 index 6527dd46869..00000000000 --- a/services/sensors/client.go +++ /dev/null @@ -1,94 +0,0 @@ -// Package sensors contains a gRPC based sensors service client -// -//nolint:staticcheck -package sensors - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/sensors/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// client implements SensorsServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.SensorsServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Service, error) { - grpcClient := pb.NewSensorsServiceClient(conn) - c := &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: grpcClient, - logger: logger, - } - return c, nil -} - -func (c *client) Sensors(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetSensors(ctx, &pb.GetSensorsRequest{Name: c.name, Extra: ext}) - if err != nil { - return nil, err - } - sensorNames := make([]resource.Name, 0, len(resp.SensorNames)) - for _, name := range resp.SensorNames { - sensorNames = append(sensorNames, rprotoutils.ResourceNameFromProto(name)) - } - return sensorNames, nil -} - -func (c *client) Readings(ctx context.Context, sensorNames []resource.Name, extra map[string]interface{}) ([]Readings, error) { - names := make([]*commonpb.ResourceName, 0, len(sensorNames)) - for _, name := range sensorNames { - names = append(names, rprotoutils.ResourceNameToProto(name)) - } - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetReadings(ctx, &pb.GetReadingsRequest{Name: c.name, SensorNames: names, Extra: ext}) - if err != nil { - return nil, err - } - - readings := make([]Readings, 0, len(resp.Readings)) - for _, reading := range resp.Readings { - sReading, err := rprotoutils.ReadingProtoToGo(reading.Readings) - if err != nil { - return nil, err - } - readings = append( - readings, Readings{ - Name: rprotoutils.ResourceNameFromProto(reading.Name), - Readings: sReading, - }) - } - return readings, nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/services/sensors/client_test.go b/services/sensors/client_test.go deleted file mode 100644 index 5f1c4b07413..00000000000 --- a/services/sensors/client_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package sensors_test - -import ( - "context" - "net" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/components/movementsensor" - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/sensors" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var testSvcName1 = sensors.Named("sen1") - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - var extraOptions map[string]interface{} - - injectSensors := &inject.SensorsService{} - ssMap := map[resource.Name]sensors.Service{ - testSvcName1: injectSensors, - } - svc, err := resource.NewAPIResourceCollection(sensors.API, ssMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[sensors.Service](sensors.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - // working client - t.Run("sensors client 1", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - client, err := sensors.NewClientFromConn(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - names := []resource.Name{movementsensor.Named("gps"), movementsensor.Named("imu")} - injectSensors.SensorsFunc = func(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) { - extraOptions = extra - return names, nil - } - extra := map[string]interface{}{"foo": "Sensors"} - sensorNames, err := client.Sensors(context.Background(), extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, sensorNames, test.ShouldResemble, names) - test.That(t, extraOptions, test.ShouldResemble, extra) - - gReading := sensors.Readings{Name: movementsensor.Named("gps"), Readings: map[string]interface{}{"a": 4.5, "b": 5.6, "c": 6.7}} - readings := []sensors.Readings{gReading} - expected := map[resource.Name]interface{}{ - gReading.Name: gReading.Readings, - } - - injectSensors.ReadingsFunc = func( - ctx context.Context, sensors []resource.Name, extra map[string]interface{}, - ) ([]sensors.Readings, error) { - extraOptions = extra - return readings, nil - } - extra = map[string]interface{}{"foo": "Readings"} - readings, err = client.Readings(context.Background(), []resource.Name{}, extra) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(readings), test.ShouldEqual, 1) - observed := map[resource.Name]interface{}{ - readings[0].Name: readings[0].Readings, - } - test.That(t, observed, test.ShouldResemble, expected) - test.That(t, extraOptions, test.ShouldResemble, extra) - - // DoCommand - injectSensors.DoCommandFunc = testutils.EchoFunc - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - // broken client - t.Run("sensors client 2", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client2, err := resourceAPI.RPCClient(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - passedErr := errors.New("can't get sensors") - injectSensors.SensorsFunc = func(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) { - return nil, passedErr - } - - _, err = client2.Sensors(context.Background(), map[string]interface{}{}) - test.That(t, err.Error(), test.ShouldContainSubstring, passedErr.Error()) - - passedErr = errors.New("can't get readings") - injectSensors.ReadingsFunc = func( - ctx context.Context, sensors []resource.Name, extra map[string]interface{}, - ) ([]sensors.Readings, error) { - return nil, passedErr - } - _, err = client2.Readings(context.Background(), []resource.Name{}, map[string]interface{}{}) - test.That(t, err.Error(), test.ShouldContainSubstring, passedErr.Error()) - - test.That(t, client2.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/sensors/register/register.go b/services/sensors/register/register.go deleted file mode 100644 index 28269bde1e1..00000000000 --- a/services/sensors/register/register.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package register registers all relevant sensors models and also API specific functions -package register - -import ( - // for sensors models. - _ "go.viam.com/rdk/services/sensors/builtin" -) diff --git a/services/sensors/sensors.go b/services/sensors/sensors.go deleted file mode 100644 index 3559b53a842..00000000000 --- a/services/sensors/sensors.go +++ /dev/null @@ -1,68 +0,0 @@ -// Package sensors implements a sensors service. -package sensors - -import ( - "context" - - pb "go.viam.com/api/service/sensors/v1" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Service]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterSensorsServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.SensorsService_ServiceDesc, - RPCClient: NewClientFromConn, - MaxInstance: resource.DefaultMaxInstance, - }) -} - -// A Readings ties both the sensor name and its reading together. -// -// // Get the readings provided by the sensor. -// readings, err := mySensor.Readings(context.Background(), nil) -type Readings struct { - Name resource.Name - Readings map[string]interface{} -} - -// A Service centralizes all sensors into one place. -type Service interface { - resource.Resource - Sensors(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) - Readings(ctx context.Context, sensorNames []resource.Name, extra map[string]interface{}) ([]Readings, error) -} - -// SubtypeName is the name of the type of service. -const SubtypeName = "sensors" - -// API is a variable that identifies the sensor service resource API. -var API = resource.APINamespaceRDK.WithServiceType(SubtypeName) - -// Named is a helper for getting the named sensor's typed resource name. -// RSDK-347 Implements senors's Named. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named sensor service from the given Robot. -func FromRobot(r robot.Robot, name string) (Service, error) { - return robot.ResourceFromRobot[Service](r, Named(name)) -} - -// FindFirstName returns name of first sensors service found. -func FindFirstName(r robot.Robot) string { - for _, val := range robot.NamesByAPI(r, API) { - return val - } - return "" -} - -// FirstFromRobot returns the first sensor service in this robot. -func FirstFromRobot(r robot.Robot) (Service, error) { - name := FindFirstName(r) - return FromRobot(r, name) -} diff --git a/services/sensors/server.go b/services/sensors/server.go deleted file mode 100644 index 570d6216bf4..00000000000 --- a/services/sensors/server.go +++ /dev/null @@ -1,91 +0,0 @@ -// Package sensors contains a gRPC based sensors service server -// -//nolint:staticcheck -package sensors - -import ( - "context" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/sensors/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" -) - -// serviceServer implements the SensorsService from sensors.proto. -type serviceServer struct { - pb.UnimplementedSensorsServiceServer - coll resource.APIResourceCollection[Service] -} - -// NewRPCServiceServer constructs a sensors gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Service]) interface{} { - return &serviceServer{coll: coll} -} - -func (server *serviceServer) GetSensors( - ctx context.Context, - req *pb.GetSensorsRequest, -) (*pb.GetSensorsResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - names, err := svc.Sensors(ctx, req.Extra.AsMap()) - if err != nil { - return nil, err - } - sensorNames := make([]*commonpb.ResourceName, 0, len(names)) - for _, name := range names { - sensorNames = append(sensorNames, protoutils.ResourceNameToProto(name)) - } - - return &pb.GetSensorsResponse{SensorNames: sensorNames}, nil -} - -func (server *serviceServer) GetReadings( - ctx context.Context, - req *pb.GetReadingsRequest, -) (*pb.GetReadingsResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - sensorNames := make([]resource.Name, 0, len(req.SensorNames)) - for _, name := range req.SensorNames { - sensorNames = append(sensorNames, protoutils.ResourceNameFromProto(name)) - } - - readings, err := svc.Readings(ctx, sensorNames, req.Extra.AsMap()) - if err != nil { - return nil, err - } - - readingsP := make([]*pb.Readings, 0, len(readings)) - for _, reading := range readings { - rReading, err := protoutils.ReadingGoToProto(reading.Readings) - if err != nil { - return nil, err - } - readingP := &pb.Readings{ - Name: protoutils.ResourceNameToProto(reading.Name), - Readings: rReading, - } - readingsP = append(readingsP, readingP) - } - - return &pb.GetReadingsResponse{Readings: readingsP}, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, svc, req) -} diff --git a/services/sensors/server_test.go b/services/sensors/server_test.go deleted file mode 100644 index 356a3ff5e2b..00000000000 --- a/services/sensors/server_test.go +++ /dev/null @@ -1,188 +0,0 @@ -//nolint:staticcheck -package sensors_test - -import ( - "context" - "errors" - "testing" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/sensors/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - "google.golang.org/protobuf/types/known/structpb" - - "go.viam.com/rdk/components/movementsensor" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/sensors" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func newServer(sMap map[resource.Name]sensors.Service) (pb.SensorsServiceServer, error) { - coll, err := resource.NewAPIResourceCollection(sensors.API, sMap) - if err != nil { - return nil, err - } - return sensors.NewRPCServiceServer(coll).(pb.SensorsServiceServer), nil -} - -func TestServerGetSensors(t *testing.T) { - t.Run("no sensors service", func(t *testing.T) { - sMap := map[resource.Name]sensors.Service{} - server, err := newServer(sMap) - test.That(t, err, test.ShouldBeNil) - _, err = server.GetSensors(context.Background(), &pb.GetSensorsRequest{}) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:sensors/\" not found")) - }) - - t.Run("failed Sensors", func(t *testing.T) { - injectSensors := &inject.SensorsService{} - sMap := map[resource.Name]sensors.Service{ - testSvcName1: injectSensors, - } - server, err := newServer(sMap) - test.That(t, err, test.ShouldBeNil) - passedErr := errors.New("can't get sensors") - injectSensors.SensorsFunc = func(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) { - return nil, passedErr - } - _, err = server.GetSensors(context.Background(), &pb.GetSensorsRequest{Name: testSvcName1.ShortName()}) - test.That(t, err, test.ShouldBeError, passedErr) - }) - - t.Run("working Sensors", func(t *testing.T) { - injectSensors := &inject.SensorsService{} - sMap := map[resource.Name]sensors.Service{ - testSvcName1: injectSensors, - } - server, err := newServer(sMap) - test.That(t, err, test.ShouldBeNil) - - var extraOptions map[string]interface{} - names := []resource.Name{movementsensor.Named("gps"), movementsensor.Named("imu")} - injectSensors.SensorsFunc = func(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) { - extraOptions = extra - return names, nil - } - extra := map[string]interface{}{"foo": "Sensors"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - resp, err := server.GetSensors(context.Background(), &pb.GetSensorsRequest{Name: testSvcName1.ShortName(), Extra: ext}) - test.That(t, err, test.ShouldBeNil) - test.That(t, extraOptions, test.ShouldResemble, extra) - - convertedNames := make([]resource.Name, 0, len(resp.SensorNames)) - for _, rn := range resp.SensorNames { - convertedNames = append(convertedNames, rprotoutils.ResourceNameFromProto(rn)) - } - test.That(t, testutils.NewResourceNameSet(convertedNames...), test.ShouldResemble, testutils.NewResourceNameSet(names...)) - }) -} - -func TestServerGetReadings(t *testing.T) { - t.Run("no sensors service", func(t *testing.T) { - sMap := map[resource.Name]sensors.Service{} - server, err := newServer(sMap) - test.That(t, err, test.ShouldBeNil) - _, err = server.GetReadings(context.Background(), &pb.GetReadingsRequest{}) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:sensors/\" not found")) - }) - - t.Run("failed Readings", func(t *testing.T) { - injectSensors := &inject.SensorsService{} - sMap := map[resource.Name]sensors.Service{ - testSvcName1: injectSensors, - } - server, err := newServer(sMap) - test.That(t, err, test.ShouldBeNil) - passedErr := errors.New("can't get readings") - injectSensors.ReadingsFunc = func( - ctx context.Context, sensors []resource.Name, extra map[string]interface{}, - ) ([]sensors.Readings, error) { - return nil, passedErr - } - req := &pb.GetReadingsRequest{ - Name: testSvcName1.ShortName(), - SensorNames: []*commonpb.ResourceName{}, - } - _, err = server.GetReadings(context.Background(), req) - test.That(t, err, test.ShouldBeError, passedErr) - }) - - t.Run("working Readings", func(t *testing.T) { - injectSensors := &inject.SensorsService{} - sMap := map[resource.Name]sensors.Service{ - testSvcName1: injectSensors, - } - server, err := newServer(sMap) - test.That(t, err, test.ShouldBeNil) - iReading := sensors.Readings{Name: movementsensor.Named("imu"), Readings: map[string]interface{}{"a": 1.2, "b": 2.3, "c": 3.4}} - gReading := sensors.Readings{Name: movementsensor.Named("gps"), Readings: map[string]interface{}{"a": 4.5, "b": 5.6, "c": 6.7}} - readings := []sensors.Readings{iReading, gReading} - expected := map[resource.Name]interface{}{ - iReading.Name: iReading.Readings, - gReading.Name: gReading.Readings, - } - var extraOptions map[string]interface{} - injectSensors.ReadingsFunc = func( - ctx context.Context, sensors []resource.Name, extra map[string]interface{}, - ) ([]sensors.Readings, error) { - extraOptions = extra - return readings, nil - } - extra := map[string]interface{}{"foo": "Readings"} - ext, err := protoutils.StructToStructPb(extra) - test.That(t, err, test.ShouldBeNil) - - req := &pb.GetReadingsRequest{ - Name: testSvcName1.ShortName(), - SensorNames: []*commonpb.ResourceName{}, - Extra: ext, - } - resp, err := server.GetReadings(context.Background(), req) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.Readings), test.ShouldEqual, 2) - test.That(t, extraOptions, test.ShouldResemble, extra) - - conv := func(rs map[string]*structpb.Value) map[string]interface{} { - r := map[string]interface{}{} - for k, value := range rs { - r[k] = value.AsInterface() - } - return r - } - - observed := map[resource.Name]interface{}{ - rprotoutils.ResourceNameFromProto(resp.Readings[0].Name): conv(resp.Readings[0].Readings), - rprotoutils.ResourceNameFromProto(resp.Readings[1].Name): conv(resp.Readings[1].Readings), - } - test.That(t, observed, test.ShouldResemble, expected) - }) -} - -func TestServerDoCommand(t *testing.T) { - resourceMap := map[resource.Name]sensors.Service{ - testSvcName1: &inject.SensorsService{ - DoCommandFunc: testutils.EchoFunc, - }, - } - server, err := newServer(resourceMap) - test.That(t, err, test.ShouldBeNil) - - cmd, err := protoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - doCommandRequest := &commonpb.DoCommandRequest{ - Name: testSvcName1.ShortName(), - Command: cmd, - } - doCommandResponse, err := server.DoCommand(context.Background(), doCommandRequest) - test.That(t, err, test.ShouldBeNil) - - // Assert that do command response is an echoed request. - respMap := doCommandResponse.Result.AsMap() - test.That(t, respMap["command"], test.ShouldResemble, "test") - test.That(t, respMap["data"], test.ShouldResemble, 500.0) -} diff --git a/services/sensors/verify_main_test.go b/services/sensors/verify_main_test.go deleted file mode 100644 index fc976bbc3f2..00000000000 --- a/services/sensors/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package sensors - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/shell/client_test.go b/services/shell/client_test.go deleted file mode 100644 index f4adeb5d2cd..00000000000 --- a/services/shell/client_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package shell_test - -import ( - "context" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/shell" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -var testSvcName1 = shell.Named("shell1") - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectShell := &inject.ShellService{} - ssMap := map[resource.Name]shell.Service{ - testSvcName1: injectShell, - } - svc, err := resource.NewAPIResourceCollection(shell.API, ssMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[shell.Service](shell.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - // working client - t.Run("shell client", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - client, err := shell.NewClientFromConn(context.Background(), conn, "", testSvcName1, logger) - test.That(t, err, test.ShouldBeNil) - - // DoCommand - injectShell.DoCommandFunc = testutils.EchoFunc - resp, err := client.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/shell/copy_local_test.go b/services/shell/copy_local_test.go deleted file mode 100644 index 63e67b11563..00000000000 --- a/services/shell/copy_local_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package shell - -import ( - "context" - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "go.viam.com/test" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - shelltestutils "go.viam.com/rdk/services/shell/testutils" -) - -func TestFixPeerPath(t *testing.T) { - tempDir := t.TempDir() - cwd, err := os.Getwd() - test.That(t, err, test.ShouldBeNil) - t.Cleanup(func() { os.Chdir(cwd) }) - test.That(t, os.Chdir(tempDir), test.ShouldBeNil) - // macos uses /private for /var temp dirs, getwd will give us that path - realTempDir, err := os.Getwd() - test.That(t, err, test.ShouldBeNil) - - fixed, err := fixPeerPath("/one/two/three", false, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, fixed, test.ShouldEqual, "/one/two/three") - - homeDir, err := os.UserHomeDir() - test.That(t, err, test.ShouldBeNil) - - fixed, err = fixPeerPath("one/two/three", false, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, fixed, test.ShouldEqual, filepath.Join(homeDir, "one/two/three")) - - fixed, err = fixPeerPath("~/one/two/three", false, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, fixed, test.ShouldEqual, filepath.Join(homeDir, "one/two/three")) - - fixed, err = fixPeerPath("~/one/two/~/three", false, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, fixed, test.ShouldEqual, filepath.Join(homeDir, "one/two/~/three")) - - fixed, err = fixPeerPath("one/two/three", false, false) - test.That(t, err, test.ShouldBeNil) - test.That(t, fixed, test.ShouldEqual, filepath.Join(realTempDir, "one/two/three")) - - _, err = fixPeerPath("", false, true) - test.That(t, err, test.ShouldEqual, errUnexpectedEmptyPath) - - fixed, err = fixPeerPath("", true, true) - test.That(t, err, test.ShouldBeNil) - test.That(t, fixed, test.ShouldEqual, homeDir) - - fixed, err = fixPeerPath("", true, false) - test.That(t, err, test.ShouldBeNil) - test.That(t, fixed, test.ShouldEqual, realTempDir) -} - -// TestLocalFileCopy contains tests are very similar to cli.TestShellFileCopy but -// it includes some more detailed testing at the unit level that is more annoying to -// test in the CLI. The RPC side of this is covered by the CLI. -func TestLocalFileCopy(t *testing.T) { - ctx := context.Background() - tfs := shelltestutils.SetupTestFileSystem(t) - - t.Run("single file", func(t *testing.T) { - tempDir := t.TempDir() - - factory, err := NewLocalFileCopyFactory(tempDir, false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err := NewLocalFileReadCopier([]string{tfs.SingleFileNested}, false, false, factory) - test.That(t, err, test.ShouldBeNil) - - test.That(t, readCopier.ReadAll(ctx), test.ShouldBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - }) - - t.Run("single file but destination does not exist", func(t *testing.T) { - tempDir := t.TempDir() - tempDirInner := filepath.Join(tempDir, "inner") - test.That(t, os.Mkdir(tempDirInner, 0o750), test.ShouldBeNil) - test.That(t, os.RemoveAll(tempDirInner), test.ShouldBeNil) - - factory, err := NewLocalFileCopyFactory(tempDirInner, false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err := NewLocalFileReadCopier([]string{tfs.SingleFileNested}, false, false, factory) - test.That(t, err, test.ShouldBeNil) - - test.That(t, readCopier.ReadAll(ctx), test.ShouldBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - - rd, err := os.ReadFile(tempDirInner) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - - t.Log("parent exists but it is a file not a directory") - factory, err = NewLocalFileCopyFactory(filepath.Join(tempDirInner, "notthere"), false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err = NewLocalFileReadCopier([]string{tfs.SingleFileNested}, false, false, factory) - test.That(t, err, test.ShouldBeNil) - - err = readCopier.ReadAll(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "is an existing file") - }) - - t.Run("single file relative", func(t *testing.T) { - tempDir := t.TempDir() - cwd, err := os.Getwd() - test.That(t, err, test.ShouldBeNil) - t.Cleanup(func() { os.Chdir(cwd) }) - test.That(t, os.Chdir(tempDir), test.ShouldBeNil) - - factory, err := NewLocalFileCopyFactory("foo", false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err := NewLocalFileReadCopier([]string{tfs.SingleFileNested}, false, false, factory) - test.That(t, err, test.ShouldBeNil) - - test.That(t, readCopier.ReadAll(ctx), test.ShouldBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, "foo")) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - }) - - t.Run("single directory", func(t *testing.T) { - tempDir := t.TempDir() - - t.Log("without recursion set") - factory, err := NewLocalFileCopyFactory(tempDir, false, false) - test.That(t, err, test.ShouldBeNil) - - _, err = NewLocalFileReadCopier([]string{tfs.Root}, false, false, factory) - test.That(t, err, test.ShouldNotBeNil) - s, ok := status.FromError(err) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, s.Code(), test.ShouldEqual, codes.InvalidArgument) - test.That(t, s.Message(), test.ShouldContainSubstring, "recursion") - _, err = os.ReadFile(filepath.Join(tempDir, "example")) - test.That(t, errors.Is(err, fs.ErrNotExist), test.ShouldBeTrue) - - t.Log("with recursion set") - readCopier, err := NewLocalFileReadCopier([]string{tfs.Root}, true, false, factory) - test.That(t, err, test.ShouldBeNil) - - test.That(t, readCopier.ReadAll(ctx), test.ShouldBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - }) - - t.Run("single directory but destination does not exist", func(t *testing.T) { - tempDir := t.TempDir() - tempDirInner := filepath.Join(tempDir, "inner") - test.That(t, os.Mkdir(tempDirInner, 0o750), test.ShouldBeNil) - test.That(t, os.RemoveAll(tempDirInner), test.ShouldBeNil) - - factory, err := NewLocalFileCopyFactory(tempDirInner, false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err := NewLocalFileReadCopier([]string{tfs.Root}, true, false, factory) - test.That(t, err, test.ShouldBeNil) - - test.That(t, readCopier.ReadAll(ctx), test.ShouldBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, tempDirInner), test.ShouldBeNil) - - t.Log("parent exists but it is a file not a directory") - fileNotDirectory := filepath.Join(tempDir, "file") - test.That(t, os.WriteFile(fileNotDirectory, nil, 0o640), test.ShouldBeNil) - factory, err = NewLocalFileCopyFactory(filepath.Join(fileNotDirectory, "notthere"), false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err = NewLocalFileReadCopier([]string{tfs.Root}, true, false, factory) - test.That(t, err, test.ShouldBeNil) - - err = readCopier.ReadAll(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "is an existing file") - }) - - t.Run("multiple files", func(t *testing.T) { - tempDir := t.TempDir() - - factory, err := NewLocalFileCopyFactory(tempDir, false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err := NewLocalFileReadCopier([]string{ - tfs.SingleFileNested, - tfs.InnerDir, - }, true, false, factory) - test.That(t, err, test.ShouldBeNil) - - test.That(t, readCopier.ReadAll(ctx), test.ShouldBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - - rd, err := os.ReadFile(filepath.Join(tempDir, filepath.Base(tfs.SingleFileNested))) - test.That(t, err, test.ShouldBeNil) - test.That(t, rd, test.ShouldResemble, tfs.SingleFileNestedData) - - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.InnerDir, filepath.Join(tempDir, filepath.Base(tfs.InnerDir))), test.ShouldBeNil) - }) - - t.Run("multiple files but destination does not exist", func(t *testing.T) { - tempDir := t.TempDir() - test.That(t, os.RemoveAll(tempDir), test.ShouldBeNil) - - factory, err := NewLocalFileCopyFactory(tempDir, false, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err := NewLocalFileReadCopier([]string{ - tfs.SingleFileNested, - tfs.InnerDir, - }, true, false, factory) - test.That(t, err, test.ShouldBeNil) - - err = readCopier.ReadAll(ctx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not exist or is not a directory") - }) - - t.Run("preserve permissions on a nested file", func(t *testing.T) { - tfs := shelltestutils.SetupTestFileSystem(t) - - beforeInfo, err := os.Stat(tfs.SingleFileNested) - test.That(t, err, test.ShouldBeNil) - t.Log("start with mode", beforeInfo.Mode()) - newMode := os.FileMode(0o444) - test.That(t, beforeInfo.Mode(), test.ShouldNotEqual, newMode) - test.That(t, os.Chmod(tfs.SingleFileNested, newMode), test.ShouldBeNil) - modTime := time.Date(1988, 1, 2, 3, 0, 0, 0, time.UTC) - test.That(t, os.Chtimes(tfs.SingleFileNested, time.Time{}, modTime), test.ShouldBeNil) - relNestedPath := strings.TrimPrefix(tfs.SingleFileNested, tfs.Root) - - for _, preserve := range []bool{false, true} { - t.Run(fmt.Sprintf("preserve=%t", preserve), func(t *testing.T) { - tempDir := t.TempDir() - - factory, err := NewLocalFileCopyFactory(tempDir, preserve, false) - test.That(t, err, test.ShouldBeNil) - - readCopier, err := NewLocalFileReadCopier([]string{tfs.Root}, true, false, factory) - test.That(t, err, test.ShouldBeNil) - - test.That(t, readCopier.ReadAll(ctx), test.ShouldBeNil) - test.That(t, readCopier.Close(ctx), test.ShouldBeNil) - - nestedCopy := filepath.Join(tempDir, filepath.Base(tfs.Root), relNestedPath) - test.That(t, shelltestutils.DirectoryContentsEqual(tfs.Root, filepath.Join(tempDir, filepath.Base(tfs.Root))), test.ShouldBeNil) - afterInfo, err := os.Stat(nestedCopy) - test.That(t, err, test.ShouldBeNil) - if preserve { - test.That(t, afterInfo.ModTime().UTC().String(), test.ShouldEqual, modTime.String()) - test.That(t, afterInfo.Mode(), test.ShouldEqual, newMode) - } else { - test.That(t, afterInfo.ModTime().UTC().String(), test.ShouldNotEqual, modTime.String()) - test.That(t, afterInfo.Mode(), test.ShouldNotEqual, newMode) - } - }) - } - }) -} diff --git a/services/shell/copy_rpc_test.go b/services/shell/copy_rpc_test.go deleted file mode 100644 index 863e19094ec..00000000000 --- a/services/shell/copy_rpc_test.go +++ /dev/null @@ -1,330 +0,0 @@ -package shell - -import ( - "bytes" - "context" - "fmt" - "io" - "io/fs" - "os" - "strings" - "testing" - "time" - - pb "go.viam.com/api/service/shell/v1" - "go.viam.com/test" - "google.golang.org/protobuf/types/known/timestamppb" - - shelltestutils "go.viam.com/rdk/services/shell/testutils" -) - -func TestShellRPCFileReadCopier(t *testing.T) { - t.Run("no files", func(t *testing.T) { - memCopier := inMemoryFileCopier{ - files: map[string]copiedFile{}, - } - memReader := inMemoryRPCCopyReader{} - readCopier := newShellRPCFileReadCopier(&memReader, &memCopier) - test.That(t, readCopier.ReadAll(context.Background()), test.ShouldBeNil) - test.That(t, readCopier.Close(context.Background()), test.ShouldBeNil) - test.That(t, memReader.closeCalled, test.ShouldEqual, 1) - }) - - modTime := time.Unix(time.Now().Unix(), 0).UTC() - mode := uint32(0o222) - smallFile := []*pb.FileData{ - { - Name: "small_file", - Size: 25, - Data: bytes.Repeat([]byte{'a'}, 20), - ModTime: timestamppb.New(modTime), - Mode: &mode, - }, - { - Data: bytes.Repeat([]byte{'b'}, 5), - }, - { - Eof: true, - }, - } - - t.Run("single small file", func(t *testing.T) { - memCopier := inMemoryFileCopier{ - files: map[string]copiedFile{}, - } - memReader := inMemoryRPCCopyReader{ - fileDatas: smallFile, - } - readCopier := newShellRPCFileReadCopier(&memReader, &memCopier) - test.That(t, readCopier.ReadAll(context.Background()), test.ShouldBeNil) - test.That(t, readCopier.Close(context.Background()), test.ShouldBeNil) - test.That(t, memReader.ackCalled, test.ShouldEqual, 1) - test.That(t, memReader.closeCalled, test.ShouldEqual, 1) - test.That(t, memCopier.files, test.ShouldResemble, map[string]copiedFile{ - "small_file": { - name: "small_file", - data: bytes.Join([][]byte{smallFile[0].Data, smallFile[1].Data}, nil), - mtime: modTime, - mode: mode, - }, - }) - }) - - t.Run("single small file incomplete", func(t *testing.T) { - memCopier := inMemoryFileCopier{ - files: map[string]copiedFile{}, - } - memReader := inMemoryRPCCopyReader{ - fileDatas: smallFile[:1], - } - readCopier := newShellRPCFileReadCopier(&memReader, &memCopier) - test.That(t, readCopier.ReadAll(context.Background()), test.ShouldBeNil) - test.That(t, readCopier.Close(context.Background()), test.ShouldBeNil) - test.That(t, memReader.ackCalled, test.ShouldEqual, 0) - test.That(t, memReader.closeCalled, test.ShouldEqual, 1) - test.That(t, memCopier.files, test.ShouldBeEmpty) - }) - - t.Run("single small file duplicate info", func(t *testing.T) { - memCopier := inMemoryFileCopier{ - files: map[string]copiedFile{}, - } - memReader := inMemoryRPCCopyReader{ - fileDatas: []*pb.FileData{smallFile[0], smallFile[0]}, - } - readCopier := newShellRPCFileReadCopier(&memReader, &memCopier) - err := readCopier.ReadAll(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "unexpected file name") - }) - - otherModTime := time.Unix(time.Now().Unix(), 0).UTC() - otherMode := uint32(0o333) - otherSmallFile := []*pb.FileData{ - { - Name: "other_file", - Size: 250, - Data: bytes.Repeat([]byte{'c'}, 200), - ModTime: timestamppb.New(otherModTime), - Mode: &otherMode, - }, - { - Data: bytes.Repeat([]byte{'d'}, 50), - }, - { - Eof: true, - }, - } - - t.Run("multiple files", func(t *testing.T) { - memCopier := inMemoryFileCopier{ - files: map[string]copiedFile{}, - } - var datas []*pb.FileData - datas = append(datas, smallFile...) - datas = append(datas, otherSmallFile...) - memReader := inMemoryRPCCopyReader{ - fileDatas: datas, - } - readCopier := newShellRPCFileReadCopier(&memReader, &memCopier) - test.That(t, readCopier.ReadAll(context.Background()), test.ShouldBeNil) - test.That(t, readCopier.Close(context.Background()), test.ShouldBeNil) - test.That(t, memReader.ackCalled, test.ShouldEqual, 2) - test.That(t, memReader.closeCalled, test.ShouldEqual, 1) - test.That(t, memCopier.files, test.ShouldResemble, map[string]copiedFile{ - "small_file": { - name: "small_file", - data: bytes.Join([][]byte{smallFile[0].Data, smallFile[1].Data}, nil), - mtime: modTime, - mode: mode, - }, - "other_file": { - name: "other_file", - data: bytes.Join([][]byte{otherSmallFile[0].Data, otherSmallFile[1].Data}, nil), - mtime: otherModTime, - mode: otherMode, - }, - }) - }) -} - -func TestShellRPCFileCopier(t *testing.T) { - t.Run("no files", func(t *testing.T) { - memWriter := inMemoryRPCCopyWriter{} - - copier := newShellRPCFileCopier(&memWriter, false) - test.That(t, copier.Close(context.Background()), test.ShouldBeNil) - test.That(t, memWriter.ackCalled, test.ShouldEqual, 0) - test.That(t, memWriter.closeCalled, test.ShouldEqual, 1) - }) - - tfs := shelltestutils.SetupTestFileSystem(t, strings.Repeat("a", 1<<8)) - beforeInfo, err := os.Stat(tfs.SingleFileNested) - test.That(t, err, test.ShouldBeNil) - newMode := os.FileMode(0o444) - test.That(t, beforeInfo.Mode(), test.ShouldNotEqual, newMode) - test.That(t, os.Chmod(tfs.SingleFileNested, newMode), test.ShouldBeNil) - modTime := time.Date(1988, 1, 2, 3, 0, 0, 0, time.UTC) - test.That(t, os.Chtimes(tfs.SingleFileNested, time.Time{}, modTime), test.ShouldBeNil) - - t.Run("single file", func(t *testing.T) { - for _, preserve := range []bool{false, true} { - t.Run(fmt.Sprintf("preserve=%t", preserve), func(t *testing.T) { - file, err := os.Open(tfs.SingleFileNested) - test.That(t, err, test.ShouldBeNil) - - shellFile := File{ - RelativeName: "this/is_a_file", - Data: file, - } - - memWriter := inMemoryRPCCopyWriter{} - copier := newShellRPCFileCopier(&memWriter, preserve) - test.That(t, copier.Copy(context.Background(), shellFile), test.ShouldBeNil) - test.That(t, copier.Close(context.Background()), test.ShouldBeNil) - test.That(t, memWriter.ackCalled, test.ShouldEqual, 1) - test.That(t, memWriter.closeCalled, test.ShouldEqual, 1) - test.That(t, memWriter.fileDatas[0].Name, test.ShouldEqual, shellFile.RelativeName) - test.That(t, memWriter.fileDatas[0].Eof, test.ShouldBeFalse) - - if preserve { - test.That(t, memWriter.fileDatas[0].ModTime, test.ShouldNotBeNil) - test.That(t, memWriter.fileDatas[0].Mode, test.ShouldNotBeNil) - test.That(t, memWriter.fileDatas[0].ModTime.AsTime().String(), test.ShouldEqual, modTime.String()) - test.That(t, fs.FileMode(*memWriter.fileDatas[0].Mode), test.ShouldEqual, newMode) - } else { - test.That(t, memWriter.fileDatas[0].ModTime, test.ShouldBeNil) - test.That(t, memWriter.fileDatas[0].Mode, test.ShouldBeNil) - } - - test.That(t, len(memWriter.fileDatas), test.ShouldEqual, 6) - test.That(t, memWriter.fileDatas[1].Name, test.ShouldBeEmpty) - test.That(t, memWriter.fileDatas[1].Eof, test.ShouldBeFalse) - test.That(t, memWriter.fileDatas[len(memWriter.fileDatas)-1].Data, test.ShouldHaveLength, 0) - test.That(t, memWriter.fileDatas[len(memWriter.fileDatas)-1].Eof, test.ShouldBeTrue) - }) - } - }) - - t.Run("multiple files", func(t *testing.T) { - file1, err := os.Open(tfs.SingleFileNested) - test.That(t, err, test.ShouldBeNil) - file2, err := os.Open(tfs.InnerDir) - test.That(t, err, test.ShouldBeNil) - shellFile1 := File{ - RelativeName: "this/is_a_file", - Data: file1, - } - shellFile2 := File{ - RelativeName: "another/file", - Data: file2, - } - - memWriter := inMemoryRPCCopyWriter{} - copier := newShellRPCFileCopier(&memWriter, false) - test.That(t, copier.Copy(context.Background(), shellFile1), test.ShouldBeNil) - test.That(t, memWriter.ackCalled, test.ShouldEqual, 1) - test.That(t, copier.Copy(context.Background(), shellFile2), test.ShouldBeNil) - test.That(t, memWriter.ackCalled, test.ShouldEqual, 2) - test.That(t, copier.Close(context.Background()), test.ShouldBeNil) - test.That(t, memWriter.ackCalled, test.ShouldEqual, 2) - test.That(t, memWriter.closeCalled, test.ShouldEqual, 1) - - test.That(t, memWriter.fileDatas[0].Name, test.ShouldEqual, shellFile1.RelativeName) - test.That(t, memWriter.fileDatas[0].Eof, test.ShouldBeFalse) - test.That(t, memWriter.fileDatas[0].IsDir, test.ShouldBeFalse) - test.That(t, len(memWriter.fileDatas), test.ShouldEqual, 7) - test.That(t, memWriter.fileDatas[1].Name, test.ShouldBeEmpty) - test.That(t, memWriter.fileDatas[1].Eof, test.ShouldBeFalse) - test.That(t, memWriter.fileDatas[5].Data, test.ShouldHaveLength, 0) - test.That(t, memWriter.fileDatas[5].Eof, test.ShouldBeTrue) - - // directory is 0 bytes so EOF - test.That(t, memWriter.fileDatas[6].Name, test.ShouldEqual, shellFile2.RelativeName) - test.That(t, memWriter.fileDatas[6].Eof, test.ShouldBeTrue) - test.That(t, memWriter.fileDatas[6].IsDir, test.ShouldBeTrue) - }) -} - -type inMemoryRPCCopyWriter struct { - fileDatas []*pb.FileData - ackCalled int - closeCalled int -} - -func (mem *inMemoryRPCCopyWriter) SendFile(fileDataProto *pb.FileData) error { - mem.fileDatas = append(mem.fileDatas, fileDataProto) - return nil -} - -func (mem *inMemoryRPCCopyWriter) WaitLastACK() error { - mem.ackCalled++ - return nil -} - -func (mem *inMemoryRPCCopyWriter) Close() error { - mem.closeCalled++ - return nil -} - -type inMemoryRPCCopyReader struct { - fileDatas []*pb.FileData - ackCalled int - closeCalled int -} - -func (mem *inMemoryRPCCopyReader) NextFileData() (*pb.FileData, error) { - if len(mem.fileDatas) == 0 { - return nil, io.EOF - } - next := mem.fileDatas[0] - mem.fileDatas = mem.fileDatas[1:] - return next, nil -} - -func (mem *inMemoryRPCCopyReader) AckLastFile() error { - mem.ackCalled++ - return nil -} - -func (mem *inMemoryRPCCopyReader) Close() error { - mem.closeCalled++ - return nil -} - -type copiedFile struct { - name string - data []byte - mode uint32 - mtime time.Time -} - -type inMemoryFileCopier struct { - files map[string]copiedFile -} - -func (mem *inMemoryFileCopier) Copy(ctx context.Context, file File) error { - var buf bytes.Buffer - n, err := io.Copy(&buf, file.Data) - if err != nil { - return err - } - info, err := file.Data.Stat() - if err != nil { - return err - } - if info.Size() != n { - return fmt.Errorf("size mismatch %d!=%d (read)", info.Size(), n) - } - mem.files[file.RelativeName] = copiedFile{ - name: file.RelativeName, - data: buf.Bytes(), - mode: uint32(info.Mode()), - mtime: info.ModTime(), - } - return nil -} - -func (mem *inMemoryFileCopier) Close(ctx context.Context) error { - return nil -} diff --git a/services/shell/copy_test.go b/services/shell/copy_test.go deleted file mode 100644 index b7853477d2c..00000000000 --- a/services/shell/copy_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package shell - -import ( - "testing" - - "go.viam.com/test" -) - -func TestCopyFilesSourceTypeProtoRoundTrip(t *testing.T) { - for _, tc := range []CopyFilesSourceType{ - CopyFilesSourceTypeSingleFile, - CopyFilesSourceTypeSingleDirectory, - CopyFilesSourceTypeMultipleFiles, - CopyFilesSourceTypeMultipleUnknown, - } { - t.Run(tc.String(), func(t *testing.T) { - test.That(t, CopyFilesSourceTypeFromProto(tc.ToProto()), test.ShouldEqual, tc) - }) - } -} diff --git a/services/shell/server_test.go b/services/shell/server_test.go deleted file mode 100644 index b379570aa39..00000000000 --- a/services/shell/server_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package shell_test - -import ( - "context" - "testing" - - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/shell/v1" - "go.viam.com/test" - "go.viam.com/utils/protoutils" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/shell" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -func newServer(sMap map[resource.Name]shell.Service) (pb.ShellServiceServer, error) { - coll, err := resource.NewAPIResourceCollection(shell.API, sMap) - if err != nil { - return nil, err - } - return shell.NewRPCServiceServer(coll).(pb.ShellServiceServer), nil -} - -func TestServerDoCommand(t *testing.T) { - resourceMap := map[resource.Name]shell.Service{ - testSvcName1: &inject.ShellService{ - DoCommandFunc: testutils.EchoFunc, - }, - } - server, err := newServer(resourceMap) - test.That(t, err, test.ShouldBeNil) - - cmd, err := protoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - doCommandRequest := &commonpb.DoCommandRequest{ - Name: testSvcName1.ShortName(), - Command: cmd, - } - doCommandResponse, err := server.DoCommand(context.Background(), doCommandRequest) - test.That(t, err, test.ShouldBeNil) - - // Assert that do command response is an echoed request. - respMap := doCommandResponse.Result.AsMap() - test.That(t, respMap["command"], test.ShouldResemble, "test") - test.That(t, respMap["data"], test.ShouldResemble, 500.0) -} diff --git a/services/shell/testutils/utils_test.go b/services/shell/testutils/utils_test.go deleted file mode 100644 index 1da5795e2b0..00000000000 --- a/services/shell/testutils/utils_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package shelltestutils - -import ( - "testing" - - "go.viam.com/test" -) - -func TestDirectoryContentsEqual(t *testing.T) { - tfs := SetupTestFileSystem(t) - - err := DirectoryContentsEqual(tfs.Root, tfs.SingleFileNested) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not a directory") - - err = DirectoryContentsEqual(tfs.Root, tfs.InnerDir) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "has 8 files") - test.That(t, err.Error(), test.ShouldContainSubstring, "has 4 files") - - test.That(t, DirectoryContentsEqual(tfs.Root, tfs.Root), test.ShouldBeNil) - - tfs2 := SetupTestFileSystem(t, "diff") - err = DirectoryContentsEqual(tfs.Root, tfs2.Root) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "contents not equal") -} diff --git a/services/shell/verify_main_test.go b/services/shell/verify_main_test.go deleted file mode 100644 index e222534539e..00000000000 --- a/services/shell/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package shell - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/slam/client.go b/services/slam/client.go deleted file mode 100644 index 5f07e85f0e4..00000000000 --- a/services/slam/client.go +++ /dev/null @@ -1,134 +0,0 @@ -package slam - -import ( - "context" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - pb "go.viam.com/api/service/slam/v1" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/logging" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// client implements SLAMServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.SLAMServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from the connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Service, error) { - grpcClient := pb.NewSLAMServiceClient(conn) - c := &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: grpcClient, - logger: logger, - } - return c, nil -} - -// Position creates a request, calls the slam service Position, and parses the response into a Pose with a component reference string. -func (c *client) Position(ctx context.Context) (spatialmath.Pose, string, error) { - ctx, span := trace.StartSpan(ctx, "slam::client::Position") - defer span.End() - - req := &pb.GetPositionRequest{ - Name: c.name, - } - - resp, err := c.client.GetPosition(ctx, req) - if err != nil { - return nil, "", err - } - - p := resp.GetPose() - componentReference := resp.GetComponentReference() - - return spatialmath.NewPoseFromProtobuf(p), componentReference, nil -} - -// PointCloudMap creates a request, calls the slam service PointCloudMap and returns a callback -// function which will return the next chunk of the current pointcloud map when called. -func (c *client) PointCloudMap(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - ctx, span := trace.StartSpan(ctx, "slam::client::PointCloudMap") - defer span.End() - - return PointCloudMapCallback(ctx, c.name, c.client, returnEditedMap) -} - -// InternalState creates a request, calls the slam service InternalState and returns a callback -// function which will return the next chunk of the current internal state of the slam algo when called. -func (c *client) InternalState(ctx context.Context) (func() ([]byte, error), error) { - ctx, span := trace.StartSpan(ctx, "slam::client::InternalState") - defer span.End() - - return InternalStateCallback(ctx, c.name, c.client) -} - -// Properties returns information regarding the current SLAM session, including -// if the session is running in the cloud and what mapping mode it is in. -func (c *client) Properties(ctx context.Context) (Properties, error) { - ctx, span := trace.StartSpan(ctx, "slam::client::GetProperties") - defer span.End() - - req := &pb.GetPropertiesRequest{ - Name: c.name, - } - - resp, err := c.client.GetProperties(ctx, req) - if err != nil { - return Properties{}, errors.Wrapf(err, "failure to get properties") - } - - mappingMode, err := protobufToMappingMode(resp.MappingMode) - if err != nil { - return Properties{}, err - } - - sensorInfo := []SensorInfo{} - for _, sInfo := range resp.SensorInfo { - sensorType, err := protobufToSensorType(sInfo.Type) - if err != nil { - return Properties{}, err - } - sensorInfo = append(sensorInfo, SensorInfo{ - Name: sInfo.Name, - Type: sensorType, - }) - } - - var internalStateFileType string - if resp.InternalStateFileType != nil { - internalStateFileType = *resp.InternalStateFileType - } - - prop := Properties{ - CloudSlam: resp.CloudSlam, - MappingMode: mappingMode, - InternalStateFileType: internalStateFileType, - SensorInfo: sensorInfo, - } - return prop, err -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - ctx, span := trace.StartSpan(ctx, "slam::client::DoCommand") - defer span.End() - - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/services/slam/client_test.go b/services/slam/client_test.go deleted file mode 100644 index 15c0ecc2240..00000000000 --- a/services/slam/client_test.go +++ /dev/null @@ -1,399 +0,0 @@ -// Package slam_test client_test.go tests the client for the SLAM service's GRPC server. -package slam_test - -import ( - "bytes" - "context" - "math" - "net" - "os" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/artifact" - "go.viam.com/utils/rpc" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/slam/internal/testhelper" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision" -) - -const ( - nameSucc = "viam" - nameFail = "maiv" - chunkSizeInternalState = 2 - chunkSizePointCloud = 100 -) - -func TestClientWorkingService(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - workingServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - poseSucc := spatial.NewPose(r3.Vector{X: 1, Y: 2, Z: 3}, &spatial.OrientationVector{Theta: math.Pi / 2, OX: 0, OY: 0, OZ: -1}) - componentRefSucc := "cam" - pcSucc := &vision.Object{} - pcSucc.PointCloud = pointcloud.New() - pcdPath := artifact.MustPath("slam/mock_lidar/0.pcd") - pcd, err := os.ReadFile(pcdPath) - test.That(t, err, test.ShouldBeNil) - pcdPathEdited := artifact.MustPath("slam/mock_lidar/1.pcd") - pcdEdited, err := os.ReadFile(pcdPathEdited) - test.That(t, err, test.ShouldBeNil) - - propSucc := slam.Properties{ - CloudSlam: false, - MappingMode: slam.MappingModeNewMap, - InternalStateFileType: ".pbstream", - SensorInfo: []slam.SensorInfo{ - {Name: "my-camera", Type: slam.SensorTypeCamera}, - {Name: "my-movement-sensor", Type: slam.SensorTypeMovementSensor}, - }, - } - - err = pcSucc.PointCloud.Set(pointcloud.NewVector(5, 5, 5), nil) - test.That(t, err, test.ShouldBeNil) - internalStateSucc := []byte{0, 1, 2, 3, 4} - - workingSLAMService := &inject.SLAMService{} - - workingSLAMService.PositionFunc = func(ctx context.Context) (spatial.Pose, string, error) { - return poseSucc, componentRefSucc, nil - } - - workingSLAMService.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - var reader *bytes.Reader - if returnEditedMap { - reader = bytes.NewReader(pcdEdited) - } else { - reader = bytes.NewReader(pcd) - } - clientBuffer := make([]byte, chunkSizePointCloud) - f := func() ([]byte, error) { - n, err := reader.Read(clientBuffer) - if err != nil { - return nil, err - } - return clientBuffer[:n], err - } - return f, nil - } - - workingSLAMService.InternalStateFunc = func(ctx context.Context) (func() ([]byte, error), error) { - reader := bytes.NewReader(internalStateSucc) - clientBuffer := make([]byte, chunkSizeInternalState) - f := func() ([]byte, error) { - n, err := reader.Read(clientBuffer) - if err != nil { - return nil, err - } - - return clientBuffer[:n], err - } - return f, nil - } - - workingSLAMService.PropertiesFunc = func(ctx context.Context) (slam.Properties, error) { - return propSucc, nil - } - - workingSvc, err := resource.NewAPIResourceCollection(slam.API, map[resource.Name]slam.Service{slam.Named(nameSucc): workingSLAMService}) - test.That(t, err, test.ShouldBeNil) - - resourceAPI, ok, err := resource.LookupAPIRegistration[slam.Service](slam.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - resourceAPI.RegisterRPCService(context.Background(), workingServer, workingSvc) - - go workingServer.Serve(listener) - defer workingServer.Stop() - - t.Run("test that context canceled stops client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - //nolint:dupl - t.Run("client tests for using working SLAM client connection", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - workingSLAMClient, err := slam.NewClientFromConn(context.Background(), conn, "", slam.Named(nameSucc), logger) - test.That(t, err, test.ShouldBeNil) - // test position - pose, componentRef, err := workingSLAMClient.Position(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(poseSucc, pose), test.ShouldBeTrue) - test.That(t, componentRef, test.ShouldEqual, componentRefSucc) - - // test point cloud map - fullBytesPCD, err := slam.PointCloudMapFull(context.Background(), workingSLAMClient, false) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, fullBytesPCD, test.ShouldResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, fullBytesPCD, pcd) - - // test point cloud map returning the edited map - fullBytesPCDEdited, err := slam.PointCloudMapFull(context.Background(), workingSLAMClient, true) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, fullBytesPCDEdited, test.ShouldResemble, pcdEdited) - test.That(t, fullBytesPCDEdited, test.ShouldNotResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, fullBytesPCDEdited, pcdEdited) - - // test internal state - fullBytesInternalState, err := slam.InternalStateFull(context.Background(), workingSLAMClient) - test.That(t, err, test.ShouldBeNil) - test.That(t, fullBytesInternalState, test.ShouldResemble, internalStateSucc) - - // test properties - prop, err := workingSLAMClient.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.CloudSlam, test.ShouldBeFalse) - test.That(t, prop.CloudSlam, test.ShouldEqual, propSucc.CloudSlam) - test.That(t, prop.MappingMode, test.ShouldEqual, propSucc.MappingMode) - test.That(t, prop.InternalStateFileType, test.ShouldEqual, propSucc.InternalStateFileType) - test.That(t, prop.SensorInfo, test.ShouldResemble, propSucc.SensorInfo) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - t.Run("client tests using working GRPC dial connection", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingDialedClient, err := slam.NewClientFromConn(context.Background(), conn, "", slam.Named(nameSucc), logger) - test.That(t, err, test.ShouldBeNil) - - // test position - pose, componentRef, err := workingDialedClient.Position(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(poseSucc, pose), test.ShouldBeTrue) - test.That(t, componentRef, test.ShouldEqual, componentRefSucc) - - // test point cloud map - fullBytesPCD, err := slam.PointCloudMapFull(context.Background(), workingDialedClient, false) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, fullBytesPCD, test.ShouldResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, fullBytesPCD, pcd) - - // test point cloud map returning the edited map - fullBytesPCDEdited, err := slam.PointCloudMapFull(context.Background(), workingDialedClient, true) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, fullBytesPCDEdited, test.ShouldResemble, pcdEdited) - test.That(t, fullBytesPCDEdited, test.ShouldNotResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, fullBytesPCDEdited, pcdEdited) - - // test internal state - fullBytesInternalState, err := slam.InternalStateFull(context.Background(), workingDialedClient) - test.That(t, err, test.ShouldBeNil) - test.That(t, fullBytesInternalState, test.ShouldResemble, internalStateSucc) - - // test properties - prop, err := workingDialedClient.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.CloudSlam, test.ShouldBeFalse) - test.That(t, prop.CloudSlam, test.ShouldEqual, propSucc.CloudSlam) - test.That(t, prop.MappingMode, test.ShouldEqual, propSucc.MappingMode) - test.That(t, prop.InternalStateFileType, test.ShouldEqual, propSucc.InternalStateFileType) - test.That(t, prop.SensorInfo, test.ShouldResemble, propSucc.SensorInfo) - - // test do command - workingSLAMService.DoCommandFunc = testutils.EchoFunc - resp, err := workingDialedClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - //nolint:dupl - t.Run("client tests using working GRPC dial connection converted to SLAM client", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - dialedClient, err := resourceAPI.RPCClient(context.Background(), conn, "", slam.Named(nameSucc), logger) - test.That(t, err, test.ShouldBeNil) - - // test position - pose, componentRef, err := dialedClient.Position(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(poseSucc, pose), test.ShouldBeTrue) - test.That(t, componentRef, test.ShouldEqual, componentRefSucc) - - // test point cloud map - fullBytesPCD, err := slam.PointCloudMapFull(context.Background(), dialedClient, false) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, fullBytesPCD, test.ShouldResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, fullBytesPCD, pcd) - - // test point cloud map returning the edited map - fullBytesPCDEdited, err := slam.PointCloudMapFull(context.Background(), dialedClient, true) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, fullBytesPCDEdited, test.ShouldResemble, pcdEdited) - test.That(t, fullBytesPCDEdited, test.ShouldNotResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, fullBytesPCDEdited, pcdEdited) - - // test internal state - fullBytesInternalState, err := slam.InternalStateFull(context.Background(), dialedClient) - test.That(t, err, test.ShouldBeNil) - test.That(t, fullBytesInternalState, test.ShouldResemble, internalStateSucc) - - // test properties - prop, err := dialedClient.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.CloudSlam, test.ShouldBeFalse) - test.That(t, prop.CloudSlam, test.ShouldEqual, propSucc.CloudSlam) - test.That(t, prop.MappingMode, test.ShouldEqual, propSucc.MappingMode) - test.That(t, prop.InternalStateFileType, test.ShouldEqual, propSucc.InternalStateFileType) - test.That(t, prop.SensorInfo, test.ShouldResemble, propSucc.SensorInfo) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} - -func TestFailingClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - - failingServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - pcFail := &vision.Object{} - pcFail.PointCloud = pointcloud.New() - err = pcFail.PointCloud.Set(pointcloud.NewVector(5, 5, 5), nil) - test.That(t, err, test.ShouldBeNil) - - failingSLAMService := &inject.SLAMService{} - - failingSLAMService.PositionFunc = func(ctx context.Context) (spatial.Pose, string, error) { - return nil, "", errors.New("failure to get position") - } - - failingSLAMService.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - return nil, errors.New("failure during get pointcloud map") - } - - failingSLAMService.InternalStateFunc = func(ctx context.Context) (func() ([]byte, error), error) { - return nil, errors.New("failure during get internal state") - } - - failingSLAMService.PropertiesFunc = func(ctx context.Context) (slam.Properties, error) { - return slam.Properties{}, errors.New("failure to get properties") - } - - failingSvc, err := resource.NewAPIResourceCollection(slam.API, map[resource.Name]slam.Service{slam.Named(nameFail): failingSLAMService}) - test.That(t, err, test.ShouldBeNil) - - resourceAPI, ok, err := resource.LookupAPIRegistration[slam.Service](slam.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - resourceAPI.RegisterRPCService(context.Background(), failingServer, failingSvc) - - go failingServer.Serve(listener) - defer failingServer.Stop() - - t.Run("client test using bad SLAM client connection", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - failingSLAMClient, err := slam.NewClientFromConn(context.Background(), conn, "", slam.Named(nameFail), logger) - test.That(t, err, test.ShouldBeNil) - - // testing context cancel for streaming apis - ctx := context.Background() - cancelCtx, cancelFunc := context.WithCancel(ctx) - cancelFunc() - - _, err = failingSLAMClient.PointCloudMap(cancelCtx, false) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "context cancel") - _, err = failingSLAMClient.InternalState(cancelCtx) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "context cancel") - - // test position - pose, componentRef, err := failingSLAMClient.Position(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure to get position") - test.That(t, pose, test.ShouldBeNil) - test.That(t, componentRef, test.ShouldBeEmpty) - - // test pointcloud map - fullBytesPCD, err := slam.PointCloudMapFull(context.Background(), failingSLAMClient, false) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure during get pointcloud map") - test.That(t, fullBytesPCD, test.ShouldBeNil) - - // test internal state - fullBytesInternalState, err := slam.InternalStateFull(context.Background(), failingSLAMClient) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure during get internal state") - test.That(t, fullBytesInternalState, test.ShouldBeNil) - - // test properties - prop, err := failingSLAMClient.Properties(context.Background()) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure to get properties") - test.That(t, prop, test.ShouldResemble, slam.Properties{}) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) - - failingSLAMService.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - f := func() ([]byte, error) { - return nil, errors.New("failure during callback") - } - return f, nil - } - - failingSLAMService.InternalStateFunc = func(ctx context.Context) (func() ([]byte, error), error) { - f := func() ([]byte, error) { - return nil, errors.New("failure during callback") - } - return f, nil - } - - t.Run("client test with failed streaming callback function", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - - failingSLAMClient, err := slam.NewClientFromConn(context.Background(), conn, "", slam.Named(nameFail), logger) - test.That(t, err, test.ShouldBeNil) - - // test pointcloud map - fullBytesPCD, err := slam.PointCloudMapFull(context.Background(), failingSLAMClient, false) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure during callback") - test.That(t, fullBytesPCD, test.ShouldBeNil) - - // test internal state - fullBytesInternalState, err := slam.InternalStateFull(context.Background(), failingSLAMClient) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure during callback") - test.That(t, fullBytesInternalState, test.ShouldBeNil) - - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/slam/collector.go b/services/slam/collector.go deleted file mode 100644 index 41a1ad1a854..00000000000 --- a/services/slam/collector.go +++ /dev/null @@ -1,75 +0,0 @@ -package slam - -import ( - "context" - - pb "go.viam.com/api/service/slam/v1" - "google.golang.org/protobuf/types/known/anypb" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/spatialmath" -) - -type method int64 - -const ( - position method = iota - pointCloudMap -) - -func (m method) String() string { - if m == position { - return "Position" - } - if m == pointCloudMap { - return "PointCloudMap" - } - return "Unknown" -} - -func newPositionCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - slam, err := assertSLAM(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - pose, componentRef, err := slam.Position(ctx) - if err != nil { - return nil, data.FailedToReadErr(params.ComponentName, position.String(), err) - } - return &pb.GetPositionResponse{Pose: spatialmath.PoseToProtobuf(pose), ComponentReference: componentRef}, nil - }) - return data.NewCollector(cFunc, params) -} - -func newPointCloudMapCollector(resource interface{}, params data.CollectorParams) (data.Collector, error) { - slam, err := assertSLAM(resource) - if err != nil { - return nil, err - } - - cFunc := data.CaptureFunc(func(ctx context.Context, _ map[string]*anypb.Any) (interface{}, error) { - // edited maps do not need to be captured because they should not be modified - f, err := slam.PointCloudMap(ctx, false) - if err != nil { - return nil, data.FailedToReadErr(params.ComponentName, pointCloudMap.String(), err) - } - - pcd, err := HelperConcatenateChunksToFull(f) - if err != nil { - return nil, data.FailedToReadErr(params.ComponentName, pointCloudMap.String(), err) - } - - return pcd, nil - }) - return data.NewCollector(cFunc, params) -} - -func assertSLAM(resource interface{}) (Service, error) { - slamService, ok := resource.(Service) - if !ok { - return nil, data.InvalidInterfaceErr(API) - } - return slamService, nil -} diff --git a/services/slam/fake/data_loader.go b/services/slam/fake/data_loader.go deleted file mode 100644 index 9a0d6a8ab93..00000000000 --- a/services/slam/fake/data_loader.go +++ /dev/null @@ -1,116 +0,0 @@ -package fake - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/golang/geo/r3" - "go.viam.com/utils" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/spatialmath" -) - -const chunkSizeBytes = 1 * 1024 * 1024 - -type pose struct { - X float64 `json:"x"` - Y float64 `json:"y"` - Z float64 `json:"z"` -} - -type quat struct { - Imag float64 `json:"imag"` - Jmag float64 `json:"jmag"` - Kmag float64 `json:"kmag"` - Real float64 `json:"real"` -} - -type extra struct { - Quat quat `json:"quat"` -} - -type position struct { - Pose pose `json:"pose"` - ComponentReference string `json:"component_reference"` - Extra extra `json:"extra"` -} - -var maxDataCount = 24 - -const ( - internalStateTemplate = "%s/internal_state/internal_state_%d.pbstream" - pcdTemplate = "%s/pointcloud/pointcloud_%d.pcd" - positionTemplate = "%s/position/position_%d.json" -) - -func fakePointCloudMap(ctx context.Context, datasetDir string, slamSvc *SLAM) (func() ([]byte, error), error) { - path := filepath.Clean(artifact.MustPath(fmt.Sprintf(pcdTemplate, datasetDir, slamSvc.getCount()))) - slamSvc.logger.CDebug(ctx, "Reading "+path) - file, err := os.Open(path) - if err != nil { - return nil, err - } - chunk := make([]byte, chunkSizeBytes) - f := func() ([]byte, error) { - bytesRead, err := file.Read(chunk) - if err != nil { - defer utils.UncheckedErrorFunc(file.Close) - return nil, err - } - return chunk[:bytesRead], err - } - return f, nil -} - -func fakeInternalState(ctx context.Context, datasetDir string, slamSvc *SLAM) (func() ([]byte, error), error) { - path := filepath.Clean(artifact.MustPath(fmt.Sprintf(internalStateTemplate, datasetDir, slamSvc.getCount()))) - slamSvc.logger.CDebug(ctx, "Reading "+path) - file, err := os.Open(path) - if err != nil { - return nil, err - } - chunk := make([]byte, chunkSizeBytes) - f := func() ([]byte, error) { - bytesRead, err := file.Read(chunk) - if err != nil { - defer utils.UncheckedErrorFunc(file.Close) - return nil, err - } - return chunk[:bytesRead], err - } - return f, nil -} - -func fakePosition(ctx context.Context, datasetDir string, slamSvc *SLAM) (spatialmath.Pose, string, error) { - path := filepath.Clean(artifact.MustPath(fmt.Sprintf(positionTemplate, datasetDir, slamSvc.getCount()))) - slamSvc.logger.CDebug(ctx, "Reading "+path) - data, err := os.ReadFile(path) - if err != nil { - return nil, "", err - } - - position, err := positionFromJSON(data) - if err != nil { - return nil, "", err - } - p := r3.Vector{X: position.Pose.X, Y: position.Pose.Y, Z: position.Pose.Z} - - quat := position.Extra.Quat - orientation := &spatialmath.Quaternion{Real: quat.Real, Imag: quat.Imag, Jmag: quat.Jmag, Kmag: quat.Kmag} - pose := spatialmath.NewPose(p, orientation) - - return pose, position.ComponentReference, nil -} - -func positionFromJSON(data []byte) (position, error) { - position := position{} - - if err := json.Unmarshal(data, &position); err != nil { - return position, err - } - return position, nil -} diff --git a/services/slam/fake/slam.go b/services/slam/fake/slam.go deleted file mode 100644 index c70315c947d..00000000000 --- a/services/slam/fake/slam.go +++ /dev/null @@ -1,131 +0,0 @@ -// Package fake implements a fake slam service -package fake - -import ( - "bytes" - "context" - "time" - - "go.opencensus.io/trace" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/spatialmath" -) - -var model = resource.DefaultModelFamily.WithModel("fake") - -const datasetDirectory = "slam/example_cartographer_outputs/viam-office-02-22-3" - -func init() { - resource.RegisterService( - slam.API, - model, - resource.Registration[slam.Service, resource.NoNativeConfig]{ - Constructor: func( - ctx context.Context, - _ resource.Dependencies, - conf resource.Config, - logger logging.Logger, - ) (slam.Service, error) { - return NewSLAM(conf.ResourceName(), logger), nil - }, - }, - ) -} - -// SLAM is a fake slam that returns generic data. -type SLAM struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - dataCount int - logger logging.Logger - mapTimestamp time.Time -} - -// NewSLAM is a constructor for a fake slam service. -func NewSLAM(name resource.Name, logger logging.Logger) *SLAM { - return &SLAM{ - Named: name.AsNamed(), - logger: logger, - dataCount: -1, - mapTimestamp: time.Now().UTC(), - } -} - -func (slamSvc *SLAM) getCount() int { - if slamSvc.dataCount < 0 { - return 0 - } - return slamSvc.dataCount -} - -// Position returns a Pose and a component reference string of the robot's current location according to SLAM. -func (slamSvc *SLAM) Position(ctx context.Context) (spatialmath.Pose, string, error) { - ctx, span := trace.StartSpan(ctx, "slam::fake::Position") - defer span.End() - return fakePosition(ctx, datasetDirectory, slamSvc) -} - -// PointCloudMap returns a callback function which will return the next chunk of the current pointcloud -// map. -func (slamSvc *SLAM) PointCloudMap(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - ctx, span := trace.StartSpan(ctx, "slam::fake::PointCloudMap") - defer span.End() - slamSvc.incrementDataCount() - return fakePointCloudMap(ctx, datasetDirectory, slamSvc) -} - -// InternalState returns a callback function which will return the next chunk of the current internal -// state of the slam algo. -func (slamSvc *SLAM) InternalState(ctx context.Context) (func() ([]byte, error), error) { - ctx, span := trace.StartSpan(ctx, "slam::fake::InternalState") - defer span.End() - return fakeInternalState(ctx, datasetDirectory, slamSvc) -} - -// Properties returns the mapping mode of the slam service as well as a boolean indicating if it is running -// in the cloud or locally. In the case of fake slam, it will return that the service is being run locally -// and is creating a new map. -func (slamSvc *SLAM) Properties(ctx context.Context) (slam.Properties, error) { - _, span := trace.StartSpan(ctx, "slam::fake::Properties") - defer span.End() - - prop := slam.Properties{ - CloudSlam: false, - MappingMode: slam.MappingModeNewMap, - InternalStateFileType: ".pbstream", - SensorInfo: []slam.SensorInfo{ - {Name: "my-camera", Type: slam.SensorTypeCamera}, - {Name: "my-movement-sensor", Type: slam.SensorTypeMovementSensor}, - }, - } - return prop, nil -} - -// incrementDataCount is not thread safe but that is ok as we only intend a single user to be interacting -// with it at a time. -func (slamSvc *SLAM) incrementDataCount() { - slamSvc.dataCount = ((slamSvc.dataCount + 1) % maxDataCount) -} - -// Limits returns the bounds of the slam map as a list of referenceframe.Limits. -func (slamSvc *SLAM) Limits(ctx context.Context, useEditedMap bool) ([]referenceframe.Limit, error) { - data, err := slam.PointCloudMapFull(ctx, slamSvc, useEditedMap) - if err != nil { - return nil, err - } - dims, err := pointcloud.GetPCDMetaData(bytes.NewReader(data)) - if err != nil { - return nil, err - } - - return []referenceframe.Limit{ - {Min: dims.MinX, Max: dims.MaxX}, - {Min: dims.MinY, Max: dims.MaxY}, - }, nil -} diff --git a/services/slam/fake/slam_test.go b/services/slam/fake/slam_test.go deleted file mode 100644 index 2ddb5eaa854..00000000000 --- a/services/slam/fake/slam_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package fake - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - "path/filepath" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/spatialmath" -) - -func TestFakeSLAMPosition(t *testing.T) { - expectedComponentReference := "" - slamSvc := NewSLAM(slam.Named("test"), logging.NewTestLogger(t)) - - p, componentReference, err := slamSvc.Position(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, componentReference, test.ShouldEqual, expectedComponentReference) - - // spatialmath.PoseAlmostEqual is used here as tiny differences were observed - // in floating point values between M1 mac & arm64 linux which - // were causing tests to pass on M1 mac but fail on ci. - expectedPose := spatialmath.NewPose( - r3.Vector{X: 5.921536787524187, Y: 13.296696037491639, Z: 0.0000000000000}, - &spatialmath.Quaternion{Real: 0.9999997195238413, Imag: 0, Jmag: 0, Kmag: 0.0007489674483818071}) - test.That(t, spatialmath.PoseAlmostEqual(p, expectedPose), test.ShouldBeTrue) - - p2, componentReference, err := slamSvc.Position(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, componentReference, test.ShouldEqual, expectedComponentReference) - test.That(t, p, test.ShouldResemble, p2) -} - -func TestFakeProperties(t *testing.T) { - slamSvc := NewSLAM(slam.Named("test"), logging.NewTestLogger(t)) - - prop, err := slamSvc.Properties(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, prop.CloudSlam, test.ShouldBeFalse) - test.That(t, prop.MappingMode, test.ShouldEqual, slam.MappingModeNewMap) - test.That(t, prop.InternalStateFileType, test.ShouldEqual, ".pbstream") - test.That(t, prop.SensorInfo, test.ShouldResemble, - []slam.SensorInfo{ - {Name: "my-camera", Type: slam.SensorTypeCamera}, - {Name: "my-movement-sensor", Type: slam.SensorTypeMovementSensor}, - }, - ) -} - -func TestFakeSLAMStateful(t *testing.T) { - t.Run("Test getting a PCD map via streaming APIs advances the test data", func(t *testing.T) { - orgMaxDataCount := maxDataCount - defer func() { - maxDataCount = orgMaxDataCount - }() - // maxDataCount lowered under test to reduce test runtime - maxDataCount = 5 - slamSvc := &SLAM{Named: slam.Named("test").AsNamed(), logger: logging.NewTestLogger(t)} - verifyPointCloudMapStateful(t, slamSvc) - }) -} - -func TestFakeSLAMInternalState(t *testing.T) { - testName := "Returns a callback function which, returns the current fake internal state in chunks" - t.Run(testName, func(t *testing.T) { - slamSvc := NewSLAM(slam.Named("test"), logging.NewTestLogger(t)) - - path := filepath.Clean(artifact.MustPath(fmt.Sprintf(internalStateTemplate, datasetDirectory, slamSvc.getCount()))) - expectedData, err := os.ReadFile(path) - test.That(t, err, test.ShouldBeNil) - internalStateFunc, err := slamSvc.InternalState(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, internalStateFunc, test.ShouldNotBeNil) - data := getDataFromStream(t, internalStateFunc) - - test.That(t, len(data), test.ShouldBeGreaterThan, 0) - test.That(t, data, test.ShouldResemble, expectedData) - - internalStateFunc2, err := slamSvc.InternalState(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, internalStateFunc2, test.ShouldNotBeNil) - data2 := getDataFromStream(t, internalStateFunc2) - test.That(t, len(data2), test.ShouldBeGreaterThan, 0) - test.That(t, data, test.ShouldResemble, data2) - test.That(t, data2, test.ShouldResemble, expectedData) - }) -} - -func TestFakeSLAMPointMap(t *testing.T) { - testName := "Returns a callback function which, returns the current fake pointcloud map state in chunks and advances the dataset" - t.Run(testName, func(t *testing.T) { - slamSvc := NewSLAM(slam.Named("test"), logging.NewTestLogger(t)) - - pointCloudFunc, err := slamSvc.PointCloudMap(context.Background(), false) - test.That(t, err, test.ShouldBeNil) - test.That(t, pointCloudFunc, test.ShouldNotBeNil) - data := getDataFromStream(t, pointCloudFunc) - test.That(t, len(data), test.ShouldBeGreaterThan, 0) - - path := filepath.Clean(artifact.MustPath(fmt.Sprintf(pcdTemplate, datasetDirectory, slamSvc.getCount()))) - expectedData, err := os.ReadFile(path) - test.That(t, err, test.ShouldBeNil) - - test.That(t, data, test.ShouldResemble, expectedData) - - pointCloudFunc2, err := slamSvc.PointCloudMap(context.Background(), false) - test.That(t, err, test.ShouldBeNil) - test.That(t, pointCloudFunc, test.ShouldNotBeNil) - data2 := getDataFromStream(t, pointCloudFunc2) - test.That(t, len(data2), test.ShouldBeGreaterThan, 0) - - path2 := filepath.Clean(artifact.MustPath(fmt.Sprintf(pcdTemplate, datasetDirectory, slamSvc.getCount()))) - expectedData2, err := os.ReadFile(path2) - test.That(t, err, test.ShouldBeNil) - - test.That(t, data2, test.ShouldResemble, expectedData2) - // Doesn't resemble as every call returns the next data set. - test.That(t, data, test.ShouldNotResemble, data2) - }) -} - -func getDataFromStream(t *testing.T, f func() ([]byte, error)) []byte { - data, err := helperConcatenateChunksToFull(f) - test.That(t, err, test.ShouldBeNil) - return data -} - -func reverse[T any](slice []T) []T { - for i := len(slice)/2 - 1; i >= 0; i-- { - opp := len(slice) - 1 - i - slice[i], slice[opp] = slice[opp], slice[i] - } - return slice -} - -func verifyPointCloudMapStateful(t *testing.T, slamSvc *SLAM) { - testDataCount := maxDataCount - getPointCloudMapResults := []float64{} - getPositionResults := []spatialmath.Pose{} - getInternalStateResults := []int{} - - // Call GetPointCloudMap twice for every testData artifact - for i := 0; i < testDataCount*2; i++ { - f, err := slamSvc.PointCloudMap(context.Background(), false) - test.That(t, err, test.ShouldBeNil) - test.That(t, f, test.ShouldNotBeNil) - pcd, err := helperConcatenateChunksToFull(f) - test.That(t, err, test.ShouldBeNil) - pc, err := pointcloud.ReadPCD(bytes.NewReader(pcd)) - test.That(t, err, test.ShouldBeNil) - - getPointCloudMapResults = append(getPointCloudMapResults, pc.MetaData().MaxX) - test.That(t, err, test.ShouldBeNil) - - p, _, err := slamSvc.Position(context.Background()) - test.That(t, err, test.ShouldBeNil) - getPositionResults = append(getPositionResults, p) - - f, err = slamSvc.InternalState(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, f, test.ShouldNotBeNil) - internalState, err := helperConcatenateChunksToFull(f) - test.That(t, err, test.ShouldBeNil) - getInternalStateResults = append(getInternalStateResults, len(internalState)) - } - - getPositionResultsFirst := getPositionResults[len(getPositionResults)/2:] - getPositionResultsLast := getPositionResults[:len(getPositionResults)/2] - - getInternalStateResultsFirst := getInternalStateResults[len(getInternalStateResults)/2:] - getInternalStateResultsLast := getInternalStateResults[:len(getInternalStateResults)/2] - - getPointCloudMapResultsFirst := getPointCloudMapResults[len(getPointCloudMapResults)/2:] - getPointCloudMapResultsLast := getPointCloudMapResults[:len(getPointCloudMapResults)/2] - - // Confirm that the first half of the - // results equal the last. - // This proves that each call to GetPointCloudMap - // advances the test data (both for GetPointCloudMap & other endpoints) - // over a dataset of size maxDataCount that loops around. - test.That(t, getPositionResultsFirst, test.ShouldResemble, getPositionResultsLast) - test.That(t, getInternalStateResultsFirst, test.ShouldResemble, getInternalStateResultsLast) - test.That(t, getPointCloudMapResultsFirst, test.ShouldResemble, getPointCloudMapResultsLast) - - // Confirm that the first half of the - // results do NOT equal the last half in reverse. - // This proves that each call to GetPointCloudMap - // advances the test data (both for GetPointCloudMap & other endpoints) - // over a dataset of size maxDataCount that loops around. - test.That(t, getPositionResultsFirst, test.ShouldNotResemble, reverse(getPositionResultsLast)) - test.That(t, getInternalStateResultsFirst, test.ShouldNotResemble, reverse(getInternalStateResultsLast)) - test.That(t, getPointCloudMapResultsFirst, test.ShouldNotResemble, reverse(getPointCloudMapResultsLast)) -} - -func helperConcatenateChunksToFull(f func() ([]byte, error)) ([]byte, error) { - var fullBytes []byte - for { - chunk, err := f() - if errors.Is(err, io.EOF) { - return fullBytes, nil - } - if err != nil { - return nil, err - } - - fullBytes = append(fullBytes, chunk...) - } -} diff --git a/services/slam/grpchelper.go b/services/slam/grpchelper.go deleted file mode 100644 index 66becc7be53..00000000000 --- a/services/slam/grpchelper.go +++ /dev/null @@ -1,115 +0,0 @@ -package slam - -import ( - "context" - - "github.com/pkg/errors" - pb "go.viam.com/api/service/slam/v1" -) - -// PointCloudMapCallback helps a client request the point cloud stream from a SLAM server, -// returning a callback function for accessing the stream data. -func PointCloudMapCallback(ctx context.Context, name string, slamClient pb.SLAMServiceClient, returnEditedMap bool) ( - func() ([]byte, error), error, -) { - req := &pb.GetPointCloudMapRequest{Name: name, ReturnEditedMap: &returnEditedMap} - - // If the target gRPC server returns an error status, this call doesn't return an error. - // Instead, the error status will be returned to the first call to resp.Recv(). - // This call only returns an error if the connection to the target gRPC server can't be established, is canceled, etc. - resp, err := slamClient.GetPointCloudMap(ctx, req) - if err != nil { - return nil, errors.Wrap(err, "error getting the pointcloud map from the SLAM client") - } - - f := func() ([]byte, error) { - chunk, err := resp.Recv() - if err != nil { - return nil, errors.Wrap(err, "error receiving pointcloud chunk") - } - - return chunk.GetPointCloudPcdChunk(), err - } - - return f, nil -} - -// InternalStateCallback helps a client request the internal state stream from a SLAM server, -// returning a callback function for accessing the stream data. -func InternalStateCallback(ctx context.Context, name string, slamClient pb.SLAMServiceClient) (func() ([]byte, error), error) { - req := &pb.GetInternalStateRequest{Name: name} - - // If the target gRPC server returns an error status, this call doesn't return an error. - // Instead, the error status will be returned to the first call to resp.Recv(). - // This call only returns an error if the connection to the target gRPC server can't be established, is canceled, etc. - resp, err := slamClient.GetInternalState(ctx, req) - if err != nil { - return nil, errors.Wrap(err, "error getting the internal state from the SLAM client") - } - - f := func() ([]byte, error) { - chunk, err := resp.Recv() - if err != nil { - return nil, errors.Wrap(err, "error receiving internal state chunk") - } - - return chunk.GetInternalStateChunk(), nil - } - return f, err -} - -// mappingModeToProtobuf converts a MappingMode value to a protobuf MappingMode value. -func mappingModeToProtobuf(mappingMode MappingMode) pb.MappingMode { - switch mappingMode { - case MappingModeNewMap: - return pb.MappingMode_MAPPING_MODE_CREATE_NEW_MAP - case MappingModeLocalizationOnly: - return pb.MappingMode_MAPPING_MODE_LOCALIZE_ONLY - case MappingModeUpdateExistingMap: - return pb.MappingMode_MAPPING_MODE_UPDATE_EXISTING_MAP - default: - return pb.MappingMode_MAPPING_MODE_UNSPECIFIED - } -} - -// protobufToMappingMode converts protobuf MappingMode value to a MappingMode value. -func protobufToMappingMode(mappingMode pb.MappingMode) (MappingMode, error) { - switch mappingMode { - case pb.MappingMode_MAPPING_MODE_CREATE_NEW_MAP: - return MappingModeNewMap, nil - case pb.MappingMode_MAPPING_MODE_LOCALIZE_ONLY: - return MappingModeLocalizationOnly, nil - case pb.MappingMode_MAPPING_MODE_UPDATE_EXISTING_MAP: - return MappingModeUpdateExistingMap, nil - case pb.MappingMode_MAPPING_MODE_UNSPECIFIED: - fallthrough - default: - return 0, errors.New("mapping mode unspecified") - } -} - -// sensorTypeToProtobuf converts a SensorType value to a protobuf SensorType value. -func sensorTypeToProtobuf(sensorType SensorType) pb.SensorType { - switch sensorType { - case SensorTypeCamera: - return pb.SensorType_SENSOR_TYPE_CAMERA - case SensorTypeMovementSensor: - return pb.SensorType_SENSOR_TYPE_MOVEMENT_SENSOR - default: - return pb.SensorType_SENSOR_TYPE_UNSPECIFIED - } -} - -// protobufToSensorType converts protobuf SensorType value to a SensorType value. -func protobufToSensorType(sensorType pb.SensorType) (SensorType, error) { - switch sensorType { - case pb.SensorType_SENSOR_TYPE_CAMERA: - return SensorTypeCamera, nil - case pb.SensorType_SENSOR_TYPE_MOVEMENT_SENSOR: - return SensorTypeMovementSensor, nil - case pb.SensorType_SENSOR_TYPE_UNSPECIFIED: - fallthrough - default: - return 0, errors.New("sensor type unspecified") - } -} diff --git a/services/slam/internal/testhelper/testhelper.go b/services/slam/internal/testhelper/testhelper.go deleted file mode 100644 index aebe041205f..00000000000 --- a/services/slam/internal/testhelper/testhelper.go +++ /dev/null @@ -1,30 +0,0 @@ -// Package testhelper implements a slam service definition with additional exported functions for -// the purpose of testing -package testhelper - -import ( - "bytes" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/pointcloud" -) - -// TestComparePointCloudsFromPCDs is a helper function for checking GetPointCloudMapFull response along with associated pcd validity checks. -func TestComparePointCloudsFromPCDs(t *testing.T, pcdInput, pcdOutput []byte) { - pcInput, err := pointcloud.ReadPCD(bytes.NewReader(pcdInput)) - test.That(t, err, test.ShouldBeNil) - pcOutput, err := pointcloud.ReadPCD(bytes.NewReader(pcdOutput)) - test.That(t, err, test.ShouldBeNil) - - test.That(t, pcInput.MetaData(), test.ShouldResemble, pcOutput.MetaData()) - - pcInput.Iterate(0, 0, func(p r3.Vector, d pointcloud.Data) bool { - dOutput, ok := pcOutput.At(p.X, p.Y, p.Z) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, dOutput, test.ShouldResemble, d) - return true - }) -} diff --git a/services/slam/register/register.go b/services/slam/register/register.go deleted file mode 100644 index 2a702eec8eb..00000000000 --- a/services/slam/register/register.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package register registers all relevant slam models and also API specific functions -package register - -import ( - // for slam models. - _ "go.viam.com/rdk/services/slam/fake" -) diff --git a/services/slam/server.go b/services/slam/server.go deleted file mode 100644 index a87021fa40c..00000000000 --- a/services/slam/server.go +++ /dev/null @@ -1,177 +0,0 @@ -package slam - -import ( - "context" - "io" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/slam/v1" - - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// serviceServer implements the SLAMService from the slam proto. -type serviceServer struct { - pb.UnimplementedSLAMServiceServer - coll resource.APIResourceCollection[Service] -} - -// NewRPCServiceServer constructs a the slam gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Service]) interface{} { - return &serviceServer{coll: coll} -} - -// GetPosition returns a Pose and a component reference string of the robot's current location according to SLAM. -func (server *serviceServer) GetPosition(ctx context.Context, req *pb.GetPositionRequest) ( - *pb.GetPositionResponse, error, -) { - ctx, span := trace.StartSpan(ctx, "slam::server::GetPosition") - defer span.End() - - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - p, componentReference, err := svc.Position(ctx) - if err != nil { - return nil, err - } - - return &pb.GetPositionResponse{ - Pose: spatialmath.PoseToProtobuf(p), - ComponentReference: componentReference, - }, nil -} - -// GetPointCloudMap returns the slam service's slam algo's current map state in PCD format as -// a stream of byte chunks. -func (server *serviceServer) GetPointCloudMap(req *pb.GetPointCloudMapRequest, - stream pb.SLAMService_GetPointCloudMapServer, -) error { - ctx := context.Background() - - ctx, span := trace.StartSpan(ctx, "slam::server::GetPointCloudMap") - defer span.End() - - svc, err := server.coll.Resource(req.Name) - if err != nil { - return err - } - returnEditedMap := req.ReturnEditedMap != nil && *req.ReturnEditedMap - - f, err := svc.PointCloudMap(ctx, returnEditedMap) - if err != nil { - return errors.Wrap(err, "getting callback function from PointCloudMap encountered an issue") - } - - // In the future, channel buffer could be used here to optimize for latency - for { - rawChunk, err := f() - - if errors.Is(err, io.EOF) { - return nil - } - - if err != nil { - return errors.Wrap(err, "getting data from callback function encountered an issue") - } - - chunk := &pb.GetPointCloudMapResponse{PointCloudPcdChunk: rawChunk} - if err := stream.Send(chunk); err != nil { - return err - } - } -} - -// GetInternalState returns the internal state of the slam service's slam algo in a stream of -// byte chunks. -func (server *serviceServer) GetInternalState(req *pb.GetInternalStateRequest, - stream pb.SLAMService_GetInternalStateServer, -) error { - ctx := context.Background() - ctx, span := trace.StartSpan(ctx, "slam::server::GetInternalState") - defer span.End() - - svc, err := server.coll.Resource(req.Name) - if err != nil { - return err - } - - f, err := svc.InternalState(ctx) - if err != nil { - return err - } - - // In the future, channel buffer could be used here to optimize for latency - for { - rawChunk, err := f() - - if errors.Is(err, io.EOF) { - return nil - } - - if err != nil { - return errors.Wrap(err, "getting data from callback function encountered an issue") - } - - chunk := &pb.GetInternalStateResponse{InternalStateChunk: rawChunk} - if err := stream.Send(chunk); err != nil { - return err - } - } -} - -// GetProperties returns the mapping mode and of the slam process and whether it is being done locally -// or in the cloud. -func (server *serviceServer) GetProperties(ctx context.Context, req *pb.GetPropertiesRequest) ( - *pb.GetPropertiesResponse, error, -) { - ctx, span := trace.StartSpan(ctx, "slam::server::GetProperties") - defer span.End() - - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - - prop, err := svc.Properties(ctx) - if err != nil { - return nil, err - } - - sensorInfo := []*pb.SensorInfo{} - for _, sInfo := range prop.SensorInfo { - sensorType := sensorTypeToProtobuf(sInfo.Type) - sensorInfo = append(sensorInfo, &pb.SensorInfo{ - Name: sInfo.Name, - Type: sensorType, - }) - } - - return &pb.GetPropertiesResponse{ - CloudSlam: prop.CloudSlam, - MappingMode: mappingModeToProtobuf(prop.MappingMode), - InternalStateFileType: &prop.InternalStateFileType, - SensorInfo: sensorInfo, - }, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - ctx, span := trace.StartSpan(ctx, "slam::server::DoCommand") - defer span.End() - - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, svc, req) -} diff --git a/services/slam/server_test.go b/services/slam/server_test.go deleted file mode 100644 index 5f7c324d4fe..00000000000 --- a/services/slam/server_test.go +++ /dev/null @@ -1,403 +0,0 @@ -// Package slam_test server_test.go tests the SLAM service's GRPC server. -package slam_test - -import ( - "bytes" - "context" - "errors" - "math" - "os" - "testing" - - "github.com/golang/geo/r3" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/slam/v1" - "go.viam.com/test" - "go.viam.com/utils/artifact" - "go.viam.com/utils/protoutils" - "google.golang.org/grpc" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/services/slam/internal/testhelper" - spatial "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" -) - -const ( - testSlamServiceName = "slam1" - testSlamServiceName2 = "slam2" - chunkSizeServer = 100 -) - -// Create mock server that satisfies the pb.SLAMService_GetPointCloudMapServer contract. -type pointCloudServerMock struct { - grpc.ServerStream - rawBytes []byte -} -type internalStateServerMock struct { - grpc.ServerStream - rawBytes []byte -} - -func makePointCloudServerMock() *pointCloudServerMock { - return &pointCloudServerMock{} -} - -// Concatenate received messages into single slice. -func (m *pointCloudServerMock) Send(chunk *pb.GetPointCloudMapResponse) error { - m.rawBytes = append(m.rawBytes, chunk.PointCloudPcdChunk...) - return nil -} - -func makeInternalStateServerMock() *internalStateServerMock { - return &internalStateServerMock{} -} - -// Concatenate received messages into single slice. -func (m *internalStateServerMock) Send(chunk *pb.GetInternalStateResponse) error { - m.rawBytes = append(m.rawBytes, chunk.InternalStateChunk...) - return nil -} - -func TestWorkingServer(t *testing.T) { - injectSvc := &inject.SLAMService{} - resourceMap := map[resource.Name]slam.Service{ - slam.Named(testSlamServiceName): injectSvc, - } - injectAPISvc, err := resource.NewAPIResourceCollection(slam.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - slamServer := slam.NewRPCServiceServer(injectAPISvc).(pb.SLAMServiceServer) - cloudPath := artifact.MustPath("slam/mock_lidar/0.pcd") - cloudPathEdited := artifact.MustPath("slam/mock_lidar/1.pcd") - pcd, err := os.ReadFile(cloudPath) - test.That(t, err, test.ShouldBeNil) - pcdEdited, err := os.ReadFile(cloudPathEdited) - test.That(t, err, test.ShouldBeNil) - - t.Run("working GetPosition", func(t *testing.T) { - poseSucc := spatial.NewPose(r3.Vector{X: 1, Y: 2, Z: 3}, &spatial.OrientationVector{Theta: math.Pi / 2, OX: 0, OY: 0, OZ: -1}) - componentRefSucc := "cam" - - injectSvc.PositionFunc = func(ctx context.Context) (spatial.Pose, string, error) { - return poseSucc, componentRefSucc, nil - } - - reqPos := &pb.GetPositionRequest{ - Name: testSlamServiceName, - } - respPos, err := slamServer.GetPosition(context.Background(), reqPos) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(poseSucc, spatial.NewPoseFromProtobuf(respPos.Pose)), test.ShouldBeTrue) - test.That(t, respPos.ComponentReference, test.ShouldEqual, componentRefSucc) - }) - - t.Run("working GetPointCloudMap", func(t *testing.T) { - injectSvc.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - var reader *bytes.Reader - if returnEditedMap { - reader = bytes.NewReader(pcdEdited) - } else { - reader = bytes.NewReader(pcd) - } - - serverBuffer := make([]byte, chunkSizeServer) - f := func() ([]byte, error) { - n, err := reader.Read(serverBuffer) - if err != nil { - return nil, err - } - - return serverBuffer[:n], err - } - - return f, nil - } - - reqPointCloudMap := &pb.GetPointCloudMapRequest{Name: testSlamServiceName, ReturnEditedMap: nil} - mockServer := makePointCloudServerMock() - err = slamServer.GetPointCloudMap(reqPointCloudMap, mockServer) - test.That(t, err, test.ShouldBeNil) - - // comparing raw bytes to ensure order is correct - test.That(t, mockServer.rawBytes, test.ShouldResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, mockServer.rawBytes, pcd) - }) - t.Run("working GetPointCloudMap with an edited map", func(t *testing.T) { - injectSvc.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - var reader *bytes.Reader - if returnEditedMap { - reader = bytes.NewReader(pcdEdited) - } else { - reader = bytes.NewReader(pcd) - } - - serverBuffer := make([]byte, chunkSizeServer) - f := func() ([]byte, error) { - n, err := reader.Read(serverBuffer) - if err != nil { - return nil, err - } - - return serverBuffer[:n], err - } - - return f, nil - } - returnEditedMap := true - reqPointCloudMap := &pb.GetPointCloudMapRequest{Name: testSlamServiceName, ReturnEditedMap: &returnEditedMap} - mockServer := makePointCloudServerMock() - err = slamServer.GetPointCloudMap(reqPointCloudMap, mockServer) - test.That(t, err, test.ShouldBeNil) - - // comparing raw bytes to ensure order is correct - test.That(t, mockServer.rawBytes, test.ShouldResemble, pcdEdited) - test.That(t, mockServer.rawBytes, test.ShouldNotResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, mockServer.rawBytes, pcdEdited) - }) - - t.Run("working GetInternalState", func(t *testing.T) { - internalStateSucc := []byte{0, 1, 2, 3, 4} - chunkSizeInternalState := 2 - injectSvc.InternalStateFunc = func(ctx context.Context) (func() ([]byte, error), error) { - reader := bytes.NewReader(internalStateSucc) - f := func() ([]byte, error) { - serverBuffer := make([]byte, chunkSizeInternalState) - n, err := reader.Read(serverBuffer) - if err != nil { - return nil, err - } - - return serverBuffer[:n], err - } - return f, nil - } - - req := &pb.GetInternalStateRequest{ - Name: testSlamServiceName, - } - mockServer := makeInternalStateServerMock() - err := slamServer.GetInternalState(req, mockServer) - test.That(t, err, test.ShouldBeNil) - test.That(t, mockServer.rawBytes, test.ShouldResemble, internalStateSucc) - }) - - t.Run("working GetProperties", func(t *testing.T) { - prop := slam.Properties{ - CloudSlam: false, - MappingMode: slam.MappingModeNewMap, - InternalStateFileType: ".pbstream", - SensorInfo: []slam.SensorInfo{ - {Name: "my-camera", Type: slam.SensorTypeCamera}, - {Name: "my-movement-sensor", Type: slam.SensorTypeMovementSensor}, - }, - } - injectSvc.PropertiesFunc = func(ctx context.Context) (slam.Properties, error) { - return prop, nil - } - - reqInfo := &pb.GetPropertiesRequest{ - Name: testSlamServiceName, - } - - sensorInfoSuccess := []*pb.SensorInfo{ - {Name: prop.SensorInfo[0].Name, Type: pb.SensorType_SENSOR_TYPE_CAMERA}, - {Name: prop.SensorInfo[1].Name, Type: pb.SensorType_SENSOR_TYPE_MOVEMENT_SENSOR}, - } - - respInfo, err := slamServer.GetProperties(context.Background(), reqInfo) - test.That(t, err, test.ShouldBeNil) - test.That(t, respInfo.CloudSlam, test.ShouldResemble, prop.CloudSlam) - test.That(t, respInfo.MappingMode, test.ShouldEqual, pb.MappingMode_MAPPING_MODE_CREATE_NEW_MAP) - test.That(t, *respInfo.InternalStateFileType, test.ShouldEqual, prop.InternalStateFileType) - test.That(t, respInfo.SensorInfo, test.ShouldResemble, sensorInfoSuccess) - }) - - t.Run("Multiple services Valid", func(t *testing.T) { - resourceMap = map[resource.Name]slam.Service{ - slam.Named(testSlamServiceName): injectSvc, - slam.Named(testSlamServiceName2): injectSvc, - } - injectAPISvc, err := resource.NewAPIResourceCollection(slam.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - slamServer = slam.NewRPCServiceServer(injectAPISvc).(pb.SLAMServiceServer) - poseSucc := spatial.NewPose(r3.Vector{X: 1, Y: 2, Z: 3}, &spatial.OrientationVector{Theta: math.Pi / 2, OX: 0, OY: 0, OZ: -1}) - componentRefSucc := "cam" - - injectSvc.PositionFunc = func(ctx context.Context) (spatial.Pose, string, error) { - return poseSucc, componentRefSucc, nil - } - - injectSvc.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - reader := bytes.NewReader(pcd) - serverBuffer := make([]byte, chunkSizeServer) - f := func() ([]byte, error) { - n, err := reader.Read(serverBuffer) - if err != nil { - return nil, err - } - - return serverBuffer[:n], err - } - return f, nil - } - // test unary endpoint using GetPosition - reqPos := &pb.GetPositionRequest{Name: testSlamServiceName} - respPos, err := slamServer.GetPosition(context.Background(), reqPos) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(poseSucc, spatial.NewPoseFromProtobuf(respPos.Pose)), test.ShouldBeTrue) - test.That(t, respPos.ComponentReference, test.ShouldEqual, componentRefSucc) - - reqPos = &pb.GetPositionRequest{Name: testSlamServiceName2} - respPos, err = slamServer.GetPosition(context.Background(), reqPos) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatial.PoseAlmostEqual(poseSucc, spatial.NewPoseFromProtobuf(respPos.Pose)), test.ShouldBeTrue) - test.That(t, respPos.ComponentReference, test.ShouldEqual, componentRefSucc) - - // test streaming endpoint using GetPointCloudMap - reqGetPointCloudMap := &pb.GetPointCloudMapRequest{Name: testSlamServiceName} - mockServer1 := makePointCloudServerMock() - err = slamServer.GetPointCloudMap(reqGetPointCloudMap, mockServer1) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, mockServer1.rawBytes, test.ShouldResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, mockServer1.rawBytes, pcd) - - reqGetPointCloudMap = &pb.GetPointCloudMapRequest{Name: testSlamServiceName2} - mockServer2 := makePointCloudServerMock() - err = slamServer.GetPointCloudMap(reqGetPointCloudMap, mockServer2) - test.That(t, err, test.ShouldBeNil) - // comparing raw bytes to ensure order is correct - test.That(t, mockServer2.rawBytes, test.ShouldResemble, pcd) - // comparing pointclouds to ensure PCDs are correct - testhelper.TestComparePointCloudsFromPCDs(t, mockServer2.rawBytes, pcd) - }) -} - -func TestFailingServer(t *testing.T) { - injectSvc := &inject.SLAMService{} - resourceMap := map[resource.Name]slam.Service{ - slam.Named(testSlamServiceName): injectSvc, - } - injectAPISvc, err := resource.NewAPIResourceCollection(slam.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - slamServer := slam.NewRPCServiceServer(injectAPISvc).(pb.SLAMServiceServer) - - t.Run("failing GetPosition", func(t *testing.T) { - injectSvc.PositionFunc = func(ctx context.Context) (spatial.Pose, string, error) { - return nil, "", errors.New("failure to get position") - } - - req := &pb.GetPositionRequest{ - Name: testSlamServiceName, - } - resp, err := slamServer.GetPosition(context.Background(), req) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure to get position") - test.That(t, resp, test.ShouldBeNil) - }) - - t.Run("failing GetPointCloudMap", func(t *testing.T) { - // PointCloudMapFunc failure - injectSvc.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - return nil, errors.New("failure to get pointcloud map") - } - - reqPointCloudMap := &pb.GetPointCloudMapRequest{Name: testSlamServiceName} - - mockServer := makePointCloudServerMock() - err = slamServer.GetPointCloudMap(reqPointCloudMap, mockServer) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure to get pointcloud map") - - // Callback failure - injectSvc.PointCloudMapFunc = func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - f := func() ([]byte, error) { - return []byte{}, errors.New("callback error") - } - return f, nil - } - - mockServer = makePointCloudServerMock() - err = slamServer.GetPointCloudMap(reqPointCloudMap, mockServer) - test.That(t, err.Error(), test.ShouldContainSubstring, "callback error") - }) - - t.Run("failing GetInternalState", func(t *testing.T) { - // InternalStateFunc error - injectSvc.InternalStateFunc = func(ctx context.Context) (func() ([]byte, error), error) { - return nil, errors.New("failure to get internal state") - } - - req := &pb.GetInternalStateRequest{Name: testSlamServiceName} - mockServer := makeInternalStateServerMock() - err := slamServer.GetInternalState(req, mockServer) - test.That(t, err.Error(), test.ShouldContainSubstring, "failure to get internal state") - - // Callback failure - injectSvc.InternalStateFunc = func(ctx context.Context) (func() ([]byte, error), error) { - f := func() ([]byte, error) { - return []byte{}, errors.New("callback error") - } - return f, nil - } - - err = slamServer.GetInternalState(req, mockServer) - test.That(t, err.Error(), test.ShouldContainSubstring, "callback error") - }) - - t.Run("failing GetProperties", func(t *testing.T) { - injectSvc.PropertiesFunc = func(ctx context.Context) (slam.Properties, error) { - return slam.Properties{}, errors.New("failure to get properties") - } - reqInfo := &pb.GetPropertiesRequest{Name: testSlamServiceName} - - respInfo, err := slamServer.GetProperties(context.Background(), reqInfo) - test.That(t, err, test.ShouldBeError, errors.New("failure to get properties")) - test.That(t, respInfo, test.ShouldBeNil) - }) - - injectAPISvc, _ = resource.NewAPIResourceCollection(slam.API, map[resource.Name]slam.Service{}) - slamServer = slam.NewRPCServiceServer(injectAPISvc).(pb.SLAMServiceServer) - t.Run("failing on nonexistent server", func(t *testing.T) { - // test unary endpoint using GetPosition - reqGetPositionRequest := &pb.GetPositionRequest{Name: testSlamServiceName} - respPosNew, err := slamServer.GetPosition(context.Background(), reqGetPositionRequest) - test.That(t, respPosNew, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(slam.Named(testSlamServiceName))) - - // test streaming endpoint using GetPointCloudMap - mockPointCloudServer := makePointCloudServerMock() - getPointCloudMapReq := &pb.GetPointCloudMapRequest{Name: testSlamServiceName} - err = slamServer.GetPointCloudMap(getPointCloudMapReq, mockPointCloudServer) - test.That(t, err, test.ShouldBeError, resource.NewNotFoundError(slam.Named(testSlamServiceName))) - }) -} - -func TestServerDoCommand(t *testing.T) { - testSvcName1 := slam.Named("svc1") - resourceMap := map[resource.Name]slam.Service{ - testSvcName1: &inject.SLAMService{ - DoCommandFunc: testutils.EchoFunc, - }, - } - injectAPISvc, err := resource.NewAPIResourceCollection(slam.API, resourceMap) - test.That(t, err, test.ShouldBeNil) - server := slam.NewRPCServiceServer(injectAPISvc).(pb.SLAMServiceServer) - - cmd, err := protoutils.StructToStructPb(testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - doCommandRequest := &commonpb.DoCommandRequest{ - Name: testSvcName1.ShortName(), - Command: cmd, - } - doCommandResponse, err := server.DoCommand(context.Background(), doCommandRequest) - test.That(t, err, test.ShouldBeNil) - - // Assert that do command response is an echoed request. - respMap := doCommandResponse.Result.AsMap() - test.That(t, respMap["command"], test.ShouldResemble, "test") - test.That(t, respMap["data"], test.ShouldResemble, 500.0) -} diff --git a/services/slam/slam.go b/services/slam/slam.go deleted file mode 100644 index 98f2983bb96..00000000000 --- a/services/slam/slam.go +++ /dev/null @@ -1,159 +0,0 @@ -// Package slam implements simultaneous localization and mapping. -// This is an Experimental package. -package slam - -import ( - "bytes" - "context" - "io" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - pb "go.viam.com/api/service/slam/v1" - - "go.viam.com/rdk/data" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/spatialmath" -) - -// TBD 05/04/2022: Needs more work once GRPC is included (future PR). -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Service]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: pb.RegisterSLAMServiceHandlerFromEndpoint, - RPCServiceDesc: &pb.SLAMService_ServiceDesc, - RPCClient: NewClientFromConn, - }) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: position.String(), - }, newPositionCollector) - data.RegisterCollector(data.MethodMetadata{ - API: API, - MethodName: pointCloudMap.String(), - }, newPointCloudMapCollector) -} - -// SubtypeName is the name of the type of service. -const ( - SubtypeName = "slam" - MappingModeNewMap = MappingMode(iota) - MappingModeLocalizationOnly - MappingModeUpdateExistingMap -) - -// SensorTypeCamera is a camera sensor. -const ( - SensorTypeCamera = SensorType(iota) - SensorTypeMovementSensor -) - -// API is a variable that identifies the slam resource API. -var API = resource.APINamespaceRDK.WithServiceType(SubtypeName) - -// MappingMode describes what mapping mode the slam service is in, including -// creating a new map, localizing on an existing map or updating an existing map. -type MappingMode uint8 - -// SensorType describes what sensor type the sensor is, including -// camera or movement sensor. -type SensorType uint8 - -// SensorInfo holds information about the sensor name and sensor type. -type SensorInfo struct { - Name string - Type SensorType -} - -// Properties returns various information regarding the current slam service, -// including whether the slam process is running in the cloud and its mapping mode. -type Properties struct { - CloudSlam bool - MappingMode MappingMode - InternalStateFileType string - SensorInfo []SensorInfo -} - -// Named is a helper for getting the named service's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named SLAM service from the given Robot. -func FromRobot(r robot.Robot, name string) (Service, error) { - return robot.ResourceFromRobot[Service](r, Named(name)) -} - -// FromDependencies is a helper for getting the named SLAM service from a collection of -// dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Service, error) { - return resource.FromDependencies[Service](deps, Named(name)) -} - -// Service describes the functions that are available to the service. -type Service interface { - resource.Resource - Position(ctx context.Context) (spatialmath.Pose, string, error) - PointCloudMap(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) - InternalState(ctx context.Context) (func() ([]byte, error), error) - Properties(ctx context.Context) (Properties, error) -} - -// HelperConcatenateChunksToFull concatenates the chunks from a streamed grpc endpoint. -func HelperConcatenateChunksToFull(f func() ([]byte, error)) ([]byte, error) { - var fullBytes []byte - for { - chunk, err := f() - if errors.Is(err, io.EOF) { - return fullBytes, nil - } - if err != nil { - return nil, err - } - - fullBytes = append(fullBytes, chunk...) - } -} - -// PointCloudMapFull concatenates the streaming responses from PointCloudMap into a full point cloud. -func PointCloudMapFull(ctx context.Context, slamSvc Service, returnEditedMap bool) ([]byte, error) { - ctx, span := trace.StartSpan(ctx, "slam::PointCloudMapFull") - defer span.End() - callback, err := slamSvc.PointCloudMap(ctx, returnEditedMap) - if err != nil { - return nil, err - } - return HelperConcatenateChunksToFull(callback) -} - -// InternalStateFull concatenates the streaming responses from InternalState into -// the internal serialized state of the slam algorithm. -func InternalStateFull(ctx context.Context, slamSvc Service) ([]byte, error) { - ctx, span := trace.StartSpan(ctx, "slam::InternalStateFull") - defer span.End() - callback, err := slamSvc.InternalState(ctx) - if err != nil { - return nil, err - } - return HelperConcatenateChunksToFull(callback) -} - -// Limits returns the bounds of the slam map as a list of referenceframe.Limits. -func Limits(ctx context.Context, svc Service, useEditedMap bool) ([]referenceframe.Limit, error) { - data, err := PointCloudMapFull(ctx, svc, useEditedMap) - if err != nil { - return nil, err - } - dims, err := pointcloud.GetPCDMetaData(bytes.NewReader(data)) - if err != nil { - return nil, err - } - - return []referenceframe.Limit{ - {Min: dims.MinX, Max: dims.MaxX}, - {Min: dims.MinY, Max: dims.MaxY}, - }, nil -} diff --git a/services/slam/verify_main_test.go b/services/slam/verify_main_test.go deleted file mode 100644 index f7d7896ffa8..00000000000 --- a/services/slam/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package slam - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/vision/client.go b/services/vision/client.go deleted file mode 100644 index a4c3c5e4ccf..00000000000 --- a/services/vision/client.go +++ /dev/null @@ -1,250 +0,0 @@ -package vision - -import ( - "bytes" - "context" - "fmt" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/vision/v1" - "go.viam.com/utils/protoutils" - "go.viam.com/utils/rpc" - - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - rprotoutils "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" - "go.viam.com/rdk/vision/classification" - objdet "go.viam.com/rdk/vision/objectdetection" -) - -// client implements VisionServiceClient. -type client struct { - resource.Named - resource.TriviallyReconfigurable - resource.TriviallyCloseable - name string - client pb.VisionServiceClient - logger logging.Logger -} - -// NewClientFromConn constructs a new Client from connection passed in. -func NewClientFromConn( - ctx context.Context, - conn rpc.ClientConn, - remoteName string, - name resource.Name, - logger logging.Logger, -) (Service, error) { - grpcClient := pb.NewVisionServiceClient(conn) - c := &client{ - Named: name.PrependRemote(remoteName).AsNamed(), - name: name.ShortName(), - client: grpcClient, - logger: logger, - } - return c, nil -} - -func (c *client) DetectionsFromCamera( - ctx context.Context, - cameraName string, - extra map[string]interface{}, -) ([]objdet.Detection, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::client::DetectionsFromCamera") - defer span.End() - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetDetectionsFromCamera(ctx, &pb.GetDetectionsFromCameraRequest{ - Name: c.name, - CameraName: cameraName, - Extra: ext, - }) - if err != nil { - return nil, err - } - detections := make([]objdet.Detection, 0, len(resp.Detections)) - for _, d := range resp.Detections { - if d.XMin == nil || d.XMax == nil || d.YMin == nil || d.YMax == nil { - return nil, fmt.Errorf("invalid detection %+v", d) - } - box := image.Rect(int(*d.XMin), int(*d.YMin), int(*d.XMax), int(*d.YMax)) - det := objdet.NewDetection(box, d.Confidence, d.ClassName) - detections = append(detections, det) - } - return detections, nil -} - -func (c *client) Detections(ctx context.Context, img image.Image, extra map[string]interface{}, -) ([]objdet.Detection, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::client::Detections") - defer span.End() - if img == nil { - return nil, errors.New("nil image input to given client.Detections") - } - mimeType := gostream.MIMETypeHint(ctx, utils.MimeTypeJPEG) - imgBytes, err := rimage.EncodeImage(ctx, img, mimeType) - if err != nil { - return nil, err - } - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetDetections(ctx, &pb.GetDetectionsRequest{ - Name: c.name, - Image: imgBytes, - Width: int64(img.Bounds().Dx()), - Height: int64(img.Bounds().Dy()), - MimeType: mimeType, - Extra: ext, - }) - if err != nil { - return nil, err - } - detections := make([]objdet.Detection, 0, len(resp.Detections)) - for _, d := range resp.Detections { - if d.XMin == nil || d.XMax == nil || d.YMin == nil || d.YMax == nil { - return nil, fmt.Errorf("invalid detection %+v", d) - } - box := image.Rect(int(*d.XMin), int(*d.YMin), int(*d.XMax), int(*d.YMax)) - det := objdet.NewDetection(box, d.Confidence, d.ClassName) - detections = append(detections, det) - } - return detections, nil -} - -func (c *client) ClassificationsFromCamera( - ctx context.Context, - cameraName string, - n int, - extra map[string]interface{}, -) (classification.Classifications, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::client::ClassificationsFromCamera") - defer span.End() - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetClassificationsFromCamera(ctx, &pb.GetClassificationsFromCameraRequest{ - Name: c.name, - CameraName: cameraName, - N: int32(n), - Extra: ext, - }) - if err != nil { - return nil, err - } - classifications := make([]classification.Classification, 0, len(resp.Classifications)) - for _, c := range resp.Classifications { - classif := classification.NewClassification(c.Confidence, c.ClassName) - classifications = append(classifications, classif) - } - return classifications, nil -} - -func (c *client) Classifications(ctx context.Context, img image.Image, - n int, extra map[string]interface{}, -) (classification.Classifications, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::client::Classifications") - defer span.End() - if img == nil { - return nil, errors.New("nil image input to given client.Classifications") - } - mimeType := gostream.MIMETypeHint(ctx, utils.MimeTypeJPEG) - imgBytes, err := rimage.EncodeImage(ctx, img, mimeType) - if err != nil { - return nil, err - } - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetClassifications(ctx, &pb.GetClassificationsRequest{ - Name: c.name, - Image: imgBytes, - Width: int32(img.Bounds().Dx()), - Height: int32(img.Bounds().Dy()), - MimeType: mimeType, - N: int32(n), - Extra: ext, - }) - if err != nil { - return nil, err - } - classifications := make([]classification.Classification, 0, len(resp.Classifications)) - for _, c := range resp.Classifications { - classif := classification.NewClassification(c.Confidence, c.ClassName) - classifications = append(classifications, classif) - } - return classifications, nil -} - -func (c *client) GetObjectPointClouds( - ctx context.Context, - cameraName string, - extra map[string]interface{}, -) ([]*vision.Object, error) { - ext, err := protoutils.StructToStructPb(extra) - if err != nil { - return nil, err - } - resp, err := c.client.GetObjectPointClouds(ctx, &pb.GetObjectPointCloudsRequest{ - Name: c.name, - CameraName: cameraName, - MimeType: utils.MimeTypePCD, - Extra: ext, - }) - if err != nil { - return nil, err - } - - if resp.MimeType != utils.MimeTypePCD { - return nil, fmt.Errorf("unknown pc mime type %s", resp.MimeType) - } - return protoToObjects(resp.Objects) -} - -func protoToObjects(pco []*commonpb.PointCloudObject) ([]*vision.Object, error) { - objects := make([]*vision.Object, len(pco)) - for i, o := range pco { - pc, err := pointcloud.ReadPCD(bytes.NewReader(o.PointCloud)) - if err != nil { - return nil, err - } - // Sets the label to the first non-empty label of any geometry; defaults to the empty string. - label := func() string { - for _, g := range o.Geometries.GetGeometries() { - if g.GetLabel() != "" { - return g.GetLabel() - } - } - return "" - }() - if len(o.Geometries.Geometries) >= 1 { - objects[i], err = vision.NewObjectWithLabel(pc, label, o.Geometries.GetGeometries()[0]) - } else { - objects[i], err = vision.NewObjectWithLabel(pc, label, nil) - } - if err != nil { - return nil, err - } - } - return objects, nil -} - -func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::client::DoCommand") - defer span.End() - - return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd) -} diff --git a/services/vision/client_test.go b/services/vision/client_test.go deleted file mode 100644 index b339af8837c..00000000000 --- a/services/vision/client_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package vision_test - -import ( - "context" - "image" - "net" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/rpc" - - viamgrpc "go.viam.com/rdk/grpc" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - _ "go.viam.com/rdk/services/register" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision/objectdetection" -) - -var visName1 = vision.Named("vision1") - -func TestClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - srv := &inject.VisionService{} - srv.DetectionsFunc = func(ctx context.Context, img image.Image, extra map[string]interface{}) ([]objectdetection.Detection, error) { - det1 := objectdetection.NewDetection(image.Rect(5, 10, 15, 20), 0.5, "yes") - return []objectdetection.Detection{det1}, nil - } - srv.DetectionsFromCameraFunc = func( - ctx context.Context, - camName string, - extra map[string]interface{}, - ) ([]objectdetection.Detection, error) { - det1 := objectdetection.NewDetection(image.Rect(0, 0, 10, 20), 0.8, "camera") - return []objectdetection.Detection{det1}, nil - } - test.That(t, err, test.ShouldBeNil) - m := map[resource.Name]vision.Service{ - vision.Named(testVisionServiceName): srv, - } - svc, err := resource.NewAPIResourceCollection(vision.API, m) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[vision.Service](vision.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Failing client", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(context.Background()) - cancel() - _, err = viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "canceled") - }) - - t.Run("get detections from img", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := vision.NewClientFromConn(context.Background(), conn, "", vision.Named(testVisionServiceName), logger) - test.That(t, err, test.ShouldBeNil) - - dets, err := client.Detections(context.Background(), image.NewRGBA(image.Rect(0, 0, 10, 20)), nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, dets, test.ShouldNotBeNil) - test.That(t, dets[0].Label(), test.ShouldResemble, "yes") - test.That(t, dets[0].Score(), test.ShouldEqual, 0.5) - box := dets[0].BoundingBox() - test.That(t, box.Min, test.ShouldResemble, image.Point{5, 10}) - test.That(t, box.Max, test.ShouldResemble, image.Point{15, 20}) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) - t.Run("get detections from cam", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client, err := vision.NewClientFromConn(context.Background(), conn, "", vision.Named(testVisionServiceName), logger) - test.That(t, err, test.ShouldBeNil) - - dets, err := client.DetectionsFromCamera(context.Background(), "fake_cam", nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, dets, test.ShouldHaveLength, 1) - test.That(t, dets[0].Label(), test.ShouldEqual, "camera") - test.That(t, dets[0].Score(), test.ShouldEqual, 0.8) - box := dets[0].BoundingBox() - test.That(t, box.Min, test.ShouldResemble, image.Point{0, 0}) - test.That(t, box.Max, test.ShouldResemble, image.Point{10, 20}) - - test.That(t, client.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} - -func TestInjectedServiceClient(t *testing.T) { - logger := logging.NewTestLogger(t) - listener1, err := net.Listen("tcp", "localhost:0") - test.That(t, err, test.ShouldBeNil) - rpcServer, err := rpc.NewServer(logger.AsZap(), rpc.WithUnauthenticated()) - test.That(t, err, test.ShouldBeNil) - - injectVision := &inject.VisionService{} - osMap := map[resource.Name]vision.Service{ - visName1: injectVision, - } - svc, err := resource.NewAPIResourceCollection(vision.API, osMap) - test.That(t, err, test.ShouldBeNil) - resourceAPI, ok, err := resource.LookupAPIRegistration[vision.Service](vision.API) - test.That(t, err, test.ShouldBeNil) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, svc), test.ShouldBeNil) - - go rpcServer.Serve(listener1) - defer rpcServer.Stop() - - t.Run("Do Command", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - workingDialedClient, err := vision.NewClientFromConn(context.Background(), conn, "", vision.Named(testVisionServiceName), logger) - test.That(t, err, test.ShouldBeNil) - injectVision.DoCommandFunc = testutils.EchoFunc - resp, err := workingDialedClient.DoCommand(context.Background(), testutils.TestCommand) - test.That(t, err, test.ShouldBeNil) - test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"]) - test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"]) - - test.That(t, workingDialedClient.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }) -} diff --git a/services/vision/colordetector/color_detector.go b/services/vision/colordetector/color_detector.go deleted file mode 100644 index 468ad484c49..00000000000 --- a/services/vision/colordetector/color_detector.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package colordetector uses a heuristic based on hue and connected components to create -// bounding boxes around objects of a specified color. -package colordetector - -import ( - "context" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/utils" - objdet "go.viam.com/rdk/vision/objectdetection" -) - -var model = resource.DefaultModelFamily.WithModel("color_detector") - -func init() { - resource.RegisterService(vision.API, model, resource.Registration[vision.Service, *objdet.ColorDetectorConfig]{ - DeprecatedRobotConstructor: func( - ctx context.Context, r any, c resource.Config, logger logging.Logger, - ) (vision.Service, error) { - attrs, err := resource.NativeConfig[*objdet.ColorDetectorConfig](c) - if err != nil { - return nil, err - } - actualR, err := utils.AssertType[robot.Robot](r) - if err != nil { - return nil, err - } - return registerColorDetector(ctx, c.ResourceName(), attrs, actualR) - }, - }) -} - -// registerColorDetector creates a new Color Detector from the config. -func registerColorDetector( - ctx context.Context, - name resource.Name, - conf *objdet.ColorDetectorConfig, - r robot.Robot, -) (vision.Service, error) { - _, span := trace.StartSpan(ctx, "service::vision::registerColorDetector") - defer span.End() - if conf == nil { - return nil, errors.New("object detection config for color detector cannot be nil") - } - detector, err := objdet.NewColorDetector(conf) - if err != nil { - return nil, errors.Wrapf(err, "error registering color detector %q", name) - } - return vision.NewService(name, r, nil, nil, detector, nil) -} diff --git a/services/vision/colordetector/color_detector_test.go b/services/vision/colordetector/color_detector_test.go deleted file mode 100644 index eb68ad3b0c9..00000000000 --- a/services/vision/colordetector/color_detector_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package colordetector - -import ( - "context" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision/objectdetection" -) - -func TestColorDetector(t *testing.T) { - inp := objectdetection.ColorDetectorConfig{ - SegmentSize: 150000, - HueTolerance: 0.44, - DetectColorString: "#4F3815", - } - ctx := context.Background() - r := &inject.Robot{} - name := vision.Named("test_cd") - srv, err := registerColorDetector(ctx, name, &inp, r) - test.That(t, err, test.ShouldBeNil) - test.That(t, srv.Name(), test.ShouldResemble, name) - img, err := rimage.NewImageFromFile(artifact.MustPath("vision/objectdetection/detection_test.jpg")) - test.That(t, err, test.ShouldBeNil) - - // Does implement Detections - det, err := srv.Detections(ctx, img, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, det, test.ShouldHaveLength, 1) - - // Does not implement Classifications - _, err = srv.Classifications(ctx, img, 1, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not implement") - - // with error - bad parameters - inp.HueTolerance = 4.0 // value out of range - _, err = registerColorDetector(ctx, name, &inp, r) - test.That(t, err.Error(), test.ShouldContainSubstring, "hue_tolerance_pct must be between") - - // with error - nil parameters - _, err = registerColorDetector(ctx, name, nil, r) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot be nil") -} diff --git a/services/vision/detectionstosegments/detections_to_3dsegments.go b/services/vision/detectionstosegments/detections_to_3dsegments.go deleted file mode 100644 index b7766578665..00000000000 --- a/services/vision/detectionstosegments/detections_to_3dsegments.go +++ /dev/null @@ -1,69 +0,0 @@ -// Package detectionstosegments uses a 2D segmenter and a camera that can project its images -// to 3D to project the bounding boxes to 3D in order to created a segmented point cloud. -package detectionstosegments - -import ( - "context" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/objectdetection" - "go.viam.com/rdk/vision/segmentation" -) - -var model = resource.DefaultModelFamily.WithModel("detector_3d_segmenter") - -func init() { - resource.RegisterService(vision.API, model, resource.Registration[vision.Service, *segmentation.DetectionSegmenterConfig]{ - DeprecatedRobotConstructor: func( - ctx context.Context, r any, c resource.Config, logger logging.Logger, - ) (vision.Service, error) { - attrs, err := resource.NativeConfig[*segmentation.DetectionSegmenterConfig](c) - if err != nil { - return nil, err - } - actualR, err := utils.AssertType[robot.Robot](r) - if err != nil { - return nil, err - } - return register3DSegmenterFromDetector(ctx, c.ResourceName(), attrs, actualR) - }, - }) -} - -// register3DSegmenterFromDetector creates a 3D segmenter from a previously registered detector. -func register3DSegmenterFromDetector( - ctx context.Context, - name resource.Name, - conf *segmentation.DetectionSegmenterConfig, - r robot.Robot, -) (vision.Service, error) { - _, span := trace.StartSpan(ctx, "service::vision::register3DSegmenterFromDetector") - defer span.End() - if conf == nil { - return nil, errors.New("config for 3D segmenter made from a detector cannot be nil") - } - detectorService, err := vision.FromRobot(r, conf.DetectorName) - if err != nil { - return nil, errors.Wrapf(err, "could not find necessary dependency, detector %q", conf.DetectorName) - } - confThresh := 0.5 // default value - if conf.ConfidenceThresh > 0.0 { - confThresh = conf.ConfidenceThresh - } - detector := func(ctx context.Context, img image.Image) ([]objectdetection.Detection, error) { - return detectorService.Detections(ctx, img, nil) - } - segmenter, err := segmentation.DetectionSegmenter(objectdetection.Detector(detector), conf.MeanK, conf.Sigma, confThresh) - if err != nil { - return nil, errors.Wrap(err, "cannot create 3D segmenter from detector") - } - return vision.NewService(name, r, nil, nil, detector, segmenter) -} diff --git a/services/vision/detectionstosegments/detections_to_3dsegments_test.go b/services/vision/detectionstosegments/detections_to_3dsegments_test.go deleted file mode 100644 index 9891b9ad5ae..00000000000 --- a/services/vision/detectionstosegments/detections_to_3dsegments_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package detectionstosegments - -import ( - "context" - "image" - "image/color" - "testing" - "time" - - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/components/camera" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision/objectdetection" - "go.viam.com/rdk/vision/segmentation" -) - -type simpleDetector struct{} - -func (s *simpleDetector) Detect(context.Context, image.Image) ([]objectdetection.Detection, error) { - det1 := objectdetection.NewDetection(image.Rect(10, 10, 20, 20), 0.5, "yes") - return []objectdetection.Detection{det1}, nil -} - -func Test3DSegmentsFromDetector(t *testing.T) { - r := &inject.Robot{} - m := &simpleDetector{} - name := vision.Named("testDetector") - svc, err := vision.NewService(name, r, nil, nil, m.Detect, nil) - test.That(t, err, test.ShouldBeNil) - cam := &inject.Camera{} - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return nil, errors.New("no pointcloud") - } - cam.ImagesFunc = func(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - return nil, resource.ResponseMetadata{}, errors.New("no images") - } - cam.ProjectorFunc = func(ctx context.Context) (transform.Projector, error) { - return &transform.ParallelProjection{}, nil - } - r.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{camera.Named("fakeCamera"), name} - } - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "fakeCamera": - return cam, nil - case "testDetector": - return svc, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - params := &segmentation.DetectionSegmenterConfig{ - DetectorName: "testDetector", - ConfidenceThresh: 0.2, - } - // bad registration, no parameters - name2 := vision.Named("test_seg") - _, err = register3DSegmenterFromDetector(context.Background(), name2, nil, r) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot be nil") - // bad registration, no such detector - params.DetectorName = "noDetector" - _, err = register3DSegmenterFromDetector(context.Background(), name2, params, r) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "could not find necessary dependency") - // successful registration - params.DetectorName = "testDetector" - name3 := vision.Named("test_rcs") - seg, err := register3DSegmenterFromDetector(context.Background(), name3, params, r) - test.That(t, err, test.ShouldBeNil) - test.That(t, seg.Name(), test.ShouldResemble, name3) - - // fails on not finding camera - _, err = seg.GetObjectPointClouds(context.Background(), "no_camera", map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - // fails since camera cannot return images - _, err = seg.GetObjectPointClouds(context.Background(), "fakeCamera", map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "no images") - - // successful, creates one object with some points in it - cam.ImagesFunc = func(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - img := rimage.NewImage(150, 150) - dm := rimage.NewEmptyDepthMap(150, 150) - dm.Set(0, 0, rimage.Depth(5)) - dm.Set(0, 100, rimage.Depth(6)) - dm.Set(50, 0, rimage.Depth(8)) - dm.Set(50, 100, rimage.Depth(4)) - dm.Set(15, 15, rimage.Depth(3)) - dm.Set(16, 14, rimage.Depth(10)) - imgs := []camera.NamedImage{{img, "color"}, {dm, "depth"}} - return imgs, resource.ResponseMetadata{CapturedAt: time.Now()}, nil - } - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - cloud := pc.New() - err = cloud.Set(pc.NewVector(0, 0, 5), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(0, 100, 6), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(50, 0, 8), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(50, 100, 4), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(15, 15, 3), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(16, 14, 10), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - return cloud, nil - } - objects, err := seg.GetObjectPointClouds(context.Background(), "fakeCamera", map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(objects), test.ShouldEqual, 1) - test.That(t, objects[0].Size(), test.ShouldEqual, 2) - // does implement detector - dets, err := seg.Detections(context.Background(), nil, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(dets), test.ShouldEqual, 1) - // does not implement classifier - _, err = seg.Classifications(context.Background(), nil, 1, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not implement") -} diff --git a/services/vision/mlvision/classifier.go b/services/vision/mlvision/classifier.go deleted file mode 100644 index ce885c93c08..00000000000 --- a/services/vision/mlvision/classifier.go +++ /dev/null @@ -1,183 +0,0 @@ -package mlvision - -import ( - "context" - "image" - "strconv" - "strings" - "sync" - - "github.com/nfnt/resize" - "github.com/pkg/errors" - "gorgonia.org/tensor" - - "go.viam.com/rdk/ml" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/services/mlmodel" - "go.viam.com/rdk/vision/classification" -) - -const ( - classifierProbabilityName = "probability" - classifierInputName = "image" -) - -func attemptToBuildClassifier(mlm mlmodel.Service, - inNameMap, outNameMap *sync.Map, - params *MLModelConfig, -) (classification.Classifier, error) { - md, err := mlm.Metadata(context.Background()) - if err != nil { - return nil, errors.New("could not get any metadata") - } - - // Set up input type, height, width, and labels - var inHeight, inWidth int - if len(md.Inputs) < 1 { - return nil, errors.New("no input tensors received") - } - inType := md.Inputs[0].DataType - labels := getLabelsFromMetadata(md) - if shapeLen := len(md.Inputs[0].Shape); shapeLen < 4 { - return nil, errors.Errorf("invalid length of shape array (expected 4, got %d)", shapeLen) - } - channelsFirst := false // if channelFirst is true, then shape is (1, 3, height, width) - if shape := md.Inputs[0].Shape; getIndex(shape, 3) == 1 { - channelsFirst = true - inHeight, inWidth = shape[2], shape[3] - } else { - inHeight, inWidth = shape[1], shape[2] - } - // creates postprocessor to filter on labels and confidences - postprocessor := createClassificationFilter(params.DefaultConfidence, params.LabelConfidenceMap) - - return func(ctx context.Context, img image.Image) (classification.Classifications, error) { - origW, origH := img.Bounds().Dx(), img.Bounds().Dy() - resizeW := inWidth - if resizeW == -1 { - resizeW = origW - } - resizeH := inHeight - if resizeH == -1 { - resizeH = origH - } - resized := img - if (origW != resizeW) || (origH != resizeH) { - resized = resize.Resize(uint(resizeW), uint(resizeH), img, resize.Bilinear) - } - inputName := classifierInputName - if mapName, ok := inNameMap.Load(inputName); ok { - if name, ok := mapName.(string); ok { - inputName = name - } - } - inMap := ml.Tensors{} - switch inType { - case UInt8: - inMap[inputName] = tensor.New( - tensor.WithShape(1, resized.Bounds().Dy(), resized.Bounds().Dx(), 3), - tensor.WithBacking(rimage.ImageToUInt8Buffer(resized, params.IsBGR)), - ) - case Float32: - inMap[inputName] = tensor.New( - tensor.WithShape(1, resized.Bounds().Dy(), resized.Bounds().Dx(), 3), - tensor.WithBacking(rimage.ImageToFloatBuffer(resized, params.IsBGR, params.MeanValue, params.StdDev)), - ) - default: - return nil, errors.Errorf("invalid input type of %s. try uint8 or float32", inType) - } - if channelsFirst { - err := inMap[inputName].T(0, 3, 1, 2) - if err != nil { - return nil, errors.New("could not transponse tensor of input image") - } - err = inMap[inputName].Transpose() - if err != nil { - return nil, errors.New("could not transponse the data of the tensor of input image") - } - } - outMap, err := mlm.Infer(ctx, inMap) - if err != nil { - return nil, err - } - - // check if output tensor name that classifier is looking for is already present - // in the nameMap. If not, find the probability name, and cache it in the nameMap - pName, ok := outNameMap.Load(classifierProbabilityName) - if !ok { - _, ok := outMap[classifierProbabilityName] - if !ok { - if len(outMap) == 1 { - for name := range outMap { // only 1 element in map, assume its probabilities - outNameMap.Store(classifierProbabilityName, name) - pName = name - } - } - } else { - outNameMap.Store(classifierProbabilityName, classifierProbabilityName) - pName = classifierProbabilityName - } - } - probabilityName, ok := pName.(string) - if !ok { - return nil, errors.Errorf("name map did not store a string of the tensor name, but an object of type %T instead", pName) - } - data, ok := outMap[probabilityName] - if !ok { - return nil, errors.Errorf("no tensor named 'probability' among output tensors [%s]", strings.Join(tensorNames(outMap), ", ")) - } - probs, err := convertToFloat64Slice(data.Data()) - if err != nil { - return nil, err - } - confs := checkClassificationScores(probs) - if labels != nil && len(labels) != len(confs) { - return nil, errors.New("length of output expected to be length of label list (but is not)") - } - classifications := make(classification.Classifications, 0, len(confs)) - for i := 0; i < len(confs); i++ { - if labels == nil { - classifications = append(classifications, classification.NewClassification(confs[i], strconv.Itoa(i))) - } else { - if i >= len(labels) { - return nil, errors.Errorf("cannot access label number %v from label file with %v labels", i, len(labels)) - } - classifications = append(classifications, classification.NewClassification(confs[i], labels[i])) - } - } - if postprocessor != nil { - classifications = postprocessor(classifications) - } - return classifications, nil - }, nil -} - -// In the case that the model provided is not a classifier, attemptToBuildClassifier will return a -// classifier function that function fails because the expected keys are not in the outputTensor. -// use checkIfClassifierWorks to get sample output tensors on gray image so we know if the functions -// returned from attemptToBuildClassifier will fail ahead of time. -func checkIfClassifierWorks(ctx context.Context, cf classification.Classifier) error { - if cf == nil { - return errors.New("nil classifier function") - } - - // test image to check if the classifier function works - img := image.NewGray(image.Rectangle{Min: image.Point{0, 0}, Max: image.Point{5, 5}}) - - _, err := cf(ctx, img) - if err != nil { - return errors.Wrap(err, "cannot use model as a classifier") - } - return nil -} - -// createClassificationFilter creates a post processor function that filters on the outputs of the model. -func createClassificationFilter(minConf float64, labelMap map[string]float64) classification.Postprocessor { - if len(labelMap) != 0 { - return classification.NewLabelConfidenceFilter(labelMap) - } - if minConf != 0.0 { - return classification.NewScoreFilter(minConf) - } - return nil -} diff --git a/services/vision/mlvision/detector.go b/services/vision/mlvision/detector.go deleted file mode 100644 index d333553b760..00000000000 --- a/services/vision/mlvision/detector.go +++ /dev/null @@ -1,456 +0,0 @@ -package mlvision - -import ( - "context" - "image" - "math" - "strconv" - "strings" - "sync" - - "github.com/nfnt/resize" - "github.com/pkg/errors" - "gorgonia.org/tensor" - - "go.viam.com/rdk/ml" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/services/mlmodel" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/objectdetection" -) - -const ( - detectorLocationName = "location" - detectorCategoryName = "category" - detectorScoreName = "score" - detectorInputName = "image" -) - -func attemptToBuildDetector(mlm mlmodel.Service, - inNameMap, outNameMap *sync.Map, - params *MLModelConfig, -) (objectdetection.Detector, error) { - md, err := mlm.Metadata(context.Background()) - if err != nil { - return nil, errors.New("could not get any metadata") - } - - // Set up input type, height, width, and labels - var inHeight, inWidth int - if len(md.Inputs) < 1 { - return nil, errors.New("no input tensors received") - } - inType := md.Inputs[0].DataType - labels := getLabelsFromMetadata(md) - var boxOrder []int - if len(params.BoxOrder) == 4 { - boxOrder = params.BoxOrder - } else { - boxOrder, err = getBoxOrderFromMetadata(md) - if err != nil || len(boxOrder) < 4 { - boxOrder = []int{1, 0, 3, 2} - } - } - - if shapeLen := len(md.Inputs[0].Shape); shapeLen < 4 { - return nil, errors.Errorf("invalid length of shape array (expected 4, got %d)", shapeLen) - } - - channelsFirst := false // if channelFirst is true, then shape is (1, 3, height, width) - if shape := md.Inputs[0].Shape; getIndex(shape, 3) == 1 { - channelsFirst = true - inHeight, inWidth = shape[2], shape[3] - } else { - inHeight, inWidth = shape[1], shape[2] - } - // creates postprocessor to filter on labels and confidences - postprocessor := createDetectionFilter(params.DefaultConfidence, params.LabelConfidenceMap) - - return func(ctx context.Context, img image.Image) ([]objectdetection.Detection, error) { - origW, origH := img.Bounds().Dx(), img.Bounds().Dy() - resizeW := inWidth - if resizeW == -1 { - resizeW = origW - } - resizeH := inHeight - if resizeH == -1 { - resizeH = origH - } - resized := img - if (origW != resizeW) || (origH != resizeH) { - resized = resize.Resize(uint(resizeW), uint(resizeH), img, resize.Bilinear) - } - inputName := detectorInputName - if mapName, ok := inNameMap.Load(inputName); ok { - if name, ok := mapName.(string); ok { - inputName = name - } - } - inMap := ml.Tensors{} - switch inType { - case UInt8: - inMap[inputName] = tensor.New( - tensor.WithShape(1, resized.Bounds().Dy(), resized.Bounds().Dx(), 3), - tensor.WithBacking(rimage.ImageToUInt8Buffer(resized, params.IsBGR)), - ) - case Float32: - inMap[inputName] = tensor.New( - tensor.WithShape(1, resized.Bounds().Dy(), resized.Bounds().Dx(), 3), - tensor.WithBacking(rimage.ImageToFloatBuffer(resized, params.IsBGR, params.MeanValue, params.StdDev)), - ) - default: - return nil, errors.Errorf("invalid input type of %s. try uint8 or float32", inType) - } - if channelsFirst { - err := inMap[inputName].T(0, 3, 1, 2) - if err != nil { - return nil, errors.New("could not transponse tensor of input image") - } - err = inMap[inputName].Transpose() - if err != nil { - return nil, errors.New("could not transponse the data of the tensor of input image") - } - } - outMap, err := mlm.Infer(ctx, inMap) - if err != nil { - return nil, err - } - - // use the outNameMap to find the tensor names, or guess and cache the names - locationName, categoryName, scoreName, err := findDetectionTensorNames(outMap, outNameMap) - if err != nil { - return nil, err - } - locations, err := convertToFloat64Slice(outMap[locationName].Data()) - if err != nil { - return nil, err - } - scores, err := convertToFloat64Slice(outMap[scoreName].Data()) - if err != nil { - return nil, err - } - hasCategoryTensor := false - categories := make([]float64, len(scores)) // default 0 category if no category output - if categoryName != "" { - hasCategoryTensor = true - categories, err = convertToFloat64Slice(outMap[categoryName].Data()) - if err != nil { - return nil, err - } - } - // sometimes categories are stuffed into the score output. separate them out. - if !hasCategoryTensor { - shape := outMap[scoreName].Shape() - if len(shape) == 3 { // cartegories are stored in 3rd dimension - nCategories := shape[2] // nCategories usually in 3rd dim, but sometimes in 2nd - if 4*nCategories == len(locations) { // it's actually in 2nd dim - nCategories = shape[1] - } - scores, categories, err = extractCategoriesFromScores(scores, nCategories) - if err != nil { - return nil, errors.Wrap(err, "could not extract categories from score tensor") - } - } - } - - // Now reshape outMap into Detections - if len(categories) != len(scores) || 4*len(scores) != len(locations) { - return nil, errors.Errorf( - "output tensor sizes did not match each other as expected. score: %v, category: %v, location: %v", - len(scores), - len(categories), - len(locations), - ) - } - detections := make([]objectdetection.Detection, 0, len(scores)) - detectionBoxesAreProportional := false - for i := 0; i < len(scores); i++ { - // heuristic for knowing if bounding box coordinates are abolute pixel locations, or - // proportional pixel locations. Absolute bounding boxes will not usually be less than a pixel - // and purely located in the upper left corner. - if i == 0 && (locations[0]+locations[1]+locations[2]+locations[3] < 4.) { - detectionBoxesAreProportional = true - } - var xmin, ymin, xmax, ymax float64 - if detectionBoxesAreProportional { - xmin = utils.Clamp(locations[4*i+getIndex(boxOrder, 0)], 0, 1) * float64(origW-1) - ymin = utils.Clamp(locations[4*i+getIndex(boxOrder, 1)], 0, 1) * float64(origH-1) - xmax = utils.Clamp(locations[4*i+getIndex(boxOrder, 2)], 0, 1) * float64(origW-1) - ymax = utils.Clamp(locations[4*i+getIndex(boxOrder, 3)], 0, 1) * float64(origH-1) - } else { - xmin = utils.Clamp(locations[4*i+getIndex(boxOrder, 0)], 0, float64(origW-1)) - ymin = utils.Clamp(locations[4*i+getIndex(boxOrder, 1)], 0, float64(origH-1)) - xmax = utils.Clamp(locations[4*i+getIndex(boxOrder, 2)], 0, float64(origW-1)) - ymax = utils.Clamp(locations[4*i+getIndex(boxOrder, 3)], 0, float64(origH-1)) - } - rect := image.Rect(int(xmin), int(ymin), int(xmax), int(ymax)) - labelNum := int(utils.Clamp(categories[i], 0, math.MaxInt)) - - if labels == nil { - detections = append(detections, objectdetection.NewDetection(rect, scores[i], strconv.Itoa(labelNum))) - } else { - if labelNum >= len(labels) { - return nil, errors.Errorf("cannot access label number %v from label file with %v labels", labelNum, len(labels)) - } - detections = append(detections, objectdetection.NewDetection(rect, scores[i], labels[labelNum])) - } - } - if postprocessor != nil { - detections = postprocessor(detections) - } - return detections, nil - }, nil -} - -func extractCategoriesFromScores(scores []float64, nCategories int) ([]float64, []float64, error) { - if nCategories == 1 { // trivially every category has the same label - categories := make([]float64, len(scores)) - return scores, categories, nil - } - // ensure even division of data into categories - if len(scores)%nCategories != 0 { - return nil, nil, errors.Errorf("nCategories %v does not divide evenly into score tensor of length %v", nCategories, len(scores)) - } - nEntries := len(scores) / nCategories - newCategories := make([]float64, 0, nEntries) - newScores := make([]float64, 0, nEntries) - for i := 0; i < nEntries; i++ { - argMax, floatMax, err := argMaxAndMax(scores[nCategories*i : nCategories*i+nCategories]) - if err != nil { - return nil, nil, err - } - newCategories = append(newCategories, float64(argMax)) - newScores = append(newScores, floatMax) - } - return newScores, newCategories, nil -} - -func argMaxAndMax(slice []float64) (int, float64, error) { - if len(slice) == 0 { - return 0, 0.0, errors.New("slice cannot be nil or empty") - } - argMax := 0 - floatMax := -math.MaxFloat64 - for i, v := range slice { - if v > floatMax { - floatMax = v - argMax = i - } - } - return argMax, floatMax, nil -} - -// findDetectionTensors finds the tensors that are necessary for object detection -// the returned tensor order is location, category, score. It caches results. -// category is optional, and will return "" if not present. -func findDetectionTensorNames(outMap ml.Tensors, nameMap *sync.Map) (string, string, string, error) { - // first try the nameMap - loc, okLoc := nameMap.Load(detectorLocationName) - score, okScores := nameMap.Load(detectorScoreName) - cat, okCat := nameMap.Load(detectorCategoryName) - if okLoc && okCat && okScores { // names are known - locString, ok := loc.(string) - if !ok { - return "", "", "", errors.Errorf("name map was not storing string, but a type %T", loc) - } - catString, ok := cat.(string) - if !ok { - return "", "", "", errors.Errorf("name map was not storing string, but a type %T", cat) - } - scoreString, ok := score.(string) - if !ok { - return "", "", "", errors.Errorf("name map was not storing string, but a type %T", score) - } - return locString, catString, scoreString, nil - } - if okLoc && okScores { // names are known, just no categories - locString, ok := loc.(string) - if !ok { - return "", "", "", errors.Errorf("name map was not storing string, but a type %T", loc) - } - scoreString, ok := score.(string) - if !ok { - return "", "", "", errors.Errorf("name map was not storing string, but a type %T", score) - } - if len(outMap[scoreString].Shape()) == 3 || len(outMap) == 2 { // the categories are in the score - return locString, "", scoreString, nil - } - } - // next, if nameMap is not set, just see if the outMap has expected names - // if the outMap only has two outputs, it might just be locations and scores. - _, okLoc = outMap[detectorLocationName] - _, okCat = outMap[detectorCategoryName] - _, okScores = outMap[detectorScoreName] - if okLoc && okCat && okScores { // names are as expected - nameMap.Store(detectorLocationName, detectorLocationName) - nameMap.Store(detectorCategoryName, detectorCategoryName) - nameMap.Store(detectorScoreName, detectorScoreName) - return detectorLocationName, detectorCategoryName, detectorScoreName, nil - } - // last, do a hack-y thing to try to guess the tensor names for the detection output tensors - locationName, categoryName, scoreName, err := guessDetectionTensorNames(outMap) - if err != nil { - return "", "", "", err - } - nameMap.Store(detectorLocationName, locationName) - nameMap.Store(detectorCategoryName, categoryName) - nameMap.Store(detectorScoreName, scoreName) - return locationName, categoryName, scoreName, nil -} - -// guessDetectionTensors is a hack-y function meant to find the correct detection tensors if the tensors -// were not given the expected names, or have no metadata. This function should succeed -// for models built with the viam platform. -func guessDetectionTensorNames(outMap ml.Tensors) (string, string, string, error) { - foundTensor := map[string]bool{} - mappedNames := map[string]string{} - outNames := tensorNames(outMap) - _, okLoc := outMap[detectorLocationName] - if okLoc { - foundTensor[detectorLocationName] = true - mappedNames[detectorLocationName] = detectorLocationName - } - _, okCat := outMap[detectorCategoryName] - if okCat { - foundTensor[detectorCategoryName] = true - mappedNames[detectorCategoryName] = detectorCategoryName - } - _, okScores := outMap[detectorScoreName] - if okScores { - foundTensor[detectorScoreName] = true - mappedNames[detectorScoreName] = detectorScoreName - } - // first find how many detections there were - // this will be used to find the other tensors - nDetections := 0 - for name, t := range outMap { - if _, alreadyFound := foundTensor[name]; alreadyFound { - continue - } - if t.Dims() == 1 { // usually n-detections has its own tensor - val, err := t.At(0) - if err != nil { - return "", "", "", err - } - val64, err := convertToFloat64Slice(val) - if err != nil { - return "", "", "", err - } - nDetections = int(val64[0]) - foundTensor[name] = true - break - } - } - if !okLoc { // guess the name of the location tensor - // location tensor should have 3 dimensions usually - for name, t := range outMap { - if _, alreadyFound := foundTensor[name]; alreadyFound { - continue - } - if t.Dims() == 3 { - mappedNames[detectorLocationName] = name - foundTensor[name] = true - break - } - } - if _, ok := mappedNames[detectorLocationName]; !ok { - return "", "", "", errors.Errorf("could not find an output tensor named 'location' among [%s]", strings.Join(outNames, ", ")) - } - } - if !okCat { // guess the name of the category tensor - // a category usually has a whole number in its elements, so either look for - // int data types in the tensor, or sum the elements and make sure they dont have any decimals - for name, t := range outMap { - if _, alreadyFound := foundTensor[name]; alreadyFound { - continue - } - dt := t.Dtype() - if t.Dims() == 2 { - if dt == tensor.Int || dt == tensor.Int32 || dt == tensor.Int64 || - dt == tensor.Uint32 || dt == tensor.Uint64 || dt == tensor.Int8 || dt == tensor.Uint8 { - mappedNames[detectorCategoryName] = name - foundTensor[name] = true - break - } - // check if fully whole number - var whole tensor.Tensor - var err error - if nDetections == 0 { - whole, err = tensor.Sum(t) - if err != nil { - return "", "", "", err - } - } else { - s, err := t.Slice(nil, tensor.S(0, nDetections)) - if err != nil { - return "", "", "", err - } - whole, err = tensor.Sum(s) - if err != nil { - return "", "", "", err - } - } - val, err := convertToFloat64Slice(whole.Data()) - if err != nil { - return "", "", "", err - } - if math.Mod(val[0], 1) == 0 { - mappedNames[detectorCategoryName] = name - foundTensor[name] = true - break - } - } - } - if _, ok := mappedNames[detectorCategoryName]; !ok { - return "", "", "", errors.Errorf("could not find an output tensor named 'category' among [%s]", strings.Join(outNames, ", ")) - } - } - if !okScores { // guess the name of the scores tensor - // a score usually has a float data type - for name, t := range outMap { - if _, alreadyFound := foundTensor[name]; alreadyFound { - continue - } - dt := t.Dtype() - if t.Dims() == 2 && (dt == tensor.Float32 || dt == tensor.Float64) { - mappedNames[detectorScoreName] = name - foundTensor[name] = true - break - } - } - if _, ok := mappedNames[detectorScoreName]; !ok { - return "", "", "", errors.Errorf("could not find an output tensor named 'score' among [%s]", strings.Join(outNames, ", ")) - } - } - return mappedNames[detectorLocationName], mappedNames[detectorCategoryName], mappedNames[detectorScoreName], nil -} - -// In the case that the model provided is not a detector, attemptToBuildDetector will return a -// detector function that function fails because the expected keys are not in the outputTensor. -// use checkIfDetectorWorks to get sample output tensors on gray image so we know if the functions -// returned from attemptToBuildDetector will fail ahead of time. -func checkIfDetectorWorks(ctx context.Context, df objectdetection.Detector) error { - if df == nil { - return errors.New("nil detector function") - } - - // test image to check if the detector function works - img := image.NewGray(image.Rectangle{Min: image.Point{0, 0}, Max: image.Point{5, 5}}) - - _, err := df(ctx, img) - if err != nil { - return errors.Wrap(err, "cannot use model as a detector") - } - return nil -} - -// createDetectionFilter creates a post processor function that filters on the outputs of the model. -func createDetectionFilter(minConf float64, labelMap map[string]float64) objectdetection.Postprocessor { - if len(labelMap) != 0 { - return objectdetection.NewLabelConfidenceFilter(labelMap) - } - if minConf != 0.0 { - return objectdetection.NewScoreFilter(minConf) - } - return nil -} diff --git a/services/vision/mlvision/ml_model.go b/services/vision/mlvision/ml_model.go deleted file mode 100644 index 05bdbe50e7f..00000000000 --- a/services/vision/mlvision/ml_model.go +++ /dev/null @@ -1,392 +0,0 @@ -// Package mlvision uses an underlying model from the ML model service as a vision model, -// and wraps the ML model with the vision service methods. -package mlvision - -import ( - "bufio" - "context" - "fmt" - "math" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/montanaflynn/stats" - "github.com/pkg/errors" - "go.opencensus.io/trace" - "golang.org/x/exp/constraints" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/ml" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/mlmodel" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/utils" -) - -var model = resource.DefaultModelFamily.WithModel("mlmodel") - -const ( - // UInt8 is one of the possible input/output types for tensors. - UInt8 = "uint8" - // Float32 is one of the possible input/output types for tensors. - Float32 = "float32" - // DefaultOutTensorName is the prefix key given to output tensors in the map - // if there is no metadata. (output0, output1, etc.) - DefaultOutTensorName = "output" -) - -func init() { - resource.RegisterService(vision.API, model, resource.Registration[vision.Service, *MLModelConfig]{ - DeprecatedRobotConstructor: func( - ctx context.Context, r any, c resource.Config, logger logging.Logger, - ) (vision.Service, error) { - attrs, err := resource.NativeConfig[*MLModelConfig](c) - if err != nil { - return nil, err - } - actualR, err := utils.AssertType[robot.Robot](r) - if err != nil { - return nil, err - } - return registerMLModelVisionService(ctx, c.ResourceName(), attrs, actualR, logger) - }, - }) -} - -// MLModelConfig specifies the parameters needed to turn an ML model into a vision Model. -type MLModelConfig struct { - ModelName string `json:"mlmodel_name"` - RemapInputNames map[string]string `json:"remap_input_names"` - RemapOutputNames map[string]string `json:"remap_output_names"` - BoxOrder []int `json:"xmin_ymin_xmax_ymax_order"` - // optional parameter used to normalize the input image if the ML Model expects it - MeanValue []float32 `json:"input_image_mean_value"` - // optional parameter used to normalize the input image if the ML Model expects it - StdDev []float32 `json:"input_image_std_dev"` - // optional parameter used to change the input image to BGR format if the ML Model expects it - IsBGR bool `json:"input_image_bgr"` - DefaultConfidence float64 `json:"default_minimum_confidence"` - LabelConfidenceMap map[string]float64 `json:"label_confidences"` -} - -// Validate will add the ModelName as an implicit dependency to the robot. -func (conf *MLModelConfig) Validate(path string) ([]string, error) { - if conf.ModelName == "" { - return nil, errors.New("mlmodel_name cannot be empty") - } - if len(conf.MeanValue) != 0 { - if len(conf.MeanValue) < 3 { - return nil, errors.New("input_image_mean_value attribute must have at least 3 values, one for each color channel") - } - } - if len(conf.StdDev) != 0 { - if len(conf.StdDev) < 3 { - return nil, errors.New("input_image_std_dev attribute must have at least 3 values, one for each color channel") - } - } - for _, v := range conf.StdDev { - if v == 0.0 { - return nil, errors.New("input_image_std_dev is not allowed to have 0 values, will cause division by 0") - } - } - return []string{conf.ModelName}, nil -} - -func registerMLModelVisionService( - ctx context.Context, - name resource.Name, - params *MLModelConfig, - r robot.Robot, - logger logging.Logger, -) (vision.Service, error) { - _, span := trace.StartSpan(ctx, "service::vision::registerMLModelVisionService") - defer span.End() - - mlm, err := mlmodel.FromRobot(r, params.ModelName) - if err != nil { - return nil, err - } - - // the Maps that associates the tensor names as they are found in the model, to - // what the vision service expects. - inNameMap := &sync.Map{} - for oldName, newName := range params.RemapInputNames { - inNameMap.Store(newName, oldName) - } - outNameMap := &sync.Map{} - for oldName, newName := range params.RemapOutputNames { - outNameMap.Store(newName, oldName) - } - if len(params.BoxOrder) != 0 { - if len(params.BoxOrder) != 4 { - return nil, errors.Errorf( - "attribute xmin_ymin_xmax_ymax_order for model %q must have only 4 entries in the list. Got %v", - params.ModelName, - params.BoxOrder, - ) - } - checkOrder := map[int]bool{0: false, 1: false, 2: false, 3: false} - for _, entry := range params.BoxOrder { - val, ok := checkOrder[entry] - if !ok || val { // if val is true, it means value was repeated - return nil, errors.Errorf( - "attribute xmin_ymin_xmax_ymax_order for model %q can only have entries 0, 1, 2 and 3, and only one instance of each. Got %v", - params.ModelName, - params.BoxOrder, - ) - } - checkOrder[entry] = true - } - } - var errList []error - classifierFunc, err := attemptToBuildClassifier(mlm, inNameMap, outNameMap, params) - if err != nil { - logger.CDebugw(ctx, "unable to use ml model as a classifier, will attempt to evaluate as"+ - "detector and segmenter", "model", params.ModelName, "error", err) - } else { - err := checkIfClassifierWorks(ctx, classifierFunc) - errList = append(errList, err) - if err != nil { - classifierFunc = nil - logger.CDebugw(ctx, "unable to use ml model as a classifier, will attempt to evaluate as detector"+ - " and 3D segmenter", "model", params.ModelName, "error", err) - } else { - logger.CInfow(ctx, "model fulfills a vision service classifier", "model", params.ModelName) - } - } - - detectorFunc, err := attemptToBuildDetector(mlm, inNameMap, outNameMap, params) - if err != nil { - logger.CDebugw(ctx, "unable to use ml model as a detector, will attempt to evaluate as 3D segmenter", - "model", params.ModelName, "error", err) - } else { - err = checkIfDetectorWorks(ctx, detectorFunc) - errList = append(errList, err) - if err != nil { - detectorFunc = nil - logger.CDebugw(ctx, "unable to use ml model as a detector, will attempt to evaluate as 3D segmenter", - "model", params.ModelName, "error", err) - } else { - logger.CInfow(ctx, "model fulfills a vision service detector", "model", params.ModelName) - } - } - - segmenter3DFunc, err := attemptToBuild3DSegmenter(mlm, inNameMap, outNameMap) - errList = append(errList, err) - if err != nil { - logger.CDebugw(ctx, "unable to use ml model as 3D segmenter", "model", params.ModelName, "error", err) - } else { - logger.CInfow(ctx, "model fulfills a vision service 3D segmenter", "model", params.ModelName) - } - - // If nothing worked, give more info - if errList[0] != nil && errList[1] != nil && errList[2] != nil { - for _, e := range errList { - logger.Error(e) - } - md, err := mlm.Metadata(ctx) - if err != nil { - logger.Error("could not get metadata from the model") - } else { - inputs := "" - for _, tensor := range md.Inputs { - inputs += fmt.Sprintf("%s(%v) ", tensor.Name, tensor.Shape) - } - outputs := "" - for _, tensor := range md.Outputs { - outputs += fmt.Sprintf("%s(%v) ", tensor.Name, tensor.Shape) - } - logger.Infow("the model has the following input and outputs tensors, name(shape)", - "inputs", inputs, - "outputs", outputs, - ) - } - } - - // Don't return a close function, because you don't want to close the underlying ML service - return vision.NewService(name, r, nil, classifierFunc, detectorFunc, segmenter3DFunc) -} - -// getLabelsFromMetadata returns a slice of strings--the intended labels. -func getLabelsFromMetadata(md mlmodel.MLMetadata) []string { - if len(md.Outputs) < 1 { - return nil - } - - if labelPath, ok := md.Outputs[0].Extra["labels"].(string); ok { - if labelPath == "" { // no label file specified - return nil - } - var labels []string - f, err := os.Open(filepath.Clean(labelPath)) - if err != nil { - return nil - } - defer func() { - if err := f.Close(); err != nil { - logger := logging.NewLogger("labelFile") - logger.Warnw("could not get labels from file", "error", err) - return - } - }() - scanner := bufio.NewScanner(f) - for scanner.Scan() { - labels = append(labels, scanner.Text()) - } - // if the labels come out as one line, try splitting that line by spaces or commas to extract labels - // Check if the labels should be comma split first and then space split. - if len(labels) == 1 { - labels = strings.Split(labels[0], ",") - } - if len(labels) == 1 { - labels = strings.Split(labels[0], " ") - } - return labels - } - return nil -} - -// getBoxOrderFromMetadata returns a slice of ints--the bounding box -// display order, where 0=xmin, 1=ymin, 2=xmax, 3=ymax. -func getBoxOrderFromMetadata(md mlmodel.MLMetadata) ([]int, error) { - for _, o := range md.Outputs { - if strings.Contains(o.Name, "location") { - out := make([]int, 0, 4) - if order, ok := o.Extra["boxOrder"].([]uint32); ok { - for _, o := range order { - out = append(out, int(o)) - } - return out, nil - } - } - } - return nil, errors.New("could not grab bbox order") -} - -// getIndex returns the index of an int in an array of ints -// Will return -1 if it's not there. -func getIndex(s []int, num int) int { - for i, v := range s { - if v == num { - return i - } - } - return -1 -} - -// softmax takes the input slice and applies the softmax function. -func softmax(in []float64) []float64 { - out := make([]float64, 0, len(in)) - bigSum := 0.0 - for _, x := range in { - bigSum += math.Exp(x) - } - for _, x := range in { - out = append(out, math.Exp(x)/bigSum) - } - return out -} - -// checkClassification scores ensures that the input scores (output of classifier) -// will represent confidence values (from 0-1). -func checkClassificationScores(in []float64) []float64 { - if len(in) > 1 { - for _, p := range in { - if p < 0 || p > 1 { // is logit, needs softmax - confs := softmax(in) - return confs - } - } - return in // no need to softmax - } - // otherwise, this is a binary classifier - if in[0] < -1 || in[0] > 1 { // needs sigmoid - out, err := stats.Sigmoid(in) - if err != nil { - return in - } - return out - } - return in // no need to sigmoid -} - -// Number interface for converting between numbers. -type number interface { - constraints.Integer | constraints.Float -} - -// convertNumberSlice converts any number slice into another number slice. -func convertNumberSlice[T1, T2 number](t1 []T1) []T2 { - t2 := make([]T2, len(t1)) - for i := range t1 { - t2[i] = T2(t1[i]) - } - return t2 -} - -func convertToFloat64Slice(slice interface{}) ([]float64, error) { - switch v := slice.(type) { - case []float64: - return v, nil - case float64: - return []float64{v}, nil - case []float32: - return convertNumberSlice[float32, float64](v), nil - case float32: - return convertNumberSlice[float32, float64]([]float32{v}), nil - case []int: - return convertNumberSlice[int, float64](v), nil - case int: - return convertNumberSlice[int, float64]([]int{v}), nil - case []uint: - return convertNumberSlice[uint, float64](v), nil - case uint: - return convertNumberSlice[uint, float64]([]uint{v}), nil - case []int8: - return convertNumberSlice[int8, float64](v), nil - case int8: - return convertNumberSlice[int8, float64]([]int8{v}), nil - case []int16: - return convertNumberSlice[int16, float64](v), nil - case int16: - return convertNumberSlice[int16, float64]([]int16{v}), nil - case []int32: - return convertNumberSlice[int32, float64](v), nil - case int32: - return convertNumberSlice[int32, float64]([]int32{v}), nil - case []int64: - return convertNumberSlice[int64, float64](v), nil - case int64: - return convertNumberSlice[int64, float64]([]int64{v}), nil - case []uint8: - return convertNumberSlice[uint8, float64](v), nil - case uint8: - return convertNumberSlice[uint8, float64]([]uint8{v}), nil - case []uint16: - return convertNumberSlice[uint16, float64](v), nil - case uint16: - return convertNumberSlice[uint16, float64]([]uint16{v}), nil - case []uint32: - return convertNumberSlice[uint32, float64](v), nil - case uint32: - return convertNumberSlice[uint32, float64]([]uint32{v}), nil - case []uint64: - return convertNumberSlice[uint64, float64](v), nil - case uint64: - return convertNumberSlice[uint64, float64]([]uint64{v}), nil - default: - return nil, errors.Errorf("dont know how to convert slice of %T into a []float64", slice) - } -} - -// tensorNames returns all the names of the tensors. -func tensorNames(t ml.Tensors) []string { - names := []string{} - for name := range t { - names = append(names, name) - } - return names -} diff --git a/services/vision/mlvision/ml_model_test.go b/services/vision/mlvision/ml_model_test.go deleted file mode 100644 index 71619fd50a4..00000000000 --- a/services/vision/mlvision/ml_model_test.go +++ /dev/null @@ -1,642 +0,0 @@ -//go:build !no_tflite - -package mlvision - -import ( - "context" - "sync" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/services/mlmodel" - "go.viam.com/rdk/services/mlmodel/tflitecpu" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision/classification" -) - -func BenchmarkAddMLVisionModel(b *testing.B) { - modelLoc := artifact.MustPath("vision/tflite/effdet0.tflite") - - name := mlmodel.Named("myMLModel") - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - } - ctx := context.Background() - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, name) - test.That(b, err, test.ShouldBeNil) - test.That(b, out, test.ShouldNotBeNil) - modelCfg := MLModelConfig{ModelName: name.Name} - - b.ResetTimer() - for i := 0; i < b.N; i++ { - service, err := registerMLModelVisionService(ctx, name, &modelCfg, &inject.Robot{}, logging.NewLogger("benchmark")) - test.That(b, err, test.ShouldBeNil) - test.That(b, service, test.ShouldNotBeNil) - test.That(b, service.Name(), test.ShouldResemble, name) - } -} - -func BenchmarkUseMLVisionModel(b *testing.B) { - modelLoc := artifact.MustPath("vision/tflite/effdet0.tflite") - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/dogscute.jpeg")) - test.That(b, err, test.ShouldBeNil) - test.That(b, pic, test.ShouldNotBeNil) - name := mlmodel.Named("myMLModel") - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - } - ctx := context.Background() - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, name) - test.That(b, err, test.ShouldBeNil) - test.That(b, out, test.ShouldNotBeNil) - modelCfg := MLModelConfig{ModelName: name.Name} - - service, err := registerMLModelVisionService(ctx, name, &modelCfg, &inject.Robot{}, logging.NewLogger("benchmark")) - test.That(b, err, test.ShouldBeNil) - test.That(b, service, test.ShouldNotBeNil) - test.That(b, service.Name(), test.ShouldResemble, name) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - // Detections should be worst case (more to unpack) - detections, err := service.Detections(ctx, pic, nil) - test.That(b, err, test.ShouldBeNil) - test.That(b, detections, test.ShouldNotBeNil) - } -} - -func getTestMlModel(modelLoc string) (mlmodel.Service, error) { - ctx := context.Background() - testMLModelServiceName := "test-model" - - name := mlmodel.Named(testMLModelServiceName) - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - } - return tflitecpu.NewTFLiteCPUModel(ctx, &cfg, name) -} - -func TestAddingIncorrectModelTypeToModel(t *testing.T) { - modelLocDetector := artifact.MustPath("vision/tflite/effdet0.tflite") - ctx := context.Background() - - // get detector model - mlm, err := getTestMlModel(modelLocDetector) - test.That(t, err, test.ShouldBeNil) - - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - conf := &MLModelConfig{} - classifier, err := attemptToBuildClassifier(mlm, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, classifier, test.ShouldNotBeNil) - - err = checkIfClassifierWorks(ctx, classifier) - test.That(t, err, test.ShouldNotBeNil) - - detector, err := attemptToBuildDetector(mlm, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, detector, test.ShouldNotBeNil) - - err = checkIfDetectorWorks(ctx, detector) - test.That(t, err, test.ShouldBeNil) - - modelLocClassifier := artifact.MustPath("vision/tflite/mobilenetv2_class.tflite") - - mlm, err = getTestMlModel(modelLocClassifier) - test.That(t, err, test.ShouldBeNil) - - inNameMap = &sync.Map{} - outNameMap = &sync.Map{} - classifier, err = attemptToBuildClassifier(mlm, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, classifier, test.ShouldNotBeNil) - - err = checkIfClassifierWorks(ctx, classifier) - test.That(t, err, test.ShouldBeNil) - - mlm, err = getTestMlModel(modelLocClassifier) - test.That(t, err, test.ShouldBeNil) - - detector, err = attemptToBuildDetector(mlm, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, detector, test.ShouldNotBeNil) - - err = checkIfDetectorWorks(ctx, detector) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestNewMLDetector(t *testing.T) { - // Test that a detector would give an expected output on the dog image - // Set it up as a ML Model - - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/effdet0.tflite") - labelLoc := artifact.MustPath("vision/tflite/effdetlabels.txt") - cfg := tflitecpu.TFLiteConfig{ // detector config - ModelPath: modelLoc, - NumThreads: 2, - LabelPath: labelLoc, - } - noLabelCfg := tflitecpu.TFLiteConfig{ // detector config - ModelPath: modelLoc, - NumThreads: 2, - } - - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/dogscute.jpeg")) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic, test.ShouldNotBeNil) - - // Test that a detector would give the expected output on the dog image - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("myMLDet")) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - check, err := out.Metadata(ctx) - test.That(t, check, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, check.Inputs[0].Name, test.ShouldResemble, "image") - test.That(t, check.Outputs[0].Name, test.ShouldResemble, "location") - test.That(t, check.Outputs[1].Name, test.ShouldResemble, "category") - test.That(t, check.Outputs[0].Extra["labels"], test.ShouldNotBeNil) - - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - conf := &MLModelConfig{} - gotDetector, err := attemptToBuildDetector(out, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetector, test.ShouldNotBeNil) - - gotDetections, err := gotDetector(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetections[0].Score(), test.ShouldBeGreaterThan, 0.789) - test.That(t, gotDetections[1].Score(), test.ShouldBeGreaterThan, 0.7) - test.That(t, gotDetections[0].BoundingBox().Min.X, test.ShouldBeGreaterThan, 124) - test.That(t, gotDetections[0].BoundingBox().Min.X, test.ShouldBeLessThan, 127) - test.That(t, gotDetections[0].BoundingBox().Min.Y, test.ShouldBeGreaterThan, 40) - test.That(t, gotDetections[0].BoundingBox().Min.Y, test.ShouldBeLessThan, 44) - test.That(t, gotDetections[0].BoundingBox().Max.X, test.ShouldBeGreaterThan, 196) - test.That(t, gotDetections[0].BoundingBox().Max.X, test.ShouldBeLessThan, 202) - test.That(t, gotDetections[0].BoundingBox().Max.Y, test.ShouldBeGreaterThan, 158) - test.That(t, gotDetections[0].BoundingBox().Max.Y, test.ShouldBeLessThan, 163) - - test.That(t, gotDetections[0].Label(), test.ShouldResemble, "Dog") - test.That(t, gotDetections[1].Label(), test.ShouldResemble, "Dog") - - // Ensure that the same model without labelpath responds similarly - outNL, err := tflitecpu.NewTFLiteCPUModel(ctx, &noLabelCfg, mlmodel.Named("myOtherMLDet")) - test.That(t, err, test.ShouldBeNil) - test.That(t, outNL, test.ShouldNotBeNil) - inNameMap = &sync.Map{} - outNameMap = &sync.Map{} - conf = &MLModelConfig{} - gotDetectorNL, err := attemptToBuildDetector(outNL, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetectorNL, test.ShouldNotBeNil) - gotDetectionsNL, err := gotDetectorNL(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetectionsNL[0].Score(), test.ShouldBeGreaterThan, 0.789) - test.That(t, gotDetectionsNL[1].Score(), test.ShouldBeGreaterThan, 0.7) - test.That(t, gotDetectionsNL[0].BoundingBox().Min.X, test.ShouldBeGreaterThan, 124) - test.That(t, gotDetectionsNL[0].BoundingBox().Min.X, test.ShouldBeLessThan, 127) - test.That(t, gotDetectionsNL[0].BoundingBox().Min.Y, test.ShouldBeGreaterThan, 40) - test.That(t, gotDetectionsNL[0].BoundingBox().Min.Y, test.ShouldBeLessThan, 44) - test.That(t, gotDetectionsNL[0].BoundingBox().Max.X, test.ShouldBeGreaterThan, 196) - test.That(t, gotDetectionsNL[0].BoundingBox().Max.X, test.ShouldBeLessThan, 202) - test.That(t, gotDetectionsNL[0].BoundingBox().Max.Y, test.ShouldBeGreaterThan, 158) - test.That(t, gotDetectionsNL[0].BoundingBox().Max.Y, test.ShouldBeLessThan, 163) - - test.That(t, gotDetectionsNL[0].Label(), test.ShouldResemble, "17") - test.That(t, gotDetectionsNL[1].Label(), test.ShouldResemble, "17") -} - -func TestNewMLClassifier(t *testing.T) { - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/effnet0.tflite") - labelLoc := artifact.MustPath("vision/tflite/imagenetlabels.txt") - - cfg := tflitecpu.TFLiteConfig{ // detector config - ModelPath: modelLoc, - NumThreads: 2, - LabelPath: labelLoc, - } - noLabelCfg := tflitecpu.TFLiteConfig{ // detector config - ModelPath: modelLoc, - NumThreads: 2, - } - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/lion.jpeg")) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic, test.ShouldNotBeNil) - - // Test that a classifier would give the expected result on the lion image - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("myMLClassif")) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - check, err := out.Metadata(ctx) - test.That(t, check, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, check.Inputs[0].Name, test.ShouldResemble, "image") - test.That(t, check.Outputs[0].Name, test.ShouldResemble, "probability") - test.That(t, check.Outputs[0].Extra["labels"], test.ShouldNotBeNil) - - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - conf := &MLModelConfig{} - gotClassifier, err := attemptToBuildClassifier(out, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifier, test.ShouldNotBeNil) - - gotClassifications, err := gotClassifier(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifications, test.ShouldNotBeNil) - gotTop, err := gotClassifications.TopN(5) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotTop, test.ShouldNotBeNil) - test.That(t, gotTop[0].Label(), test.ShouldContainSubstring, "lion") - test.That(t, gotTop[0].Score(), test.ShouldBeGreaterThan, 0.99) - test.That(t, gotTop[1].Score(), test.ShouldBeLessThan, 0.01) - - // Ensure that the same model without labelpath responds similarly - outNL, err := tflitecpu.NewTFLiteCPUModel(ctx, &noLabelCfg, mlmodel.Named("myOtherMLClassif")) - test.That(t, err, test.ShouldBeNil) - test.That(t, outNL, test.ShouldNotBeNil) - inNameMap = &sync.Map{} - outNameMap = &sync.Map{} - conf = &MLModelConfig{} - gotClassifierNL, err := attemptToBuildClassifier(outNL, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifierNL, test.ShouldNotBeNil) - gotClassificationsNL, err := gotClassifierNL(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassificationsNL, test.ShouldNotBeNil) - topNL, err := gotClassificationsNL.TopN(5) - test.That(t, err, test.ShouldBeNil) - test.That(t, topNL, test.ShouldNotBeNil) - test.That(t, topNL[0].Label(), test.ShouldContainSubstring, "291") - test.That(t, topNL[0].Score(), test.ShouldBeGreaterThan, 0.99) - test.That(t, topNL[1].Score(), test.ShouldBeLessThan, 0.01) -} - -func TestMLDetectorWithNoCategory(t *testing.T) { - // Test that a detector would give an expected output on the person - // This detector only has two output tensors, Identity (location) and Identity_1 (score) - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/person.jpg")) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic, test.ShouldNotBeNil) - - name := mlmodel.Named("yolo_person") - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/yolov4-tiny-416_person.tflite") - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - } - - // Test that a detector would give the expected output on the dog image - outModel, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, name) - test.That(t, err, test.ShouldBeNil) - test.That(t, outModel, test.ShouldNotBeNil) - check, err := outModel.Metadata(ctx) - test.That(t, check, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - // Even without metadata we should find - test.That(t, check.Inputs[0].Shape, test.ShouldResemble, []int{1, 416, 416, 3}) - test.That(t, check.Inputs[0].DataType, test.ShouldResemble, "float32") - test.That(t, len(check.Outputs), test.ShouldEqual, 2) // only two output tensors - - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - outNameMap.Store("location", "Identity") - outNameMap.Store("score", "Identity_1") - conf := &MLModelConfig{} - gotDetector, err := attemptToBuildDetector(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetector, test.ShouldNotBeNil) - - gotDetections, err := gotDetector(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetections[2297].Score(), test.ShouldBeGreaterThan, 0.7) - test.That(t, gotDetections[2297].Label(), test.ShouldResemble, "0") -} - -func TestMoreMLDetectors(t *testing.T) { - // Test that a detector would give an expected output on the dog image - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/dogscute.jpeg")) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic, test.ShouldNotBeNil) - - name := mlmodel.Named("ssd") - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/ssdmobilenet.tflite") - labelLoc := artifact.MustPath("vision/tflite/effdetlabels.txt") - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - LabelPath: labelLoc, - } - - // Test that a detector would give the expected output on the dog image - outModel, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, name) - test.That(t, err, test.ShouldBeNil) - test.That(t, outModel, test.ShouldNotBeNil) - check, err := outModel.Metadata(ctx) - test.That(t, check, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - // Even without metadata we should find - test.That(t, check.Inputs[0].Shape, test.ShouldResemble, []int{1, 320, 320, 3}) - test.That(t, check.Inputs[0].DataType, test.ShouldResemble, "float32") - test.That(t, len(check.Outputs), test.ShouldEqual, 4) - - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - conf := &MLModelConfig{} - gotDetector, err := attemptToBuildDetector(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetector, test.ShouldNotBeNil) - - gotDetections, err := gotDetector(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(gotDetections), test.ShouldEqual, 10) - test.That(t, gotDetections[0].Score(), test.ShouldBeGreaterThan, 0.82) - test.That(t, gotDetections[1].Score(), test.ShouldBeGreaterThan, 0.8) - test.That(t, gotDetections[0].Label(), test.ShouldResemble, "Dog") - test.That(t, gotDetections[1].Label(), test.ShouldResemble, "Dog") - - // test filters - // add min confidence first - minConf := 0.81 - conf = &MLModelConfig{DefaultConfidence: minConf} - gotDetector, err = attemptToBuildDetector(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetector, test.ShouldNotBeNil) - - gotDetections, err = gotDetector(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(gotDetections), test.ShouldEqual, 3) - test.That(t, gotDetections[0].Score(), test.ShouldBeGreaterThan, minConf) - - // then add label filter - labelMap := map[string]float64{"DOG": 0.8, "CARROT": 0.3} - conf = &MLModelConfig{DefaultConfidence: minConf, LabelConfidenceMap: labelMap} - gotDetector, err = attemptToBuildDetector(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotDetector, test.ShouldNotBeNil) - - gotDetections, err = gotDetector(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(gotDetections), test.ShouldEqual, 3) - test.That(t, gotDetections[0].Score(), test.ShouldBeGreaterThan, labelMap["DOG"]) - test.That(t, gotDetections[0].Label(), test.ShouldResemble, "Dog") - test.That(t, gotDetections[1].Score(), test.ShouldBeGreaterThan, labelMap["DOG"]) - test.That(t, gotDetections[1].Label(), test.ShouldResemble, "Dog") - test.That(t, gotDetections[2].Score(), test.ShouldBeGreaterThan, labelMap["CARROT"]) - test.That(t, gotDetections[2].Label(), test.ShouldResemble, "Carrot") -} - -func TestMoreMLClassifiers(t *testing.T) { - // Test that mobileNet classifier gives expected output on the redpanda image - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/mobilenetv2_class.tflite") - pic, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/redpanda.jpeg")) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic, test.ShouldNotBeNil) - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - } - outModel, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("mobileNet")) - test.That(t, err, test.ShouldBeNil) - test.That(t, outModel, test.ShouldNotBeNil) - check, err := outModel.Metadata(ctx) - test.That(t, check, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - conf := &MLModelConfig{} - gotClassifier, err := attemptToBuildClassifier(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifier, test.ShouldNotBeNil) - - gotClassifications, err := gotClassifier(ctx, pic) - test.That(t, err, test.ShouldBeNil) - bestClass, err := gotClassifications.TopN(1) - test.That(t, err, test.ShouldBeNil) - test.That(t, bestClass[0].Label(), test.ShouldResemble, "390") - test.That(t, bestClass[0].Score(), test.ShouldBeGreaterThan, 0.93) - // add min confidence first - minConf := 0.05 - conf = &MLModelConfig{DefaultConfidence: minConf} - gotClassifier, err = attemptToBuildClassifier(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifier, test.ShouldNotBeNil) - - gotClassifications, err = gotClassifier(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(gotClassifications), test.ShouldEqual, 1) - test.That(t, gotClassifications[0].Score(), test.ShouldBeGreaterThan, minConf) - - // then add label filter - labelMap := map[string]float64{"390": 0.8} - conf = &MLModelConfig{DefaultConfidence: minConf, LabelConfidenceMap: labelMap} - gotClassifier, err = attemptToBuildClassifier(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifier, test.ShouldNotBeNil) - - gotClassifications, err = gotClassifier(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(gotClassifications), test.ShouldEqual, 1) - test.That(t, gotClassifications[0].Score(), test.ShouldBeGreaterThan, labelMap["390"]) - test.That(t, gotClassifications[0].Label(), test.ShouldResemble, "390") - - // Test that mobileNet imageNet classifier gives expected output on lion image - modelLoc = artifact.MustPath("vision/tflite/mobilenetv2_imagenet.tflite") - pic, err = rimage.NewImageFromFile(artifact.MustPath("vision/tflite/lion.jpeg")) - test.That(t, err, test.ShouldBeNil) - test.That(t, pic, test.ShouldNotBeNil) - cfg = tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - } - outModel, err = tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("mobileNet")) - test.That(t, err, test.ShouldBeNil) - test.That(t, outModel, test.ShouldNotBeNil) - check, err = outModel.Metadata(ctx) - test.That(t, check, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - - inNameMap = &sync.Map{} - outNameMap = &sync.Map{} - conf = &MLModelConfig{} - gotClassifier, err = attemptToBuildClassifier(outModel, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifier, test.ShouldNotBeNil) - gotClassifications, err = gotClassifier(ctx, pic) - test.That(t, err, test.ShouldBeNil) - test.That(t, gotClassifications, test.ShouldNotBeNil) - bestClass, err = gotClassifications.TopN(1) - test.That(t, err, test.ShouldBeNil) - test.That(t, bestClass[0].Label(), test.ShouldResemble, "292") - test.That(t, bestClass[0].Score(), test.ShouldBeGreaterThan, 0.93) -} - -func TestLabelReader(t *testing.T) { - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/effdet0.tflite") - labelLoc := artifact.MustPath("vision/tflite/fakelabels.txt") - cfg := tflitecpu.TFLiteConfig{ // detector config - ModelPath: modelLoc, - NumThreads: 2, - LabelPath: labelLoc, - } - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("fakeLabels")) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - outMD, err := out.Metadata(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, outMD, test.ShouldNotBeNil) - outLabels := getLabelsFromMetadata(outMD) - test.That(t, err, test.ShouldBeNil) - test.That(t, outLabels[0], test.ShouldResemble, "this") - test.That(t, outLabels[1], test.ShouldResemble, "could") - test.That(t, outLabels[2], test.ShouldResemble, "be") - test.That(t, len(outLabels), test.ShouldEqual, 12) -} - -func TestSpaceDelineatedLabels(t *testing.T) { - ctx := context.Background() - modelLoc := artifact.MustPath("vision/tflite/effdet0.tflite") - labelLoc := artifact.MustPath("vision/classification/lorem.txt") - cfg := tflitecpu.TFLiteConfig{ // detector config - ModelPath: modelLoc, - NumThreads: 2, - LabelPath: labelLoc, - } - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("spacedLabels")) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - outMD, err := out.Metadata(ctx) - test.That(t, err, test.ShouldBeNil) - test.That(t, outMD, test.ShouldNotBeNil) - outLabels := getLabelsFromMetadata(outMD) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(outLabels), test.ShouldEqual, 10) -} - -func TestOneClassifierOnManyCameras(t *testing.T) { - ctx := context.Background() - - // Test that one classifier can be used in two goroutines - picPanda, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/redpanda.jpeg")) - test.That(t, err, test.ShouldBeNil) - picLion, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/lion.jpeg")) - test.That(t, err, test.ShouldBeNil) - - modelLoc := artifact.MustPath("vision/tflite/mobilenetv2_class.tflite") - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - } - - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("testClassifier")) - test.That(t, err, test.ShouldBeNil) - test.That(t, out, test.ShouldNotBeNil) - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - conf := &MLModelConfig{} - outClassifier, err := attemptToBuildClassifier(out, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - test.That(t, outClassifier, test.ShouldNotBeNil) - valuePanda, valueLion := classifyTwoImages(picPanda, picLion, outClassifier) - test.That(t, valuePanda, test.ShouldNotBeNil) - test.That(t, valueLion, test.ShouldNotBeNil) -} - -func TestMultipleClassifiersOneModel(t *testing.T) { - modelLoc := artifact.MustPath("vision/tflite/mobilenetv2_class.tflite") - cfg := tflitecpu.TFLiteConfig{ - ModelPath: modelLoc, - NumThreads: 2, - } - ctx := context.Background() - out, err := tflitecpu.NewTFLiteCPUModel(ctx, &cfg, mlmodel.Named("testClassifier")) - test.That(t, err, test.ShouldBeNil) - - inNameMap := &sync.Map{} - outNameMap := &sync.Map{} - conf := &MLModelConfig{} - Classifier1, err := attemptToBuildClassifier(out, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - - inNameMap = &sync.Map{} - outNameMap = &sync.Map{} - conf = &MLModelConfig{} - Classifier2, err := attemptToBuildClassifier(out, inNameMap, outNameMap, conf) - test.That(t, err, test.ShouldBeNil) - - picPanda, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/redpanda.jpeg")) - test.That(t, err, test.ShouldBeNil) - - var wg sync.WaitGroup - wg.Add(2) - - go func() { - defer wg.Done() - getNClassifications(ctx, t, picPanda, 10, Classifier1) - }() - - go func() { - defer wg.Done() - getNClassifications(ctx, t, picPanda, 10, Classifier2) - }() - - wg.Wait() -} - -func classifyTwoImages(picPanda, picLion *rimage.Image, - got classification.Classifier, -) (classification.Classifications, classification.Classifications) { - resultPanda := make(chan classification.Classifications) - resultLion := make(chan classification.Classifications) - - go gotWithCallback(picPanda, resultPanda, got) - go gotWithCallback(picLion, resultLion, got) - - valuePanda := <-resultPanda - valueLion := <-resultLion - - close(resultPanda) - close(resultLion) - - return valuePanda, valueLion -} - -func gotWithCallback(img *rimage.Image, result chan classification.Classifications, got classification.Classifier) { - classifications, _ := got(context.Background(), img) - result <- classifications -} - -func getNClassifications( - ctx context.Context, - t *testing.T, - img *rimage.Image, - n int, - c classification.Classifier, -) { - t.Helper() - results := make([]classification.Classifications, n) - var err error - - for i := 0; i < n; i++ { - results[i], err = c(ctx, img) - test.That(t, err, test.ShouldBeNil) - res, err := results[i].TopN(1) - test.That(t, err, test.ShouldBeNil) - test.That(t, res[0].Score(), test.ShouldNotBeNil) - } -} diff --git a/services/vision/mlvision/segmenter3d.go b/services/vision/mlvision/segmenter3d.go deleted file mode 100644 index 9fb1af7004d..00000000000 --- a/services/vision/mlvision/segmenter3d.go +++ /dev/null @@ -1,14 +0,0 @@ -package mlvision - -import ( - "errors" - "sync" - - "go.viam.com/rdk/services/mlmodel" - "go.viam.com/rdk/vision/segmentation" -) - -// TODO: RSDK-2665, build 3D segmenter from ML models. -func attemptToBuild3DSegmenter(mlm mlmodel.Service, inNameMap, outNameMap *sync.Map) (segmentation.Segmenter, error) { - return nil, errors.New("cannot use model as a 3D segmenter: vision 3D segmenters from ML models are currently not supported") -} diff --git a/services/vision/obstaclesdepth/obstacles_depth.go b/services/vision/obstaclesdepth/obstacles_depth.go deleted file mode 100644 index ed450b98d9b..00000000000 --- a/services/vision/obstaclesdepth/obstacles_depth.go +++ /dev/null @@ -1,163 +0,0 @@ -//go:build !no_cgo - -// Package obstaclesdepth uses an underlying depth camera to fulfill GetObjectPointClouds, -// projecting its depth map to a point cloud, an then applying a point cloud clustering algorithm -package obstaclesdepth - -import ( - "context" - "sort" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/depthadapter" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/robot" - svision "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" - vision "go.viam.com/rdk/vision" - "go.viam.com/rdk/vision/segmentation" -) - -var model = resource.DefaultModelFamily.WithModel("obstacles_depth") - -func init() { - resource.RegisterService(svision.API, model, resource.Registration[svision.Service, *ObsDepthConfig]{ - DeprecatedRobotConstructor: func( - ctx context.Context, r any, c resource.Config, logger logging.Logger, - ) (svision.Service, error) { - attrs, err := resource.NativeConfig[*ObsDepthConfig](c) - if err != nil { - return nil, err - } - actualR, err := utils.AssertType[robot.Robot](r) - if err != nil { - return nil, err - } - return registerObstaclesDepth(ctx, c.ResourceName(), attrs, actualR, logger) - }, - }) -} - -// ObsDepthConfig specifies the parameters to be used for the obstacle depth service. -type ObsDepthConfig struct { - resource.TriviallyValidateConfig - MinPtsInPlane int `json:"min_points_in_plane"` - MinPtsInSegment int `json:"min_points_in_segment"` - MaxDistFromPlane float64 `json:"max_dist_from_plane_mm"` - ClusteringRadius int `json:"clustering_radius"` - ClusteringStrictness float64 `json:"clustering_strictness"` - AngleTolerance float64 `json:"ground_angle_tolerance_degs"` -} - -// obsDepth is the underlying struct actually used by the service. -type obsDepth struct { - clusteringConf *segmentation.ErCCLConfig - intrinsics *transform.PinholeCameraIntrinsics -} - -func registerObstaclesDepth( - ctx context.Context, - name resource.Name, - conf *ObsDepthConfig, - r robot.Robot, - logger logging.Logger, -) (svision.Service, error) { - _, span := trace.StartSpan(ctx, "service::vision::registerObstacleDepth") - defer span.End() - if conf == nil { - return nil, errors.New("config for obstacles_depth cannot be nil") - } - // build the clustering config - cfg := &segmentation.ErCCLConfig{ - MinPtsInPlane: conf.MinPtsInPlane, - MinPtsInSegment: conf.MinPtsInSegment, - MaxDistFromPlane: conf.MaxDistFromPlane, - NormalVec: r3.Vector{0, -1, 0}, - AngleTolerance: conf.AngleTolerance, - ClusteringRadius: conf.ClusteringRadius, - ClusteringStrictness: conf.ClusteringStrictness, - } - err := cfg.CheckValid() - if err != nil { - return nil, errors.Wrap(err, "error building clustering config for obstacles_depth") - } - myObsDep := &obsDepth{ - clusteringConf: cfg, - } - - segmenter := myObsDep.buildObsDepth(logger) // does the thing - return svision.NewService(name, r, nil, nil, nil, segmenter) -} - -// BuildObsDepth will check for intrinsics and determine how to build based on that. -func (o *obsDepth) buildObsDepth(logger logging.Logger) func( - ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - return func(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - props, err := src.Properties(ctx) - if err != nil { - logger.CWarnw(ctx, "could not find camera properties. obstacles depth started without camera's intrinsic parameters", "error", err) - return o.obsDepthNoIntrinsics(ctx, src) - } - if props.IntrinsicParams == nil { - logger.CWarn(ctx, "obstacles depth started but camera did not have intrinsic parameters") - return o.obsDepthNoIntrinsics(ctx, src) - } - o.intrinsics = props.IntrinsicParams - return o.obsDepthWithIntrinsics(ctx, src) - } -} - -// buildObsDepthNoIntrinsics will return the median depth in the depth map as a Geometry point. -func (o *obsDepth) obsDepthNoIntrinsics(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - pic, release, err := camera.ReadImage(ctx, src) - if err != nil { - return nil, errors.Errorf("could not get image from %s", src) - } - defer release() - - dm, err := rimage.ConvertImageToDepthMap(ctx, pic) - if err != nil { - return nil, errors.New("could not convert image to depth map") - } - depData := dm.Data() - if len(depData) == 0 { - return nil, errors.New("could not get info from depth map") - } - // Sort the depth data [smallest...largest] - sort.Slice(depData, func(i, j int) bool { - return depData[i] < depData[j] - }) - med := int(0.5 * float64(len(depData))) - pt := spatialmath.NewPoint(r3.Vector{X: 0, Y: 0, Z: float64(depData[med])}, "") - toReturn := make([]*vision.Object, 1) - toReturn[0] = &vision.Object{Geometry: pt} - return toReturn, nil -} - -// buildObsDepthWithIntrinsics will use the methodology in Manduchi et al. to find obstacle points -// before clustering and projecting those points into 3D obstacles. -func (o *obsDepth) obsDepthWithIntrinsics(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - // Check if we have intrinsics here. If not, don't even try - if o.intrinsics == nil { - return nil, errors.New("tried to build obstacles depth with intrinsics but no instrinsics found") - } - pic, release, err := camera.ReadImage(ctx, src) - if err != nil { - return nil, errors.Errorf("could not get image from %s", src) - } - defer release() - dm, err := rimage.ConvertImageToDepthMap(ctx, pic) - if err != nil { - return nil, errors.New("could not convert image to depth map") - } - cloud := depthadapter.ToPointCloud(dm, o.intrinsics) - return segmentation.ApplyERCCLToPointCloud(ctx, cloud, o.clusteringConf) -} diff --git a/services/vision/obstaclesdepth/obstacles_depth_test.go b/services/vision/obstaclesdepth/obstacles_depth_test.go deleted file mode 100644 index a075bf04b9c..00000000000 --- a/services/vision/obstaclesdepth/obstacles_depth_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package obstaclesdepth - -import ( - "context" - "image" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" -) - -// testReader creates and serves a fake depth image for testing. -type testReader struct{} - -func (r testReader) Read(ctx context.Context) (image.Image, func(), error) { - d := rimage.NewEmptyDepthMap(50, 50) - for i := 0; i < 40; i++ { - for j := 5; j < 45; j++ { - d.Set(i, j, rimage.Depth(400)) - } - } - return d, nil, nil -} - -func (r testReader) Close(ctx context.Context) error { - return nil -} - -// fullReader grabs and serves a fake depth image for testing. -type fullReader struct{} - -func (r fullReader) Read(ctx context.Context) (image.Image, func(), error) { - // We want this to return a valid depth image of known size (424 x 240) - pic, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("pointcloud/the_depth_image_intel_424.png")) - return pic, nil, err -} - -func (r fullReader) Close(ctx context.Context) error { - return nil -} - -func TestObstacleDepth(t *testing.T) { - someIntrinsics := transform.PinholeCameraIntrinsics{ - Width: 424, - Height: 240, - Fx: 304.1299133300781, - Fy: 304.2772216796875, - Ppx: 213.47967529296875, - Ppy: 124.63351440429688, - } - noIntrinsicsCfg := ObsDepthConfig{} - withIntrinsicsCfg := ObsDepthConfig{ - MinPtsInPlane: 2000, - MinPtsInSegment: 500, - MaxDistFromPlane: 12.0, - ClusteringRadius: 10, - ClusteringStrictness: 0.00000001, - } - - ctx := context.Background() - testLogger := logging.NewLogger("test") - r := &inject.Robot{ResourceNamesFunc: func() []resource.Name { - return []resource.Name{camera.Named("testCam"), camera.Named("noIntrinsicsCam")} - }} - // camera with intrinsics - fr := fullReader{} - syst := transform.PinholeCameraModel{&someIntrinsics, nil} - myCamSrcIntrinsics, err := camera.NewVideoSourceFromReader(ctx, fr, &syst, camera.DepthStream) - test.That(t, err, test.ShouldBeNil) - test.That(t, myCamSrcIntrinsics, test.ShouldNotBeNil) - myIntrinsicsCam := camera.FromVideoSource(resource.Name{Name: "testCam"}, myCamSrcIntrinsics, testLogger) - // camera without intrinsics - tr := testReader{} - myCamSrcNoIntrinsics, err := camera.NewVideoSourceFromReader(ctx, tr, nil, camera.DepthStream) - test.That(t, err, test.ShouldBeNil) - test.That(t, myCamSrcNoIntrinsics, test.ShouldNotBeNil) - noIntrinsicsCam := camera.FromVideoSource(resource.Name{Name: "noIntrinsicsCam"}, myCamSrcNoIntrinsics, testLogger) - // set up the fake robot - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "testCam": - return myIntrinsicsCam, nil - case "noIntrinsicsCam": - return noIntrinsicsCam, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - name := vision.Named("test") - srv, err := registerObstaclesDepth(ctx, name, &noIntrinsicsCfg, r, testLogger) - test.That(t, err, test.ShouldBeNil) - test.That(t, srv.Name(), test.ShouldResemble, name) - - // Not a detector or classifier - img, err := rimage.NewImageFromFile(artifact.MustPath("vision/objectdetection/detection_test.jpg")) - test.That(t, err, test.ShouldBeNil) - test.That(t, img, test.ShouldNotBeNil) - _, err = srv.Detections(ctx, img, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not implement") - _, err = srv.Classifications(ctx, img, 1, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not implement") - - t.Run("no intrinsics version", func(t *testing.T) { - // Test that it is a segmenter - obs, err := srv.GetObjectPointClouds(ctx, "noIntrinsicsCam", nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, obs, test.ShouldNotBeNil) - test.That(t, len(obs), test.ShouldEqual, 1) - test.That(t, obs[0].PointCloud, test.ShouldBeNil) - poseShouldBe := spatialmath.NewPose(r3.Vector{0, 0, 400}, nil) - test.That(t, obs[0].Geometry.Pose(), test.ShouldResemble, poseShouldBe) - }) - t.Run("intrinsics version", func(t *testing.T) { - // Now with intrinsics (and pointclouds)! - srv2, err := registerObstaclesDepth(ctx, name, &withIntrinsicsCfg, r, testLogger) - test.That(t, err, test.ShouldBeNil) - test.That(t, srv2, test.ShouldNotBeNil) - obs, err := srv2.GetObjectPointClouds(ctx, "testCam", nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, obs, test.ShouldNotBeNil) - test.That(t, len(obs), test.ShouldEqual, 2) - for _, o := range obs { - test.That(t, o.PointCloud, test.ShouldNotBeNil) - test.That(t, o.Geometry, test.ShouldNotBeNil) - } - }) -} - -func BenchmarkObstacleDepthIntrinsics(b *testing.B) { - someIntrinsics := transform.PinholeCameraIntrinsics{ - Width: 424, - Height: 240, - Fx: 304.1299133300781, - Fy: 304.2772216796875, - Ppx: 213.47967529296875, - Ppy: 124.63351440429688, - } - withIntrinsicsCfg := ObsDepthConfig{ - MinPtsInPlane: 2000, - MinPtsInSegment: 500, - MaxDistFromPlane: 12.0, - ClusteringRadius: 10, - ClusteringStrictness: 0.0001, - } - - ctx := context.Background() - testLogger := logging.NewLogger("test") - r := &inject.Robot{ResourceNamesFunc: func() []resource.Name { - return []resource.Name{camera.Named("testCam")} - }} - tr := fullReader{} - syst := transform.PinholeCameraModel{&someIntrinsics, nil} - myCamSrc, _ := camera.NewVideoSourceFromReader(ctx, tr, &syst, camera.DepthStream) - myCam := camera.FromVideoSource(resource.Name{Name: "testCam"}, myCamSrc, testLogger) - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "testCam": - return myCam, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - name := vision.Named("test") - srv, _ := registerObstaclesDepth(ctx, name, &withIntrinsicsCfg, r, testLogger) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - srv.GetObjectPointClouds(ctx, "testCam", nil) - } -} - -func BenchmarkObstacleDepthNoIntrinsics(b *testing.B) { - noIntrinsicsCfg := ObsDepthConfig{} - - ctx := context.Background() - testLogger := logging.NewLogger("test") - r := &inject.Robot{ResourceNamesFunc: func() []resource.Name { - return []resource.Name{camera.Named("testCam")} - }} - tr := fullReader{} - myCamSrc, _ := camera.NewVideoSourceFromReader(ctx, tr, nil, camera.DepthStream) - myCam := camera.FromVideoSource(resource.Name{Name: "testCam"}, myCamSrc, testLogger) - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "testCam": - return myCam, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - name := vision.Named("test") - srv, _ := registerObstaclesDepth(ctx, name, &noIntrinsicsCfg, r, testLogger) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - srv.GetObjectPointClouds(ctx, "testCam", nil) - } -} diff --git a/services/vision/obstaclesdistance/obstacles_distance.go b/services/vision/obstaclesdistance/obstacles_distance.go deleted file mode 100644 index ef887e48275..00000000000 --- a/services/vision/obstaclesdistance/obstacles_distance.go +++ /dev/null @@ -1,179 +0,0 @@ -// Package obstaclesdistance uses an underlying camera to fulfill vision service methods, specifically -// GetObjectPointClouds, which performs several queries of NextPointCloud and returns a median point. -package obstaclesdistance - -import ( - "context" - "math" - "sort" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - svision "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/utils" - vision "go.viam.com/rdk/vision" -) - -var model = resource.DefaultModelFamily.WithModel("obstacles_distance") - -// DefaultNumQueries is the default number of times the camera should be queried before averaging. -const DefaultNumQueries = 10 - -// DistanceDetectorConfig specifies the parameters for the camera to be used -// for the obstacle distance detection service. -type DistanceDetectorConfig struct { - NumQueries int `json:"num_queries"` -} - -func init() { - resource.RegisterService(svision.API, model, resource.Registration[svision.Service, *DistanceDetectorConfig]{ - DeprecatedRobotConstructor: func( - ctx context.Context, r any, c resource.Config, logger logging.Logger, - ) (svision.Service, error) { - attrs, err := resource.NativeConfig[*DistanceDetectorConfig](c) - if err != nil { - return nil, err - } - actualR, err := utils.AssertType[robot.Robot](r) - if err != nil { - return nil, err - } - return registerObstacleDistanceDetector(ctx, c.ResourceName(), attrs, actualR) - }, - }) -} - -// Validate ensures all parts of the config are valid. -func (config *DistanceDetectorConfig) Validate(path string) ([]string, error) { - deps := []string{} - if config.NumQueries == 0 { - config.NumQueries = DefaultNumQueries - } - if config.NumQueries < 1 || config.NumQueries > 20 { - return nil, errors.New("invalid number of queries, pick a number between 1 and 20") - } - return deps, nil -} - -func registerObstacleDistanceDetector( - ctx context.Context, - name resource.Name, - conf *DistanceDetectorConfig, - r robot.Robot, -) (svision.Service, error) { - _, span := trace.StartSpan(ctx, "service::vision::registerObstacleDistanceDetector") - defer span.End() - if conf == nil { - return nil, errors.New("config for obstacles_distance cannot be nil") - } - - segmenter := func(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - clouds := make([]pointcloud.PointCloud, 0, conf.NumQueries) - - for i := 0; i < conf.NumQueries; i++ { - nxtPC, err := src.NextPointCloud(ctx) - if err != nil { - return nil, err - } - if nxtPC.Size() == 0 { - continue - } - clouds = append(clouds, nxtPC) - } - if len(clouds) == 0 { - return nil, errors.New("none of the input point clouds contained any points") - } - - median, err := medianFromPointClouds(ctx, clouds) - if err != nil { - return nil, err - } - - // package the result into a vision.Object - vector := pointcloud.NewVector(median.X, median.Y, median.Z) - pt := spatialmath.NewPoint(vector, "obstacle") - - pcToReturn := pointcloud.New() - basicData := pointcloud.NewBasicData() - err = pcToReturn.Set(vector, basicData) - if err != nil { - return nil, err - } - - toReturn := make([]*vision.Object, 1) - toReturn[0] = &vision.Object{PointCloud: pcToReturn, Geometry: pt} - - return toReturn, nil - } - return svision.NewService(name, r, nil, nil, nil, segmenter) -} - -func medianFromPointClouds(ctx context.Context, clouds []pointcloud.PointCloud) (r3.Vector, error) { - var results [][]r3.Vector // a slice for each process, which will contain a slice of vectors - err := utils.GroupWorkParallel( - ctx, - len(clouds), - func(numGroups int) { - results = make([][]r3.Vector, numGroups) - }, - func(groupNum, groupSize, from, to int) (utils.MemberWorkFunc, utils.GroupWorkDoneFunc) { - closestPoints := make([]r3.Vector, 0, groupSize) - return func(memberNum, workNum int) { - closestPoint := getClosestPoint(clouds[workNum]) - closestPoints = append(closestPoints, closestPoint) - }, func() { - results[groupNum] = closestPoints - } - }, - ) - if err != nil { - return r3.Vector{}, err - } - candidates := make([]r3.Vector, 0, len(clouds)) - for _, r := range results { - candidates = append(candidates, r...) - } - if len(candidates) == 0 { - return r3.Vector{}, errors.New("point cloud list is empty, could not find median point") - } - return getMedianPoint(candidates), nil -} - -func getClosestPoint(cloud pointcloud.PointCloud) r3.Vector { - minDistance := math.MaxFloat64 - minPoint := r3.Vector{} - cloud.Iterate(0, 0, func(pt r3.Vector, d pointcloud.Data) bool { - dist := pt.Norm2() - if dist < minDistance { - minDistance = dist - minPoint = pt - } - return true - }) - return minPoint -} - -// to calculate the median, will need to sort the vectors by distance from origin. -func sortVectors(v []r3.Vector) { - sort.Sort(points(v)) -} - -type points []r3.Vector - -func (p points) Len() int { return len(p) } -func (p points) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -func (p points) Less(i, j int) bool { return p[i].Norm2() < p[j].Norm2() } - -func getMedianPoint(pts []r3.Vector) r3.Vector { - sortVectors(pts) - index := (len(pts) - 1) / 2 - return pts[index] -} diff --git a/services/vision/obstaclesdistance/obstacles_distance_test.go b/services/vision/obstaclesdistance/obstacles_distance_test.go deleted file mode 100644 index 8d250971d1a..00000000000 --- a/services/vision/obstaclesdistance/obstacles_distance_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package obstaclesdistance - -import ( - "context" - "image/color" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" -) - -func TestObstacleDist(t *testing.T) { - // Setting a global in utils is unsafe, and was originally in an init() which causes races. - // This is still not ideal, but as this is the only test function in this package, it should be okay for now. - origParallelFactor := utils.ParallelFactor - utils.ParallelFactor = 1 - defer func() { - utils.ParallelFactor = origParallelFactor - }() - - inp := DistanceDetectorConfig{ - NumQueries: 10, - } - ctx := context.Background() - r := &inject.Robot{} - cam := &inject.Camera{} - - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return nil, errors.New("no pointcloud") - } - r.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{camera.Named("fakeCamera")} - } - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "fakeCamera": - return cam, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - name := vision.Named("test_odd") - srv, err := registerObstacleDistanceDetector(ctx, name, &inp, r) - test.That(t, err, test.ShouldBeNil) - test.That(t, srv.Name(), test.ShouldResemble, name) - img, err := rimage.NewImageFromFile(artifact.MustPath("vision/objectdetection/detection_test.jpg")) - test.That(t, err, test.ShouldBeNil) - - // Does not implement Detections - _, err = srv.Detections(ctx, img, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not implement") - - // Does not implement Classifications - _, err = srv.Classifications(ctx, img, 1, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not implement") - - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - cloud := pc.New() - err = cloud.Set(pc.NewVector(0, 0, 1), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - return cloud, err - } - objects, err := srv.GetObjectPointClouds(ctx, "fakeCamera", nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(objects), test.ShouldEqual, 1) - _, isPoint := objects[0].PointCloud.At(0, 0, 1) - test.That(t, isPoint, test.ShouldBeTrue) - - point := objects[0].Geometry.Pose().Point() - test.That(t, point.X, test.ShouldEqual, 0) - test.That(t, point.Y, test.ShouldEqual, 0) - test.That(t, point.Z, test.ShouldEqual, 1) - - count := 0 - nums := []float64{10, 9, 4, 5, 3, 1, 2, 6, 7, 8} - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - cloud := pc.New() - err = cloud.Set(pc.NewVector(0, 0, nums[count]), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - count++ - return cloud, err - } - objects, err = srv.GetObjectPointClouds(ctx, "fakeCamera", nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(objects), test.ShouldEqual, 1) - - _, isPoint = objects[0].PointCloud.At(0, 0, 5) - test.That(t, isPoint, test.ShouldBeTrue) - - // more than one point in cloud - count = 0 - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - cloud := pc.New() - err = cloud.Set(pc.NewVector(0, 0, nums[count]), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(0, 0, 6.0), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - return cloud, err - } - objects, err = srv.GetObjectPointClouds(ctx, "fakeCamera", nil) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(objects), test.ShouldEqual, 1) - - _, isPoint = objects[0].PointCloud.At(0, 0, 6) - test.That(t, isPoint, test.ShouldBeTrue) - - // with error - nil parameters - _, err = registerObstacleDistanceDetector(ctx, name, nil, r) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot be nil") -} diff --git a/services/vision/obstaclespointcloud/obstacles_pointcloud.go b/services/vision/obstaclespointcloud/obstacles_pointcloud.go deleted file mode 100644 index aaff39bc572..00000000000 --- a/services/vision/obstaclespointcloud/obstacles_pointcloud.go +++ /dev/null @@ -1,57 +0,0 @@ -// Package obstaclespointcloud uses the 3D radius clustering algorithm as defined in the -// RDK vision/segmentation package as vision model. -package obstaclespointcloud - -import ( - "context" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/segmentation" -) - -var model = resource.DefaultModelFamily.WithModel("obstacles_pointcloud") - -func init() { - resource.RegisterService(vision.API, model, resource.Registration[vision.Service, *segmentation.ErCCLConfig]{ - DeprecatedRobotConstructor: func( - ctx context.Context, r any, c resource.Config, logger logging.Logger, - ) (vision.Service, error) { - attrs, err := resource.NativeConfig[*segmentation.ErCCLConfig](c) - if err != nil { - return nil, err - } - actualR, err := utils.AssertType[robot.Robot](r) - if err != nil { - return nil, err - } - return registerOPSegmenter(ctx, c.ResourceName(), attrs, actualR) - }, - }) -} - -// registerOPSegmenter creates a new 3D radius clustering segmenter from the config. -func registerOPSegmenter( - ctx context.Context, - name resource.Name, - conf *segmentation.ErCCLConfig, - r robot.Robot, -) (vision.Service, error) { - _, span := trace.StartSpan(ctx, "service::vision::registerObstaclesPointcloud") - defer span.End() - if conf == nil { - return nil, errors.New("config for obstacles pointcloud segmenter cannot be nil") - } - err := conf.CheckValid() - if err != nil { - return nil, errors.Wrap(err, "obstacles pointcloud segmenter config error") - } - segmenter := segmentation.Segmenter(conf.ErCCLAlgorithm) - return vision.NewService(name, r, nil, nil, nil, segmenter) -} diff --git a/services/vision/obstaclespointcloud/obstacles_pointcloud_test.go b/services/vision/obstaclespointcloud/obstacles_pointcloud_test.go deleted file mode 100644 index 6c79420d887..00000000000 --- a/services/vision/obstaclespointcloud/obstacles_pointcloud_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package obstaclespointcloud - -import ( - "context" - "image/color" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/components/camera" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision/segmentation" -) - -func TestRadiusClusteringSegmentation(t *testing.T) { - r := &inject.Robot{} - cam := &inject.Camera{} - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return nil, errors.New("no pointcloud") - } - r.ResourceNamesFunc = func() []resource.Name { - return []resource.Name{camera.Named("fakeCamera")} - } - r.ResourceByNameFunc = func(n resource.Name) (resource.Resource, error) { - switch n.Name { - case "fakeCamera": - return cam, nil - default: - return nil, resource.NewNotFoundError(n) - } - } - params := &segmentation.ErCCLConfig{ - MinPtsInPlane: 100, - MaxDistFromPlane: 10, - MinPtsInSegment: 3, - AngleTolerance: 20, - NormalVec: r3.Vector{0, 0, 1}, - ClusteringRadius: 5, - ClusteringStrictness: 3, - } - // bad registration, no parameters - name := vision.Named("test_rcs") - _, err := registerOPSegmenter(context.Background(), name, nil, r) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "cannot be nil") - // bad registration, parameters out of bounds - params.ClusteringRadius = -3 - _, err = registerOPSegmenter(context.Background(), name, params, r) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "segmenter config error") - // successful registration - params.ClusteringRadius = 1 - seg, err := registerOPSegmenter(context.Background(), name, params, r) - test.That(t, err, test.ShouldBeNil) - test.That(t, seg.Name(), test.ShouldResemble, name) - - // fails on not finding camera - _, err = seg.GetObjectPointClouds(context.Background(), "no_camera", map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "not found") - - // fails since camera cannot generate point clouds - _, err = seg.GetObjectPointClouds(context.Background(), "fakeCamera", map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "no pointcloud") - - // successful, creates two clusters of points - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - cloud := pc.New() - // cluster 1 - err = cloud.Set(pc.NewVector(1, 1, 1), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(1, 1, 2), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(1, 1, 3), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(1, 1, 4), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - // cluster 2 - err = cloud.Set(pc.NewVector(2, 2, 101), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(2, 2, 102), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(2, 2, 103), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(2, 2, 104), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - return cloud, nil - } - objects, err := seg.GetObjectPointClouds(context.Background(), "fakeCamera", map[string]interface{}{}) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(objects), test.ShouldEqual, 2) - // does not implement detector - _, err = seg.Detections(context.Background(), nil, nil) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "does not implement") -} diff --git a/services/vision/register/register.go b/services/vision/register/register.go deleted file mode 100644 index 9603c19b0cf..00000000000 --- a/services/vision/register/register.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package register registers all relevant vision models and also API specific functions -package register - -import ( - // for vision models. - _ "go.viam.com/rdk/services/vision" - _ "go.viam.com/rdk/services/vision/mlvision" -) diff --git a/services/vision/register/register_cgo.go b/services/vision/register/register_cgo.go deleted file mode 100644 index 214a61afa30..00000000000 --- a/services/vision/register/register_cgo.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !no_cgo - -// Package register registers all relevant vision models and also API specific functions -package register - -import ( - // for vision models. - _ "go.viam.com/rdk/services/vision/colordetector" - _ "go.viam.com/rdk/services/vision/detectionstosegments" - _ "go.viam.com/rdk/services/vision/obstaclesdepth" - _ "go.viam.com/rdk/services/vision/obstaclesdistance" - _ "go.viam.com/rdk/services/vision/obstaclespointcloud" -) diff --git a/services/vision/server.go b/services/vision/server.go deleted file mode 100644 index 9c58cd643a8..00000000000 --- a/services/vision/server.go +++ /dev/null @@ -1,232 +0,0 @@ -package vision - -import ( - "bytes" - "context" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - commonpb "go.viam.com/api/common/v1" - pb "go.viam.com/api/service/vision/v1" - - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/protoutils" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" -) - -// serviceServer implements the Vision Service. -type serviceServer struct { - pb.UnimplementedVisionServiceServer - coll resource.APIResourceCollection[Service] -} - -// NewRPCServiceServer constructs a vision gRPC service server. -// It is intentionally untyped to prevent use outside of tests. -func NewRPCServiceServer(coll resource.APIResourceCollection[Service]) interface{} { - return &serviceServer{coll: coll} -} - -func (server *serviceServer) GetDetections( - ctx context.Context, - req *pb.GetDetectionsRequest, -) (*pb.GetDetectionsResponse, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::server::GetDetections") - defer span.End() - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - img, err := rimage.DecodeImage(ctx, req.Image, req.MimeType) - if err != nil { - return nil, err - } - detections, err := svc.Detections(ctx, img, req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoDets := make([]*pb.Detection, 0, len(detections)) - for _, det := range detections { - box := det.BoundingBox() - if box == nil { - return nil, errors.New("detection has no bounding box, must return a bounding box") - } - xMin := int64(box.Min.X) - yMin := int64(box.Min.Y) - xMax := int64(box.Max.X) - yMax := int64(box.Max.Y) - d := &pb.Detection{ - XMin: &xMin, - YMin: &yMin, - XMax: &xMax, - YMax: &yMax, - Confidence: det.Score(), - ClassName: det.Label(), - } - protoDets = append(protoDets, d) - } - return &pb.GetDetectionsResponse{ - Detections: protoDets, - }, nil -} - -func (server *serviceServer) GetDetectionsFromCamera( - ctx context.Context, - req *pb.GetDetectionsFromCameraRequest, -) (*pb.GetDetectionsFromCameraResponse, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::server::GetDetectionsFromCamera") - defer span.End() - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - detections, err := svc.DetectionsFromCamera(ctx, req.CameraName, req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoDets := make([]*pb.Detection, 0, len(detections)) - for _, det := range detections { - box := det.BoundingBox() - if box == nil { - return nil, errors.New("detection has no bounding box, must return a bounding box") - } - xMin := int64(box.Min.X) - yMin := int64(box.Min.Y) - xMax := int64(box.Max.X) - yMax := int64(box.Max.Y) - d := &pb.Detection{ - XMin: &xMin, - YMin: &yMin, - XMax: &xMax, - YMax: &yMax, - Confidence: det.Score(), - ClassName: det.Label(), - } - protoDets = append(protoDets, d) - } - return &pb.GetDetectionsFromCameraResponse{ - Detections: protoDets, - }, nil -} - -func (server *serviceServer) GetClassifications( - ctx context.Context, - req *pb.GetClassificationsRequest, -) (*pb.GetClassificationsResponse, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::server::GetClassifications") - defer span.End() - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - img, err := rimage.DecodeImage(ctx, req.Image, req.MimeType) - if err != nil { - return nil, err - } - classifications, err := svc.Classifications(ctx, img, int(req.N), req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoCs := make([]*pb.Classification, 0, len(classifications)) - for _, c := range classifications { - cc := &pb.Classification{ - ClassName: c.Label(), - Confidence: c.Score(), - } - protoCs = append(protoCs, cc) - } - return &pb.GetClassificationsResponse{ - Classifications: protoCs, - }, nil -} - -func (server *serviceServer) GetClassificationsFromCamera( - ctx context.Context, - req *pb.GetClassificationsFromCameraRequest, -) (*pb.GetClassificationsFromCameraResponse, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::server::GetClassificationsFromCamera") - defer span.End() - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - classifications, err := svc.ClassificationsFromCamera(ctx, req.CameraName, int(req.N), req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoCs := make([]*pb.Classification, 0, len(classifications)) - for _, c := range classifications { - cc := &pb.Classification{ - ClassName: c.Label(), - Confidence: c.Score(), - } - protoCs = append(protoCs, cc) - } - return &pb.GetClassificationsFromCameraResponse{ - Classifications: protoCs, - }, nil -} - -// GetObjectPointClouds returns an array of objects from the frame from a camera of the underlying robot. A specific MIME type -// can be requested but may not necessarily be the same one returned. Also returns a Vector3 array of the center points of each object. -func (server *serviceServer) GetObjectPointClouds( - ctx context.Context, - req *pb.GetObjectPointCloudsRequest, -) (*pb.GetObjectPointCloudsResponse, error) { - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - objects, err := svc.GetObjectPointClouds(ctx, req.CameraName, req.Extra.AsMap()) - if err != nil { - return nil, err - } - protoSegments, err := segmentsToProto(req.CameraName, objects) - if err != nil { - return nil, err - } - - return &pb.GetObjectPointCloudsResponse{ - MimeType: utils.MimeTypePCD, - Objects: protoSegments, - }, nil -} - -func segmentsToProto(frame string, segs []*vision.Object) ([]*commonpb.PointCloudObject, error) { - protoSegs := make([]*commonpb.PointCloudObject, 0, len(segs)) - for _, seg := range segs { - var buf bytes.Buffer - if seg.PointCloud == nil { - seg.PointCloud = pointcloud.New() - } - err := pointcloud.ToPCD(seg, &buf, pointcloud.PCDBinary) - if err != nil { - return nil, err - } - ps := &commonpb.PointCloudObject{ - PointCloud: buf.Bytes(), - Geometries: &commonpb.GeometriesInFrame{ - Geometries: []*commonpb.Geometry{seg.Geometry.ToProtobuf()}, - ReferenceFrame: frame, - }, - } - protoSegs = append(protoSegs, ps) - } - return protoSegs, nil -} - -// DoCommand receives arbitrary commands. -func (server *serviceServer) DoCommand(ctx context.Context, - req *commonpb.DoCommandRequest, -) (*commonpb.DoCommandResponse, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::server::DoCommand") - defer span.End() - - svc, err := server.coll.Resource(req.Name) - if err != nil { - return nil, err - } - return protoutils.DoFromResourceServer(ctx, svc, req) -} diff --git a/services/vision/server_test.go b/services/vision/server_test.go deleted file mode 100644 index 282e84cf76c..00000000000 --- a/services/vision/server_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package vision_test - -import ( - "context" - "image" - "testing" - - "github.com/pkg/errors" - pb "go.viam.com/api/service/vision/v1" - "go.viam.com/test" - "go.viam.com/utils/artifact" - "go.viam.com/utils/protoutils" - - _ "go.viam.com/rdk/components/camera/register" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/objectdetection" -) - -func newServer(m map[resource.Name]vision.Service) (pb.VisionServiceServer, error) { - coll, err := resource.NewAPIResourceCollection(vision.API, m) - if err != nil { - return nil, err - } - return vision.NewRPCServiceServer(coll).(pb.VisionServiceServer), nil -} - -func TestVisionServerFailures(t *testing.T) { - img, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/dogscute.jpeg")) - test.That(t, err, test.ShouldBeNil) - imgBytes, err := rimage.EncodeImage(context.Background(), img, utils.MimeTypeJPEG) - test.That(t, err, test.ShouldBeNil) - detectRequest := &pb.GetDetectionsRequest{ - Name: testVisionServiceName, - Image: imgBytes, - Width: int64(img.Width()), - Height: int64(img.Height()), - MimeType: utils.MimeTypeJPEG, - } - // no service - m := map[resource.Name]vision.Service{} - server, err := newServer(m) - test.That(t, err, test.ShouldBeNil) - _, err = server.GetDetections(context.Background(), detectRequest) - test.That(t, err, test.ShouldBeError, errors.New("resource \"rdk:service:vision/vision1\" not found")) - // correct server with error returned - injectVS := &inject.VisionService{} - passedErr := errors.New("fake error") - injectVS.DetectionsFunc = func(ctx context.Context, img image.Image, extra map[string]interface{}) ([]objectdetection.Detection, error) { - return nil, passedErr - } - m = map[resource.Name]vision.Service{ - vision.Named(testVisionServiceName): injectVS, - vision.Named(testVisionServiceName2): injectVS, - } - server, err = newServer(m) - test.That(t, err, test.ShouldBeNil) - _, err = server.GetDetections(context.Background(), detectRequest) - test.That(t, err, test.ShouldBeError, passedErr) -} - -func TestServerGetDetections(t *testing.T) { - injectVS := &inject.VisionService{} - m := map[resource.Name]vision.Service{ - visName1: injectVS, - } - server, err := newServer(m) - test.That(t, err, test.ShouldBeNil) - - // returns response - img, err := rimage.NewImageFromFile(artifact.MustPath("vision/tflite/dogscute.jpeg")) - test.That(t, err, test.ShouldBeNil) - imgBytes, err := rimage.EncodeImage(context.Background(), img, utils.MimeTypeJPEG) - test.That(t, err, test.ShouldBeNil) - extra := map[string]interface{}{"foo": "GetDetections"} - ext, err := protoutils.StructToStructPb(extra) - detectRequest := &pb.GetDetectionsRequest{ - Name: testVisionServiceName, - Image: imgBytes, - Width: int64(img.Width()), - Height: int64(img.Height()), - MimeType: utils.MimeTypeJPEG, - Extra: ext, - } - injectVS.DetectionsFunc = func(ctx context.Context, img image.Image, extra map[string]interface{}) ([]objectdetection.Detection, error) { - det1 := objectdetection.NewDetection(image.Rectangle{}, 0.5, "yes") - return []objectdetection.Detection{det1}, nil - } - test.That(t, err, test.ShouldBeNil) - resp, err := server.GetDetections(context.Background(), detectRequest) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(resp.GetDetections()), test.ShouldEqual, 1) - test.That(t, resp.GetDetections()[0].GetClassName(), test.ShouldEqual, "yes") -} diff --git a/services/vision/verify_main_test.go b/services/vision/verify_main_test.go deleted file mode 100644 index 807ac775c31..00000000000 --- a/services/vision/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package vision - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/services/vision/vision.go b/services/vision/vision.go deleted file mode 100644 index db56d222f2e..00000000000 --- a/services/vision/vision.go +++ /dev/null @@ -1,291 +0,0 @@ -// Package vision is the service that allows you to access various computer vision algorithms -// (like detection, segmentation, tracking, etc) that usually only require a camera or image input. -package vision - -import ( - "context" - "image" - - "github.com/pkg/errors" - "go.opencensus.io/trace" - servicepb "go.viam.com/api/service/vision/v1" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - viz "go.viam.com/rdk/vision" - "go.viam.com/rdk/vision/classification" - "go.viam.com/rdk/vision/objectdetection" - "go.viam.com/rdk/vision/segmentation" -) - -func init() { - resource.RegisterAPI(API, resource.APIRegistration[Service]{ - RPCServiceServerConstructor: NewRPCServiceServer, - RPCServiceHandler: servicepb.RegisterVisionServiceHandlerFromEndpoint, - RPCServiceDesc: &servicepb.VisionService_ServiceDesc, - RPCClient: NewClientFromConn, - }) -} - -// A Service that implements various computer vision algorithms like detection and segmentation. -// -// DetectionsFromCamera example: -// -// // Get detections from the camera output -// detections, err := visService.DetectionsFromCamera(context.Background(), myCam, nil) -// if err != nil { -// logger.Fatalf("Could not get detections: %v", err) -// } -// if len(detections) > 0 { -// logger.Info(detections[0]) -// } -// -// Detections example: -// -// // Get the stream from a camera -// camStream, err := myCam.Stream(context.Background()) -// -// // Get an image from the camera stream -// img, release, err := camStream.Next(context.Background()) -// defer release() -// -// // Get the detections from the image -// detections, err := visService.Detections(context.Background(), img, nil) -// if err != nil { -// logger.Fatalf("Could not get detections: %v", err) -// } -// if len(detections) > 0 { -// logger.Info(detections[0]) -// } -// -// ClassificationsFromCamera example: -// -// // Get the 2 classifications with the highest confidence scores from the camera output -// classifications, err := visService.ClassificationsFromCamera(context.Background(), myCam, 2, nil) -// if err != nil { -// logger.Fatalf("Could not get classifications: %v", err) -// } -// if len(classifications) > 0 { -// logger.Info(classifications[0]) -// } -// -// Classifications example: -// -// // Get the stream from a camera -// camStream, err := myCam.Stream(context.Background()) -// if err!=nil { -// logger.Error(err) -// return -// } -// -// // Get an image from the camera stream -// img, release, err := camStream.Next(context.Background()) -// defer release() -// -// // Get the 2 classifications with the highest confidence scores from the image -// classifications, err := visService.Classifications(context.Background(), img, 2, nil) -// if err != nil { -// logger.Fatalf("Could not get classifications: %v", err) -// } -// if len(classifications) > 0 { -// logger.Info(classifications[0]) -// } -// -// GetObjectPointClouds example: -// -// // Get the objects from the camera output -// objects, err := visService.GetObjectPointClouds(context.Background(), "cam1", nil) -// if err != nil { -// logger.Fatalf("Could not get point clouds: %v", err) -// } -// if len(objects) > 0 { -// logger.Info(objects[0]) -// } -type Service interface { - resource.Resource - // DetectionsFromCamera returns a list of detections from the next image from a specified camera using a configured detector. - DetectionsFromCamera(ctx context.Context, cameraName string, extra map[string]interface{}) ([]objectdetection.Detection, error) - - // Detections returns a list of detections from a given image using a configured detector. - Detections(ctx context.Context, img image.Image, extra map[string]interface{}) ([]objectdetection.Detection, error) - - // ClassificationsFromCamera returns a list of classifications from the next image from a specified camera using a configured classifier. - ClassificationsFromCamera( - ctx context.Context, - cameraName string, - n int, - extra map[string]interface{}, - ) (classification.Classifications, error) - - // Classifications returns a list of classifications from a given image using a configured classifier. - Classifications( - ctx context.Context, - img image.Image, - n int, - extra map[string]interface{}, - ) (classification.Classifications, error) - - // GetObjectPointClouds returns a list of 3D point cloud objects and metadata from the latest 3D camera image using a specified segmenter. - GetObjectPointClouds(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) -} - -// SubtypeName is the name of the type of service. -const SubtypeName = "vision" - -// API is a variable that identifies the vision service resource API. -var API = resource.APINamespaceRDK.WithServiceType(SubtypeName) - -// Named is a helper for getting the named vision's typed resource name. -func Named(name string) resource.Name { - return resource.NewName(API, name) -} - -// FromRobot is a helper for getting the named vision service from the given Robot. -func FromRobot(r robot.Robot, name string) (Service, error) { - return robot.ResourceFromRobot[Service](r, Named(name)) -} - -// FromDependencies is a helper for getting the named vision service from a collection of dependencies. -func FromDependencies(deps resource.Dependencies, name string) (Service, error) { - return resource.FromDependencies[Service](deps, Named(name)) -} - -// vizModel wraps the vision model with all the service interface methods. -type vizModel struct { - resource.Named - resource.AlwaysRebuild - r robot.Robot // in order to get access to all cameras - closerFunc func(ctx context.Context) error // close the underlying model - classifierFunc classification.Classifier - detectorFunc objectdetection.Detector - segmenter3DFunc segmentation.Segmenter -} - -// NewService wraps the vision model in the struct that fulfills the vision service interface. -func NewService( - name resource.Name, - r robot.Robot, - c func(ctx context.Context) error, - cf classification.Classifier, - df objectdetection.Detector, - s3f segmentation.Segmenter, -) (Service, error) { - if cf == nil && df == nil && s3f == nil { - return nil, errors.Errorf( - "model %q does not fulfill any method of the vision service. It is neither a detector, nor classifier, nor 3D segmenter", name) - } - return &vizModel{ - Named: name.AsNamed(), - r: r, - closerFunc: c, - classifierFunc: cf, - detectorFunc: df, - segmenter3DFunc: s3f, - }, nil -} - -// Detections returns the detections of given image if the model implements objectdetector.Detector. -func (vm *vizModel) Detections( - ctx context.Context, - img image.Image, - extra map[string]interface{}, -) ([]objectdetection.Detection, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::Detections::"+vm.Named.Name().String()) - defer span.End() - if vm.detectorFunc == nil { - return nil, errors.Errorf("vision model %q does not implement a Detector", vm.Named.Name()) - } - return vm.detectorFunc(ctx, img) -} - -// DetectionsFromCamera returns the detections of the next image from the given camera. -func (vm *vizModel) DetectionsFromCamera( - ctx context.Context, - cameraName string, - extra map[string]interface{}, -) ([]objectdetection.Detection, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::DetectionsFromCamera::"+vm.Named.Name().String()) - defer span.End() - if vm.detectorFunc == nil { - return nil, errors.Errorf("vision model %q does not implement a Detector", vm.Named.Name()) - } - cam, err := camera.FromRobot(vm.r, cameraName) - if err != nil { - return nil, errors.Wrapf(err, "could not find camera named %s", cameraName) - } - img, release, err := camera.ReadImage(ctx, cam) - if err != nil { - return nil, errors.Wrapf(err, "could not get image from %s", cameraName) - } - defer release() - return vm.detectorFunc(ctx, img) -} - -// Classifications returns the classifications of given image if the model implements classifications.Classifier. -func (vm *vizModel) Classifications( - ctx context.Context, - img image.Image, - n int, - extra map[string]interface{}, -) (classification.Classifications, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::Classifications::"+vm.Named.Name().String()) - defer span.End() - if vm.classifierFunc == nil { - return nil, errors.Errorf("vision model %q does not implement a Classifier", vm.Named.Name()) - } - fullClassifications, err := vm.classifierFunc(ctx, img) - if err != nil { - return nil, errors.Wrap(err, "could not get classifications from image") - } - return fullClassifications.TopN(n) -} - -// ClassificationsFromCamera returns the classifications of the next image from the given camera. -func (vm *vizModel) ClassificationsFromCamera( - ctx context.Context, - cameraName string, - n int, - extra map[string]interface{}, -) (classification.Classifications, error) { - ctx, span := trace.StartSpan(ctx, "service::vision::ClassificationsFromCamera::"+vm.Named.Name().String()) - defer span.End() - if vm.classifierFunc == nil { - return nil, errors.Errorf("vision model %q does not implement a Classifier", vm.Named.Name()) - } - cam, err := camera.FromRobot(vm.r, cameraName) - if err != nil { - return nil, errors.Wrapf(err, "could not find camera named %s", cameraName) - } - img, release, err := camera.ReadImage(ctx, cam) - if err != nil { - return nil, errors.Wrapf(err, "could not get image from %s", cameraName) - } - defer release() - fullClassifications, err := vm.classifierFunc(ctx, img) - if err != nil { - return nil, errors.Wrap(err, "could not get classifications from image") - } - return fullClassifications.TopN(n) -} - -// GetObjectPointClouds returns all the found objects in a 3D image if the model implements Segmenter3D. -func (vm *vizModel) GetObjectPointClouds(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) { - if vm.segmenter3DFunc == nil { - return nil, errors.Errorf("vision model %q does not implement a 3D segmenter", vm.Named.Name().String()) - } - ctx, span := trace.StartSpan(ctx, "service::vision::GetObjectPointClouds::"+vm.Named.Name().String()) - defer span.End() - cam, err := camera.FromRobot(vm.r, cameraName) - if err != nil { - return nil, err - } - return vm.segmenter3DFunc(ctx, cam) -} - -func (vm *vizModel) Close(ctx context.Context) error { - if vm.closerFunc == nil { - return nil - } - return vm.closerFunc(ctx) -} diff --git a/services/vision/vision_test.go b/services/vision/vision_test.go deleted file mode 100644 index 8f42c56666c..00000000000 --- a/services/vision/vision_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package vision_test - -import ( - "context" - "image" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/vision" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/vision/objectdetection" -) - -const ( - testVisionServiceName = "vision1" - testVisionServiceName2 = "vision2" -) - -func TestFromRobot(t *testing.T) { - svc1 := &inject.VisionService{} - svc1.DetectionsFunc = func(ctx context.Context, img image.Image, extra map[string]interface{}) ([]objectdetection.Detection, error) { - det1 := objectdetection.NewDetection(image.Rectangle{}, 0.5, "yes") - return []objectdetection.Detection{det1}, nil - } - var r inject.Robot - r.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - return svc1, nil - } - svc, err := vision.FromRobot(&r, testVisionServiceName) - test.That(t, err, test.ShouldBeNil) - test.That(t, svc, test.ShouldNotBeNil) - result, err := svc.Detections(context.Background(), nil, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(result), test.ShouldEqual, 1) - test.That(t, result[0].Score(), test.ShouldEqual, 0.5) -} - -type simpleDetector struct{} - -func (s *simpleDetector) Detect(context.Context, image.Image) ([]objectdetection.Detection, error) { - det1 := objectdetection.NewDetection(image.Rectangle{}, 0.5, "yes") - return []objectdetection.Detection{det1}, nil -} - -func TestNewService(t *testing.T) { - var r inject.Robot - var m simpleDetector - svc, err := vision.NewService(vision.Named("testService"), &r, nil, nil, m.Detect, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, svc, test.ShouldNotBeNil) - result, err := svc.Detections(context.Background(), nil, nil) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(result), test.ShouldEqual, 1) - test.That(t, result[0].Score(), test.ShouldEqual, 0.5) -} diff --git a/session/context_test.go b/session/context_test.go deleted file mode 100644 index 171dec25760..00000000000 --- a/session/context_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package session_test - -import ( - "context" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/google/uuid" - "go.viam.com/test" - "google.golang.org/grpc" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/session" - "go.viam.com/rdk/testutils" -) - -func TestToFromContext(t *testing.T) { - _, ok := session.FromContext(context.Background()) - test.That(t, ok, test.ShouldBeFalse) - - sess1 := session.New(context.Background(), "ownerID", time.Minute, func(id uuid.UUID, resourceName resource.Name) { - }) - nextCtx := session.ToContext(context.Background(), sess1) - sess2, ok := session.FromContext(nextCtx) - test.That(t, ok, test.ShouldBeTrue) - test.That(t, sess2, test.ShouldEqual, sess1) -} - -func TestSafetyMonitor(t *testing.T) { - session.SafetyMonitor(context.Background(), nil) - name := resource.NewName(resource.APINamespace("foo").WithType("bar").WithSubtype("baz"), "barf") - session.SafetyMonitor(context.Background(), myThing{Named: name.AsNamed()}) - - var stored sync.Once - var storedCount int32 - var storedID uuid.UUID - var storedResourceName resource.Name - sess1 := session.New(context.Background(), "ownerID", time.Minute, func(id uuid.UUID, resourceName resource.Name) { - atomic.AddInt32(&storedCount, 1) - stored.Do(func() { - storedID = id - storedResourceName = resourceName - }) - }) - nextCtx := session.ToContext(context.Background(), sess1) - - session.SafetyMonitor(nextCtx, myThing{Named: name.AsNamed()}) - test.That(t, storedID, test.ShouldEqual, sess1.ID()) - test.That(t, storedResourceName, test.ShouldResemble, name) - test.That(t, atomic.LoadInt32(&storedCount), test.ShouldEqual, 1) - - sess1 = session.New(context.Background(), "ownerID", 0, func(id uuid.UUID, resourceName resource.Name) { - atomic.AddInt32(&storedCount, 1) - stored.Do(func() { - storedID = id - storedResourceName = resourceName - }) - }) - nextCtx = session.ToContext(context.Background(), sess1) - session.SafetyMonitor(nextCtx, myThing{Named: name.AsNamed()}) - test.That(t, atomic.LoadInt32(&storedCount), test.ShouldEqual, 1) -} - -func TestSafetyMonitorForMetadata(t *testing.T) { - stream1 := testutils.NewServerTransportStream() - streamCtx := grpc.NewContextWithServerTransportStream(context.Background(), stream1) - - sess1 := session.New(context.Background(), "ownerID", time.Minute, nil) - nextCtx := session.ToContext(streamCtx, sess1) - - name1 := resource.NewName(resource.APINamespace("foo").WithType("bar").WithSubtype("baz"), "barf") - name2 := resource.NewName(resource.APINamespace("woo").WithType("war").WithSubtype("waz"), "warf") - session.SafetyMonitor(nextCtx, myThing{Named: name1.AsNamed()}) - test.That(t, stream1.Value(session.SafetyMonitoredResourceMetadataKey), test.ShouldResemble, []string{name1.String()}) - session.SafetyMonitor(nextCtx, myThing{Named: name2.AsNamed()}) - test.That(t, stream1.Value(session.SafetyMonitoredResourceMetadataKey), test.ShouldResemble, []string{name2.String()}) -} - -type myThing struct { - resource.Named - resource.AlwaysRebuild - resource.TriviallyCloseable -} diff --git a/session/session_test.go b/session/session_test.go deleted file mode 100644 index cf6755a6cfb..00000000000 --- a/session/session_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package session - -import ( - "context" - "net" - "testing" - "time" - - "github.com/google/uuid" - v1 "go.viam.com/api/robot/v1" - "go.viam.com/test" - "google.golang.org/grpc/peer" - - "go.viam.com/rdk/resource" -) - -func TestNew(t *testing.T) { - ownerID := "owner1" - remoteAddr := &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 5} - remoteAddrStr := remoteAddr.String() - info := &v1.PeerConnectionInfo{ - Type: v1.PeerConnectionType_PEER_CONNECTION_TYPE_GRPC, - RemoteAddress: &remoteAddrStr, - LocalAddress: nil, - } - ctx := peer.NewContext(context.Background(), &peer.Peer{ - Addr: remoteAddr, - }) - now := time.Now() - dur := time.Second - someFunc := func(id uuid.UUID, resourceName resource.Name) { - } - sess1 := New(ctx, ownerID, dur, someFunc) - test.That(t, sess1.PeerConnectionInfo(), test.ShouldResembleProto, info) - sess2 := New(ctx, ownerID+"other", dur, someFunc) - test.That(t, sess2.PeerConnectionInfo(), test.ShouldResembleProto, info) - test.That(t, sess2.CheckOwnerID(ownerID), test.ShouldBeFalse) - test.That(t, sess2.CheckOwnerID(ownerID+"other"), test.ShouldBeTrue) - - test.That(t, sess1.ID(), test.ShouldNotEqual, uuid.Nil) - test.That(t, sess1.ID(), test.ShouldNotEqual, sess2.ID()) - test.That(t, sess1.CheckOwnerID(ownerID), test.ShouldBeTrue) - test.That(t, sess1.CheckOwnerID(ownerID+"other"), test.ShouldBeFalse) - test.That(t, sess1.Active(now), test.ShouldBeTrue) - time.Sleep(2 * dur) - test.That(t, sess1.Active(time.Now()), test.ShouldBeFalse) - remoteAddr = &net.TCPAddr{IP: net.IPv4(1, 2, 5, 4), Port: 6} - remoteAddrStr = remoteAddr.String() - info = &v1.PeerConnectionInfo{ - Type: v1.PeerConnectionType_PEER_CONNECTION_TYPE_GRPC, - RemoteAddress: &remoteAddrStr, - } - ctx = peer.NewContext(context.Background(), &peer.Peer{ - Addr: remoteAddr, - }) - sess1.Heartbeat(ctx) - test.That(t, sess1.Active(time.Now()), test.ShouldBeTrue) - test.That(t, sess1.PeerConnectionInfo(), test.ShouldResembleProto, info) - test.That(t, sess1.HeartbeatWindow(), test.ShouldEqual, dur) - test.That(t, sess1.Deadline().After(now), test.ShouldBeTrue) - test.That(t, sess1.Deadline().Before(now.Add(2*dur)), test.ShouldBeFalse) -} - -func TestNewWithID(t *testing.T) { - ownerID := "owner1" - remoteAddr := &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 5} - remoteAddrStr := remoteAddr.String() - info := &v1.PeerConnectionInfo{ - Type: v1.PeerConnectionType_PEER_CONNECTION_TYPE_GRPC, - RemoteAddress: &remoteAddrStr, - LocalAddress: nil, - } - ctx := peer.NewContext(context.Background(), &peer.Peer{ - Addr: remoteAddr, - }) - now := time.Now() - dur := time.Second - someFunc := func(id uuid.UUID, resourceName resource.Name) { - } - - id1 := uuid.New() - id2 := uuid.New() - - sess1 := NewWithID(ctx, id1, ownerID, dur, someFunc) - sess2 := NewWithID(ctx, id2, ownerID+"other", dur, someFunc) - test.That(t, sess2.CheckOwnerID(ownerID), test.ShouldBeFalse) - test.That(t, sess2.CheckOwnerID(ownerID+"other"), test.ShouldBeTrue) - - test.That(t, sess1.ID(), test.ShouldEqual, id1) - test.That(t, sess2.ID(), test.ShouldEqual, id2) - test.That(t, sess1.CheckOwnerID(ownerID), test.ShouldBeTrue) - test.That(t, sess1.CheckOwnerID(ownerID+"other"), test.ShouldBeFalse) - test.That(t, sess1.Active(now), test.ShouldBeTrue) - time.Sleep(2 * dur) - test.That(t, sess1.Active(time.Now()), test.ShouldBeFalse) - sess1.Heartbeat(ctx) - test.That(t, sess1.Active(time.Now()), test.ShouldBeTrue) - test.That(t, sess1.PeerConnectionInfo(), test.ShouldResembleProto, info) - test.That(t, sess1.HeartbeatWindow(), test.ShouldEqual, dur) - test.That(t, sess1.Deadline().After(now), test.ShouldBeTrue) - test.That(t, sess1.Deadline().Before(now.Add(2*dur)), test.ShouldBeFalse) -} diff --git a/spatialmath/angular_velocity_test.go b/spatialmath/angular_velocity_test.go deleted file mode 100644 index 66ff2f97ff9..00000000000 --- a/spatialmath/angular_velocity_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package spatialmath - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestConversions(t *testing.T) { - dt := 2.0 - - for _, angVs := range []struct { - testName string - rate r3.Vector - }{ - {"unitary roll", r3.Vector{X: 1, Y: 0, Z: 0}}, - {"unitary pitch", r3.Vector{X: 0, Y: 1, Z: 0}}, - {"unitary yaw", r3.Vector{X: 0, Y: 0, Z: 1}}, - {"roll", r3.Vector{X: 2, Y: 0, Z: 0}}, - {"pitch", r3.Vector{X: 0, Y: 4, Z: 0}}, - {"yaw", r3.Vector{X: 0, Y: 0, Z: 5}}, - } { - t.Run(angVs.testName, func(t *testing.T) { - // set up single axis frame speeds - diffEu := &EulerAngles{ - Roll: dt * angVs.rate.X, - Pitch: dt * angVs.rate.Y, - Yaw: dt * angVs.rate.Z, - } - oav := OrientationToAngularVel(diffEu, dt) - eav := EulerToAngVel(*diffEu, dt) - - t.Run("orientation", func(t *testing.T) { - test.That(t, oav, test.ShouldResemble, *R3ToAngVel(angVs.rate)) - }) - t.Run("euler", func(t *testing.T) { - test.That(t, eav, test.ShouldResemble, *R3ToAngVel(angVs.rate)) - }) - }) - } -} diff --git a/spatialmath/axisAngle_test.go b/spatialmath/axisAngle_test.go deleted file mode 100644 index ecdafe3c804..00000000000 --- a/spatialmath/axisAngle_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package spatialmath - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestAAConversion(t *testing.T) { - r3aa := r3.Vector{1.5, 1.5, 1.5} - r4 := R3ToR4(r3aa) - test.That(t, r4.Theta, test.ShouldAlmostEqual, 2.598076211353316) - test.That(t, r4.RX, test.ShouldAlmostEqual, 0.5773502691896257) - test.That(t, r4.RY, test.ShouldAlmostEqual, 0.5773502691896257) - test.That(t, r4.RZ, test.ShouldAlmostEqual, 0.5773502691896257) - r3_2 := r4.ToR3() - test.That(t, r3_2.X, test.ShouldAlmostEqual, 1.5) - test.That(t, r3_2.Y, test.ShouldAlmostEqual, 1.5) - test.That(t, r3_2.Z, test.ShouldAlmostEqual, 1.5) -} diff --git a/spatialmath/box_test.go b/spatialmath/box_test.go deleted file mode 100644 index d9256c78b56..00000000000 --- a/spatialmath/box_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package spatialmath - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func makeTestBox(o Orientation, pt, dims r3.Vector, label string) Geometry { - box, _ := NewBox(NewPose(pt, o), dims, label) - return box -} - -func TestNewBox(t *testing.T) { - offset := NewPose(r3.Vector{X: 1, Y: 0, Z: 0}, &EulerAngles{0, 0, math.Pi}) - - // test box created from NewBox method - geometry, err := NewBox(offset, r3.Vector{1, 1, 1}, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, geometry, test.ShouldResemble, &box{pose: offset, halfSize: [3]float64{0.5, 0.5, 0.5}, boundingSphereR: math.Sqrt(0.75)}) - _, err = NewBox(offset, r3.Vector{-1, 0, 0}, "") - test.That(t, err.Error(), test.ShouldContainSubstring, newBadGeometryDimensionsError(&box{}).Error()) - - // test box created from GeometryCreator with offset - gc, err := NewBox(offset, r3.Vector{1, 1, 1}, "") - test.That(t, err, test.ShouldBeNil) - geometry = gc.Transform(PoseInverse(offset)) - test.That(t, PoseAlmostCoincident(geometry.Pose(), NewZeroPose()), test.ShouldBeTrue) -} - -func TestBoxAlmostEqual(t *testing.T) { - original := makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{1, 1, 1}, "") - good := makeTestBox(NewZeroOrientation(), r3.Vector{1e-16, 1e-16, 1e-16}, r3.Vector{1 + 1e-16, 1 + 1e-16, 1 + 1e-16}, "") - bad := makeTestBox(NewZeroOrientation(), r3.Vector{1e-2, 1e-2, 1e-2}, r3.Vector{1 + 1e-2, 1 + 1e-2, 1 + 1e-2}, "") - test.That(t, original.(*box).almostEqual(good), test.ShouldBeTrue) - test.That(t, original.(*box).almostEqual(bad), test.ShouldBeFalse) -} - -func TestBoxVertices(t *testing.T) { - offset := r3.Vector{2, 2, 2} - boxGeom := makeTestBox(NewZeroOrientation(), offset, r3.Vector{2, 2, 2}, "") - box, ok := boxGeom.(*box) - test.That(t, ok, test.ShouldBeTrue) - vertices := box.vertices() - test.That(t, R3VectorAlmostEqual(vertices[0], r3.Vector{1, 1, 1}.Add(offset), 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(vertices[1], r3.Vector{1, 1, -1}.Add(offset), 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(vertices[2], r3.Vector{1, -1, 1}.Add(offset), 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(vertices[3], r3.Vector{1, -1, -1}.Add(offset), 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(vertices[4], r3.Vector{-1, 1, 1}.Add(offset), 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(vertices[5], r3.Vector{-1, 1, -1}.Add(offset), 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(vertices[6], r3.Vector{-1, -1, 1}.Add(offset), 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(vertices[7], r3.Vector{-1, -1, -1}.Add(offset), 1e-8), test.ShouldBeTrue) -} - -func TestBoxPC(t *testing.T) { - offset1 := r3.Vector{2, 2, 0} - dims1 := r3.Vector{2, 2, 2} - eulerAngle1 := &EulerAngles{45, 45, 0} - pose1 := NewPose(offset1, eulerAngle1) - box1, err := NewBox(pose1, dims1, "") - test.That(t, err, test.ShouldBeNil) - customDensity := 1. - output1 := box1.ToPoints(customDensity) - - checkAgainst1 := []r3.Vector{ - {2.525321988817730733956068, 2.000000000000001332267630, -0.850903524534118327338206}, - {1.474678011182271264445376, 2.000000000000000888178420, 0.850903524534118549382811}, - {2.972320320618009770186063, 1.149096475465882560840214, -0.574940332598703474076274}, - {2.078323657017452141815284, 2.850903524534119881650440, -1.126866716469533624689348}, - {1.921676342982550300675371, 1.149096475465882782884819, 1.126866716469533624689348}, - {1.027679679381992006170776, 2.850903524534119881650440, 0.574940332598703696120879}, - {2.724036808064585812871883, 2.525321988817730733956068, 0.446998331800279036229995}, - {1.275963191935416185529562, 1.474678011182271486489981, -0.446998331800278925207692}, - {3.171035139864865737280297, 1.674418464283612628662468, 0.722961523735694333581137}, - {2.277038476264307220731098, 3.376225513351849727428089, 0.171035139864864182968063}, - {1.722961523735694999714951, 0.623774486648152826084868, -0.171035139864864044190185}, - {0.828964860135136816232659, 2.325581535716389591783582, -0.722961523735694111536532}, - {3.249358796882316102738741, 2.525321988817730733956068, -0.403905192733839402130513}, - {1.801285180753145809262605, 1.474678011182271264445376, -1.297901856334397363568200}, - {2.198714819246856411183444, 2.525321988817731178045278, 1.297901856334397585612805}, - {0.750641203117686117707308, 1.474678011182271486489981, 0.403905192733839513152816}, - {3.696357128682595138968736, 1.674418464283612628662468, -0.127942000798424243557250}, - {2.802360465082037066508747, 3.376225513351848839249669, -0.679868384669254366414748}, - {2.248283512553424845492600, 0.623774486648152715062565, -1.021938664398982510306269}, - {1.354286848952866773032611, 2.325581535716390035872791, -1.573865048269812660919342}, - {2.645713151047135447413439, 1.674418464283612184573258, 1.573865048269812660919342}, - {1.751716487446577152908844, 3.376225513351849283338879, 1.021938664398982510306269}, - {0.303642871317407081477313, 2.325581535716390035872791, 0.127942000798424410090703}, - {1.197639534917965153937303, 0.623774486648152715062565, 0.679868384669254477437050}, - {2.446998331800279924408414, 1.149096475465882782884819, 0.275963191935414964284234}, - {1.553001668199722073993030, 2.850903524534119881650440, -0.275963191935414853261932}, - } - for i, v := range output1 { - test.That(t, R3VectorAlmostEqual(v, checkAgainst1[i], 1e-2), test.ShouldBeTrue) - } - - // second check - offset2 := r3.Vector{2, 2, 2} - dims2 := r3.Vector{1, 1.5, 4} - eulerAngle2 := &EulerAngles{0, 45, 0} - pose2 := NewPose(offset2, eulerAngle2) - box2, err := NewBox(pose2, dims2, "") - test.That(t, err, test.ShouldBeNil) - output2 := box2.ToPoints(customDensity) - - checkAgainst2 := []r3.Vector{ - {2.262660994408865811067244, 2.000000000000000888178420, 1.574548237732941391442409}, - {1.737339005591135743244990, 2.000000000000000888178420, 2.425451762267059496736010}, - {3.113564518942984360450055, 2.000000000000000888178420, 2.099870226550671237220058}, - {1.411757469874747261684433, 2.000000000000000888178420, 1.049226248915211323620156}, - {2.588242530125254514672406, 2.000000000000000888178420, 2.950773751084789342513659}, - {0.886435481057017415906785, 2.000000000000000888178420, 1.900129773449329650958362}, - {3.964468043477102909832865, 2.000000000000000888178420, 2.625192215368401527086917}, - {0.560853945340628823323925, 2.000000000000000888178420, 0.523904260097481366820205}, - {3.439146054659373064055217, 2.000000000000000888178420, 3.476095739902519188291308}, - {0.035531956522898887340656, 2.000000000000000888178420, 1.374807784631600027225318}, - {2.000000000000000888178420, 2.750000000000000888178420, 2.000000000000000444089210}, - {2.000000000000000888178420, 1.250000000000000666133815, 2.000000000000000444089210}, - {2.850903524534118993472021, 2.750000000000000888178420, 2.525321988817730289866859}, - {1.149096475465882338795609, 2.750000000000000888178420, 1.474678011182270598311561}, - {2.850903524534118993472021, 1.250000000000000666133815, 2.525321988817730289866859}, - {1.149096475465882338795609, 1.250000000000000666133815, 1.474678011182270598311561}, - {3.701807049068237986944041, 2.750000000000000888178420, 3.050643977635460579733717}, - {0.298192950931763844923950, 2.750000000000000888178420, 0.949356022364540641511610}, - {3.701807049068237986944041, 1.250000000000000666133815, 3.050643977635460579733717}, - {0.298192950931763844923950, 1.250000000000000666133815, 0.949356022364540641511610}, - {3.701807049068237986944041, 2.000000000000000888178420, 3.050643977635460579733717}, - {0.298192950931763844923950, 2.000000000000000888178420, 0.949356022364540641511610}, - } - for i, v := range output2 { - test.That(t, R3VectorAlmostEqual(v, checkAgainst2[i], 1e-2), test.ShouldBeTrue) - } -} diff --git a/spatialmath/capsule_test.go b/spatialmath/capsule_test.go deleted file mode 100644 index 94caab2a7fd..00000000000 --- a/spatialmath/capsule_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package spatialmath - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func makeTestCapsule(o Orientation, pt r3.Vector, radius, length float64) Geometry { - c, _ := NewCapsule(NewPose(pt, o), radius, length, "") - return c -} - -func TestCapsuleConstruction(t *testing.T) { - c := makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 0.1}, 1, 6.75).(*capsule) - test.That(t, c.segA.ApproxEqual(r3.Vector{0, 0, -2.275}), test.ShouldBeTrue) - test.That(t, c.segB.ApproxEqual(r3.Vector{0, 0, 2.475}), test.ShouldBeTrue) -} - -func TestBoxCapsuleCollision(t *testing.T) { - pt := r3.Vector{-178.95551585002903, 15.388321162835881, -10.110465843295357} - ov := &OrientationVectorDegrees{OX: -0.43716334939336904, OY: -0.3861114135400337, OZ: -0.812284545144919, Theta: -180} - pose := NewPose(pt, ov) - c, err := NewCapsule(pose, 65, 550, "") - test.That(t, err, test.ShouldBeNil) - - box1Pt := r3.Vector{X: -450, Y: 0, Z: -266} - box1, err := NewBox(NewPoseFromPoint(box1Pt), r3.Vector{X: 900, Y: 2000, Z: 100}, "") - test.That(t, err, test.ShouldBeNil) - - col, err := c.CollidesWith(box1, defaultCollisionBufferMM) - test.That(t, err, test.ShouldBeNil) - test.That(t, col, test.ShouldBeTrue) - - dist, err := c.DistanceFrom(box1) - test.That(t, err, test.ShouldBeNil) - test.That(t, dist, test.ShouldAlmostEqual, -29.69, 1e-3) -} diff --git a/spatialmath/geo_obstacle_test.go b/spatialmath/geo_obstacle_test.go deleted file mode 100644 index 4da5cb9cf77..00000000000 --- a/spatialmath/geo_obstacle_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package spatialmath - -import ( - "fmt" - "math" - "testing" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" - - "go.viam.com/rdk/utils" -) - -func TestGeoPointToPoint(t *testing.T) { - origin := geo.NewPoint(0, 0) - testCases := []struct { - *geo.Point - r3.Vector - }{ - {geo.NewPoint(9e-9, 9e-9), r3.Vector{1, 1, 0}}, - {geo.NewPoint(0, 9e-9), r3.Vector{1, 0, 0}}, - {geo.NewPoint(-9e-9, 9e-9), r3.Vector{1, -1, 0}}, - {geo.NewPoint(9e-9, 0), r3.Vector{0, 1, 0}}, - {geo.NewPoint(0, 0), r3.Vector{0, 0, 0}}, - {geo.NewPoint(-9e-9, -9e-9), r3.Vector{-1, -1, 0}}, - {geo.NewPoint(0, -9e-9), r3.Vector{-1, 0, 0}}, - {geo.NewPoint(9e-9, -9e-9), r3.Vector{-1, 1, 0}}, - } - - for i, tc := range testCases { - t.Run(fmt.Sprint(i), func(t *testing.T) { - point := GeoPointToPoint(tc.Point, origin) - test.That(t, R3VectorAlmostEqual(point, tc.Vector, 0.1), test.ShouldBeTrue) - }) - } -} - -func TestGeoObstacles(t *testing.T) { - testLatitude := 39.58836 - testLongitude := -105.64464 - testPoint := geo.NewPoint(testLatitude, testLongitude) - testPose := NewPoseFromPoint(r3.Vector{2, 3, 4}) - testSphere, err := NewSphere(testPose, 100, "sphere") - test.That(t, err, test.ShouldBeNil) - testGeoms := []Geometry{testSphere} - - testGeoObst := NewGeoObstacle(testPoint, testGeoms) - test.That(t, testPoint, test.ShouldResemble, testGeoObst.Location()) - test.That(t, testGeoms, test.ShouldResemble, testGeoObst.Geometries()) - - t.Run("Conversion from GeoObstacle to Protobuf", func(t *testing.T) { - convGeoObstProto := GeoObstacleToProtobuf(testGeoObst) - test.That(t, err, test.ShouldBeNil) - test.That(t, testPoint.Lat(), test.ShouldEqual, convGeoObstProto.GetLocation().GetLatitude()) - test.That(t, testPoint.Lng(), test.ShouldEqual, convGeoObstProto.GetLocation().GetLongitude()) - test.That(t, len(testGeoms), test.ShouldEqual, len(convGeoObstProto.GetGeometries())) - }) - - t.Run("Conversion from Protobuf to GeoObstacle", func(t *testing.T) { - testProtobuf := &commonpb.GeoObstacle{ - Location: &commonpb.GeoPoint{Latitude: testLatitude, Longitude: testLongitude}, - Geometries: []*commonpb.Geometry{testSphere.ToProtobuf()}, - } - - convGeoObst, err := GeoObstacleFromProtobuf(testProtobuf) - test.That(t, err, test.ShouldBeNil) - test.That(t, testPoint, test.ShouldResemble, convGeoObst.Location()) - test.That(t, testGeoms, test.ShouldResemble, convGeoObst.Geometries()) - }) - - // test forward and backward conversion from GeoObstacleConfig to GeoObstacle - gc, err := NewGeometryConfig(testSphere) - test.That(t, err, test.ShouldBeNil) - - gobCfg := GeoObstacleConfig{ - Location: &commonpb.GeoPoint{Latitude: testLatitude, Longitude: testLongitude}, - Geometries: []*GeometryConfig{gc}, - } - - t.Run("Conversion from GeoObstacle to GeoObstacleConfig", func(t *testing.T) { - conv, err := NewGeoObstacleConfig(testGeoObst) - test.That(t, err, test.ShouldBeNil) - - test.That(t, testPoint.Lat(), test.ShouldEqual, conv.Location.Latitude) - test.That(t, testPoint.Lng(), test.ShouldEqual, conv.Location.Longitude) - test.That(t, conv.Geometries, test.ShouldResemble, []*GeometryConfig{gc}) - }) - - t.Run("Conversion from GeoObstacleConfig to GeoObstacle", func(t *testing.T) { - conv, err := GeoObstaclesFromConfig(&gobCfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(conv), test.ShouldEqual, 1) - test.That(t, conv[0].location, test.ShouldResemble, testGeoObst.location) - test.That(t, conv[0].geometries, test.ShouldResemble, testGeoObst.geometries) - }) -} - -func TestPoseToGeoPose(t *testing.T) { - type testCase struct { - name string - relativeTo, expectedGeoPose *GeoPose - pose Pose - } - - // degree of accuracy to expect on results - mmTol := 1e-3 - gpsTol := 1e-6 - - // The number of mm required to move one one thousandth of a degree long or lat from the GPS point (0, 0) - mmToOneThousandthDegree := 1.1119492664455873e+05 - - // values are left handed - north is 0 degrees - LHNortheast := 45. - LHEast := 90. - LHSouth := 180. - LHWest := 270. - LHNorthwest := 315. - - // values are right handed - north is 0 degrees - RHNortheast := 360 - LHNortheast - RHEast := 360 - LHEast - RHSouth := 360 - LHSouth - RHWest := 360 - LHWest - - tcs := []testCase{ - { - name: "zero geopose + zero pose = zero geopose", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), 0), - pose: NewZeroPose(), - expectedGeoPose: NewGeoPose(geo.NewPoint(0, 0), 0), - }, - { - name: "zero geopose heading west + zero pose = original geopose", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), LHWest), - pose: NewZeroPose(), - expectedGeoPose: NewGeoPose(geo.NewPoint(0, 0), LHWest), - }, - { - name: "zero geopose + pose heading east = zero geopoint heading east", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), 0), - pose: NewPose(r3.Vector{}, &OrientationVectorDegrees{OZ: 1, Theta: RHEast}), - expectedGeoPose: NewGeoPose(geo.NewPoint(0, 0), LHEast), - }, - { - name: "nonzero geopose + pose heading west = in same geopoint heading west", - relativeTo: NewGeoPose(geo.NewPoint(50, 50), 0), - pose: NewPose(r3.Vector{}, &OrientationVectorDegrees{OZ: 1, Theta: RHWest}), - expectedGeoPose: NewGeoPose(geo.NewPoint(50, 50), LHWest), - }, - { - name: "nonzero geopose heading west + pose heading east = same geopoint heading north", - relativeTo: NewGeoPose(geo.NewPoint(50, 50), LHWest), - pose: NewPose(r3.Vector{}, &OrientationVectorDegrees{OZ: 1, Theta: RHEast}), - expectedGeoPose: NewGeoPose(geo.NewPoint(50, 50), 0), - }, - { - name: "zero geopose + pose that moves 0.001 degree north = 0.001 degree diff geopose", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), 0), - pose: NewPose(r3.Vector{X: 0, Y: mmToOneThousandthDegree, Z: 0}, NewZeroOrientation()), - expectedGeoPose: NewGeoPose(geo.NewPoint(1e-3, 0), 0), - }, - { - name: "zero geopose + pose that moves 0.001 degree east = 0.001 degree diff geopose", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), 0), - pose: NewPose(r3.Vector{X: mmToOneThousandthDegree, Y: 0, Z: 0}, NewZeroOrientation()), - expectedGeoPose: NewGeoPose(geo.NewPoint(0, 1e-3), 0), - }, - { - name: "zero geopose + pose that moves 0.001 lat degree north with a south orientation = " + - "0.001 lat degree diff geopose facing south", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), 0), - pose: NewPose(r3.Vector{X: 0, Y: mmToOneThousandthDegree, Z: 0}, &OrientationVectorDegrees{OZ: 1, Theta: RHSouth}), - expectedGeoPose: NewGeoPose(geo.NewPoint(1e-3, 0), LHSouth), - }, - { - name: "zero geopose + pose that moves 0.001 lat degree south with an east orientation = " + - "0.001 lat degree diff geopose facing east", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), 0), - pose: NewPose(r3.Vector{X: 0, Y: -mmToOneThousandthDegree, Z: 0}, &OrientationVectorDegrees{OZ: 1, Theta: RHEast}), - expectedGeoPose: NewGeoPose(geo.NewPoint(-1e-3, 0), LHEast), - }, - { - name: "zero geopose heading south + pose that rotates east = zero geopose facing west" + - "even when 360 is added multiple times", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), LHSouth+360+360+360+360), - pose: NewPose(r3.Vector{X: 0, Y: 0, Z: 0}, &OrientationVectorDegrees{OZ: 1, Theta: RHEast}), - expectedGeoPose: NewGeoPose(geo.NewPoint(0, 0), LHWest), - }, - { - name: "zero geopose heading south + pose that rotates east = zero geopose facing west", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), LHSouth-360-360-360-360), - pose: NewPose(r3.Vector{X: 0, Y: 0, Z: 0}, &OrientationVectorDegrees{OZ: 1, Theta: RHEast}), - expectedGeoPose: NewGeoPose(geo.NewPoint(0, 0), LHWest), - }, - { - name: "zero geopose heading northwest + pose that rotates northeast", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), LHNorthwest), - pose: NewPose( - r3.Vector{X: mmToOneThousandthDegree, Y: mmToOneThousandthDegree, Z: 0}, - &OrientationVectorDegrees{OZ: 1, Theta: RHNortheast}, - ), - expectedGeoPose: NewGeoPose(geo.NewPoint(math.Sqrt2*1e-3, 0), 0), - }, - { - name: "zero geopose heading north + pose that rotates northeast", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), 0), - pose: NewPose( - r3.Vector{X: mmToOneThousandthDegree, Y: mmToOneThousandthDegree, Z: 0}, - &OrientationVectorDegrees{OZ: 1, Theta: RHNortheast}, - ), - expectedGeoPose: NewGeoPose(geo.NewPoint(1e-3, 1e-3), LHNortheast), - }, - { - name: "zero geopose heading east + pose that rotates north", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), LHWest), - pose: NewPose(r3.Vector{X: mmToOneThousandthDegree, Y: mmToOneThousandthDegree, Z: 0}, NewZeroOrientation()), - expectedGeoPose: NewGeoPose(geo.NewPoint(1e-3, -1e-3), LHWest), - }, - { - name: "zero geopose heading east", - relativeTo: NewGeoPose(geo.NewPoint(1e-3, 5e-3), LHEast), - pose: NewPose(r3.Vector{X: mmToOneThousandthDegree, Y: mmToOneThousandthDegree, Z: 0}, NewZeroOrientation()), - expectedGeoPose: NewGeoPose(geo.NewPoint(0, 6e-3), LHEast), - }, - { - name: "zero geopose heading west", - relativeTo: NewGeoPose(geo.NewPoint(0, 0), LHWest), - pose: NewPose(r3.Vector{X: mmToOneThousandthDegree, Y: mmToOneThousandthDegree, Z: 0}, NewZeroOrientation()), - expectedGeoPose: NewGeoPose(geo.NewPoint(1e-3, -1e-3), LHWest), - }, - } - - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - gp := PoseToGeoPose(tc.relativeTo, tc.pose) - t.Logf("gp: %#v %#v\n", gp.Location(), gp.Heading()) - t.Logf("tc: %#v %#v\n", tc.expectedGeoPose.Location(), tc.expectedGeoPose.Heading()) - test.That(t, gp.Heading(), test.ShouldAlmostEqual, tc.expectedGeoPose.Heading()) - test.That(t, utils.Float64AlmostEqual(gp.Location().Lat(), tc.expectedGeoPose.Location().Lat(), gpsTol), test.ShouldBeTrue) - test.That(t, utils.Float64AlmostEqual(gp.Location().Lng(), tc.expectedGeoPose.Location().Lng(), gpsTol), test.ShouldBeTrue) - geoPointToPose := GeoPoseToPose(gp, tc.relativeTo) - msga := "geoPointToPose.Point(): %#v, geoPointToPose.Orientation().OrientationVectorDegrees().: %#v\n" - t.Logf(msga, geoPointToPose.Point(), geoPointToPose.Orientation().OrientationVectorDegrees()) - msgb := "tc.p.Point(): %#v tc.p.Orientation().OrientationVectorDegrees().: %#v\n" - t.Logf(msgb, tc.pose.Point(), tc.pose.Orientation().OrientationVectorDegrees()) - test.That(t, PoseAlmostEqualEps(geoPointToPose, tc.pose, mmTol), test.ShouldBeTrue) - }) - } -} diff --git a/spatialmath/geometry_test.go b/spatialmath/geometry_test.go deleted file mode 100644 index 050429e2bf9..00000000000 --- a/spatialmath/geometry_test.go +++ /dev/null @@ -1,824 +0,0 @@ -package spatialmath - -import ( - "encoding/json" - "fmt" - "math" - "testing" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - commonpb "go.viam.com/api/common/v1" - "go.viam.com/test" -) - -func TestGeometrySerializationJSON(t *testing.T) { - translation := r3.Vector{1, 1, 1} - orientation := OrientationConfig{} - testMap := loadOrientationTests(t) - err := json.Unmarshal(testMap["euler"], &orientation) - test.That(t, err, test.ShouldBeNil) - - testCases := []struct { - name string - config GeometryConfig - success bool - }{ - { - "box", - GeometryConfig{Type: "box", X: 1, Y: 1, Z: 1, TranslationOffset: translation, OrientationOffset: orientation, Label: "box"}, - true, - }, - {"bounding box dims", GeometryConfig{Type: "box", X: 1, Y: 0, Z: 1, Label: "bounding box dims"}, true}, - {"box bad dims", GeometryConfig{Type: "box", X: 1, Y: 0, Z: -1}, false}, - {"infer box", GeometryConfig{X: 1, Y: 1, Z: 1, Label: "infer box"}, true}, - {"sphere", GeometryConfig{Type: "sphere", R: 1, TranslationOffset: translation, OrientationOffset: orientation, Label: "sphere"}, true}, - {"sphere bad dims", GeometryConfig{Type: "sphere", R: -1}, false}, - {"infer sphere", GeometryConfig{R: 1, OrientationOffset: orientation, Label: "infer sphere"}, true}, - {"point", GeometryConfig{Type: "point", TranslationOffset: translation, OrientationOffset: orientation, Label: "point"}, true}, - {"infer point", GeometryConfig{}, false}, - {"bad type", GeometryConfig{Type: "bad"}, false}, - {"c", GeometryConfig{Type: "capsule", L: 4, R: 1, TranslationOffset: translation, OrientationOffset: orientation, Label: "c"}, true}, - {"infer c", GeometryConfig{L: 4, R: 1, TranslationOffset: translation, OrientationOffset: orientation, Label: "infer c"}, true}, - } - - pose := NewPoseFromPoint(r3.Vector{X: 1, Y: 1, Z: 1}) - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - gc, err := testCase.config.ParseConfig() - if testCase.success == false { - test.That(t, err, test.ShouldNotBeNil) - return - } - test.That(t, err, test.ShouldBeNil) - data, err := gc.MarshalJSON() - test.That(t, err, test.ShouldBeNil) - config := GeometryConfig{} - err = json.Unmarshal(data, &config) - test.That(t, err, test.ShouldBeNil) - newVc, err := config.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, GeometriesAlmostEqual(gc.Transform(pose), newVc.Transform(pose)), test.ShouldBeTrue) - test.That(t, config.Label, test.ShouldEqual, testCase.name) - }) - } -} - -func TestGeometryToFromProtobuf(t *testing.T) { - deg45 := math.Pi / 4 - testCases := []struct { - name string - geometry Geometry - }{ - {"box", makeTestBox(&EulerAngles{0, 0, deg45}, r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, "box")}, - {"sphere", makeTestSphere(r3.Vector{3, 4, 5}, 10, "sphere")}, - {"point", NewPoint(r3.Vector{3, 4, 5}, "point")}, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - newVol, err := NewGeometryFromProto(testCase.geometry.ToProtobuf()) - test.That(t, err, test.ShouldBeNil) - test.That(t, GeometriesAlmostEqual(testCase.geometry, newVol), test.ShouldBeTrue) - test.That(t, testCase.geometry.Label(), test.ShouldEqual, testCase.name) - }) - } - - // test that bad message does not generate error - _, err := NewGeometryFromProto(&commonpb.Geometry{Center: PoseToProtobuf(NewZeroPose())}) - test.That(t, err.Error(), test.ShouldContainSubstring, errGeometryTypeUnsupported.Error()) -} - -type geometryComparisonTestCase struct { - testname string - geometries [2]Geometry - expected float64 -} - -func testGeometryCollision(t *testing.T, cases []geometryComparisonTestCase) { - t.Helper() - for _, c := range cases { - for i := 0; i < 2; i++ { - t.Run(fmt.Sprintf("%s %T %T collision", c.testname, c.geometries[i], c.geometries[(i+1)%2]), func(t *testing.T) { - fn := test.ShouldBeFalse - if c.expected <= defaultCollisionBufferMM { - fn = test.ShouldBeTrue - } - collides, err := c.geometries[i].CollidesWith(c.geometries[(i+1)%2], defaultCollisionBufferMM) - test.That(t, err, test.ShouldBeNil) - test.That(t, collides, fn) - }) - t.Run(fmt.Sprintf("%s %T %T distance", c.testname, c.geometries[i], c.geometries[(i+1)%2]), func(t *testing.T) { - distance, err := c.geometries[i].DistanceFrom(c.geometries[(i+1)%2]) - test.That(t, err, test.ShouldBeNil) - test.That(t, distance, test.ShouldAlmostEqual, c.expected, 1e-3) - }) - } - } -} - -func TestBoxVsBoxCollision(t *testing.T) { - deg45 := math.Pi / 4. - cases := []geometryComparisonTestCase{ - { - "inscribed", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{1, 1, 1}, ""), - }, - -1.5, - }, - { - "face to face contact", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2, 0, 0}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "face to face near contact", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2.01, 0, 0}, r3.Vector{2, 2, 2}, ""), - }, - 0.01, - }, - { - "coincident edge contact", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2, 4, 0}, r3.Vector{2, 6, 2}, ""), - }, - 0, - }, - { - "coincident edges near contact", - [2]Geometry{ - makeTestBox((NewZeroOrientation()), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2, 4.01, 0}, r3.Vector{2, 6, 2}, ""), - }, - 0.01, - }, - { - "vertex to vertex contact", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2, 2, 2}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "vertex to vertex near contact", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2.01, 2, 2}, r3.Vector{2, 2, 2}, ""), - }, - 0.005, - }, - { - "edge along face contact", - [2]Geometry{ - makeTestBox(&EulerAngles{deg45, 0, 0}, r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{0, 1 + math.Sqrt2, 0}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "edge along face near contact", - [2]Geometry{ - makeTestBox(&EulerAngles{deg45, 0, 0}, r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{0, 1.01 + math.Sqrt2, 0}, r3.Vector{2, 2, 2}, ""), - }, - 0.01, - }, - { - "edge to edge contact", - [2]Geometry{ - makeTestBox(&EulerAngles{0, 0, deg45}, r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(&EulerAngles{0, deg45, 0}, r3.Vector{2 * math.Sqrt2, 0, 0}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "edge to edge near contact", - [2]Geometry{ - makeTestBox(&EulerAngles{0, 0, deg45}, r3.Vector{-.01, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(&EulerAngles{0, deg45, 0}, r3.Vector{2 * math.Sqrt2, 0, 0}, r3.Vector{2, 2, 2}, ""), - }, - 0.01, - }, - { - "vertex to face contact", - [2]Geometry{ - makeTestBox(&EulerAngles{deg45, deg45, 0}, r3.Vector{0.5, -.5, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(&EulerAngles{0, 0, 0}, r3.Vector{0, 0, 0.97 + math.Sqrt(3)}, r3.Vector{2, 2, 2}, ""), - }, - -.005, - }, - { - "vertex to face near contact", - [2]Geometry{ - makeTestBox(&EulerAngles{deg45, deg45, 0}, r3.Vector{0, 0, -0.01}, r3.Vector{2, 2, 2}, ""), - makeTestBox(&EulerAngles{0, 0, 0}, r3.Vector{0, 0, 0.97 + math.Sqrt(3)}, r3.Vector{2, 2, 2}, ""), - }, - 0.005, - }, - { - "separated axis aligned", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{5, 6, 0}, r3.Vector{2, 2, 2}, ""), - }, - 4.346, // upper bound on separation distance - }, - { - "axis aligned overlap", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{20, 20, 20}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{20, 20, 20}, r3.Vector{24, 26, 28}, ""), - }, - -2, - }, - { - "full overlap", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{10, 10, 10}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{10, 10, 10}, ""), - }, - -10, - }, - { - "zero geometry box", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 0}, r3.Vector{20, 20, 20}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2, 2, 2}, r3.Vector{0, 0, 0}, ""), - }, - -8, - }, - } - testGeometryCollision(t, cases) -} - -func TestSphereVsSphereCollision(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "test inscribed spheres", - [2]Geometry{makeTestSphere(r3.Vector{}, 1, ""), makeTestSphere(r3.Vector{}, 2, "")}, - -3, - }, - { - "test tangent spheres", - [2]Geometry{makeTestSphere(r3.Vector{}, 1, ""), makeTestSphere(r3.Vector{0, 0, 2}, 1, "")}, - 0, - }, - { - "separated spheres", - [2]Geometry{makeTestSphere(r3.Vector{}, 1, ""), makeTestSphere(r3.Vector{0, 0, 2 + 1e-3}, 1, "")}, - 1e-3, - }, - } - testGeometryCollision(t, cases) -} - -func TestPointVsPointCollision(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "coincident", - [2]Geometry{NewPoint(r3.Vector{}, ""), NewPoint(r3.Vector{}, "")}, - 0, - }, - { - "separated", - [2]Geometry{NewPoint(r3.Vector{}, ""), NewPoint(r3.Vector{1, 0, 0}, "")}, - 1, - }, - } - testGeometryCollision(t, cases) -} - -func TestSphereVsBoxCollision(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "separated face closest", - [2]Geometry{ - makeTestSphere(r3.Vector{0, 0, 2 + 1e-3}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 1e-3, - }, - { - "separated edge closest", - [2]Geometry{ - makeTestSphere(r3.Vector{0, 2, 2}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - math.Sqrt2 - 1, - }, - { - "separated vertex closest", - [2]Geometry{ - makeTestSphere(r3.Vector{2, 2, 2}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - math.Sqrt(3) - 1, - }, - { - "face tangent", - [2]Geometry{ - makeTestSphere(r3.Vector{0, 0, 2}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "edge tangent", - [2]Geometry{ - makeTestSphere(r3.Vector{0, 2, 2}, math.Sqrt2, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "vertex tangent", - [2]Geometry{ - makeTestSphere(r3.Vector{2, 2, 2}, math.Sqrt(3), ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "center point inside", - [2]Geometry{ - makeTestSphere(r3.Vector{-.2, 0.1, .75}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - -1.25, - }, - { - "inscribed", - [2]Geometry{ - makeTestSphere(r3.Vector{2, 2, 2}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{2, 2, 2}, r3.Vector{2, 2, 2}, ""), - }, - -2, - }, - } - testGeometryCollision(t, cases) -} - -func TestPointVsBoxCollision(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "separated face closest", - [2]Geometry{ - NewPoint(r3.Vector{2, 0, 0}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 1, - }, - { - "separated edge closest", - [2]Geometry{ - NewPoint(r3.Vector{2, 2, 0}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - math.Sqrt2, - }, - { - "separated vertex closest", - [2]Geometry{ - NewPoint(r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - math.Sqrt(3), - }, - { - "inside", - [2]Geometry{ - NewPoint(r3.Vector{0, 0.3, 0.5}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - -0.5, - }, - } - testGeometryCollision(t, cases) -} - -func TestPointVsSphereCollision(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "coincident", - [2]Geometry{ - NewPoint(r3.Vector{}, ""), - makeTestSphere(r3.Vector{}, 1, ""), - }, - -1, - }, - { - "separated", - [2]Geometry{ - NewPoint(r3.Vector{2, 0, 0}, ""), - makeTestSphere(r3.Vector{}, 1, ""), - }, - 1, - }, - } - testGeometryCollision(t, cases) -} - -func testGeometryEncompassed(t *testing.T, cases []geometryComparisonTestCase) { - t.Helper() - for _, c := range cases { - t.Run(c.testname, func(t *testing.T) { - fn := test.ShouldBeTrue - if c.expected > 0.0 { - fn = test.ShouldBeFalse - } - collides, err := c.geometries[0].EncompassedBy(c.geometries[1]) - test.That(t, err, test.ShouldBeNil) - test.That(t, collides, fn) - }) - } -} - -func TestBoxVsBoxEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "encompassed", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "not encompassed", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 1, 0}, r3.Vector{2, 3, 2}, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 1, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestBoxVsSphereEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "encompassed", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - makeTestSphere(r3.Vector{}, math.Sqrt(3), ""), - }, - 0, - }, - { - "not encompassed", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{0, 1, 0}, r3.Vector{2, 2.1, 2}, ""), - makeTestSphere(r3.Vector{}, math.Sqrt(3), ""), - }, - .1, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestBoxVsPointEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "coincident", - [2]Geometry{makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{1, 1, 1}, ""), NewPoint(r3.Vector{}, "")}, - math.Sqrt(3), - }, - } - testGeometryEncompassed(t, cases) -} - -func TestSphereVsBoxEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "encompassed", - [2]Geometry{ - makeTestSphere(r3.Vector{3, 0, 0}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{8, 8, 8}, ""), - }, - 0, - }, - { - "not encompassed", - [2]Geometry{ - makeTestSphere(r3.Vector{3.5, 0, 0}, 1, ""), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{8, 8, 8}, ""), - }, - 0.5, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestSphereVsSphereEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "encompassed", - [2]Geometry{ - makeTestSphere(r3.Vector{3, 0, 0}, 1, ""), - makeTestSphere(r3.Vector{}, 4, ""), - }, - 0, - }, - { - "not encompassed", - [2]Geometry{ - makeTestSphere(r3.Vector{3, 0, 0}, 1, ""), - makeTestSphere(r3.Vector{}, 3.5, ""), - }, - 0.5, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestSphereVsPointEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "coincident", - [2]Geometry{makeTestSphere(r3.Vector{}, 1, ""), NewPoint(r3.Vector{}, "")}, - 1, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestCapsuleVsBoxCollision(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "separated face closest", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 3 + 1e-3}, 1, 4), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 1e-3, - }, - { - "separated edge closest", - [2]Geometry{ - makeTestCapsule(&OrientationVector{0, 0, 1, 1}, r3.Vector{0, 4, 4}, 1, 4*math.Sqrt2), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - math.Sqrt2, - }, - { - "separated vertex closest", - [2]Geometry{ - makeTestCapsule(&OrientationVector{0, 2, 2, 2}, r3.Vector{4, 4, 4}, 1, 4*math.Sqrt(3)), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - math.Sqrt(3), - }, - { - "face tangent", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 3}, 1, 4), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "edge tangent to capsule cylinder", - [2]Geometry{ - makeTestCapsule(&OrientationVector{0, 0, -2, 2}, r3.Vector{0, 3, 0}, math.Sqrt2/2, 6), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - 0, - }, - { - "center line segment inside", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0.3, 0.3, -0.75}, 1, 4), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - -1.7, - }, - { - "inscribed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 0}, 1, 40), - makeTestBox(NewZeroOrientation(), r3.Vector{0, 0, 1}, r3.Vector{2, 2, 2}, ""), - }, - -2, - }, - } - - adjust := func(n float64) float64 { - return n * (2 + math.Abs(n) - 1e-3) - } - - for _, norm := range boxNormals { - // Test all 6 faces with a tiny collision - cases = append(cases, - geometryComparisonTestCase{ - "colliding face closest", - [2]Geometry{ - makeTestCapsule(&OrientationVector{0, norm.X, norm.Y, norm.Z}, r3.Vector{adjust(norm.X), adjust(norm.Y), adjust(norm.Z)}, 1, 4), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{2, 2, 2}, ""), - }, - -1e-3, - }, - ) - } - testGeometryCollision(t, cases) -} - -func TestCapsuleVsCapsuleCollision(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "separated ends closest", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{1e-3, 0, 0}, 1, 4), - makeTestCapsule(NewZeroOrientation(), r3.Vector{-2, 0, 0}, 1, 4), - }, - 1e-3, - }, - { - "separated cylinders closest", - [2]Geometry{ - makeTestCapsule(&OrientationVector{0, 0, 0, -1}, r3.Vector{0, 0, -2 - 1e-3}, 1, 4), - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 2}, 1, 4), - }, - 1e-3, - }, - { - "separated cylinder closest to end", - [2]Geometry{ - makeTestCapsule(&OrientationVector{0, 1, 1, 0}, r3.Vector{0, 0, -1}, 1, 10), - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 2 + 1e-3}, 1, 4), - }, - 1e-3, - }, - { - "parallel cylinders touching", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{1, 0, 0}, 1, 4), - makeTestCapsule(NewZeroOrientation(), r3.Vector{-1, 0, 0}, 1, 4), - }, - 0, - }, - { - "orthogonal cylinders touching", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 0}, 1, 6), - makeTestCapsule(&OrientationVector{0, 1, 0, 0}, r3.Vector{0, 2, 0}, 1, 6), - }, - 0, - }, - { - "orthogonal cylinders slightly colliding", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 0}, 1, 6), - makeTestCapsule(&OrientationVector{0, 1, 0, 0}, r3.Vector{0, 1.8, 0}, 1, 6), - }, - -0.2, - }, - { - "inscribed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 1, 1}, 2, 40), - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 0}, 4, 40), - }, - -5, - }, - } - testGeometryCollision(t, cases) -} - -func TestCapsuleVsBoxEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "encompassed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 3}, 1, 4.75), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{16, 16, 16}, ""), - }, - 0, - }, - { - "not encompassed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 5.875}, 1, 4.75), - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{16, 16, 16}, ""), - }, - 0.25, - }, - { - "encompassed box", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{4, 4, 4}, ""), - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 0}, 4, 10), - }, - 0, - }, - { - "not encompassed box", - [2]Geometry{ - makeTestBox(NewZeroOrientation(), r3.Vector{}, r3.Vector{16, 16, 16}, ""), - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 3.5}, 1, 4.75), - }, - 0.25, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestCapsuleVsSphereEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "encompassed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 0.1}, 1, 6.75), - makeTestSphere(r3.Vector{}, 4, ""), - }, - 0, - }, - { - "not encompassed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 3}, 1, 6.75), - makeTestSphere(r3.Vector{}, 3.5, ""), - }, - 0.5, - }, - { - "encompassed sphere", - [2]Geometry{ - makeTestSphere(r3.Vector{}, 2, ""), - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 1.5}, 2.5, 9.75), - }, - 0, - }, - { - "not encompassed sphere", - [2]Geometry{ - makeTestSphere(r3.Vector{}, 3.5, ""), - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 3}, 1, 6.75), - }, - 0.5, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestCapsuleVsCapsuleEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "encompassed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{0, 0, 3}, 1, 3), - makeTestCapsule(NewZeroOrientation(), r3.Vector{}, 4, 10), - }, - 0, - }, - { - "not encompassed", - [2]Geometry{ - makeTestCapsule(NewZeroOrientation(), r3.Vector{3, 0, 0}, 1, 3), - makeTestCapsule(NewZeroOrientation(), r3.Vector{}, 3.5, 8), - }, - 0.5, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestCapsuleVsPointEncompassed(t *testing.T) { - cases := []geometryComparisonTestCase{ - { - "coincident", - [2]Geometry{makeTestCapsule(NewZeroOrientation(), r3.Vector{}, 1, 2), NewPoint(r3.Vector{}, "")}, - 1, - }, - } - testGeometryEncompassed(t, cases) -} - -func TestNewGeometryFromProto(t *testing.T) { - malformedGeom := commonpb.Geometry{} - viamGeom, err := NewGeometryFromProto(&malformedGeom) - test.That(t, viamGeom, test.ShouldBeNil) - test.That(t, err, test.ShouldBeError, errors.New("cannot have nil pose for geometry")) - - properGeom := commonpb.Geometry{ - Center: &commonpb.Pose{OZ: 1}, - GeometryType: &commonpb.Geometry_Sphere{ - Sphere: &commonpb.Sphere{ - RadiusMm: 1, - }, - }, - } - viamGeom, err = NewGeometryFromProto(&properGeom) - test.That(t, err, test.ShouldBeNil) - sphereGeom, err := NewSphere(NewZeroPose(), 1, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, viamGeom, test.ShouldResemble, sphereGeom) -} diff --git a/spatialmath/mesh_test.go b/spatialmath/mesh_test.go deleted file mode 100644 index 4dd30946eec..00000000000 --- a/spatialmath/mesh_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package spatialmath - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestClosestPoint(t *testing.T) { - p0 := r3.Vector{0, 0, 0} - p1 := r3.Vector{1, 0, 0} - p2 := r3.Vector{0, 0, 2} - tri := newTriangle(p0, p1, p2) - - qp1 := r3.Vector{-1, 0, 0} - cp1 := tri.closestPointToCoplanarPoint(qp1) - cp2 := tri.closestPointToPoint(qp1) - cp3, inside := tri.closestInsidePoint(qp1) - test.That(t, inside, test.ShouldBeFalse) - test.That(t, cp3.ApproxEqual(qp1), test.ShouldBeTrue) - test.That(t, cp1.ApproxEqual(cp2), test.ShouldBeTrue) -} diff --git a/spatialmath/orientation_json_test.go b/spatialmath/orientation_json_test.go deleted file mode 100644 index 9535397f869..00000000000 --- a/spatialmath/orientation_json_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package spatialmath - -import ( - "encoding/json" - "io" - "os" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils" - "gonum.org/v1/gonum/num/quat" -) - -func TestOrientation(t *testing.T) { - testMap := loadOrientationTests(t) - - // Config with unknown orientation - ro := OrientationConfig{} - err := json.Unmarshal(testMap["wrong"], &ro) - test.That(t, err, test.ShouldBeNil) - _, err = ro.ParseConfig() - test.That(t, err, test.ShouldBeError, newOrientationTypeUnsupportedError("oiler_angles")) - - // Config with good type, but bad value - ro = OrientationConfig{} - err = json.Unmarshal(testMap["wrongvalue"], &ro) - test.That(t, err, test.ShouldBeNil) - _, err = ro.ParseConfig() - test.That(t, err, test.ShouldBeError, - errors.New("json: cannot unmarshal string into Go struct field OrientationVectorDegrees.th of type float64")) - - // Empty Config - ro = OrientationConfig{} - err = json.Unmarshal(testMap["empty"], &ro) - test.That(t, err, test.ShouldBeNil) - o, err := ro.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, o.Quaternion(), test.ShouldResemble, quat.Number{1, 0, 0, 0}) - - // OrientationVectorDegrees Config - ro = OrientationConfig{} - err = json.Unmarshal(testMap["ovdegrees"], &ro) - test.That(t, err, test.ShouldBeNil) - o, err = ro.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, o.OrientationVectorDegrees(), test.ShouldResemble, &OrientationVectorDegrees{45, 0, 0, 1}) - oc, err := NewOrientationConfig(o) - test.That(t, err, test.ShouldBeNil) - test.That(t, oc.Type, test.ShouldEqual, string(OrientationVectorDegreesType)) - test.That(t, oc, test.ShouldResemble, &ro) - - // OrientationVector Radians Config - ro = OrientationConfig{} - err = json.Unmarshal(testMap["ovradians"], &ro) - test.That(t, err, test.ShouldBeNil) - o, err = ro.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, o.OrientationVectorRadians(), test.ShouldResemble, &OrientationVector{0.78539816, 0, 1, 0}) - oc, err = NewOrientationConfig(o) - test.That(t, err, test.ShouldBeNil) - test.That(t, oc.Type, test.ShouldEqual, string(OrientationVectorRadiansType)) - test.That(t, oc, test.ShouldResemble, &ro) - - // Euler Angles - ro = OrientationConfig{} - err = json.Unmarshal(testMap["euler"], &ro) - test.That(t, err, test.ShouldBeNil) - o, err = ro.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, o.EulerAngles(), test.ShouldResemble, &EulerAngles{Roll: 0, Pitch: 0, Yaw: 45}) - oc, err = NewOrientationConfig(o) - test.That(t, err, test.ShouldBeNil) - test.That(t, oc.Type, test.ShouldEqual, string(EulerAnglesType)) - test.That(t, oc, test.ShouldResemble, &ro) - - // Axis angles Config - ro = OrientationConfig{} - err = json.Unmarshal(testMap["axisangle"], &ro) - test.That(t, err, test.ShouldBeNil) - o, err = ro.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, o.AxisAngles(), test.ShouldResemble, &R4AA{0.78539816, 1, 0, 0}) - oc, err = NewOrientationConfig(o) - test.That(t, err, test.ShouldBeNil) - test.That(t, oc.Type, test.ShouldEqual, string(AxisAnglesType)) - test.That(t, oc, test.ShouldResemble, &ro) - - // Quaternion Config - ro = OrientationConfig{} - err = json.Unmarshal(testMap["quaternion"], &ro) - test.That(t, err, test.ShouldBeNil) - o, err = ro.ParseConfig() - test.That(t, err, test.ShouldBeNil) - aa := o.AxisAngles() - test.That(t, aa.Theta, test.ShouldAlmostEqual, 1.5040802, .0001) - test.That(t, aa.RX, test.ShouldAlmostEqual, 0.2672612, .0001) - test.That(t, aa.RY, test.ShouldAlmostEqual, 0.5345225, .001) - test.That(t, aa.RZ, test.ShouldAlmostEqual, 0.8017837, .001) - oc, err = NewOrientationConfig(o) - test.That(t, err, test.ShouldBeNil) - test.That(t, oc.Type, test.ShouldEqual, string(QuaternionType)) - o2, err := oc.ParseConfig() - test.That(t, err, test.ShouldBeNil) - test.That(t, OrientationAlmostEqual(o, o2), test.ShouldBeTrue) -} - -func loadOrientationTests(t *testing.T) map[string]json.RawMessage { - t.Helper() - file, err := os.Open("data/orientations.json") - test.That(t, err, test.ShouldBeNil) - defer utils.UncheckedErrorFunc(file.Close) - - data, err := io.ReadAll(file) - test.That(t, err, test.ShouldBeNil) - // Parse into map of tests - var testMap map[string]json.RawMessage - err = json.Unmarshal(data, &testMap) - test.That(t, err, test.ShouldBeNil) - return testMap -} diff --git a/spatialmath/orientation_test.go b/spatialmath/orientation_test.go deleted file mode 100644 index f52a795440d..00000000000 --- a/spatialmath/orientation_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package spatialmath - -import ( - "math" - "testing" - - "go.viam.com/test" - "gonum.org/v1/gonum/num/quat" - - "go.viam.com/rdk/utils" -) - -// represent a 45 degree rotation around the x axis in all the representations. -var ( - th = math.Pi / 4. - // in quaternion representation. - q45x = quat.Number{math.Cos(th / 2.), math.Sin(th / 2.), 0, 0} - // in axis-angle representation. - aa45x = &R4AA{th, 1., 0., 0.} - // in euler angle representation. - ea45x = &EulerAngles{Roll: th, Pitch: 0, Yaw: 0} - // in orientation vector representation. - ov45x = &OrientationVector{2. * th, 0., -math.Sqrt(2) / 2., math.Sqrt(2) / 2.} - ovd45x = &OrientationVectorDegrees{2 * utils.RadToDeg(th), 0., -math.Sqrt(2) / 2, math.Sqrt(2) / 2} - // in rotation matrix representation. - rm45x = &RotationMatrix{[9]float64{1, 0, 0, 0, math.Cos(th), math.Sin(th), 0, -math.Sin(th), math.Cos(th)}} -) - -func TestZeroOrientation(t *testing.T) { - zero := NewZeroOrientation() - test.That(t, zero.OrientationVectorRadians(), test.ShouldResemble, NewOrientationVector()) - test.That(t, zero.OrientationVectorDegrees(), test.ShouldResemble, NewOrientationVectorDegrees()) - test.That(t, zero.AxisAngles(), test.ShouldResemble, NewR4AA()) - test.That(t, zero.Quaternion(), test.ShouldResemble, quat.Number{1, 0, 0, 0}) - test.That(t, zero.EulerAngles(), test.ShouldResemble, NewEulerAngles()) - test.That(t, zero.RotationMatrix(), test.ShouldResemble, &RotationMatrix{[9]float64{1, 0, 0, 0, 1, 0, 0, 0, 1}}) -} - -func TestQuaternions(t *testing.T) { - qq45x := Quaternion(q45x) - testCompatibility(t, &qq45x) -} - -func TestEulerAngles(t *testing.T) { - testCompatibility(t, ea45x) -} - -func TestAxisAngles(t *testing.T) { - testCompatibility(t, aa45x) -} - -func TestOrientationVector(t *testing.T) { - testCompatibility(t, ov45x) -} - -func TestOrientationVectorDegrees(t *testing.T) { - testCompatibility(t, ovd45x) -} - -func TestRotationMatrix(t *testing.T) { - testCompatibility(t, rm45x) -} - -func TestSlerp(t *testing.T) { - q1 := q45x - q2 := quat.Conj(q45x) - s1 := slerp(q1, q2, 0.25) - s2 := slerp(q1, q2, 0.5) - - expect1 := quat.Number{0.9808, 0.1951, 0, 0} - expect2 := quat.Number{1, 0, 0, 0} - - test.That(t, s1.Real, test.ShouldAlmostEqual, expect1.Real, 0.001) - test.That(t, s1.Imag, test.ShouldAlmostEqual, expect1.Imag, 0.001) - test.That(t, s1.Jmag, test.ShouldAlmostEqual, expect1.Jmag, 0.001) - test.That(t, s1.Kmag, test.ShouldAlmostEqual, expect1.Kmag, 0.001) - test.That(t, s2.Real, test.ShouldAlmostEqual, expect2.Real) - test.That(t, s2.Imag, test.ShouldAlmostEqual, expect2.Imag) - test.That(t, s2.Jmag, test.ShouldAlmostEqual, expect2.Jmag) - test.That(t, s2.Kmag, test.ShouldAlmostEqual, expect2.Kmag) -} - -func TestOrientationTransform(t *testing.T) { - aa := &R4AA{Theta: math.Pi / 2., RX: 0., RY: 1., RZ: 0.} - ovd := &OrientationVectorDegrees{Theta: 0.0, OX: 1., OY: 0.0, OZ: 0.0} - ovdResult := aa.OrientationVectorDegrees() - aaResult := ovd.AxisAngles() - t.Logf("result as orientation vector: Theta: %.2f, X: %.2f, Y: %.2f, Z: %.2f", ovdResult.Theta, ovdResult.OX, ovdResult.OY, ovdResult.OZ) - test.That(t, ovdResult.Theta, test.ShouldAlmostEqual, ovd.Theta) - test.That(t, ovdResult.OX, test.ShouldAlmostEqual, ovd.OX) - test.That(t, ovdResult.OY, test.ShouldAlmostEqual, ovd.OY) - test.That(t, ovdResult.OZ, test.ShouldAlmostEqual, ovd.OZ) - t.Logf("result as axis angle: Theta: %.2f, X: %.2f, Y: %.2f, Z: %.2f", aaResult.Theta, aaResult.RX, aaResult.RY, aaResult.RZ) - test.That(t, aaResult.Theta, test.ShouldAlmostEqual, aa.Theta) - test.That(t, aaResult.RX, test.ShouldAlmostEqual, aa.RX) - test.That(t, aaResult.RY, test.ShouldAlmostEqual, aa.RY) - test.That(t, aaResult.RZ, test.ShouldAlmostEqual, aa.RZ) -} - -func TestOrientationAlmostEqual(t *testing.T) { - test.That(t, OrientationAlmostEqual(aa45x, ea45x), test.ShouldBeTrue) - test.That(t, OrientationAlmostEqual(aa45x, NewZeroOrientation()), test.ShouldBeFalse) -} - -func TestOrientationBetween(t *testing.T) { - aa := &R4AA{Theta: math.Pi / 2., RX: 0., RY: 1., RZ: 0.} - btw := OrientationBetween(aa, ov45x).OrientationVectorDegrees() - result := &OrientationVectorDegrees{Theta: 135.0, OX: -1., OY: 0.0, OZ: 0.0} - test.That(t, result.Theta, test.ShouldAlmostEqual, btw.Theta) - test.That(t, result.OX, test.ShouldAlmostEqual, btw.OX) - test.That(t, result.OY, test.ShouldAlmostEqual, btw.OY) - test.That(t, result.OZ, test.ShouldAlmostEqual, btw.OZ) -} - -func TestOrientationInverse(t *testing.T) { - test.That(t, OrientationAlmostEqual(OrientationInverse(aa45x), &R4AA{-th, 1., 0., 0.}), test.ShouldBeTrue) -} - -func testCompatibility(t *testing.T, o Orientation) { - t.Helper() - - // Orientation Vectors - test.That(t, o.OrientationVectorRadians().Theta, test.ShouldAlmostEqual, ov45x.Theta) - test.That(t, o.OrientationVectorRadians().OX, test.ShouldAlmostEqual, ov45x.OX) - test.That(t, o.OrientationVectorRadians().OY, test.ShouldAlmostEqual, ov45x.OY) - test.That(t, o.OrientationVectorRadians().OZ, test.ShouldAlmostEqual, ov45x.OZ) - test.That(t, o.OrientationVectorDegrees().Theta, test.ShouldAlmostEqual, ovd45x.Theta) - test.That(t, o.OrientationVectorDegrees().OX, test.ShouldAlmostEqual, ovd45x.OX) - test.That(t, o.OrientationVectorDegrees().OY, test.ShouldAlmostEqual, ovd45x.OY) - test.That(t, o.OrientationVectorDegrees().OZ, test.ShouldAlmostEqual, ovd45x.OZ) - - // Quaternion - test.That(t, o.Quaternion().Real, test.ShouldAlmostEqual, q45x.Real) - test.That(t, o.Quaternion().Imag, test.ShouldAlmostEqual, q45x.Imag) - test.That(t, o.Quaternion().Jmag, test.ShouldAlmostEqual, q45x.Jmag) - test.That(t, o.Quaternion().Kmag, test.ShouldAlmostEqual, q45x.Kmag) - - // Axis angles - test.That(t, o.AxisAngles().Theta, test.ShouldAlmostEqual, aa45x.Theta) - test.That(t, o.AxisAngles().RX, test.ShouldAlmostEqual, aa45x.RX) - test.That(t, o.AxisAngles().RY, test.ShouldAlmostEqual, aa45x.RY) - test.That(t, o.AxisAngles().RZ, test.ShouldAlmostEqual, aa45x.RZ) - - // Euler angles - test.That(t, o.EulerAngles().Roll, test.ShouldAlmostEqual, ea45x.Roll) - test.That(t, o.EulerAngles().Pitch, test.ShouldAlmostEqual, ea45x.Pitch) - test.That(t, o.EulerAngles().Yaw, test.ShouldAlmostEqual, ea45x.Yaw) - - // Rotation matrices - for i := 0; i < 3; i++ { - for j := 0; j < 3; j++ { - test.That(t, o.RotationMatrix().At(i, j), test.ShouldAlmostEqual, rm45x.At(i, j)) - } - } -} diff --git a/spatialmath/point_test.go b/spatialmath/point_test.go deleted file mode 100644 index 8d04bdca6ce..00000000000 --- a/spatialmath/point_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package spatialmath - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestNewPoint(t *testing.T) { - offset := NewPose(r3.Vector{X: 1, Y: 0, Z: 0}, &EulerAngles{0, 0, math.Pi}) - - // test point created from NewBox method - geometry := NewPoint(offset.Point(), "") - test.That(t, geometry, test.ShouldResemble, &point{offset.Point(), ""}) - - // test point created from GeometryCreator with offset - geometry = NewPoint(offset.Point(), "").Transform(PoseInverse(offset)) - test.That(t, PoseAlmostCoincident(geometry.Pose(), NewZeroPose()), test.ShouldBeTrue) -} - -func TestPointAlmostEqual(t *testing.T) { - original := NewPoint(r3.Vector{}, "") - good := NewPoint(r3.Vector{1e-18, 1e-18, 1e-18}, "") - bad := NewPoint(r3.Vector{1e-2, 1e-2, 1e-2}, "") - test.That(t, original.(*point).almostEqual(good), test.ShouldBeTrue) - test.That(t, original.(*point).almostEqual(bad), test.ShouldBeFalse) -} diff --git a/spatialmath/pose_test.go b/spatialmath/pose_test.go deleted file mode 100644 index ccb172a620a..00000000000 --- a/spatialmath/pose_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package spatialmath - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "gonum.org/v1/gonum/num/dualquat" - "gonum.org/v1/gonum/num/quat" -) - -func TestBasicPoseConstruction(t *testing.T) { - p := NewZeroPose() - // Should return an identity dual quat - test.That(t, p.Orientation().OrientationVectorRadians(), test.ShouldResemble, &OrientationVector{0, 0, 0, 1}) - - // point at +Y, rotate 90 degrees - ov := &OrientationVector{math.Pi / 2, 0, 1, 0} - ov.Normalize() - - p = NewPose(r3.Vector{1, 2, 3}, ov) - ovCompare(t, p.Orientation().OrientationVectorRadians(), ov) - ptCompare(t, p.Point(), r3.Vector{1, 2, 3}) - - aa := QuatToR4AA(ov.ToQuat()) - p = NewPose(r3.Vector{1, 2, 3}, &R4AA{aa.Theta, aa.RX, aa.RY, aa.RZ}) - ptCompare(t, p.Point(), r3.Vector{1, 2, 3}) - ovCompare(t, p.Orientation().OrientationVectorRadians(), ov) - - p = NewPoseFromPoint(r3.Vector{1, 2, 3}) - ptCompare(t, p.Point(), r3.Vector{1, 2, 3}) - test.That(t, p.Orientation().OrientationVectorRadians(), test.ShouldResemble, &OrientationVector{0, 0, 0, 1}) - - p1 := NewPose(r3.Vector{1, 2, 3}, ov) - p2 := NewPoseFromPoint(r3.Vector{1, 2, 3}) - pComp := Compose(p1, p2) - ptCompare(t, pComp.Point(), r3.Vector{0, 5, 5}) - - p2 = NewPose(r3.Vector{2, 3, 4}, ov) - delta := PoseDelta(p1, p2) - ptCompare(t, delta.Point(), r3.Vector{1.0, 1.0, 1.0}) - ovCompare(t, delta.Orientation().OrientationVectorRadians(), NewOrientationVector()) - - p2 = NewPoseFromPoint(r3.Vector{2, 3, 4}) - - pb := PoseBetween(p1, p2) - test.That(t, PoseAlmostEqual(Compose(p1, pb), p2), test.ShouldBeTrue) - pbi := PoseBetweenInverse(p1, p2) - test.That(t, PoseAlmostEqual(Compose(pbi, p1), p2), test.ShouldBeTrue) - pbi2 := Compose(p2, PoseInverse(p1)) - test.That(t, PoseAlmostEqual(pbi, pbi2), test.ShouldBeTrue) - - p = NewPoseFromOrientation(&R4AA{0, 4, 5, 6}) - test.That(t, p.Orientation().OrientationVectorRadians(), test.ShouldResemble, &OrientationVector{0, 0, 0, 1}) -} - -func ptCompare(t *testing.T, p1, p2 r3.Vector) { - t.Helper() - test.That(t, p1.X, test.ShouldAlmostEqual, p2.X) - test.That(t, p1.Y, test.ShouldAlmostEqual, p2.Y) - test.That(t, p1.Z, test.ShouldAlmostEqual, p2.Z) -} - -func TestDualQuatTransform(t *testing.T) { - // Start with point [3, 4, 5] - Rotate by 180 degrees around x-axis and then displace by [4,2,6] - pt := NewPoseFromPoint(r3.Vector{3., 4., 5.}) // starting point - tr := &dualQuaternion{dualquat.Number{Real: quat.Number{Real: 0, Imag: 1}}} - tr.SetTranslation(r3.Vector{4., 2., 6.}) - - trAA := NewPose(r3.Vector{4., 2., 6.}, &R4AA{math.Pi, 1, 0, 0}) // same transformation from axis angle - // ensure transformation is the same between both definitions - test.That(t, tr.Real.Real, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Real.Real) - test.That(t, tr.Real.Imag, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Real.Imag) - test.That(t, tr.Real.Jmag, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Real.Jmag) - test.That(t, tr.Real.Kmag, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Real.Kmag) - test.That(t, tr.Dual.Real, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Dual.Real) - test.That(t, tr.Dual.Imag, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Dual.Imag) - test.That(t, tr.Dual.Jmag, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Dual.Jmag) - test.That(t, tr.Dual.Kmag, test.ShouldAlmostEqual, newDualQuaternionFromPose(trAA).Dual.Kmag) - - expectedPose := NewPoseFromPoint(r3.Vector{7., -2., 1.}) - expectedPoint := expectedPose.Point() - transformedPoint := Compose(tr, pt).Point() - test.That(t, transformedPoint.X, test.ShouldAlmostEqual, expectedPoint.X) - test.That(t, transformedPoint.Y, test.ShouldAlmostEqual, expectedPoint.Y) - test.That(t, transformedPoint.Z, test.ShouldAlmostEqual, expectedPoint.Z) -} - -func TestPoseInterpolation(t *testing.T) { - p1 := NewPoseFromPoint(r3.Vector{1, 2, 3}) - p2 := NewPoseFromPoint(r3.Vector{3, 6, 9}) - intP := Interpolate(p1, p2, 0.5) - ptCompare(t, intP.Point(), r3.Vector{2, 4, 6}) - - p1 = NewPoseFromPoint(r3.Vector{0, 0, 0}) - p2 = NewPoseFromPoint(r3.Vector{10, 100, 1000}) - intP = Interpolate(p1, p2, 0.33) - ptCompare(t, intP.Point(), r3.Vector{3.3, 33, 330}) - - ov := &OrientationVector{math.Pi / 2, 0, 0, -1} - ov.Normalize() - p1 = NewPose(r3.Vector{100, 100, 200}, ov) - p2 = NewPose(r3.Vector{100, 200, 200}, ov) - intP = Interpolate(p1, p2, 0.1) - ptCompare(t, intP.Point(), r3.Vector{100, 110, 200}) -} - -func TestLidarPose(t *testing.T) { - ea := NewEulerAngles() - // 45 degrees above horizon - // Positive pitch rotation rotates from the default of pointing up at the +Z axis, forwards towards the +X axis. - ea.Pitch = math.Pi / 4 - // Point to the left (at positive Y axis) - ea.Yaw = math.Pi / 2 - - // lidar sees a point 400mm away - dist := 400. - - pose1 := NewPoseFromOrientation(ea) - pose2 := NewPoseFromPoint(r3.Vector{0, 0, dist}) - seenPoint := Compose(pose1, pose2).Point() - - expectPoint := r3.Vector{0, 282.842712474619, 282.842712474619} - - test.That(t, expectPoint.X, test.ShouldAlmostEqual, seenPoint.X) - test.That(t, expectPoint.Y, test.ShouldAlmostEqual, seenPoint.Y) - test.That(t, expectPoint.Z, test.ShouldAlmostEqual, seenPoint.Z) -} - -func TestPoseAlmostEqual(t *testing.T) { - p1 := NewPoseFromPoint(r3.Vector{1.0, 2.0, 3.0}) - p2 := NewPoseFromPoint(r3.Vector{1.0000000001, 1.999999999, 3.0000000001}) - p3 := NewPoseFromPoint(r3.Vector{1.0000001, 2.999999, 3.0000001}) - test.That(t, PoseAlmostCoincident(p1, p2), test.ShouldBeTrue) - test.That(t, PoseAlmostCoincident(p1, p3), test.ShouldBeFalse) -} - -var ( - ov = &OrientationVector{math.Pi / 2, 0, 0, -1} - p1b = NewPose(r3.Vector{1, 2, 3}, ov) - p2b = NewPose(r3.Vector{2, 3, 4}, ov) -) - -func BenchmarkDeltaPose(b *testing.B) { - for n := 0; n < b.N; n++ { - PoseDelta(p1b, p2b) - } -} - -func BenchmarkPoseBetween(b *testing.B) { - for n := 0; n < b.N; n++ { - PoseBetween(p1b, p2b) - } -} diff --git a/spatialmath/quat_test.go b/spatialmath/quat_test.go deleted file mode 100644 index 41a3eaeb3eb..00000000000 --- a/spatialmath/quat_test.go +++ /dev/null @@ -1,262 +0,0 @@ -package spatialmath - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "gonum.org/v1/gonum/num/quat" -) - -func TestAngleAxisConversion1(t *testing.T) { - // Test that we can convert back and forth losslessly between angle axis and quaternions - startAA := R4AA{2.5980762, 0.577350, 0.577350, 0.577350} - quat := startAA.ToQuat() - end1 := QuatToR4AA(quat) - test.That(t, math.Abs(end1.Theta-startAA.Theta), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RX-startAA.RX), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RY-startAA.RZ), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RZ-startAA.RZ), test.ShouldBeLessThan, 0.001) -} - -func TestAngleAxisConversion2(t *testing.T) { - // Test that we can convert back and forth losslessly between r4 and r3 angle axis - startAA := R4AA{2.5980762, 0.577350, 0.577350, 0.577350} - r3aa := startAA.ToR3() - end1 := R3ToR4(r3aa) - test.That(t, math.Abs(end1.Theta-startAA.Theta), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RX-startAA.RX), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RY-startAA.RZ), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RZ-startAA.RZ), test.ShouldBeLessThan, 0.001) -} - -func TestEulerAnglesConversion(t *testing.T) { - for _, testcase := range []struct { - name string - expectedEA EulerAngles - q quat.Number - }{ - { - "vanilla 1: roll pitch and yaw are not near edge cases", - EulerAngles{math.Pi / 4.0, math.Pi / 4.0, 3.0 * math.Pi / 4.0}, - quat.Number{Real: 0.46193978734586505, Imag: -0.19134171618254486, Jmag: 0.4619397662556434, Kmag: 0.7325378046916491}, - }, - { - "vanilla 2: roll pitch and yaw are not near edge cases", - EulerAngles{-math.Pi / 4.0, -math.Pi / 4.0, math.Pi / 4.0}, - quat.Number{Real: 0.8446231850190303, Imag: -0.19134170056642805, Jmag: -0.461939798632522, Kmag: 0.19134170056642805}, - }, - { - "gimbal lock: pitch is π/2", - EulerAngles{-3 * math.Pi / 4.0, math.Pi / 2.0, 0}, - quat.Number{Real: 0.2705980500730985, Imag: -0.6532814824381882, Jmag: 0.27059805007309856, Kmag: 0.6532814824381883}, - }, - { - "heading only", - EulerAngles{0, 0, math.Pi / 3}, - quat.Number{Real: 0.8660254042574935, Imag: 0, Jmag: 0, Kmag: 0.5}, - }, - } { - t.Run(testcase.name, func(t *testing.T) { - endEa := QuatToEulerAngles(testcase.q) - q2 := endEa.Quaternion() - - t.Run("roll", func(t *testing.T) { - test.That(t, testcase.expectedEA.Roll, test.ShouldAlmostEqual, endEa.Roll, 1e-6) - }) - t.Run("pitch", func(t *testing.T) { - test.That(t, testcase.expectedEA.Pitch, test.ShouldAlmostEqual, endEa.Pitch, 1e-6) - }) - t.Run("yaw", func(t *testing.T) { - test.That(t, testcase.expectedEA.Yaw, test.ShouldAlmostEqual, endEa.Yaw, 1e-6) - }) - t.Run("quat", func(t *testing.T) { - quatCompare(t, testcase.q, q2) - }) - }, - ) - } -} - -func TestMatrixConversion(t *testing.T) { - // Test that lossless conversion between quaternions and rotation matrices is achieved - q := quat.Number{0.7071067811865476, 0.7071067811865476, 0, 0} - quatCompare(t, q, QuatToRotationMatrix(q).Quaternion()) - q = quat.Number{0.7071067811865476, -0.7071067811865476, 0, 0} - quatCompare(t, q, QuatToRotationMatrix(q).Quaternion()) - q = quat.Number{0.96, 0, -0.28, 0} - quatCompare(t, q, QuatToRotationMatrix(q).Quaternion()) - q = quat.Number{0.96, 0, 0, -0.28} - quatCompare(t, q, QuatToRotationMatrix(q).Quaternion()) - - // Should be negative theta - q = quat.Number{0.96, -0.28, 0, 0} - quatCompare(t, q, QuatToRotationMatrix(q).Quaternion()) - - // Test the complementary angle - q = quat.Number{0.96, 0.28, 0, 0} - quatCompare(t, q, QuatToRotationMatrix(q).Quaternion()) - - // Another odd angle - q = quat.Number{0.5, -0.5, -0.5, -0.5} - quatCompare(t, q, Flip(QuatToRotationMatrix(q).Quaternion())) -} - -func TestFlip(t *testing.T) { - // Test that flipping quaternions to the opposite octant results in the same rotation - startAA := R4AA{2.5980762, 0.577350, -0.577350, -0.577350} - quat1 := startAA.ToQuat() - quat2 := startAA.ToQuat() - qb1 := quat.Mul(quat1, quat.Conj(quat2)) - qb2 := quat.Mul(quat1, quat.Conj(Flip(quat2))) - - end1 := QuatToR4AA(qb1) - end2 := QuatToR4AA(qb2) - test.That(t, math.Abs(end1.Theta-end2.Theta), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RX-end2.RX), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RY-end2.RY), test.ShouldBeLessThan, 0.001) - test.That(t, math.Abs(end1.RZ-end2.RZ), test.ShouldBeLessThan, 0.001) -} - -func TestDHConversion(t *testing.T) { - // Test conversion of a DH param to a dual quaternion - dhParam := []float64{-0.425, 0.1333, math.Pi / 2} - dq1 := newDualQuaternionFromDH(dhParam[0], dhParam[1], dhParam[2]) - dq2 := newDualQuaternionFromPose(NewPose( - r3.Vector{X: -0.425, Y: 0, Z: 0.1333}, - &OrientationVectorDegrees{OY: -1, Theta: 90}, - )) - quatCompare(t, dq1.Real, dq2.Real) - quatCompare(t, dq1.Dual, dq2.Dual) -} - -func TestQuatDefault(t *testing.T) { - q1 := newDualQuaternionFromRotation(&OrientationVector{}) - q2 := newDualQuaternionFromRotation(&OrientationVector{OZ: 1}) - quatCompare(t, q1.Real, q2.Real) -} - -func TestQuatConversion(t *testing.T) { - // Ensure a robust, lossless quaternion/ov/quaternion/ov transformation - quatConvert(t, quat.Number{0.7071067811865476, 0.7071067811865476, 0, 0}) - quatConvert(t, quat.Number{0.7071067811865476, -0.7071067811865476, 0, 0}) - quatConvert(t, quat.Number{0.96, 0, -0.28, 0}) - quatConvert(t, quat.Number{0.96, 0, 0, -0.28}) - - // Should be negative theta - quatConvert(t, quat.Number{0.96, -0.28, 0, 0}) - - // Test the complementary angle - quatConvert(t, quat.Number{0.96, 0.28, 0, 0}) - - // Another odd angle - quatConvert(t, quat.Number{0.5, -0.5, -0.5, -0.5}) - - // Some orientation vectors - ovConvert(t, &OrientationVector{Theta: 2.47208, OX: 1, OY: 0, OZ: 0}) - ovConvert(t, &OrientationVector{Theta: 2.47208, OX: -1, OY: 0, OZ: 0}) - ovConvert(t, &OrientationVector{Theta: 2.47208, OX: 0, OY: 1, OZ: 0}) - ovConvert(t, &OrientationVector{Theta: 2.47208, OX: 0, OY: -1, OZ: 0}) - - // Test a small angle that was hitting defaultAngleEpsilon erroneously - ovConvert(t, &OrientationVector{Theta: 0.02, OX: 0.5048437942940054, OY: 0.5889844266763397, OZ: 0.631054742867507}) - - // An OV that initially gave problems in testing - ovConvert(t, &OrientationVector{Theta: 0, OX: -0.32439089809469324, OY: -0.9441256803955101, OZ: -0.05828588895294498}) - ovConvert(t, &OrientationVector{Theta: -0.5732162806942777, OX: -0.32439089809469324, OY: -0.9441256803955101, OZ: -0.05828588895294498}) -} - -func TestOVConversionPoles(t *testing.T) { - // Ensure a robust, lossless quaternion/ov/quaternion/ov transformation near the poles - // North pole - ovConvert(t, &OrientationVector{Theta: 2.47208, OX: 0, OY: 0, OZ: 1}) - ovConvert(t, &OrientationVector{Theta: 0, OX: 0, OY: 0, OZ: 1}) - ovConvert(t, &OrientationVector{Theta: -2.47208, OX: 0, OY: 0, OZ: 1}) - ovConvert(t, &OrientationVector{Theta: -0.78, OX: 0, OY: 0, OZ: 1}) - - // South pole - ovConvert(t, &OrientationVector{Theta: 2.47208, OX: 0, OY: 0, OZ: -1}) - ovConvert(t, &OrientationVector{Theta: 0, OX: 0, OY: 0, OZ: -1}) - ovConvert(t, &OrientationVector{Theta: -2.47208, OX: 0, OY: 0, OZ: -1}) - ovConvert(t, &OrientationVector{Theta: -0.78, OX: 0, OY: 0, OZ: -1}) -} - -func TestQuatNormalize(t *testing.T) { - tests := []struct { - rotation quat.Number - expected quat.Number - }{ - {quat.Number{0, 0, 0, 0}, quat.Number{1, 0, 0, 0}}, - {quat.Number{0, 1, 0, 0}, quat.Number{0, 1, 0, 0}}, - {quat.Number{0, 0.0000000000001, 0, 0}, quat.Number{0, 1, 0, 0}}, - {quat.Number{0, float64(math.MaxFloat64), 1, 0}, quat.Number{0, 1, 0, 0}}, - {quat.Number{4, 2, 8, 4}, quat.Number{0.4, 0.2, 0.8, 0.4}}, - {quat.Number{0, 3.0, 4.0, 5.0}, quat.Number{0, 3.0 / math.Sqrt(50), 4.0 / math.Sqrt(50), 5.0 / math.Sqrt(50)}}, - } - - for _, c := range tests { - quatCompare(t, Normalize(c.rotation), c.expected) - } -} - -func TestR4Normalize(t *testing.T) { - // Test that Normalize() will produce a unit vector - ov1 := R4AA{0, 999, 0, 0} - ov1.Normalize() - test.That(t, ov1.Theta, test.ShouldEqual, 0) - test.That(t, ov1.RX, test.ShouldEqual, 1) - test.That(t, ov1.RY, test.ShouldEqual, 0) - test.That(t, ov1.RZ, test.ShouldEqual, 0) -} - -func TestOVNormalize(t *testing.T) { - // Test that Normalize() will produce a unit vector - ov1 := &OrientationVector{Theta: 0, OX: 999, OY: 0, OZ: 0} - ov1.Normalize() - test.That(t, ov1.Theta, test.ShouldEqual, 0) - test.That(t, ov1.OX, test.ShouldEqual, 1) - test.That(t, ov1.OY, test.ShouldEqual, 0) - test.That(t, ov1.OZ, test.ShouldEqual, 0) - ov1 = &OrientationVector{Theta: 0, OX: 0.5, OY: 0, OZ: 0} - ov1.Normalize() - test.That(t, ov1.Theta, test.ShouldEqual, 0) - test.That(t, ov1.OX, test.ShouldEqual, 1) - test.That(t, ov1.OY, test.ShouldEqual, 0) - test.That(t, ov1.OZ, test.ShouldEqual, 0) -} - -func ovConvert(t *testing.T, ov1 *OrientationVector) { - t.Helper() - q1 := ov1.ToQuat() - ov2 := QuatToOV(q1) - q2 := ov2.ToQuat() - - ovCompare(t, ov1, ov2) - quatCompare(t, q1, q2) -} - -func quatConvert(t *testing.T, q1 quat.Number) { - t.Helper() - ov1 := QuatToOV(q1) - q2 := ov1.ToQuat() - ov2 := QuatToOV(q2) - ovCompare(t, ov1, ov2) - quatCompare(t, q1, q2) -} - -func ovCompare(t *testing.T, ov1, ov2 *OrientationVector) { - t.Helper() - test.That(t, ov1.Theta, test.ShouldAlmostEqual, ov2.Theta) - test.That(t, ov1.OX, test.ShouldAlmostEqual, ov2.OX) - test.That(t, ov1.OY, test.ShouldAlmostEqual, ov2.OY) - test.That(t, ov1.OZ, test.ShouldAlmostEqual, ov2.OZ) -} - -func quatCompare(t *testing.T, q1, q2 quat.Number) { - t.Helper() - test.That(t, q1.Real, test.ShouldAlmostEqual, q2.Real, 1e-8) - test.That(t, q1.Imag, test.ShouldAlmostEqual, q2.Imag, 1e-8) - test.That(t, q1.Jmag, test.ShouldAlmostEqual, q2.Jmag, 1e-8) - test.That(t, q1.Kmag, test.ShouldAlmostEqual, q2.Kmag, 1e-8) -} diff --git a/spatialmath/r3vector_test.go b/spatialmath/r3vector_test.go deleted file mode 100644 index 44b7cf66607..00000000000 --- a/spatialmath/r3vector_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package spatialmath - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func TestR3VectorAlmostEqual(t *testing.T) { - test.That(t, R3VectorAlmostEqual(r3.Vector{1, 2, 3}, r3.Vector{1.001, 2, 3}, 1e-4), test.ShouldBeFalse) - test.That(t, R3VectorAlmostEqual(r3.Vector{1, 2, 3}, r3.Vector{1.001, 2.001, 3.001}, 1e-2), test.ShouldBeTrue) -} - -func TestAxisSerialization(t *testing.T) { - tc := NewAxisConfig(R4AA{Theta: 1, RX: 1}) - newTc := NewAxisConfig(tc.ParseConfig()) - test.That(t, tc, test.ShouldResemble, newTc) -} diff --git a/spatialmath/random_test.go b/spatialmath/random_test.go deleted file mode 100644 index f69292688b8..00000000000 --- a/spatialmath/random_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package spatialmath - -import ( - "math" - "math/rand" - "testing" - - "go.viam.com/test" -) - -func rr(r *rand.Rand, rng float64) float64 { - return (r.Float64() * rng) - (rng / 2) -} - -func rrpi(r *rand.Rand) float64 { - return rr(r, math.Pi) -} - -func TestAxisAngleRoundTrip(t *testing.T) { - data := []R4AA{ - {1, 1, 1, 1}, - {1, 1, 0, 0}, - {1, 0, 1, 0}, - {1, 0, 0, 1}, - } - - r := rand.New(rand.NewSource(517)) - for len(data) < 100 { - data = append(data, R4AA{rrpi(r), rrpi(r), rrpi(r), rrpi(r)}) - } - - // Quaternion [x, y, z, w] - // from https://www.andre-gaschler.com/rotationconverter/ - qc := [][]float64{ - {0.2767965, 0.2767965, 0.2767965, 0.8775826}, - {0.4794255, 0, 0, 0.8775826}, - {0, 0.4794255, 0, 0.8775826}, - {0, 0, 0.4794255, 0.8775826}, - } - - for idx, d := range data { - d.Normalize() - d.fixOrientation() - - q := Quaternion(d.Quaternion()) - - d2 := q.AxisAngles() - d2.fixOrientation() // TODO(bijan): should this be in AxisAngles - - test.That(t, d2.Theta, test.ShouldAlmostEqual, d.Theta) - test.That(t, d2.RX, test.ShouldAlmostEqual, d.RX) - test.That(t, d2.RY, test.ShouldAlmostEqual, d.RY) - test.That(t, d2.RZ, test.ShouldAlmostEqual, d.RZ) - - if idx < len(qc) { - test.That(t, q.Real, test.ShouldAlmostEqual, qc[idx][3], .00001) - test.That(t, q.Imag, test.ShouldAlmostEqual, qc[idx][0], .00001) - test.That(t, q.Jmag, test.ShouldAlmostEqual, qc[idx][1], .00001) - test.That(t, q.Kmag, test.ShouldAlmostEqual, qc[idx][2], .00001) - } - } -} - -func TestOrientationVectorRoundTrip(t *testing.T) { - data := []OrientationVector{ - {1, 1, 1, 1}, - {1, 1, 0, 0}, - {1, 0, 1, 0}, - {1, 0, 0, 1}, - } - - r := rand.New(rand.NewSource(517)) - for len(data) < 100 { - data = append(data, OrientationVector{rrpi(r), rrpi(r), rrpi(r), rrpi(r)}) - } - - for _, d := range data { - d.Normalize() - q := Quaternion(d.Quaternion()) - d2 := q.OrientationVectorRadians() - test.That(t, d2.Theta, test.ShouldAlmostEqual, d.Theta) - test.That(t, d2.OX, test.ShouldAlmostEqual, d.OX) - test.That(t, d2.OY, test.ShouldAlmostEqual, d.OY) - test.That(t, d2.OZ, test.ShouldAlmostEqual, d.OZ) - } -} - -func TestEulerRoundTrip(t *testing.T) { - data := []EulerAngles{ - {1, 0, 0}, - {1, 1, 0}, - {1, 0, 1}, - } - - r := rand.New(rand.NewSource(517)) - for len(data) < 100 { - data = append(data, EulerAngles{rrpi(r), rrpi(r), rrpi(r)}) - } - - // Quaternion [x, y, z, w] - // from https://www.andre-gaschler.com/rotationconverter/ - qc := [][]float64{ - {0.4794255, 0, 0, 0.8775826}, - {0.4207355, 0.4207355, -0.2298488, 0.7701512}, - {0.4207355, 0.2298488, 0.4207355, 0.7701512}, - } - - for idx, d := range data { - q := Quaternion(d.Quaternion()) - d2 := q.EulerAngles() - test.That(t, d2.Roll, test.ShouldAlmostEqual, d.Roll) - test.That(t, d2.Pitch, test.ShouldAlmostEqual, d.Pitch) - test.That(t, d2.Yaw, test.ShouldAlmostEqual, d.Yaw) - - if idx < len(qc) { - test.That(t, q.Real, test.ShouldAlmostEqual, qc[idx][3], .00001) - test.That(t, q.Imag, test.ShouldAlmostEqual, qc[idx][0], .00001) - test.That(t, q.Jmag, test.ShouldAlmostEqual, qc[idx][1], .00001) - test.That(t, q.Kmag, test.ShouldAlmostEqual, qc[idx][2], .00001) - } - } -} - -func TestOVToEuler(t *testing.T) { - type p struct { - ov OrientationVectorDegrees - e EulerAngles - } - - data := []p{ - {OrientationVectorDegrees{90, 0, 1, 0}, EulerAngles{math.Pi / 2, 0, math.Pi}}, - } - - for _, d := range data { - e2 := d.ov.EulerAngles() - test.That(t, e2.Roll, test.ShouldAlmostEqual, d.e.Roll) - test.That(t, e2.Pitch, test.ShouldAlmostEqual, d.e.Pitch) - test.That(t, e2.Yaw, test.ShouldAlmostEqual, d.e.Yaw) - } -} diff --git a/spatialmath/rotationMatrix_test.go b/spatialmath/rotationMatrix_test.go deleted file mode 100644 index d8d7c2823c4..00000000000 --- a/spatialmath/rotationMatrix_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package spatialmath - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "gonum.org/v1/gonum/mat" - "gonum.org/v1/gonum/num/quat" -) - -func TestQuaternionConversion(t *testing.T) { - // Test that conversion to rotation matrix to quaternion is correct - // http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/examples/index.htm - cos45 := 0.7071067811865476 - cases := []struct { - input [9]float64 - expected quat.Number - }{ - { - [9]float64{ - 1, 0, 0, - 0, 1, 0, - 0, 0, 1, - }, - NewZeroOrientation().Quaternion(), - }, - { - [9]float64{ - 0, 0, -1, - 0, 1, 0, - 1, 0, 0, - }, - quat.Number{cos45, 0, cos45, 0}, - }, - { - [9]float64{ - -1, 0, 0, - 0, 1, 0, - 0, 0, -1, - }, - quat.Number{0, 0, 1, 0}, - }, - { - [9]float64{ - 0, 1, 0, - -1, 0, 0, - 0, 0, 1, - }, - quat.Number{cos45, 0, 0, cos45}, - }, - { - [9]float64{ - 1, 0, 0, - 0, 0, 1, - 0, -1, 0, - }, - quat.Number{cos45, cos45, 0, 0}, - }, - { - [9]float64{ - -0.5003235, 0.1601237, 0.8509035, - 0.7536948, -0.4031713, 0.5190347, - 0.4261697, 0.9010068, 0.0810317, - }, - quat.Number{-0.21067562973908407, 0.4532703843447015, 0.5040139879925649, 0.7043661157381153}, - }, - } - - for _, c := range cases { - rm := &RotationMatrix{c.input} - quatCompare(t, rm.Quaternion(), c.expected) - } -} - -func TestMatrixAtRowsCols(t *testing.T) { - mat := [9]float64{ - 1, 2, 3, - 4, 5, 6, - 7, 8, 9, - } - rm := &RotationMatrix{mat} - - // test At function - test.That(t, rm.At(0, 0), test.ShouldEqual, 1) - test.That(t, rm.At(0, 1), test.ShouldEqual, 2) - test.That(t, rm.At(0, 2), test.ShouldEqual, 3) - test.That(t, rm.At(1, 0), test.ShouldEqual, 4) - test.That(t, rm.At(1, 1), test.ShouldEqual, 5) - test.That(t, rm.At(1, 2), test.ShouldEqual, 6) - test.That(t, rm.At(2, 0), test.ShouldEqual, 7) - test.That(t, rm.At(2, 1), test.ShouldEqual, 8) - test.That(t, rm.At(2, 2), test.ShouldEqual, 9) - - // test Rows function - test.That(t, rm.Row(0).Cmp(r3.Vector{1, 2, 3}) == 0, test.ShouldBeTrue) - test.That(t, rm.Row(1).Cmp(r3.Vector{4, 5, 6}) == 0, test.ShouldBeTrue) - test.That(t, rm.Row(2).Cmp(r3.Vector{7, 8, 9}) == 0, test.ShouldBeTrue) - - // test Cols function - test.That(t, rm.Col(0).Cmp(r3.Vector{1, 4, 7}) == 0, test.ShouldBeTrue) - test.That(t, rm.Col(1).Cmp(r3.Vector{2, 5, 8}) == 0, test.ShouldBeTrue) - test.That(t, rm.Col(2).Cmp(r3.Vector{3, 6, 9}) == 0, test.ShouldBeTrue) -} - -func TestMatrixMul(t *testing.T) { - mat1 := []float64{ - 1, 2, 3, - 4, 5, 6, - 7, 8, 9, - } - rm, err := NewRotationMatrix(mat1) - test.That(t, err, test.ShouldBeNil) - - test.That(t, R3VectorAlmostEqual(rm.Mul(r3.Vector{1, 0, 0}), r3.Vector{1, 4, 7}, 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(rm.Mul(r3.Vector{1, 1, 0}), r3.Vector{3, 9, 15}, 1e-8), test.ShouldBeTrue) - test.That(t, R3VectorAlmostEqual(rm.Mul(r3.Vector{1, 1, 1}), r3.Vector{6, 15, 24}, 1e-8), test.ShouldBeTrue) - - mat2 := []float64{ - 9, 2, 3, - 6, 5, 4, - 3, 2, 1, - } - mm, err := NewRotationMatrix(mat2) - test.That(t, err, test.ShouldBeNil) - - c, d, _ := multiplyAndconvertToFloats(mat1, mat2) - - mul := MatMul(*rm, *mm) - lMul := rm.LeftMatMul(*mm).mat - rMul := rm.RightMatMul(*mm).mat - - test.That(t, mul.mat, test.ShouldResemble, c) - test.That(t, lMul, test.ShouldResemble, c) - test.That(t, rMul, test.ShouldResemble, d) -} - -func multiplyAndconvertToFloats(in1, in2 []float64) ([9]float64, [9]float64, error) { - a := mat.NewDense(3, 3, in1) - b := mat.NewDense(3, 3, in2) - var c mat.Dense - var d mat.Dense - c.Mul(a, b) // c is left multiplication - d.Mul(b, a) // d is right multiplication - - vecC := c.RawMatrix().Data - vecD := d.RawMatrix().Data - outC, err := NewRotationMatrix(vecC) - if err != nil { - return [9]float64{}, [9]float64{}, err - } - outD, err := NewRotationMatrix(vecD) - if err != nil { - return [9]float64{}, [9]float64{}, err - } - return outC.mat, outD.mat, err -} diff --git a/spatialmath/sphere_test.go b/spatialmath/sphere_test.go deleted file mode 100644 index a9bd025e09f..00000000000 --- a/spatialmath/sphere_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package spatialmath - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -func makeTestSphere(point r3.Vector, radius float64, label string) Geometry { - sphere, _ := NewSphere(NewPoseFromPoint(point), radius, label) - return sphere -} - -func TestNewSphere(t *testing.T) { - offset := NewPose(r3.Vector{X: 1, Y: 0, Z: 0}, &EulerAngles{0, 0, math.Pi}) - - // test sphere created from NewSphere method - geometry, err := NewSphere(offset, 1, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, geometry, test.ShouldResemble, &sphere{pose: offset, radius: 1}) - _, err = NewSphere(offset, -1, "") - test.That(t, err.Error(), test.ShouldContainSubstring, newBadGeometryDimensionsError(&sphere{}).Error()) - - // test sphere created from initial sphere with offset - gc, err := NewSphere(offset, 1, "") - test.That(t, err, test.ShouldBeNil) - geometry = gc.Transform(PoseInverse(offset)) - test.That(t, PoseAlmostCoincident(geometry.Pose(), NewZeroPose()), test.ShouldBeTrue) -} - -func TestSphereAlmostEqual(t *testing.T) { - original := makeTestSphere(r3.Vector{}, 1, "") - good := makeTestSphere(r3.Vector{1e-16, 1e-16, 1e-16}, 1+1e-16, "") - bad := makeTestSphere(r3.Vector{1e-2, 1e-2, 1e-2}, 1+1e-2, "") - test.That(t, original.(*sphere).almostEqual(good), test.ShouldBeTrue) - test.That(t, original.(*sphere).almostEqual(bad), test.ShouldBeFalse) -} - -func TestSpherePC(t *testing.T) { - pt := r3.Vector{-2, -2, -2} - radius := 2.5 - label := "" - sphere := &sphere{NewPoseFromPoint(pt), radius, label} - customDensity := 0.3 - output := sphere.ToPoints(customDensity) - checkAgainst := []r3.Vector{ - {-2.000000000000000000000000, 0.500000000000000000000000, -2.000000000000000000000000}, - {-2.767965613386037304621823, 0.272727272727272485042249, -1.296480589849675402192020}, - {-1.874334356274330648517434, 0.045454545454545414173708, -3.431895194651603198110479}, - {-0.955997121732925059234276, -0.181818181818181656694833, -0.638283118191185439016522}, - {-3.898993408161034679437762, -0.409090909090909171652584, -2.335905195291390956668920}, - {-0.232036488272917562625253, -0.636363636363636464565730, -3.124633668787413220968574}, - {-2.578089165489593437285976, -0.863636363636363535434270, 0.150462881031420803168430}, - {-3.073384452255186083391436, -1.090909090909090828347416, -4.066736445864615134837550}, - {0.259282319327660104590905, -1.318181818181818343305167, -1.174913720562765773181013}, - {-4.272346794705999428742871, -1.545454545454545636218313, -1.062008275973910009781775}, - {-0.944772670927437996368781, -1.772727272727272707086854, -4.254959509928371907960809}, - {-1.251790338887817544133441, -2.000000000000000000000000, 0.385410300769742697468700}, - {-4.154071348128081098138864, -2.227272727272727070868541, -3.248328376114109072858582}, - {0.400991759990806162505805, -2.454545454545454585826292, -2.527851303122686132951458}, - {-3.383317673798449831679136, -2.681818181818181656694833, -0.032372957011671310567635}, - {-2.299282434379497530585468, -2.909090909090908727563374, -4.309541890393015606264271}, - {-0.297272844364510291370607, -3.136363636363636686610334, -0.564939750598641765577668}, - {-4.093561696420204398805254, -3.363636363636363313389666, -1.913424651021499389713654}, - {-0.633041584840636106790157, -3.590909090909091272436626, -3.360306199238909385940133}, - {-2.079258583903523138758374, -3.818181818181818343305167, -0.285960049208109579055304}, - {-2.920954643538304473793232, -4.045454545454544970084498, -3.103611456548260871812772}, - {-0.967806681180649386320169, -4.272727272727273373220669, -1.861119848839560830811024}, - {-2.000000000000000000000000, -4.500000000000000000000000, -2.000000000000000000000000}, - } - for i, v := range output { - test.That(t, R3VectorAlmostEqual(v, checkAgainst[i], 1e-2), test.ShouldBeTrue) - } -} diff --git a/testutils/inject/analog.go b/testutils/inject/analog.go deleted file mode 100644 index e3b9550e0c7..00000000000 --- a/testutils/inject/analog.go +++ /dev/null @@ -1,52 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/board" -) - -// Analog is an injected analog pin. -type Analog struct { - board.Analog - ReadFunc func(ctx context.Context, extra map[string]interface{}) (int, error) - readCap []interface{} - WriteFunc func(ctx context.Context, value int, extra map[string]interface{}) error - writeCap []interface{} -} - -// Read calls the injected Read or the real version. -func (a *Analog) Read(ctx context.Context, extra map[string]interface{}) (int, error) { - a.readCap = []interface{}{ctx} - if a.ReadFunc == nil { - return a.Analog.Read(ctx, extra) - } - return a.ReadFunc(ctx, extra) -} - -// ReadCap returns the last parameters received by Read, and then clears them. -func (a *Analog) ReadCap() []interface{} { - if a == nil { - return nil - } - defer func() { a.readCap = nil }() - return a.readCap -} - -// Write calls the injected Write or the real version. -func (a *Analog) Write(ctx context.Context, value int, extra map[string]interface{}) error { - a.writeCap = []interface{}{ctx, value} - if a.WriteFunc == nil { - return a.Analog.Write(ctx, value, extra) - } - return a.WriteFunc(ctx, value, extra) -} - -// WriteCap returns the last parameters received by Write, and then clears them. -func (a *Analog) WriteCap() []interface{} { - if a == nil { - return nil - } - defer func() { a.writeCap = nil }() - return a.writeCap -} diff --git a/testutils/inject/app_service_client.go b/testutils/inject/app_service_client.go deleted file mode 100644 index 5cffed32d2f..00000000000 --- a/testutils/inject/app_service_client.go +++ /dev/null @@ -1,121 +0,0 @@ -package inject - -import ( - "context" - - apppb "go.viam.com/api/app/v1" - "google.golang.org/grpc" -) - -// AppServiceClient represents a fake instance of an app service client. -type AppServiceClient struct { - apppb.AppServiceClient - ListOrganizationsFunc func(ctx context.Context, in *apppb.ListOrganizationsRequest, - opts ...grpc.CallOption) (*apppb.ListOrganizationsResponse, error) - ListLocationsFunc func(ctx context.Context, in *apppb.ListLocationsRequest, - opts ...grpc.CallOption) (*apppb.ListLocationsResponse, error) - ListRobotsFunc func(ctx context.Context, in *apppb.ListRobotsRequest, - opts ...grpc.CallOption) (*apppb.ListRobotsResponse, error) - CreateKeyFunc func(ctx context.Context, in *apppb.CreateKeyRequest, - opts ...grpc.CallOption) (*apppb.CreateKeyResponse, error) - GetRobotAPIKeysFunc func(ctx context.Context, in *apppb.GetRobotAPIKeysRequest, - opts ...grpc.CallOption) (*apppb.GetRobotAPIKeysResponse, error) - GetRobotPartFunc func(ctx context.Context, in *apppb.GetRobotPartRequest, - opts ...grpc.CallOption) (*apppb.GetRobotPartResponse, error) - GetRobotPartsFunc func(ctx context.Context, in *apppb.GetRobotPartsRequest, - opts ...grpc.CallOption) (*apppb.GetRobotPartsResponse, error) - GetRobotPartLogsFunc func(ctx context.Context, in *apppb.GetRobotPartLogsRequest, - opts ...grpc.CallOption) (*apppb.GetRobotPartLogsResponse, error) - UpdateRobotPartFunc func(ctx context.Context, in *apppb.UpdateRobotPartRequest, - opts ...grpc.CallOption) (*apppb.UpdateRobotPartResponse, error) -} - -// ListOrganizations calls the injected ListOrganizationsFunc or the real version. -func (asc *AppServiceClient) ListOrganizations(ctx context.Context, in *apppb.ListOrganizationsRequest, - opts ...grpc.CallOption, -) (*apppb.ListOrganizationsResponse, error) { - if asc.ListOrganizationsFunc == nil { - return asc.AppServiceClient.ListOrganizations(ctx, in, opts...) - } - return asc.ListOrganizationsFunc(ctx, in, opts...) -} - -// ListLocations calls the injected ListLocationsFunc or the real version. -func (asc *AppServiceClient) ListLocations(ctx context.Context, in *apppb.ListLocationsRequest, - opts ...grpc.CallOption, -) (*apppb.ListLocationsResponse, error) { - if asc.ListLocationsFunc == nil { - return asc.AppServiceClient.ListLocations(ctx, in, opts...) - } - return asc.ListLocationsFunc(ctx, in, opts...) -} - -// ListRobots calls the injected ListRobotsFunc or the real version. -func (asc *AppServiceClient) ListRobots(ctx context.Context, in *apppb.ListRobotsRequest, - opts ...grpc.CallOption, -) (*apppb.ListRobotsResponse, error) { - if asc.ListRobotsFunc == nil { - return asc.AppServiceClient.ListRobots(ctx, in, opts...) - } - return asc.ListRobotsFunc(ctx, in, opts...) -} - -// CreateKey calls the injected CreateKeyFunc or the real version. -func (asc *AppServiceClient) CreateKey(ctx context.Context, in *apppb.CreateKeyRequest, - opts ...grpc.CallOption, -) (*apppb.CreateKeyResponse, error) { - if asc.CreateKeyFunc == nil { - return asc.AppServiceClient.CreateKey(ctx, in, opts...) - } - return asc.CreateKeyFunc(ctx, in, opts...) -} - -// GetRobotAPIKeys wraps GetRobotAPIKeys. -func (asc *AppServiceClient) GetRobotAPIKeys(ctx context.Context, in *apppb.GetRobotAPIKeysRequest, - opts ...grpc.CallOption, -) (*apppb.GetRobotAPIKeysResponse, error) { - if asc.GetRobotAPIKeysFunc == nil { - return asc.AppServiceClient.GetRobotAPIKeys(ctx, in, opts...) - } - return asc.GetRobotAPIKeysFunc(ctx, in, opts...) -} - -// GetRobotPart wraps GetRobotPart. -func (asc *AppServiceClient) GetRobotPart(ctx context.Context, in *apppb.GetRobotPartRequest, - opts ...grpc.CallOption, -) (*apppb.GetRobotPartResponse, error) { - if asc.GetRobotPartFunc == nil { - return asc.AppServiceClient.GetRobotPart(ctx, in, opts...) - } - return asc.GetRobotPartFunc(ctx, in, opts...) -} - -// UpdateRobotPart wraps UpdateRobotPart. -func (asc *AppServiceClient) UpdateRobotPart(ctx context.Context, in *apppb.UpdateRobotPartRequest, - opts ...grpc.CallOption, -) (*apppb.UpdateRobotPartResponse, error) { - if asc.GetRobotPartFunc == nil { - return asc.AppServiceClient.UpdateRobotPart(ctx, in, opts...) - } - return asc.UpdateRobotPartFunc(ctx, in, opts...) -} - -// GetRobotParts calls the injected GetRobotPartsFunc or the real version. -func (asc *AppServiceClient) GetRobotParts(ctx context.Context, in *apppb.GetRobotPartsRequest, - opts ...grpc.CallOption, -) (*apppb.GetRobotPartsResponse, error) { - if asc.GetRobotPartsFunc == nil { - return asc.AppServiceClient.GetRobotParts(ctx, in, opts...) - } - return asc.GetRobotPartsFunc(ctx, in, opts...) -} - -// GetRobotPartLogs calls the injected GetRobotPartLogsFunc or the real version. -func (asc *AppServiceClient) GetRobotPartLogs(ctx context.Context, in *apppb.GetRobotPartLogsRequest, - opts ...grpc.CallOption, -) (*apppb.GetRobotPartLogsResponse, error) { - if asc.GetRobotPartLogsFunc == nil { - return asc.AppServiceClient.GetRobotPartLogs(ctx, in, opts...) - } - return asc.GetRobotPartLogsFunc(ctx, in, opts...) -} diff --git a/testutils/inject/arm.go b/testutils/inject/arm.go deleted file mode 100644 index 8079a08b914..00000000000 --- a/testutils/inject/arm.go +++ /dev/null @@ -1,143 +0,0 @@ -package inject - -import ( - "context" - - pb "go.viam.com/api/component/arm/v1" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// Arm is an injected arm. -type Arm struct { - arm.Arm - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - EndPositionFunc func(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) - MoveToPositionFunc func(ctx context.Context, to spatialmath.Pose, extra map[string]interface{}) error - MoveToJointPositionsFunc func(ctx context.Context, pos *pb.JointPositions, extra map[string]interface{}) error - JointPositionsFunc func(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) - StopFunc func(ctx context.Context, extra map[string]interface{}) error - IsMovingFunc func(context.Context) (bool, error) - CloseFunc func(ctx context.Context) error - ModelFrameFunc func() referenceframe.Model - CurrentInputsFunc func(ctx context.Context) ([]referenceframe.Input, error) - GoToInputsFunc func(ctx context.Context, inputSteps ...[]referenceframe.Input) error - GeometriesFunc func(ctx context.Context) ([]spatialmath.Geometry, error) -} - -// NewArm returns a new injected arm. -func NewArm(name string) *Arm { - return &Arm{name: arm.Named(name)} -} - -// Name returns the name of the resource. -func (a *Arm) Name() resource.Name { - return a.name -} - -// EndPosition calls the injected EndPosition or the real version. -func (a *Arm) EndPosition(ctx context.Context, extra map[string]interface{}) (spatialmath.Pose, error) { - if a.EndPositionFunc == nil { - return a.Arm.EndPosition(ctx, extra) - } - return a.EndPositionFunc(ctx, extra) -} - -// MoveToPosition calls the injected MoveToPosition or the real version. -func (a *Arm) MoveToPosition(ctx context.Context, to spatialmath.Pose, extra map[string]interface{}) error { - if a.MoveToPositionFunc == nil { - return a.Arm.MoveToPosition(ctx, to, extra) - } - return a.MoveToPositionFunc(ctx, to, extra) -} - -// MoveToJointPositions calls the injected MoveToJointPositions or the real version. -func (a *Arm) MoveToJointPositions(ctx context.Context, jp *pb.JointPositions, extra map[string]interface{}) error { - if a.MoveToJointPositionsFunc == nil { - return a.Arm.MoveToJointPositions(ctx, jp, extra) - } - return a.MoveToJointPositionsFunc(ctx, jp, extra) -} - -// JointPositions calls the injected JointPositions or the real version. -func (a *Arm) JointPositions(ctx context.Context, extra map[string]interface{}) (*pb.JointPositions, error) { - if a.JointPositionsFunc == nil { - return a.Arm.JointPositions(ctx, extra) - } - return a.JointPositionsFunc(ctx, extra) -} - -// Stop calls the injected Stop or the real version. -func (a *Arm) Stop(ctx context.Context, extra map[string]interface{}) error { - if a.StopFunc == nil { - return a.Arm.Stop(ctx, extra) - } - return a.StopFunc(ctx, extra) -} - -// IsMoving calls the injected IsMoving or the real version. -func (a *Arm) IsMoving(ctx context.Context) (bool, error) { - if a.IsMovingFunc == nil { - return a.Arm.IsMoving(ctx) - } - return a.IsMovingFunc(ctx) -} - -// Close calls the injected Close or the real version. -func (a *Arm) Close(ctx context.Context) error { - if a.CloseFunc == nil { - if a.Arm == nil { - return nil - } - return a.Arm.Close(ctx) - } - return a.CloseFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real version. -func (a *Arm) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if a.DoFunc == nil { - return a.Arm.DoCommand(ctx, cmd) - } - return a.DoFunc(ctx, cmd) -} - -// ModelFrame calls the injected ModelFrame or the real version. -func (a *Arm) ModelFrame() referenceframe.Model { - if a.ModelFrameFunc == nil { - if a.Arm != nil { - return a.Arm.ModelFrame() - } - model := referenceframe.NewSimpleModel("") - return model - } - return a.ModelFrameFunc() -} - -// CurrentInputs calls the injected CurrentInputs or the real version. -func (a *Arm) CurrentInputs(ctx context.Context) ([]referenceframe.Input, error) { - if a.CurrentInputsFunc == nil { - return a.Arm.CurrentInputs(ctx) - } - return a.CurrentInputsFunc(ctx) -} - -// GoToInputs calls the injected GoToInputs or the real version. -func (a *Arm) GoToInputs(ctx context.Context, inputSteps ...[]referenceframe.Input) error { - if a.GoToInputsFunc == nil { - return a.Arm.GoToInputs(ctx, inputSteps...) - } - return a.GoToInputsFunc(ctx, inputSteps...) -} - -// Geometries returns the gripper's geometries. -func (a *Arm) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - if a.GeometriesFunc == nil { - return a.Arm.Geometries(ctx, extra) - } - return a.GeometriesFunc(ctx) -} diff --git a/testutils/inject/audioinput.go b/testutils/inject/audioinput.go deleted file mode 100644 index 18695158c6e..00000000000 --- a/testutils/inject/audioinput.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build !no_cgo - -package inject - -import ( - "context" - - "github.com/pion/mediadevices/pkg/prop" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/audioinput" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/resource" -) - -// AudioInput is an injected audio input. -type AudioInput struct { - audioinput.AudioInput - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - StreamFunc func( - ctx context.Context, - errHandlers ...gostream.ErrorHandler, - ) (gostream.AudioStream, error) - MediaPropertiesFunc func(ctx context.Context) (prop.Audio, error) - CloseFunc func(ctx context.Context) error -} - -// NewAudioInput returns a new injected audio input. -func NewAudioInput(name string) *AudioInput { - return &AudioInput{name: audioinput.Named(name)} -} - -// Name returns the name of the resource. -func (ai *AudioInput) Name() resource.Name { - return ai.name -} - -// Stream calls the injected Stream or the real version. -func (ai *AudioInput) Stream( - ctx context.Context, - errHandlers ...gostream.ErrorHandler, -) (gostream.AudioStream, error) { - if ai.StreamFunc == nil { - return ai.AudioInput.Stream(ctx, errHandlers...) - } - return ai.StreamFunc(ctx, errHandlers...) -} - -// MediaProperties calls the injected MediaProperties or the real version. -func (ai *AudioInput) MediaProperties(ctx context.Context) (prop.Audio, error) { - if ai.MediaPropertiesFunc != nil { - return ai.MediaPropertiesFunc(ctx) - } - if ai.AudioInput != nil { - return ai.AudioInput.MediaProperties(ctx) - } - return prop.Audio{}, errors.Wrap(ctx.Err(), "media properties unavailable") -} - -// Close calls the injected Close or the real version. -func (ai *AudioInput) Close(ctx context.Context) error { - if ai.CloseFunc == nil { - if ai.AudioInput == nil { - return nil - } - return ai.AudioInput.Close(ctx) - } - return ai.CloseFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real version. -func (ai *AudioInput) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if ai.DoFunc == nil { - return ai.AudioInput.DoCommand(ctx, cmd) - } - return ai.DoFunc(ctx, cmd) -} diff --git a/testutils/inject/base.go b/testutils/inject/base.go deleted file mode 100644 index a4671065b95..00000000000 --- a/testutils/inject/base.go +++ /dev/null @@ -1,120 +0,0 @@ -package inject - -import ( - "context" - - "github.com/golang/geo/r3" - - "go.viam.com/rdk/components/base" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// Base is an injected base. -type Base struct { - base.Base - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - MoveStraightFunc func(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error - SpinFunc func(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error - StopFunc func(ctx context.Context, extra map[string]interface{}) error - IsMovingFunc func(context.Context) (bool, error) - CloseFunc func(ctx context.Context) error - SetPowerFunc func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error - SetVelocityFunc func(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error - PropertiesFunc func(ctx context.Context, extra map[string]interface{}) (base.Properties, error) - GeometriesFunc func(ctx context.Context) ([]spatialmath.Geometry, error) -} - -// NewBase returns a new injected base. -func NewBase(name string) *Base { - return &Base{name: base.Named(name)} -} - -// Name returns the name of the resource. -func (b *Base) Name() resource.Name { - return b.name -} - -// MoveStraight calls the injected MoveStraight or the real version. -func (b *Base) MoveStraight(ctx context.Context, distanceMm int, mmPerSec float64, extra map[string]interface{}) error { - if b.MoveStraightFunc == nil { - return b.Base.MoveStraight(ctx, distanceMm, mmPerSec, extra) - } - return b.MoveStraightFunc(ctx, distanceMm, mmPerSec, extra) -} - -// Spin calls the injected Spin or the real version. -func (b *Base) Spin(ctx context.Context, angleDeg, degsPerSec float64, extra map[string]interface{}) error { - if b.SpinFunc == nil { - return b.Base.Spin(ctx, angleDeg, degsPerSec, extra) - } - return b.SpinFunc(ctx, angleDeg, degsPerSec, extra) -} - -// Stop calls the injected Stop or the real version. -func (b *Base) Stop(ctx context.Context, extra map[string]interface{}) error { - if b.StopFunc == nil { - return b.Base.Stop(ctx, extra) - } - return b.StopFunc(ctx, extra) -} - -// IsMoving calls the injected IsMoving or the real version. -func (b *Base) IsMoving(ctx context.Context) (bool, error) { - if b.IsMovingFunc == nil { - return b.Base.IsMoving(ctx) - } - return b.IsMovingFunc(ctx) -} - -// Close calls the injected Close or the real version. -func (b *Base) Close(ctx context.Context) error { - if b.CloseFunc == nil { - if b.Base == nil { - return nil - } - return b.Base.Close(ctx) - } - return b.CloseFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real version. -func (b *Base) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if b.DoFunc == nil { - return b.Base.DoCommand(ctx, cmd) - } - return b.DoFunc(ctx, cmd) -} - -// SetPower calls the injected SetPower or the real version. -func (b *Base) SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - if b.SetPowerFunc == nil { - return b.Base.SetPower(ctx, linear, angular, extra) - } - return b.SetPowerFunc(ctx, linear, angular, extra) -} - -// SetVelocity calls the injected SetVelocity or the real version. -func (b *Base) SetVelocity(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error { - if b.SetVelocityFunc == nil { - return b.Base.SetVelocity(ctx, linear, angular, extra) - } - return b.SetVelocityFunc(ctx, linear, angular, extra) -} - -// Properties returns the base's properties. -func (b *Base) Properties(ctx context.Context, extra map[string]interface{}) (base.Properties, error) { - if b.PropertiesFunc == nil { - return b.Base.Properties(ctx, extra) - } - return b.PropertiesFunc(ctx, extra) -} - -// Geometries returns the base's geometries. -func (b *Base) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - if b.GeometriesFunc == nil { - return b.Base.Geometries(ctx, extra) - } - return b.GeometriesFunc(ctx) -} diff --git a/testutils/inject/board.go b/testutils/inject/board.go deleted file mode 100644 index ffef9073f73..00000000000 --- a/testutils/inject/board.go +++ /dev/null @@ -1,149 +0,0 @@ -package inject - -import ( - "context" - "time" - - boardpb "go.viam.com/api/component/board/v1" - - "go.viam.com/rdk/components/board" - "go.viam.com/rdk/resource" -) - -// Board is an injected board. -type Board struct { - board.Board - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - AnalogByNameFunc func(name string) (board.Analog, error) - analogByNameCap []interface{} - DigitalInterruptByNameFunc func(name string) (board.DigitalInterrupt, error) - digitalInterruptByNameCap []interface{} - GPIOPinByNameFunc func(name string) (board.GPIOPin, error) - gpioPinByNameCap []interface{} - AnalogNamesFunc func() []string - DigitalInterruptNamesFunc func() []string - CloseFunc func(ctx context.Context) error - SetPowerModeFunc func(ctx context.Context, mode boardpb.PowerMode, duration *time.Duration) error - StreamTicksFunc func(ctx context.Context, - interrupts []board.DigitalInterrupt, ch chan board.Tick, extra map[string]interface{}) error -} - -// NewBoard returns a new injected board. -func NewBoard(name string) *Board { - return &Board{name: board.Named(name)} -} - -// Name returns the name of the resource. -func (b *Board) Name() resource.Name { - return b.name -} - -// AnalogByName calls the injected AnalogByName or the real version. -func (b *Board) AnalogByName(name string) (board.Analog, error) { - b.analogByNameCap = []interface{}{name} - if b.AnalogByNameFunc == nil { - return b.Board.AnalogByName(name) - } - return b.AnalogByNameFunc(name) -} - -// AnalogByNameCap returns the last parameters received by AnalogByName, and then clears them. -func (b *Board) AnalogByNameCap() []interface{} { - if b == nil { - return nil - } - defer func() { b.analogByNameCap = nil }() - return b.analogByNameCap -} - -// DigitalInterruptByName calls the injected DigitalInterruptByName or the real version. -func (b *Board) DigitalInterruptByName(name string) (board.DigitalInterrupt, error) { - b.digitalInterruptByNameCap = []interface{}{name} - if b.DigitalInterruptByNameFunc == nil { - return b.Board.DigitalInterruptByName(name) - } - return b.DigitalInterruptByNameFunc(name) -} - -// DigitalInterruptByNameCap returns the last parameters received by DigitalInterruptByName, and then clears them. -func (b *Board) DigitalInterruptByNameCap() []interface{} { - if b == nil { - return nil - } - defer func() { b.digitalInterruptByNameCap = nil }() - return b.digitalInterruptByNameCap -} - -// GPIOPinByName calls the injected GPIOPinByName or the real version. -func (b *Board) GPIOPinByName(name string) (board.GPIOPin, error) { - b.gpioPinByNameCap = []interface{}{name} - if b.GPIOPinByNameFunc == nil { - return b.Board.GPIOPinByName(name) - } - return b.GPIOPinByNameFunc(name) -} - -// GPIOPinByNameCap returns the last parameters received by GPIOPinByName, and then clears them. -func (b *Board) GPIOPinByNameCap() []interface{} { - if b == nil { - return nil - } - defer func() { b.gpioPinByNameCap = nil }() - return b.gpioPinByNameCap -} - -// AnalogNames calls the injected AnalogNames or the real version. -func (b *Board) AnalogNames() []string { - if b.AnalogNamesFunc == nil { - return b.Board.AnalogNames() - } - return b.AnalogNamesFunc() -} - -// DigitalInterruptNames calls the injected DigitalInterruptNames or the real version. -func (b *Board) DigitalInterruptNames() []string { - if b.DigitalInterruptNamesFunc == nil { - return b.Board.DigitalInterruptNames() - } - return b.DigitalInterruptNamesFunc() -} - -// Close calls the injected Close or the real version. -func (b *Board) Close(ctx context.Context) error { - if b.CloseFunc == nil { - if b.Board == nil { - return nil - } - return b.Board.Close(ctx) - } - return b.CloseFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real version. -func (b *Board) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if b.DoFunc == nil { - return b.Board.DoCommand(ctx, cmd) - } - return b.DoFunc(ctx, cmd) -} - -// SetPowerMode sets the board to the given power mode. If -// provided, the board will exit the given power mode after -// the specified duration. -func (b *Board) SetPowerMode(ctx context.Context, mode boardpb.PowerMode, duration *time.Duration) error { - if b.SetPowerModeFunc == nil { - return b.Board.SetPowerMode(ctx, mode, duration) - } - return b.SetPowerModeFunc(ctx, mode, duration) -} - -// StreamTicks calls the injected StreamTicks or the real version. -func (b *Board) StreamTicks(ctx context.Context, - interrupts []board.DigitalInterrupt, ch chan board.Tick, extra map[string]interface{}, -) error { - if b.StreamTicksFunc == nil { - return b.Board.StreamTicks(ctx, interrupts, ch, extra) - } - return b.StreamTicksFunc(ctx, interrupts, ch, extra) -} diff --git a/testutils/inject/build_service_client.go b/testutils/inject/build_service_client.go deleted file mode 100644 index e23d83f0dc7..00000000000 --- a/testutils/inject/build_service_client.go +++ /dev/null @@ -1,35 +0,0 @@ -package inject - -import ( - "context" - - buildpb "go.viam.com/api/app/build/v1" - "google.golang.org/grpc" -) - -// BuildServiceClient is an injectable buildpb.BuildServiceClient. -type BuildServiceClient struct { - buildpb.BuildServiceClient - ListJobsFunc func(ctx context.Context, in *buildpb.ListJobsRequest, opts ...grpc.CallOption) (*buildpb.ListJobsResponse, error) - StartBuildFunc func(ctx context.Context, in *buildpb.StartBuildRequest, opts ...grpc.CallOption) (*buildpb.StartBuildResponse, error) -} - -// ListJobs calls the injected ListJobsFunc or the real version. -func (bsc *BuildServiceClient) ListJobs(ctx context.Context, in *buildpb.ListJobsRequest, - opts ...grpc.CallOption, -) (*buildpb.ListJobsResponse, error) { - if bsc.ListJobsFunc == nil { - return bsc.ListJobs(ctx, in, opts...) - } - return bsc.ListJobsFunc(ctx, in, opts...) -} - -// StartBuild calls the injected StartBuildFunc or the real version. -func (bsc *BuildServiceClient) StartBuild(ctx context.Context, in *buildpb.StartBuildRequest, - opts ...grpc.CallOption, -) (*buildpb.StartBuildResponse, error) { - if bsc.StartBuildFunc == nil { - return bsc.StartBuild(ctx, in, opts...) - } - return bsc.StartBuildFunc(ctx, in, opts...) -} diff --git a/testutils/inject/camera.go b/testutils/inject/camera.go deleted file mode 100644 index 30459d932eb..00000000000 --- a/testutils/inject/camera.go +++ /dev/null @@ -1,137 +0,0 @@ -package inject - -import ( - "context" - - "github.com/pkg/errors" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/rtppassthrough" - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage/transform" -) - -// Camera is an injected camera. -type Camera struct { - camera.Camera - name resource.Name - RTPPassthroughSource rtppassthrough.Source - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - ImagesFunc func(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) - StreamFunc func( - ctx context.Context, - errHandlers ...gostream.ErrorHandler, - ) (gostream.VideoStream, error) - NextPointCloudFunc func(ctx context.Context) (pointcloud.PointCloud, error) - ProjectorFunc func(ctx context.Context) (transform.Projector, error) - PropertiesFunc func(ctx context.Context) (camera.Properties, error) - CloseFunc func(ctx context.Context) error -} - -// NewCamera returns a new injected camera. -func NewCamera(name string) *Camera { - return &Camera{name: camera.Named(name)} -} - -// Name returns the name of the resource. -func (c *Camera) Name() resource.Name { - return c.name -} - -// NextPointCloud calls the injected NextPointCloud or the real version. -func (c *Camera) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) { - if c.NextPointCloudFunc != nil { - return c.NextPointCloudFunc(ctx) - } - if c.Camera != nil { - return c.Camera.NextPointCloud(ctx) - } - return nil, errors.New("NextPointCloud unimplemented") -} - -// Stream calls the injected Stream or the real version. -func (c *Camera) Stream( - ctx context.Context, - errHandlers ...gostream.ErrorHandler, -) (gostream.VideoStream, error) { - if c.StreamFunc != nil { - return c.StreamFunc(ctx, errHandlers...) - } - if c.Camera != nil { - return c.Camera.Stream(ctx, errHandlers...) - } - return nil, errors.Wrap(ctx.Err(), "no stream function available") -} - -// Projector calls the injected Projector or the real version. -func (c *Camera) Projector(ctx context.Context) (transform.Projector, error) { - if c.ProjectorFunc != nil { - return c.ProjectorFunc(ctx) - } - if c.Camera != nil { - return c.Camera.Projector(ctx) - } - return nil, errors.New("Projector unimplemented") -} - -// Properties calls the injected Properties or the real version. -func (c *Camera) Properties(ctx context.Context) (camera.Properties, error) { - if c.PropertiesFunc == nil { - return c.Camera.Properties(ctx) - } - return c.PropertiesFunc(ctx) -} - -// Images calls the injected Images or the real version. -func (c *Camera) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) { - if c.ImagesFunc != nil { - return c.ImagesFunc(ctx) - } - - if c.Camera != nil { - return c.Camera.Images(ctx) - } - - return nil, resource.ResponseMetadata{}, errors.New("Images unimplemented") -} - -// Close calls the injected Close or the real version. -func (c *Camera) Close(ctx context.Context) error { - if c.CloseFunc != nil { - return c.CloseFunc(ctx) - } - if c.Camera != nil { - return c.Camera.Close(ctx) - } - return nil -} - -// DoCommand calls the injected DoCommand or the real version. -func (c *Camera) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if c.DoFunc != nil { - return c.DoFunc(ctx, cmd) - } - return c.Camera.DoCommand(ctx, cmd) -} - -// SubscribeRTP calls the injected RTPPassthroughSource or returns an error if unimplemented. -func (c *Camera) SubscribeRTP( - ctx context.Context, - bufferSize int, - packetsCB rtppassthrough.PacketCallback, -) (rtppassthrough.Subscription, error) { - if c.RTPPassthroughSource != nil { - return c.RTPPassthroughSource.SubscribeRTP(ctx, bufferSize, packetsCB) - } - return rtppassthrough.NilSubscription, errors.New("SubscribeRTP unimplemented") -} - -// Unsubscribe calls the injected RTPPassthroughSource or returns an error if unimplemented. -func (c *Camera) Unsubscribe(ctx context.Context, id rtppassthrough.SubscriptionID) error { - if c.RTPPassthroughSource != nil { - return c.RTPPassthroughSource.Unsubscribe(ctx, id) - } - return errors.New("Unsubscribe unimplemented") -} diff --git a/testutils/inject/data_service_client.go b/testutils/inject/data_service_client.go deleted file mode 100644 index bba53dee407..00000000000 --- a/testutils/inject/data_service_client.go +++ /dev/null @@ -1,27 +0,0 @@ -package inject - -import ( - "context" - - datapb "go.viam.com/api/app/data/v1" - "google.golang.org/grpc" -) - -// DataServiceClient represents a fake instance of a data service client. -type DataServiceClient struct { - datapb.DataServiceClient - TabularDataByFilterFunc func( - ctx context.Context, - in *datapb.TabularDataByFilterRequest, - opts ...grpc.CallOption, - ) (*datapb.TabularDataByFilterResponse, error) -} - -// TabularDataByFilter calls the injected TabularDataByFilter or the real version. -func (client *DataServiceClient) TabularDataByFilter(ctx context.Context, in *datapb.TabularDataByFilterRequest, opts ...grpc.CallOption, -) (*datapb.TabularDataByFilterResponse, error) { - if client.TabularDataByFilterFunc == nil { - return client.DataServiceClient.TabularDataByFilter(ctx, in, opts...) - } - return client.TabularDataByFilterFunc(ctx, in, opts...) -} diff --git a/testutils/inject/datamanager_service.go b/testutils/inject/datamanager_service.go deleted file mode 100644 index 106622319ee..00000000000 --- a/testutils/inject/datamanager_service.go +++ /dev/null @@ -1,58 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/datamanager" -) - -// DataManagerService represents a fake instance of an data manager -// service. -type DataManagerService struct { - datamanager.Service - name resource.Name - SyncFunc func(ctx context.Context, extra map[string]interface{}) error - DoCommandFunc func(ctx context.Context, - cmd map[string]interface{}) (map[string]interface{}, error) - CloseFunc func(ctx context.Context) error -} - -// NewDataManagerService returns a new injected data manager service. -func NewDataManagerService(name string) *DataManagerService { - return &DataManagerService{name: datamanager.Named(name)} -} - -// Name returns the name of the resource. -func (svc *DataManagerService) Name() resource.Name { - return svc.name -} - -// Sync calls the injected Sync or the real variant. -func (svc *DataManagerService) Sync(ctx context.Context, extra map[string]interface{}) error { - if svc.SyncFunc == nil { - return svc.Service.Sync(ctx, extra) - } - return svc.SyncFunc(ctx, extra) -} - -// DoCommand calls the injected DoCommand or the real variant. -func (svc *DataManagerService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if svc.DoCommandFunc == nil { - return svc.Service.DoCommand(ctx, cmd) - } - return svc.DoCommandFunc(ctx, cmd) -} - -// Close calls the injected Close or the real version. -func (svc *DataManagerService) Close(ctx context.Context) error { - if svc.CloseFunc == nil { - if svc.Service == nil { - return nil - } - return svc.Service.Close(ctx) - } - return svc.CloseFunc(ctx) -} diff --git a/testutils/inject/digital_interrupt.go b/testutils/inject/digital_interrupt.go deleted file mode 100644 index 09ed6753218..00000000000 --- a/testutils/inject/digital_interrupt.go +++ /dev/null @@ -1,60 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/board" -) - -// DigitalInterrupt is an injected digital interrupt. -type DigitalInterrupt struct { - board.DigitalInterrupt - ValueFunc func(ctx context.Context, extra map[string]interface{}) (int64, error) - valueCap []interface{} - TickFunc func(ctx context.Context, high bool, nanoseconds uint64) error - tickCap []interface{} - AddCallbackFunc func(c chan board.Tick) - NameFunc func() string -} - -// Value calls the injected Value or the real version. -func (d *DigitalInterrupt) Value(ctx context.Context, extra map[string]interface{}) (int64, error) { - d.valueCap = []interface{}{ctx} - if d.ValueFunc == nil { - return d.DigitalInterrupt.Value(ctx, extra) - } - return d.ValueFunc(ctx, extra) -} - -// ValueCap returns the last parameters received by Value, and then clears them. -func (d *DigitalInterrupt) ValueCap() []interface{} { - if d == nil { - return nil - } - defer func() { d.valueCap = nil }() - return d.valueCap -} - -// Tick calls the injected Tick. -func (d *DigitalInterrupt) Tick(ctx context.Context, high bool, nanoseconds uint64) error { - d.tickCap = []interface{}{ctx, high, nanoseconds} - - return d.TickFunc(ctx, high, nanoseconds) -} - -// TickCap returns the last parameters received by Tick, and then clears them. -func (d *DigitalInterrupt) TickCap() []interface{} { - if d == nil { - return nil - } - defer func() { d.tickCap = nil }() - return d.tickCap -} - -// Name calls the injected name or the real version. -func (d *DigitalInterrupt) Name() string { - if d.NameFunc == nil { - return d.DigitalInterrupt.Name() - } - return d.NameFunc() -} diff --git a/testutils/inject/encoder.go b/testutils/inject/encoder.go deleted file mode 100644 index 117b4dade6b..00000000000 --- a/testutils/inject/encoder.go +++ /dev/null @@ -1,67 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/encoder" - "go.viam.com/rdk/resource" -) - -// Encoder is an injected encoder. -type Encoder struct { - encoder.Encoder - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - ResetPositionFunc func(ctx context.Context, extra map[string]interface{}) error - PositionFunc func(ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, - ) (float64, encoder.PositionType, error) - PropertiesFunc func(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) -} - -// NewEncoder returns a new injected Encoder. -func NewEncoder(name string) *Encoder { - return &Encoder{name: encoder.Named(name)} -} - -// Name returns the name of the resource. -func (e *Encoder) Name() resource.Name { - return e.name -} - -// ResetPosition calls the injected Zero or the real version. -func (e *Encoder) ResetPosition(ctx context.Context, extra map[string]interface{}) error { - if e.ResetPositionFunc == nil { - return e.Encoder.ResetPosition(ctx, extra) - } - return e.ResetPositionFunc(ctx, extra) -} - -// Position calls the injected Position or the real version. -func (e *Encoder) Position( - ctx context.Context, - positionType encoder.PositionType, - extra map[string]interface{}, -) (float64, encoder.PositionType, error) { - if e.PositionFunc == nil { - return e.Encoder.Position(ctx, positionType, extra) - } - return e.PositionFunc(ctx, positionType, extra) -} - -// Properties calls the injected Properties or the real version. -func (e *Encoder) Properties(ctx context.Context, extra map[string]interface{}) (encoder.Properties, error) { - if e.PropertiesFunc == nil { - return e.Encoder.Properties(ctx, extra) - } - return e.PropertiesFunc(ctx, extra) -} - -// DoCommand calls the injected DoCommand or the real version. -func (e *Encoder) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if e.DoFunc == nil { - return e.Encoder.DoCommand(ctx, cmd) - } - return e.DoFunc(ctx, cmd) -} diff --git a/testutils/inject/framesystem_service.go b/testutils/inject/framesystem_service.go deleted file mode 100644 index 0ebca7c0977..00000000000 --- a/testutils/inject/framesystem_service.go +++ /dev/null @@ -1,120 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot/framesystem" -) - -// FrameSystemService represents a fake instance of a framesystem service. -// Due to the nature of the framesystem service, there should never be more than one on a robot. -// If you use an injected frame system, do not also create the system's default frame system as well. -type FrameSystemService struct { - framesystem.Service - name resource.Name - TransformPoseFunc func( - ctx context.Context, - pose *referenceframe.PoseInFrame, - dst string, - additionalTransforms []*referenceframe.LinkInFrame, - ) (*referenceframe.PoseInFrame, error) - TransformPointCloudFunc func( - ctx context.Context, - srcpc pointcloud.PointCloud, - srcName, dstName string, - ) (pointcloud.PointCloud, error) - CurrentInputsFunc func(ctx context.Context) (map[string][]referenceframe.Input, map[string]referenceframe.InputEnabled, error) - FrameSystemFunc func( - ctx context.Context, - additionalTransforms []*referenceframe.LinkInFrame, - ) (referenceframe.FrameSystem, error) - DoCommandFunc func( - ctx context.Context, - cmd map[string]interface{}, - ) (map[string]interface{}, error) - CloseFunc func(ctx context.Context) error -} - -// NewFrameSystemService returns a new injected framesystem service. -func NewFrameSystemService(name string) *FrameSystemService { - resourceName := resource.NewName( - resource.APINamespaceRDKInternal.WithServiceType("framesystem"), - name, - ) - return &FrameSystemService{name: resourceName} -} - -// Name returns the name of the resource. -func (fs *FrameSystemService) Name() resource.Name { - return fs.name -} - -// TransformPose calls the injected method or the real variant. -func (fs *FrameSystemService) TransformPose( - ctx context.Context, - pose *referenceframe.PoseInFrame, - dst string, - additionalTransforms []*referenceframe.LinkInFrame, -) (*referenceframe.PoseInFrame, error) { - if fs.TransformPoseFunc == nil { - return fs.Service.TransformPose(ctx, pose, dst, additionalTransforms) - } - return fs.TransformPoseFunc(ctx, pose, dst, additionalTransforms) -} - -// TransformPointCloud calls the injected method or the real variant. -func (fs *FrameSystemService) TransformPointCloud( - ctx context.Context, - srcpc pointcloud.PointCloud, - srcName, dstName string, -) (pointcloud.PointCloud, error) { - if fs.TransformPointCloudFunc == nil { - return fs.Service.TransformPointCloud(ctx, srcpc, srcName, dstName) - } - return fs.TransformPointCloudFunc(ctx, srcpc, srcName, dstName) -} - -// CurrentInputs calls the injected method or the real variant. -func (fs *FrameSystemService) CurrentInputs( - ctx context.Context, -) (map[string][]referenceframe.Input, map[string]referenceframe.InputEnabled, error) { - if fs.CurrentInputsFunc == nil { - return fs.Service.CurrentInputs(ctx) - } - return fs.CurrentInputsFunc(ctx) -} - -// FrameSystem calls the injected method of the real variant. -func (fs *FrameSystemService) FrameSystem( - ctx context.Context, - additionalTransforms []*referenceframe.LinkInFrame, -) (referenceframe.FrameSystem, error) { - if fs.FrameSystemFunc == nil { - return fs.Service.FrameSystem(ctx, additionalTransforms) - } - return fs.FrameSystemFunc(ctx, additionalTransforms) -} - -// DoCommand calls the injected DoCommand or the real variant. -func (fs *FrameSystemService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if fs.DoCommandFunc == nil { - return fs.Service.DoCommand(ctx, cmd) - } - return fs.DoCommandFunc(ctx, cmd) -} - -// Close calls the injected Close or the real version. -func (fs *FrameSystemService) Close(ctx context.Context) error { - if fs.CloseFunc == nil { - if fs.Service == nil { - return nil - } - return fs.Service.Close(ctx) - } - return fs.CloseFunc(ctx) -} diff --git a/testutils/inject/gantry.go b/testutils/inject/gantry.go deleted file mode 100644 index cf1fef7b308..00000000000 --- a/testutils/inject/gantry.go +++ /dev/null @@ -1,109 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/gantry" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" -) - -// Gantry is an injected gantry. -type Gantry struct { - gantry.Gantry - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - PositionFunc func(ctx context.Context, extra map[string]interface{}) ([]float64, error) - MoveToPositionFunc func(ctx context.Context, pos, speed []float64, extra map[string]interface{}) error - LengthsFunc func(ctx context.Context, extra map[string]interface{}) ([]float64, error) - StopFunc func(ctx context.Context, extra map[string]interface{}) error - HomeFunc func(ctx context.Context, extra map[string]interface{}) (bool, error) - IsMovingFunc func(context.Context) (bool, error) - CloseFunc func(ctx context.Context) error - ModelFrameFunc func() referenceframe.Model -} - -// NewGantry returns a new injected gantry. -func NewGantry(name string) *Gantry { - return &Gantry{name: gantry.Named(name)} -} - -// Name returns the name of the resource. -func (g *Gantry) Name() resource.Name { - return g.name -} - -// Position calls the injected Position or the real version. -func (g *Gantry) Position(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - if g.PositionFunc == nil { - return g.Gantry.Position(ctx, extra) - } - return g.PositionFunc(ctx, extra) -} - -// MoveToPosition calls the injected MoveToPosition or the real version. -func (g *Gantry) MoveToPosition(ctx context.Context, positions, speeds []float64, extra map[string]interface{}) error { - if g.MoveToPositionFunc == nil { - return g.Gantry.MoveToPosition(ctx, positions, speeds, extra) - } - return g.MoveToPositionFunc(ctx, positions, speeds, extra) -} - -// Lengths calls the injected Lengths or the real version. -func (g *Gantry) Lengths(ctx context.Context, extra map[string]interface{}) ([]float64, error) { - if g.LengthsFunc == nil { - return g.Gantry.Lengths(ctx, extra) - } - return g.LengthsFunc(ctx, extra) -} - -// Stop calls the injected Stop or the real version. -func (g *Gantry) Stop(ctx context.Context, extra map[string]interface{}) error { - if g.StopFunc == nil { - return g.Gantry.Stop(ctx, extra) - } - return g.StopFunc(ctx, extra) -} - -// Home calls the injected Home or the real version. -func (g *Gantry) Home(ctx context.Context, extra map[string]interface{}) (bool, error) { - if g.HomeFunc == nil { - return g.Gantry.Home(ctx, extra) - } - return g.HomeFunc(ctx, extra) -} - -// IsMoving calls the injected IsMoving or the real version. -func (g *Gantry) IsMoving(ctx context.Context) (bool, error) { - if g.IsMovingFunc == nil { - return g.Gantry.IsMoving(ctx) - } - return g.IsMovingFunc(ctx) -} - -// ModelFrame returns a Gantry ModelFrame. -func (g *Gantry) ModelFrame() referenceframe.Model { - if g.ModelFrameFunc == nil { - return g.Gantry.ModelFrame() - } - return g.ModelFrameFunc() -} - -// Close calls the injected Close or the real version. -func (g *Gantry) Close(ctx context.Context) error { - if g.CloseFunc == nil { - if g.Gantry == nil { - return nil - } - return g.Gantry.Close(ctx) - } - return g.CloseFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real version. -func (g *Gantry) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if g.DoFunc == nil { - return g.Gantry.DoCommand(ctx, cmd) - } - return g.DoFunc(ctx, cmd) -} diff --git a/testutils/inject/generic_component.go b/testutils/inject/generic_component.go deleted file mode 100644 index 2d27c83786d..00000000000 --- a/testutils/inject/generic_component.go +++ /dev/null @@ -1,33 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/generic" - "go.viam.com/rdk/resource" -) - -// GenericComponent is an injectable generic component. -type GenericComponent struct { - resource.Resource - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) -} - -// NewGenericComponent returns a new injected generic component. -func NewGenericComponent(name string) *GenericComponent { - return &GenericComponent{name: generic.Named(name)} -} - -// Name returns the name of the resource. -func (g *GenericComponent) Name() resource.Name { - return g.name -} - -// DoCommand calls the injected DoCommand or the real version. -func (g *GenericComponent) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if g.DoFunc == nil { - return g.Resource.DoCommand(ctx, cmd) - } - return g.DoFunc(ctx, cmd) -} diff --git a/testutils/inject/generic_service.go b/testutils/inject/generic_service.go deleted file mode 100644 index ff676b9f7f6..00000000000 --- a/testutils/inject/generic_service.go +++ /dev/null @@ -1,33 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/generic" -) - -// GenericService is an injectable generic service. -type GenericService struct { - resource.Resource - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) -} - -// NewGenericService returns a new injected generic service. -func NewGenericService(name string) *GenericService { - return &GenericService{name: generic.Named(name)} -} - -// Name returns the name of the resource. -func (g *GenericService) Name() resource.Name { - return g.name -} - -// DoCommand calls the injected DoCommand or the real version. -func (g *GenericService) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if g.DoFunc == nil { - return g.Resource.DoCommand(ctx, cmd) - } - return g.DoFunc(ctx, cmd) -} diff --git a/testutils/inject/gpio_pin.go b/testutils/inject/gpio_pin.go deleted file mode 100644 index 1cfd53d908f..00000000000 --- a/testutils/inject/gpio_pin.go +++ /dev/null @@ -1,133 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/board" -) - -// GPIOPin is an injected GPIOPin. -type GPIOPin struct { - board.GPIOPin - - SetFunc func(ctx context.Context, high bool, extra map[string]interface{}) error - setCap []interface{} - GetFunc func(ctx context.Context, extra map[string]interface{}) (bool, error) - getCap []interface{} - PWMFunc func(ctx context.Context, extra map[string]interface{}) (float64, error) - pwmCap []interface{} - SetPWMFunc func(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error - setPWMCap []interface{} - PWMFreqFunc func(ctx context.Context, extra map[string]interface{}) (uint, error) - pwmFreqCap []interface{} - SetPWMFreqFunc func(ctx context.Context, freqHz uint, extra map[string]interface{}) error - setPWMFreqCap []interface{} -} - -// Set calls the injected Set or the real version. -func (gp *GPIOPin) Set(ctx context.Context, high bool, extra map[string]interface{}) error { - gp.setCap = []interface{}{ctx, high} - if gp.SetFunc == nil { - return gp.GPIOPin.Set(ctx, high, extra) - } - return gp.SetFunc(ctx, high, extra) -} - -// Get calls the injected Get or the real version. -func (gp *GPIOPin) Get(ctx context.Context, extra map[string]interface{}) (bool, error) { - gp.getCap = []interface{}{ctx} - if gp.GetFunc == nil { - return gp.GPIOPin.Get(ctx, extra) - } - return gp.GetFunc(ctx, extra) -} - -// PWM calls the injected PWM or the real version. -func (gp *GPIOPin) PWM(ctx context.Context, extra map[string]interface{}) (float64, error) { - gp.pwmCap = []interface{}{ctx} - if gp.PWMFunc == nil { - return gp.GPIOPin.PWM(ctx, extra) - } - return gp.PWMFunc(ctx, extra) -} - -// SetPWM calls the injected SetPWM or the real version. -func (gp *GPIOPin) SetPWM(ctx context.Context, dutyCyclePct float64, extra map[string]interface{}) error { - gp.setPWMCap = []interface{}{ctx, dutyCyclePct} - if gp.SetPWMFunc == nil { - return gp.GPIOPin.SetPWM(ctx, dutyCyclePct, extra) - } - return gp.SetPWMFunc(ctx, dutyCyclePct, extra) -} - -// PWMFreq calls the injected PWMFreq or the real version. -func (gp *GPIOPin) PWMFreq(ctx context.Context, extra map[string]interface{}) (uint, error) { - gp.pwmFreqCap = []interface{}{ctx} - if gp.PWMFreqFunc == nil { - return gp.GPIOPin.PWMFreq(ctx, extra) - } - return gp.PWMFreqFunc(ctx, extra) -} - -// SetPWMFreq calls the injected SetPWMFreq or the real version. -func (gp *GPIOPin) SetPWMFreq(ctx context.Context, freqHz uint, extra map[string]interface{}) error { - gp.setPWMFreqCap = []interface{}{ctx, freqHz} - if gp.SetPWMFreqFunc == nil { - return gp.GPIOPin.SetPWMFreq(ctx, freqHz, extra) - } - return gp.SetPWMFreqFunc(ctx, freqHz, extra) -} - -// SetCap returns the last parameters received by Set, and then clears them. -func (gp *GPIOPin) SetCap() []interface{} { - if gp == nil { - return nil - } - defer func() { gp.setCap = nil }() - return gp.setCap -} - -// GetCap returns the last parameters received by Get, and then clears them. -func (gp *GPIOPin) GetCap() []interface{} { - if gp == nil { - return nil - } - defer func() { gp.getCap = nil }() - return gp.getCap -} - -// PWMCap returns the last parameters received by PWM, and then clears them. -func (gp *GPIOPin) PWMCap() []interface{} { - if gp == nil { - return nil - } - defer func() { gp.pwmCap = nil }() - return gp.pwmCap -} - -// SetPWMCap returns the last parameters received by SetPWM, and then clears them. -func (gp *GPIOPin) SetPWMCap() []interface{} { - if gp == nil { - return nil - } - defer func() { gp.setPWMCap = nil }() - return gp.setPWMCap -} - -// PWMFreqCap returns the last parameters received by PWMFreq, and then clears them. -func (gp *GPIOPin) PWMFreqCap() []interface{} { - if gp == nil { - return nil - } - defer func() { gp.pwmFreqCap = nil }() - return gp.pwmFreqCap -} - -// SetPWMFreqCap returns the last parameters received by SetPWMFreq, and then clears them. -func (gp *GPIOPin) SetPWMFreqCap() []interface{} { - if gp == nil { - return nil - } - defer func() { gp.setPWMFreqCap = nil }() - return gp.setPWMFreqCap -} diff --git a/testutils/inject/gripper.go b/testutils/inject/gripper.go deleted file mode 100644 index 02c5b96bc60..00000000000 --- a/testutils/inject/gripper.go +++ /dev/null @@ -1,91 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/gripper" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// Gripper is an injected gripper. -type Gripper struct { - gripper.Gripper - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - OpenFunc func(ctx context.Context, extra map[string]interface{}) error - GrabFunc func(ctx context.Context, extra map[string]interface{}) (bool, error) - StopFunc func(ctx context.Context, extra map[string]interface{}) error - IsMovingFunc func(context.Context) (bool, error) - CloseFunc func(ctx context.Context) error - GeometriesFunc func(ctx context.Context) ([]spatialmath.Geometry, error) -} - -// NewGripper returns a new injected gripper. -func NewGripper(name string) *Gripper { - return &Gripper{name: gripper.Named(name)} -} - -// Name returns the name of the resource. -func (g *Gripper) Name() resource.Name { - return g.name -} - -// Open calls the injected Open or the real version. -func (g *Gripper) Open(ctx context.Context, extra map[string]interface{}) error { - if g.OpenFunc == nil { - return g.Gripper.Open(ctx, extra) - } - return g.OpenFunc(ctx, extra) -} - -// Grab calls the injected Grab or the real version. -func (g *Gripper) Grab(ctx context.Context, extra map[string]interface{}) (bool, error) { - if g.GrabFunc == nil { - return g.Gripper.Grab(ctx, extra) - } - return g.GrabFunc(ctx, extra) -} - -// Stop calls the injected Stop or the real version. -func (g *Gripper) Stop(ctx context.Context, extra map[string]interface{}) error { - if g.StopFunc == nil { - return g.Gripper.Stop(ctx, extra) - } - return g.StopFunc(ctx, extra) -} - -// IsMoving calls the injected IsMoving or the real version. -func (g *Gripper) IsMoving(ctx context.Context) (bool, error) { - if g.IsMovingFunc == nil { - return g.Gripper.IsMoving(ctx) - } - return g.IsMovingFunc(ctx) -} - -// Close calls the injected Close or the real version. -func (g *Gripper) Close(ctx context.Context) error { - if g.CloseFunc == nil { - if g.Gripper == nil { - return nil - } - return g.Gripper.Close(ctx) - } - return g.CloseFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real version. -func (g *Gripper) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if g.DoFunc == nil { - return g.Gripper.DoCommand(ctx, cmd) - } - return g.DoFunc(ctx, cmd) -} - -// Geometries returns the gripper's geometries. -func (g *Gripper) Geometries(ctx context.Context, extra map[string]interface{}) ([]spatialmath.Geometry, error) { - if g.GeometriesFunc == nil { - return g.Gripper.Geometries(ctx, extra) - } - return g.GeometriesFunc(ctx) -} diff --git a/testutils/inject/i2c.go b/testutils/inject/i2c.go deleted file mode 100644 index 5b14b1d9975..00000000000 --- a/testutils/inject/i2c.go +++ /dev/null @@ -1,89 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/board/genericlinux/buses" -) - -// I2C is an injected I2C. -type I2C struct { - buses.I2C - OpenHandleFunc func(addr byte) (buses.I2CHandle, error) -} - -// OpenHandle calls the injected OpenHandle or the real version. -func (s *I2C) OpenHandle(addr byte) (buses.I2CHandle, error) { - if s.OpenHandleFunc == nil { - return s.I2C.OpenHandle(addr) - } - return s.OpenHandleFunc(addr) -} - -// I2CHandle is an injected I2CHandle. -type I2CHandle struct { - buses.I2CHandle - WriteFunc func(ctx context.Context, tx []byte) error - ReadFunc func(ctx context.Context, count int) ([]byte, error) - ReadByteDataFunc func(ctx context.Context, register byte) (byte, error) - WriteByteDataFunc func(ctx context.Context, register, data byte) error - ReadBlockDataFunc func(ctx context.Context, register byte, numBytes uint8) ([]byte, error) - WriteBlockDataFunc func(ctx context.Context, register byte, data []byte) error - CloseFunc func() error -} - -// ReadByteData calls the injected ReadByteDataFunc or the real version. -func (handle *I2CHandle) ReadByteData(ctx context.Context, register byte) (byte, error) { - if handle.ReadByteDataFunc == nil { - return handle.I2CHandle.ReadByteData(ctx, register) - } - return handle.ReadByteDataFunc(ctx, register) -} - -// WriteByteData calls the injected WriteByteDataFunc or the real version. -func (handle *I2CHandle) WriteByteData(ctx context.Context, register, data byte) error { - if handle.WriteByteDataFunc == nil { - return handle.I2CHandle.WriteByteData(ctx, register, data) - } - return handle.WriteByteDataFunc(ctx, register, data) -} - -// ReadBlockData calls the injected ReadBlockDataFunc or the real version. -func (handle *I2CHandle) ReadBlockData(ctx context.Context, register byte, numBytes uint8) ([]byte, error) { - if handle.ReadBlockDataFunc == nil { - return handle.I2CHandle.ReadBlockData(ctx, register, numBytes) - } - return handle.ReadBlockDataFunc(ctx, register, numBytes) -} - -// WriteBlockData calls the injected WriteBlockDataFunc or the real version. -func (handle *I2CHandle) WriteBlockData(ctx context.Context, register byte, data []byte) error { - if handle.WriteBlockDataFunc == nil { - return handle.I2CHandle.WriteBlockData(ctx, register, data) - } - return handle.WriteBlockDataFunc(ctx, register, data) -} - -// Read calls the injected ReadFunc or the real version. -func (handle *I2CHandle) Read(ctx context.Context, count int) ([]byte, error) { - if handle.ReadFunc == nil { - return handle.I2CHandle.Read(ctx, count) - } - return handle.ReadFunc(ctx, count) -} - -// Write calls the injected WriteFunc or the real version. -func (handle *I2CHandle) Write(ctx context.Context, tx []byte) error { - if handle.WriteFunc == nil { - return handle.I2CHandle.Write(ctx, tx) - } - return handle.WriteFunc(ctx, tx) -} - -// Close calls the injected CloseFunc or the real version. -func (handle *I2CHandle) Close() error { - if handle.CloseFunc == nil { - return handle.I2CHandle.Close() - } - return handle.CloseFunc() -} diff --git a/testutils/inject/input.go b/testutils/inject/input.go deleted file mode 100644 index 100e8d03e0b..00000000000 --- a/testutils/inject/input.go +++ /dev/null @@ -1,88 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/input" - "go.viam.com/rdk/resource" -) - -// InputController is an injected InputController. -type InputController struct { - input.Controller - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - ControlsFunc func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) - EventsFunc func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) - RegisterControlCallbackFunc func( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, - ) error -} - -// NewInputController returns a new injected input controller. -func NewInputController(name string) *InputController { - return &InputController{name: input.Named(name)} -} - -// Name returns the name of the resource. -func (s *InputController) Name() resource.Name { - return s.name -} - -// Controls calls the injected function or the real version. -func (s *InputController) Controls(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - if s.ControlsFunc == nil { - return s.Controller.Controls(ctx, extra) - } - return s.ControlsFunc(ctx, extra) -} - -// Events calls the injected function or the real version. -func (s *InputController) Events(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - if s.EventsFunc == nil { - return s.Controller.Events(ctx, extra) - } - return s.EventsFunc(ctx, extra) -} - -// RegisterControlCallback calls the injected function or the real version. -func (s *InputController) RegisterControlCallback( - ctx context.Context, - control input.Control, - triggers []input.EventType, - ctrlFunc input.ControlFunction, - extra map[string]interface{}, -) error { - if s.RegisterControlCallbackFunc == nil { - return s.RegisterControlCallback(ctx, control, triggers, ctrlFunc, extra) - } - return s.RegisterControlCallbackFunc(ctx, control, triggers, ctrlFunc, extra) -} - -// DoCommand calls the injected DoCommand or the real version. -func (s *InputController) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if s.DoFunc == nil { - return s.Controller.DoCommand(ctx, cmd) - } - return s.DoFunc(ctx, cmd) -} - -// TriggerableInputController is an injected injectable InputController. -type TriggerableInputController struct { - InputController - input.Triggerable - - TriggerEventFunc func(ctx context.Context, event input.Event, extra map[string]interface{}) error -} - -// TriggerEvent calls the injected function or the real version. -func (s *TriggerableInputController) TriggerEvent(ctx context.Context, event input.Event, extra map[string]interface{}) error { - if s.TriggerEventFunc == nil { - return s.TriggerEvent(ctx, event, extra) - } - return s.TriggerEventFunc(ctx, event, extra) -} diff --git a/testutils/inject/io.go b/testutils/inject/io.go deleted file mode 100644 index 41e57a7df6b..00000000000 --- a/testutils/inject/io.go +++ /dev/null @@ -1,37 +0,0 @@ -package inject - -import ( - "io" -) - -// ReadWriteCloser is an injected read write closer. -type ReadWriteCloser struct { - io.ReadWriteCloser - ReadFunc func(p []byte) (n int, err error) - WriteFunc func(p []byte) (n int, err error) - CloseFunc func() error -} - -// Read calls the injected Read or the real version. -func (rwc *ReadWriteCloser) Read(p []byte) (n int, err error) { - if rwc.ReadFunc == nil { - return rwc.ReadWriteCloser.Read(p) - } - return rwc.ReadFunc(p) -} - -// Write calls the injected Write or the real version. -func (rwc *ReadWriteCloser) Write(p []byte) (n int, err error) { - if rwc.WriteFunc == nil { - return rwc.ReadWriteCloser.Write(p) - } - return rwc.WriteFunc(p) -} - -// Close calls the injected Close or the real version. -func (rwc *ReadWriteCloser) Close() error { - if rwc.CloseFunc == nil { - return rwc.ReadWriteCloser.Close() - } - return rwc.CloseFunc() -} diff --git a/testutils/inject/mlmodel_service.go b/testutils/inject/mlmodel_service.go deleted file mode 100644 index d626f742649..00000000000 --- a/testutils/inject/mlmodel_service.go +++ /dev/null @@ -1,58 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/ml" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/mlmodel" -) - -// MLModelService represents a fake instance of an MLModel service. -type MLModelService struct { - mlmodel.Service - name resource.Name - InferFunc func(ctx context.Context, tensors ml.Tensors) (ml.Tensors, error) - MetadataFunc func(ctx context.Context) (mlmodel.MLMetadata, error) - CloseFunc func(ctx context.Context) error -} - -// NewMLModelService returns a new injected mlmodel service. -func NewMLModelService(name string) *MLModelService { - return &MLModelService{name: mlmodel.Named(name)} -} - -// Name returns the name of the resource. -func (s *MLModelService) Name() resource.Name { - return s.name -} - -// Infer calls the injected Infer or the real variant. -func (s *MLModelService) Infer( - ctx context.Context, - tensors ml.Tensors, -) (ml.Tensors, error) { - if s.InferFunc == nil { - return s.Service.Infer(ctx, tensors) - } - return s.InferFunc(ctx, tensors) -} - -// Metadata calls the injected Metadata or the real variant. -func (s *MLModelService) Metadata(ctx context.Context) (mlmodel.MLMetadata, error) { - if s.MetadataFunc == nil { - return s.Service.Metadata(ctx) - } - return s.MetadataFunc(ctx) -} - -// Close calls the injected Close or the real version. -func (s *MLModelService) Close(ctx context.Context) error { - if s.CloseFunc == nil { - if s.Service == nil { - return nil - } - return s.Service.Close(ctx) - } - return s.CloseFunc(ctx) -} diff --git a/testutils/inject/motion_service.go b/testutils/inject/motion_service.go deleted file mode 100644 index b1bfb957502..00000000000 --- a/testutils/inject/motion_service.go +++ /dev/null @@ -1,168 +0,0 @@ -package inject - -import ( - "context" - - servicepb "go.viam.com/api/service/motion/v1" - - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/motion" -) - -// MotionService represents a fake instance of an motion -// service. -type MotionService struct { - motion.Service - name resource.Name - MoveFunc func( - ctx context.Context, - componentName resource.Name, - grabPose *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *servicepb.Constraints, - extra map[string]interface{}, - ) (bool, error) - MoveOnMapFunc func( - ctx context.Context, - req motion.MoveOnMapReq, - ) (motion.ExecutionID, error) - MoveOnGlobeFunc func( - ctx context.Context, - req motion.MoveOnGlobeReq, - ) (motion.ExecutionID, error) - GetPoseFunc func( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransforms []*referenceframe.LinkInFrame, - extra map[string]interface{}, - ) (*referenceframe.PoseInFrame, error) - StopPlanFunc func( - ctx context.Context, - req motion.StopPlanReq, - ) error - ListPlanStatusesFunc func( - ctx context.Context, - req motion.ListPlanStatusesReq, - ) ([]motion.PlanStatusWithID, error) - PlanHistoryFunc func( - ctx context.Context, - req motion.PlanHistoryReq, - ) ([]motion.PlanWithStatus, error) - DoCommandFunc func(ctx context.Context, - cmd map[string]interface{}) (map[string]interface{}, error) - CloseFunc func(ctx context.Context) error -} - -// NewMotionService returns a new injected motion service. -func NewMotionService(name string) *MotionService { - return &MotionService{name: motion.Named(name)} -} - -// Name returns the name of the resource. -func (mgs *MotionService) Name() resource.Name { - return mgs.name -} - -// Move calls the injected Move or the real variant. -func (mgs *MotionService) Move( - ctx context.Context, - componentName resource.Name, - destination *referenceframe.PoseInFrame, - worldState *referenceframe.WorldState, - constraints *servicepb.Constraints, - extra map[string]interface{}, -) (bool, error) { - if mgs.MoveFunc == nil { - return mgs.Service.Move(ctx, componentName, destination, worldState, constraints, extra) - } - return mgs.MoveFunc(ctx, componentName, destination, worldState, constraints, extra) -} - -// MoveOnMap calls the injected MoveOnMap or the real variant. -func (mgs *MotionService) MoveOnMap( - ctx context.Context, - req motion.MoveOnMapReq, -) (motion.ExecutionID, error) { - if mgs.MoveOnMapFunc == nil { - return mgs.Service.MoveOnMap(ctx, req) - } - return mgs.MoveOnMapFunc(ctx, req) -} - -// MoveOnGlobe calls the injected MoveOnGlobe or the real variant. -func (mgs *MotionService) MoveOnGlobe(ctx context.Context, req motion.MoveOnGlobeReq) (motion.ExecutionID, error) { - if mgs.MoveOnGlobeFunc == nil { - return mgs.Service.MoveOnGlobe(ctx, req) - } - return mgs.MoveOnGlobeFunc(ctx, req) -} - -// GetPose calls the injected GetPose or the real variant. -func (mgs *MotionService) GetPose( - ctx context.Context, - componentName resource.Name, - destinationFrame string, - supplementalTransforms []*referenceframe.LinkInFrame, - extra map[string]interface{}, -) (*referenceframe.PoseInFrame, error) { - if mgs.GetPoseFunc == nil { - return mgs.Service.GetPose(ctx, componentName, destinationFrame, supplementalTransforms, extra) - } - return mgs.GetPoseFunc(ctx, componentName, destinationFrame, supplementalTransforms, extra) -} - -// StopPlan calls the injected StopPlan or the real variant. -func (mgs *MotionService) StopPlan( - ctx context.Context, - req motion.StopPlanReq, -) error { - if mgs.StopPlanFunc == nil { - return mgs.Service.StopPlan(ctx, req) - } - return mgs.StopPlanFunc(ctx, req) -} - -// ListPlanStatuses calls the injected ListPlanStatuses or the real variant. -func (mgs *MotionService) ListPlanStatuses( - ctx context.Context, - req motion.ListPlanStatusesReq, -) ([]motion.PlanStatusWithID, error) { - if mgs.ListPlanStatusesFunc == nil { - return mgs.Service.ListPlanStatuses(ctx, req) - } - return mgs.ListPlanStatusesFunc(ctx, req) -} - -// PlanHistory calls the injected PlanHistory or the real variant. -func (mgs *MotionService) PlanHistory( - ctx context.Context, - req motion.PlanHistoryReq, -) ([]motion.PlanWithStatus, error) { - if mgs.PlanHistoryFunc == nil { - return mgs.Service.PlanHistory(ctx, req) - } - return mgs.PlanHistoryFunc(ctx, req) -} - -// DoCommand calls the injected DoCommand or the real variant. -func (mgs *MotionService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if mgs.DoCommandFunc == nil { - return mgs.Service.DoCommand(ctx, cmd) - } - return mgs.DoCommandFunc(ctx, cmd) -} - -// Close calls the injected Close or the real version. -func (mgs *MotionService) Close(ctx context.Context) error { - if mgs.CloseFunc == nil { - if mgs.Service == nil { - return nil - } - return mgs.Service.Close(ctx) - } - return mgs.CloseFunc(ctx) -} diff --git a/testutils/inject/motor.go b/testutils/inject/motor.go deleted file mode 100644 index 8fd90e73428..00000000000 --- a/testutils/inject/motor.go +++ /dev/null @@ -1,114 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/motor" - "go.viam.com/rdk/resource" -) - -// Motor is an injected motor. -type Motor struct { - motor.Motor - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - SetPowerFunc func(ctx context.Context, powerPct float64, extra map[string]interface{}) error - GoForFunc func(ctx context.Context, rpm, rotations float64, extra map[string]interface{}) error - GoToFunc func(ctx context.Context, rpm, position float64, extra map[string]interface{}) error - ResetZeroPositionFunc func(ctx context.Context, offset float64, extra map[string]interface{}) error - PositionFunc func(ctx context.Context, extra map[string]interface{}) (float64, error) - PropertiesFunc func(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) - StopFunc func(ctx context.Context, extra map[string]interface{}) error - IsPoweredFunc func(ctx context.Context, extra map[string]interface{}) (bool, float64, error) - IsMovingFunc func(context.Context) (bool, error) -} - -// NewMotor returns a new injected motor. -func NewMotor(name string) *Motor { - return &Motor{name: motor.Named(name)} -} - -// Name returns the name of the resource. -func (m *Motor) Name() resource.Name { - return m.name -} - -// SetPower calls the injected Power or the real version. -func (m *Motor) SetPower(ctx context.Context, powerPct float64, extra map[string]interface{}) error { - if m.SetPowerFunc == nil { - return m.Motor.SetPower(ctx, powerPct, extra) - } - return m.SetPowerFunc(ctx, powerPct, extra) -} - -// GoFor calls the injected GoFor or the real version. -func (m *Motor) GoFor(ctx context.Context, rpm, revolutions float64, extra map[string]interface{}) error { - if m.GoForFunc == nil { - return m.Motor.GoFor(ctx, rpm, revolutions, extra) - } - return m.GoForFunc(ctx, rpm, revolutions, extra) -} - -// GoTo calls the injected GoTo or the real version. -func (m *Motor) GoTo(ctx context.Context, rpm, positionRevolutions float64, extra map[string]interface{}) error { - if m.GoToFunc == nil { - return m.Motor.GoTo(ctx, rpm, positionRevolutions, extra) - } - return m.GoToFunc(ctx, rpm, positionRevolutions, extra) -} - -// ResetZeroPosition calls the injected Zero or the real version. -func (m *Motor) ResetZeroPosition(ctx context.Context, offset float64, extra map[string]interface{}) error { - if m.ResetZeroPositionFunc == nil { - return m.Motor.ResetZeroPosition(ctx, offset, extra) - } - return m.ResetZeroPositionFunc(ctx, offset, extra) -} - -// Position calls the injected Position or the real version. -func (m *Motor) Position(ctx context.Context, extra map[string]interface{}) (float64, error) { - if m.PositionFunc == nil { - return m.Motor.Position(ctx, extra) - } - return m.PositionFunc(ctx, extra) -} - -// Properties calls the injected Properties or the real version. -func (m *Motor) Properties(ctx context.Context, extra map[string]interface{}) (motor.Properties, error) { - if m.PropertiesFunc == nil { - return m.Motor.Properties(ctx, extra) - } - return m.PropertiesFunc(ctx, extra) -} - -// Stop calls the injected Off or the real version. -func (m *Motor) Stop(ctx context.Context, extra map[string]interface{}) error { - if m.StopFunc == nil { - return m.Motor.Stop(ctx, extra) - } - return m.StopFunc(ctx, extra) -} - -// IsPowered calls the injected IsPowered or the real version. -func (m *Motor) IsPowered(ctx context.Context, extra map[string]interface{}) (bool, float64, error) { - if m.IsPoweredFunc == nil { - return m.Motor.IsPowered(ctx, extra) - } - return m.IsPoweredFunc(ctx, extra) -} - -// DoCommand calls the injected DoCommand or the real version. -func (m *Motor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if m.DoFunc == nil { - return m.Motor.DoCommand(ctx, cmd) - } - return m.DoFunc(ctx, cmd) -} - -// IsMoving calls the injected IsMoving or the real version. -func (m *Motor) IsMoving(ctx context.Context) (bool, error) { - if m.IsMovingFunc == nil { - return m.Motor.IsMoving(ctx) - } - return m.IsMovingFunc(ctx) -} diff --git a/testutils/inject/movementsensor.go b/testutils/inject/movementsensor.go deleted file mode 100644 index 51ce68c53f5..00000000000 --- a/testutils/inject/movementsensor.go +++ /dev/null @@ -1,153 +0,0 @@ -package inject - -import ( - "context" - "sync" - - "github.com/golang/geo/r3" - geo "github.com/kellydunn/golang-geo" - - "go.viam.com/rdk/components/movementsensor" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/spatialmath" -) - -// MovementSensor is an injected MovementSensor. -type MovementSensor struct { - movementsensor.MovementSensor - name resource.Name - Mu sync.RWMutex - PositionFuncExtraCap map[string]interface{} - PositionFunc func(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) - LinearVelocityFuncExtraCap map[string]interface{} - LinearVelocityFunc func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) - AngularVelocityFuncExtraCap map[string]interface{} - AngularVelocityFunc func(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) - CompassHeadingFuncExtraCap map[string]interface{} - CompassHeadingFunc func(ctx context.Context, extra map[string]interface{}) (float64, error) - LinearAccelerationExtraCap map[string]interface{} - LinearAccelerationFunc func(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) - OrientationFuncExtraCap map[string]interface{} - OrientationFunc func(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) - PropertiesFuncExtraCap map[string]interface{} - PropertiesFunc func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) - AccuracyFuncExtraCap map[string]interface{} - AccuracyFunc func(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error) - ReadingsFuncExtraCap map[string]interface{} - ReadingsFunc func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - CloseFunc func() error -} - -// NewMovementSensor returns a new injected movement sensor. -func NewMovementSensor(name string) *MovementSensor { - return &MovementSensor{name: movementsensor.Named(name)} -} - -// Name returns the name of the resource. -func (i *MovementSensor) Name() resource.Name { - return i.name -} - -// Close calls the injected Close or the real version. -func (i *MovementSensor) Close(ctx context.Context) error { - if i.CloseFunc == nil { - if i.MovementSensor == nil { - return nil - } - return i.MovementSensor.Close(ctx) - } - return i.CloseFunc() -} - -// DoCommand calls the injected DoCommand or the real version. -func (i *MovementSensor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if i.DoFunc == nil { - return i.MovementSensor.DoCommand(ctx, cmd) - } - return i.DoFunc(ctx, cmd) -} - -// Position func or passthrough. -func (i *MovementSensor) Position(ctx context.Context, extra map[string]interface{}) (*geo.Point, float64, error) { - i.Mu.Lock() - defer i.Mu.Unlock() - if i.PositionFunc == nil { - return i.MovementSensor.Position(ctx, extra) - } - i.PositionFuncExtraCap = extra - return i.PositionFunc(ctx, extra) -} - -// LinearVelocity func or passthrough. -func (i *MovementSensor) LinearVelocity(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - if i.LinearVelocityFunc == nil { - return i.MovementSensor.LinearVelocity(ctx, extra) - } - i.LinearVelocityFuncExtraCap = extra - return i.LinearVelocityFunc(ctx, extra) -} - -// AngularVelocity func or passthrough. -func (i *MovementSensor) AngularVelocity(ctx context.Context, extra map[string]interface{}) (spatialmath.AngularVelocity, error) { - if i.AngularVelocityFunc == nil { - return i.MovementSensor.AngularVelocity(ctx, extra) - } - i.AngularVelocityFuncExtraCap = extra - return i.AngularVelocityFunc(ctx, extra) -} - -// LinearAcceleration func or passthrough. -func (i *MovementSensor) LinearAcceleration(ctx context.Context, extra map[string]interface{}) (r3.Vector, error) { - if i.LinearAccelerationFunc == nil { - return i.MovementSensor.LinearAcceleration(ctx, extra) - } - i.LinearAccelerationExtraCap = extra - return i.LinearAccelerationFunc(ctx, extra) -} - -// Orientation func or passthrough. -func (i *MovementSensor) Orientation(ctx context.Context, extra map[string]interface{}) (spatialmath.Orientation, error) { - if i.OrientationFunc == nil { - return i.MovementSensor.Orientation(ctx, extra) - } - i.OrientationFuncExtraCap = extra - return i.OrientationFunc(ctx, extra) -} - -// CompassHeading func or passthrough. -func (i *MovementSensor) CompassHeading(ctx context.Context, extra map[string]interface{}) (float64, error) { - if i.CompassHeadingFunc == nil { - return i.MovementSensor.CompassHeading(ctx, extra) - } - i.CompassHeadingFuncExtraCap = extra - return i.CompassHeadingFunc(ctx, extra) -} - -// Properties func or passthrough. -func (i *MovementSensor) Properties(ctx context.Context, extra map[string]interface{}) (*movementsensor.Properties, error) { - if i.PropertiesFunc == nil { - return i.MovementSensor.Properties(ctx, extra) - } - i.PropertiesFuncExtraCap = extra - return i.PropertiesFunc(ctx, extra) -} - -// Accuracy func or passthrough. -func (i *MovementSensor) Accuracy(ctx context.Context, extra map[string]interface{}) (*movementsensor.Accuracy, error, -) { - if i.AccuracyFunc == nil { - return i.MovementSensor.Accuracy(ctx, extra) - } - i.AccuracyFuncExtraCap = extra - return i.AccuracyFunc(ctx, extra) -} - -// Readings func or passthrough. -func (i *MovementSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - if i.ReadingsFunc == nil { - return i.MovementSensor.Readings(ctx, extra) - } - i.ReadingsFuncExtraCap = extra - return i.ReadingsFunc(ctx, extra) -} diff --git a/testutils/inject/navigation_service.go b/testutils/inject/navigation_service.go deleted file mode 100644 index 0823b0f7bdc..00000000000 --- a/testutils/inject/navigation_service.go +++ /dev/null @@ -1,137 +0,0 @@ -package inject - -import ( - "context" - - geo "github.com/kellydunn/golang-geo" - "go.mongodb.org/mongo-driver/bson/primitive" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/navigation" - "go.viam.com/rdk/spatialmath" -) - -// NavigationService represents a fake instance of a navigation service. -type NavigationService struct { - navigation.Service - name resource.Name - ModeFunc func(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) - SetModeFunc func(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error - - LocationFunc func(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) - - WaypointsFunc func(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) - AddWaypointFunc func(ctx context.Context, point *geo.Point, extra map[string]interface{}) error - RemoveWaypointFunc func(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error - - ObstaclesFunc func(ctx context.Context, extra map[string]interface{}) ([]*spatialmath.GeoObstacle, error) - PathsFunc func(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) - - PropertiesFunc func(ctx context.Context) (navigation.Properties, error) - - DoCommandFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - CloseFunc func(ctx context.Context) error -} - -// NewNavigationService returns a new injected navigation service. -func NewNavigationService(name string) *NavigationService { - return &NavigationService{name: navigation.Named(name)} -} - -// Name returns the name of the resource. -func (ns *NavigationService) Name() resource.Name { - return ns.name -} - -// Mode calls the injected ModeFunc or the real version. -func (ns *NavigationService) Mode(ctx context.Context, extra map[string]interface{}) (navigation.Mode, error) { - if ns.ModeFunc == nil { - return ns.Service.Mode(ctx, extra) - } - return ns.ModeFunc(ctx, extra) -} - -// SetMode calls the injected SetModeFunc or the real version. -func (ns *NavigationService) SetMode(ctx context.Context, mode navigation.Mode, extra map[string]interface{}) error { - if ns.SetModeFunc == nil { - return ns.Service.SetMode(ctx, mode, extra) - } - return ns.SetModeFunc(ctx, mode, extra) -} - -// Location calls the injected LocationFunc or the real version. -func (ns *NavigationService) Location(ctx context.Context, extra map[string]interface{}) (*spatialmath.GeoPose, error) { - if ns.LocationFunc == nil { - return ns.Service.Location(ctx, extra) - } - return ns.LocationFunc(ctx, extra) -} - -// Waypoints calls the injected WaypointsFunc or the real version. -func (ns *NavigationService) Waypoints(ctx context.Context, extra map[string]interface{}) ([]navigation.Waypoint, error) { - if ns.WaypointsFunc == nil { - return ns.Service.Waypoints(ctx, extra) - } - return ns.WaypointsFunc(ctx, extra) -} - -// AddWaypoint calls the injected AddWaypointFunc or the real version. -func (ns *NavigationService) AddWaypoint(ctx context.Context, point *geo.Point, extra map[string]interface{}) error { - if ns.AddWaypointFunc == nil { - return ns.Service.AddWaypoint(ctx, point, extra) - } - return ns.AddWaypointFunc(ctx, point, extra) -} - -// RemoveWaypoint calls the injected RemoveWaypointFunc or the real version. -func (ns *NavigationService) RemoveWaypoint(ctx context.Context, id primitive.ObjectID, extra map[string]interface{}) error { - if ns.RemoveWaypointFunc == nil { - return ns.Service.RemoveWaypoint(ctx, id, extra) - } - return ns.RemoveWaypointFunc(ctx, id, extra) -} - -// Obstacles calls the injected Obstacles or the real version. -func (ns *NavigationService) Obstacles(ctx context.Context, extra map[string]interface{}) ([]*spatialmath.GeoObstacle, error) { - if ns.ObstaclesFunc == nil { - return ns.Service.Obstacles(ctx, extra) - } - return ns.ObstaclesFunc(ctx, extra) -} - -// Paths calls the injected Paths or the real version. -func (ns *NavigationService) Paths(ctx context.Context, extra map[string]interface{}) ([]*navigation.Path, error) { - if ns.PathsFunc == nil { - return ns.Service.Paths(ctx, extra) - } - return ns.PathsFunc(ctx, extra) -} - -// Properties calls the injected Properties or the real variant. -func (ns *NavigationService) Properties(ctx context.Context) (navigation.Properties, error) { - if ns.PropertiesFunc == nil { - return ns.Service.Properties(ctx) - } - return ns.PropertiesFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real variant. -func (ns *NavigationService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if ns.DoCommandFunc == nil { - return ns.Service.DoCommand(ctx, cmd) - } - return ns.DoCommandFunc(ctx, cmd) -} - -// Close calls the injected Close or the real version. -func (ns *NavigationService) Close(ctx context.Context) error { - if ns.CloseFunc == nil { - if ns.Service == nil { - return nil - } - return ns.Service.Close(ctx) - } - return ns.CloseFunc(ctx) -} diff --git a/testutils/inject/pose_tracker.go b/testutils/inject/pose_tracker.go deleted file mode 100644 index 7134a55a536..00000000000 --- a/testutils/inject/pose_tracker.go +++ /dev/null @@ -1,44 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/posetracker" - "go.viam.com/rdk/resource" -) - -// PoseTracker is an injected pose tracker. -type PoseTracker struct { - posetracker.PoseTracker - name resource.Name - PosesFunc func(ctx context.Context, bodyNames []string, extra map[string]interface{}) (posetracker.BodyToPoseInFrame, error) - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) -} - -// NewPoseTracker returns a new injected pose tracker. -func NewPoseTracker(name string) *PoseTracker { - return &PoseTracker{name: posetracker.Named(name)} -} - -// Name returns the name of the resource. -func (pT *PoseTracker) Name() resource.Name { - return pT.name -} - -// Poses calls the injected Poses or the real version. -func (pT *PoseTracker) Poses( - ctx context.Context, bodyNames []string, extra map[string]interface{}, -) (posetracker.BodyToPoseInFrame, error) { - if pT.PosesFunc == nil { - return pT.PoseTracker.Poses(ctx, bodyNames, extra) - } - return pT.PosesFunc(ctx, bodyNames, extra) -} - -// DoCommand calls the injected DoCommand or the real version. -func (pT *PoseTracker) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if pT.DoFunc == nil { - return pT.PoseTracker.DoCommand(ctx, cmd) - } - return pT.DoFunc(ctx, cmd) -} diff --git a/testutils/inject/powersensor.go b/testutils/inject/powersensor.go deleted file mode 100644 index e80c69cdba2..00000000000 --- a/testutils/inject/powersensor.go +++ /dev/null @@ -1,69 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/powersensor" - "go.viam.com/rdk/resource" -) - -// A PowerSensor reports information about voltage, current and power. -type PowerSensor struct { - powersensor.PowerSensor - name resource.Name - VoltageFunc func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) - CurrentFunc func(ctx context.Context, extra map[string]interface{}) (float64, bool, error) - PowerFunc func(ctx context.Context, extra map[string]interface{}) (float64, error) - ReadingsFunc func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) -} - -// NewPowerSensor returns a new injected movement sensor. -func NewPowerSensor(name string) *PowerSensor { - return &PowerSensor{name: powersensor.Named(name)} -} - -// Name returns the name of the resource. -func (i *PowerSensor) Name() resource.Name { - return i.name -} - -// DoCommand calls the injected DoCommand or the real version. -func (i *PowerSensor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if i.DoFunc == nil { - return i.PowerSensor.DoCommand(ctx, cmd) - } - return i.DoFunc(ctx, cmd) -} - -// Voltage func or passthrough. -func (i *PowerSensor) Voltage(ctx context.Context, cmd map[string]interface{}) (float64, bool, error) { - if i.VoltageFunc == nil { - return i.PowerSensor.Voltage(ctx, cmd) - } - return i.VoltageFunc(ctx, cmd) -} - -// Current func or passthrough. -func (i *PowerSensor) Current(ctx context.Context, cmd map[string]interface{}) (float64, bool, error) { - if i.CurrentFunc == nil { - return i.PowerSensor.Current(ctx, cmd) - } - return i.CurrentFunc(ctx, cmd) -} - -// Power func or passthrough. -func (i *PowerSensor) Power(ctx context.Context, cmd map[string]interface{}) (float64, error) { - if i.PowerFunc == nil { - return i.PowerSensor.Power(ctx, cmd) - } - return i.PowerFunc(ctx, cmd) -} - -// Readings func or passthrough. -func (i *PowerSensor) Readings(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if i.ReadingsFunc == nil { - return i.PowerSensor.Readings(ctx, cmd) - } - return i.ReadingsFunc(ctx, cmd) -} diff --git a/testutils/inject/robot.go b/testutils/inject/robot.go deleted file mode 100644 index 0d41b05ff85..00000000000 --- a/testutils/inject/robot.go +++ /dev/null @@ -1,319 +0,0 @@ -// Package inject provides dependency injected structures for mocking interfaces. -package inject - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/google/uuid" - "go.viam.com/utils/pexec" - - "go.viam.com/rdk/cloud" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/operation" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/referenceframe" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/robot" - "go.viam.com/rdk/robot/framesystem" - "go.viam.com/rdk/robot/packages" - "go.viam.com/rdk/session" -) - -// Robot is an injected robot. -type Robot struct { - robot.LocalRobot - Mu sync.RWMutex // Ugly, has to be manually locked if a test means to swap funcs on an in-use robot. - DiscoverComponentsFunc func(ctx context.Context, keys []resource.DiscoveryQuery) ([]resource.Discovery, error) - RemoteByNameFunc func(name string) (robot.Robot, bool) - ResourceByNameFunc func(name resource.Name) (resource.Resource, error) - RemoteNamesFunc func() []string - ResourceNamesFunc func() []resource.Name - ResourceRPCAPIsFunc func() []resource.RPCAPI - ProcessManagerFunc func() pexec.ProcessManager - ConfigFunc func() *config.Config - LoggerFunc func() logging.Logger - CloseFunc func(ctx context.Context) error - StopAllFunc func(ctx context.Context, extra map[resource.Name]map[string]interface{}) error - FrameSystemConfigFunc func(ctx context.Context) (*framesystem.Config, error) - TransformPoseFunc func( - ctx context.Context, - pose *referenceframe.PoseInFrame, - dst string, - additionalTransforms []*referenceframe.LinkInFrame, - ) (*referenceframe.PoseInFrame, error) - TransformPointCloudFunc func(ctx context.Context, srcpc pointcloud.PointCloud, srcName, dstName string) (pointcloud.PointCloud, error) - StatusFunc func(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) - ModuleAddressFunc func() (string, error) - CloudMetadataFunc func(ctx context.Context) (cloud.Metadata, error) - - ops *operation.Manager - SessMgr session.Manager - PackageMgr packages.Manager -} - -// MockResourcesFromMap mocks ResourceNames and ResourceByName based on a resource map. -func (r *Robot) MockResourcesFromMap(rs map[resource.Name]resource.Resource) { - func() { - r.Mu.Lock() - defer r.Mu.Unlock() - r.ResourceNamesFunc = func() []resource.Name { - result := []resource.Name{} - for name := range rs { - result = append(result, name) - } - return result - } - }() - func() { - r.Mu.Lock() - defer r.Mu.Unlock() - r.ResourceByNameFunc = func(name resource.Name) (resource.Resource, error) { - result, ok := rs[name] - if ok { - return result, nil - } - return nil, errors.New("not found") - } - }() -} - -// RemoteByName calls the injected RemoteByName or the real version. -func (r *Robot) RemoteByName(name string) (robot.Robot, bool) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.RemoteByNameFunc == nil { - return r.LocalRobot.RemoteByName(name) - } - return r.RemoteByNameFunc(name) -} - -// ResourceByName calls the injected ResourceByName or the real version. -func (r *Robot) ResourceByName(name resource.Name) (resource.Resource, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.ResourceByNameFunc == nil { - return r.LocalRobot.ResourceByName(name) - } - return r.ResourceByNameFunc(name) -} - -// RemoteNames calls the injected RemoteNames or the real version. -func (r *Robot) RemoteNames() []string { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.RemoteNamesFunc == nil { - return r.LocalRobot.RemoteNames() - } - return r.RemoteNamesFunc() -} - -// ResourceNames calls the injected ResourceNames or the real version. -func (r *Robot) ResourceNames() []resource.Name { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.ResourceNamesFunc == nil { - return r.LocalRobot.ResourceNames() - } - return r.ResourceNamesFunc() -} - -// ResourceRPCAPIs returns a list of all known resource RPC APIs. -func (r *Robot) ResourceRPCAPIs() []resource.RPCAPI { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.ResourceRPCAPIsFunc == nil { - return r.LocalRobot.ResourceRPCAPIs() - } - return r.ResourceRPCAPIsFunc() -} - -// ProcessManager calls the injected ProcessManager or the real version. -func (r *Robot) ProcessManager() pexec.ProcessManager { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.ProcessManagerFunc == nil { - return r.LocalRobot.ProcessManager() - } - return r.ProcessManagerFunc() -} - -// OperationManager calls the injected OperationManager or the real version. -func (r *Robot) OperationManager() *operation.Manager { - r.Mu.RLock() - defer r.Mu.RUnlock() - - if r.ops == nil { - r.ops = operation.NewManager(r.Logger()) - } - return r.ops -} - -// SessionManager calls the injected SessionManager or the real version. -func (r *Robot) SessionManager() session.Manager { - r.Mu.RLock() - defer r.Mu.RUnlock() - - if r.SessMgr == nil { - return noopSessionManager{} - } - return r.SessMgr -} - -// PackageManager calls the injected PackageManager or the real version. -func (r *Robot) PackageManager() packages.Manager { - r.Mu.RLock() - defer r.Mu.RUnlock() - - if r.PackageMgr == nil { - return packages.NewNoopManager() - } - return r.PackageMgr -} - -// Config calls the injected Config or the real version. -func (r *Robot) Config() *config.Config { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.ConfigFunc == nil { - return r.LocalRobot.Config() - } - return r.ConfigFunc() -} - -// Logger calls the injected Logger or the real version. -func (r *Robot) Logger() logging.Logger { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.LoggerFunc == nil { - return r.LocalRobot.Logger() - } - return r.LoggerFunc() -} - -// Close calls the injected Close or the real version. -func (r *Robot) Close(ctx context.Context) error { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.CloseFunc == nil { - if r.LocalRobot == nil { - return nil - } - return r.LocalRobot.Close(ctx) - } - return r.CloseFunc(ctx) -} - -// StopAll calls the injected StopAll or the real version. -func (r *Robot) StopAll(ctx context.Context, extra map[resource.Name]map[string]interface{}) error { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.StopAllFunc == nil { - return r.LocalRobot.StopAll(ctx, extra) - } - return r.StopAllFunc(ctx, extra) -} - -// DiscoverComponents calls the injected DiscoverComponents or the real one. -func (r *Robot) DiscoverComponents(ctx context.Context, keys []resource.DiscoveryQuery) ([]resource.Discovery, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.DiscoverComponentsFunc == nil { - return r.LocalRobot.DiscoverComponents(ctx, keys) - } - return r.DiscoverComponentsFunc(ctx, keys) -} - -// FrameSystemConfig calls the injected FrameSystemConfig or the real version. -func (r *Robot) FrameSystemConfig(ctx context.Context) (*framesystem.Config, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.FrameSystemConfigFunc == nil { - return r.LocalRobot.FrameSystemConfig(ctx) - } - - return r.FrameSystemConfigFunc(ctx) -} - -// TransformPose calls the injected TransformPose or the real version. -func (r *Robot) TransformPose( - ctx context.Context, - pose *referenceframe.PoseInFrame, - dst string, - additionalTransforms []*referenceframe.LinkInFrame, -) (*referenceframe.PoseInFrame, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.TransformPoseFunc == nil { - return r.LocalRobot.TransformPose(ctx, pose, dst, additionalTransforms) - } - return r.TransformPoseFunc(ctx, pose, dst, additionalTransforms) -} - -// TransformPointCloud calls the injected TransformPointCloud or the real version. -func (r *Robot) TransformPointCloud(ctx context.Context, srcpc pointcloud.PointCloud, srcName, dstName string, -) (pointcloud.PointCloud, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.TransformPointCloudFunc == nil { - return r.LocalRobot.TransformPointCloud(ctx, srcpc, srcName, dstName) - } - return r.TransformPointCloudFunc(ctx, srcpc, srcName, dstName) -} - -// Status call the injected Status or the real one. -func (r *Robot) Status(ctx context.Context, resourceNames []resource.Name) ([]robot.Status, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.StatusFunc == nil { - return r.LocalRobot.Status(ctx, resourceNames) - } - return r.StatusFunc(ctx, resourceNames) -} - -// ModuleAddress calls the injected ModuleAddress or the real one. -func (r *Robot) ModuleAddress() (string, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.ModuleAddressFunc == nil { - return r.LocalRobot.ModuleAddress() - } - return r.ModuleAddressFunc() -} - -// CloudMetadata calls the injected CloudMetadata or the real one. -func (r *Robot) CloudMetadata(ctx context.Context) (cloud.Metadata, error) { - r.Mu.RLock() - defer r.Mu.RUnlock() - if r.CloudMetadataFunc == nil { - return r.LocalRobot.CloudMetadata(ctx) - } - return r.CloudMetadataFunc(ctx) -} - -type noopSessionManager struct{} - -func (m noopSessionManager) Start(ctx context.Context, ownerID string) (*session.Session, error) { - return session.New(ctx, ownerID, time.Minute, nil), nil -} - -func (m noopSessionManager) All() []*session.Session { - return nil -} - -func (m noopSessionManager) FindByID(ctx context.Context, id uuid.UUID, ownerID string) (*session.Session, error) { - return nil, session.ErrNoSession -} - -func (m noopSessionManager) AssociateResource(id uuid.UUID, resourceName resource.Name) { -} - -func (m noopSessionManager) Close() { -} - -func (m noopSessionManager) ServerInterceptors() session.ServerInterceptors { - return session.ServerInterceptors{} -} diff --git a/testutils/inject/sensor.go b/testutils/inject/sensor.go deleted file mode 100644 index e69c3f0106f..00000000000 --- a/testutils/inject/sensor.go +++ /dev/null @@ -1,42 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/sensor" - "go.viam.com/rdk/resource" -) - -// Sensor is an injected sensor. -type Sensor struct { - sensor.Sensor - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - ReadingsFunc func(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) -} - -// NewSensor returns a new injected sensor. -func NewSensor(name string) *Sensor { - return &Sensor{name: sensor.Named(name)} -} - -// Name returns the name of the resource. -func (s *Sensor) Name() resource.Name { - return s.name -} - -// Readings calls the injected Readings or the real version. -func (s *Sensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) { - if s.ReadingsFunc == nil { - return s.Sensor.Readings(ctx, extra) - } - return s.ReadingsFunc(ctx, extra) -} - -// DoCommand calls the injected DoCommand or the real version. -func (s *Sensor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if s.DoFunc == nil { - return s.Sensor.DoCommand(ctx, cmd) - } - return s.DoFunc(ctx, cmd) -} diff --git a/testutils/inject/sensors.go b/testutils/inject/sensors.go deleted file mode 100644 index ea8d57052c9..00000000000 --- a/testutils/inject/sensors.go +++ /dev/null @@ -1,54 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/sensors" -) - -// SensorsService represents a fake instance of a sensors service. -type SensorsService struct { - sensors.Service - name resource.Name - SensorsFunc func(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) - ReadingsFunc func(ctx context.Context, resources []resource.Name, extra map[string]interface{}) ([]sensors.Readings, error) - DoCommandFunc func(ctx context.Context, - cmd map[string]interface{}) (map[string]interface{}, error) -} - -// NewSensorsService returns a new injected sensors service. -func NewSensorsService(name string) *SensorsService { - return &SensorsService{name: sensors.Named(name)} -} - -// Name returns the name of the resource. -func (s *SensorsService) Name() resource.Name { - return s.name -} - -// Sensors call the injected Sensors or the real one. -func (s *SensorsService) Sensors(ctx context.Context, extra map[string]interface{}) ([]resource.Name, error) { - if s.SensorsFunc == nil { - return s.Service.Sensors(ctx, extra) - } - return s.SensorsFunc(ctx, extra) -} - -// Readings call the injected Readings or the real one. -func (s *SensorsService) Readings(ctx context.Context, names []resource.Name, extra map[string]interface{}) ([]sensors.Readings, error) { - if s.ReadingsFunc == nil { - return s.Service.Readings(ctx, names, extra) - } - return s.ReadingsFunc(ctx, names, extra) -} - -// DoCommand calls the injected DoCommand or the real variant. -func (s *SensorsService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if s.DoCommandFunc == nil { - return s.Service.DoCommand(ctx, cmd) - } - return s.DoCommandFunc(ctx, cmd) -} diff --git a/testutils/inject/servo.go b/testutils/inject/servo.go deleted file mode 100644 index c92294c7b99..00000000000 --- a/testutils/inject/servo.go +++ /dev/null @@ -1,69 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/components/servo" - "go.viam.com/rdk/resource" -) - -// Servo is an injected servo. -type Servo struct { - servo.Servo - name resource.Name - DoFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - MoveFunc func(ctx context.Context, angleDeg uint32, extra map[string]interface{}) error - PositionFunc func(ctx context.Context, extra map[string]interface{}) (uint32, error) - StopFunc func(ctx context.Context, extra map[string]interface{}) error - IsMovingFunc func(context.Context) (bool, error) -} - -// NewServo returns a new injected servo. -func NewServo(name string) *Servo { - return &Servo{name: servo.Named(name)} -} - -// Name returns the name of the resource. -func (s *Servo) Name() resource.Name { - return s.name -} - -// Move calls the injected Move or the real version. -func (s *Servo) Move(ctx context.Context, angleDeg uint32, extra map[string]interface{}) error { - if s.MoveFunc == nil { - return s.Servo.Move(ctx, angleDeg, extra) - } - return s.MoveFunc(ctx, angleDeg, extra) -} - -// Position calls the injected Current or the real version. -func (s *Servo) Position(ctx context.Context, extra map[string]interface{}) (uint32, error) { - if s.PositionFunc == nil { - return s.Servo.Position(ctx, extra) - } - return s.PositionFunc(ctx, extra) -} - -// Stop calls the injected Stop or the real version. -func (s *Servo) Stop(ctx context.Context, extra map[string]interface{}) error { - if s.StopFunc == nil { - return s.Servo.Stop(ctx, extra) - } - return s.StopFunc(ctx, extra) -} - -// DoCommand calls the injected DoCommand or the real version. -func (s *Servo) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if s.DoFunc == nil { - return s.Servo.DoCommand(ctx, cmd) - } - return s.DoFunc(ctx, cmd) -} - -// IsMoving calls the injected IsMoving or the real version. -func (s *Servo) IsMoving(ctx context.Context) (bool, error) { - if s.IsMovingFunc == nil { - return s.Servo.IsMoving(ctx) - } - return s.IsMovingFunc(ctx) -} diff --git a/testutils/inject/shell_service.go b/testutils/inject/shell_service.go deleted file mode 100644 index 3c2ca65391f..00000000000 --- a/testutils/inject/shell_service.go +++ /dev/null @@ -1,60 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/shell" -) - -// ShellService represents a fake instance of a shell service. -type ShellService struct { - shell.Service - name resource.Name - DoCommandFunc func(ctx context.Context, - cmd map[string]interface{}) (map[string]interface{}, error) - ReconfigureFunc func(ctx context.Context, deps resource.Dependencies, conf resource.Config) error - CloseFunc func(ctx context.Context) error -} - -// NewShellService returns a new injected shell service. -func NewShellService(name string) *ShellService { - return &ShellService{name: shell.Named(name)} -} - -// Name returns the name of the resource. -func (s *ShellService) Name() resource.Name { - return s.name -} - -// DoCommand calls the injected DoCommand or the real variant. -func (s *ShellService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if s.DoCommandFunc == nil { - return s.Service.DoCommand(ctx, cmd) - } - return s.DoCommandFunc(ctx, cmd) -} - -// Reconfigure calls the injected Reconfigure or the real variant. -func (s *ShellService) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error { - if s.ReconfigureFunc == nil { - if s.Service == nil { - return resource.NewMustRebuildError(conf.ResourceName()) - } - return s.Service.Reconfigure(ctx, deps, conf) - } - return s.ReconfigureFunc(ctx, deps, conf) -} - -// Close calls the injected Close or the real version. -func (s *ShellService) Close(ctx context.Context) error { - if s.CloseFunc == nil { - if s.Service == nil { - return nil - } - return s.Service.Close(ctx) - } - return s.CloseFunc(ctx) -} diff --git a/testutils/inject/slam_service.go b/testutils/inject/slam_service.go deleted file mode 100644 index 4a8edf5b9de..00000000000 --- a/testutils/inject/slam_service.go +++ /dev/null @@ -1,84 +0,0 @@ -package inject - -import ( - "context" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/slam" - "go.viam.com/rdk/spatialmath" -) - -// SLAMService represents a fake instance of a slam service. -type SLAMService struct { - slam.Service - name resource.Name - PositionFunc func(ctx context.Context) (spatialmath.Pose, string, error) - PointCloudMapFunc func(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) - InternalStateFunc func(ctx context.Context) (func() ([]byte, error), error) - PropertiesFunc func(ctx context.Context) (slam.Properties, error) - DoCommandFunc func(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) - CloseFunc func(ctx context.Context) error -} - -// NewSLAMService returns a new injected SLAM service. -func NewSLAMService(name string) *SLAMService { - return &SLAMService{name: slam.Named(name)} -} - -// Name returns the name of the resource. -func (slamSvc *SLAMService) Name() resource.Name { - return slamSvc.name -} - -// Position calls the injected PositionFunc or the real version. -func (slamSvc *SLAMService) Position(ctx context.Context) (spatialmath.Pose, string, error) { - if slamSvc.PositionFunc == nil { - return slamSvc.Service.Position(ctx) - } - return slamSvc.PositionFunc(ctx) -} - -// PointCloudMap calls the injected PointCloudMap or the real version. -func (slamSvc *SLAMService) PointCloudMap(ctx context.Context, returnEditedMap bool) (func() ([]byte, error), error) { - if slamSvc.PointCloudMapFunc == nil { - return slamSvc.Service.PointCloudMap(ctx, returnEditedMap) - } - return slamSvc.PointCloudMapFunc(ctx, returnEditedMap) -} - -// InternalState calls the injected InternalState or the real version. -func (slamSvc *SLAMService) InternalState(ctx context.Context) (func() ([]byte, error), error) { - if slamSvc.InternalStateFunc == nil { - return slamSvc.Service.InternalState(ctx) - } - return slamSvc.InternalStateFunc(ctx) -} - -// Properties calls the injected PropertiesFunc or the real version. -func (slamSvc *SLAMService) Properties(ctx context.Context) (slam.Properties, error) { - if slamSvc.PropertiesFunc == nil { - return slamSvc.Service.Properties(ctx) - } - return slamSvc.PropertiesFunc(ctx) -} - -// DoCommand calls the injected DoCommand or the real variant. -func (slamSvc *SLAMService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if slamSvc.DoCommandFunc == nil { - return slamSvc.Service.DoCommand(ctx, cmd) - } - return slamSvc.DoCommandFunc(ctx, cmd) -} - -// Close calls the injected Close or the real version. -func (slamSvc *SLAMService) Close(ctx context.Context) error { - if slamSvc.CloseFunc == nil { - if slamSvc.Service == nil { - return nil - } - return slamSvc.Service.Close(ctx) - } - return slamSvc.CloseFunc(ctx) -} diff --git a/testutils/inject/spi.go b/testutils/inject/spi.go deleted file mode 100644 index 220d77248ad..00000000000 --- a/testutils/inject/spi.go +++ /dev/null @@ -1,19 +0,0 @@ -package inject - -import ( - "go.viam.com/rdk/components/board/genericlinux/buses" -) - -// SPI is an injected SPI. -type SPI struct { - buses.SPI - OpenHandleFunc func() (buses.SPIHandle, error) -} - -// OpenHandle calls the injected OpenHandle or the real version. -func (s *SPI) OpenHandle() (buses.SPIHandle, error) { - if s.OpenHandleFunc == nil { - return s.SPI.OpenHandle() - } - return s.OpenHandleFunc() -} diff --git a/testutils/inject/vision_service.go b/testutils/inject/vision_service.go deleted file mode 100644 index 442ff4629a2..00000000000 --- a/testutils/inject/vision_service.go +++ /dev/null @@ -1,114 +0,0 @@ -package inject - -import ( - "context" - "image" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/services/vision" - viz "go.viam.com/rdk/vision" - "go.viam.com/rdk/vision/classification" - "go.viam.com/rdk/vision/objectdetection" -) - -// VisionService represents a fake instance of a vision service. -type VisionService struct { - vision.Service - name resource.Name - DetectionsFromCameraFunc func( - ctx context.Context, cameraName string, extra map[string]interface{}, - ) ([]objectdetection.Detection, error) - DetectionsFunc func( - ctx context.Context, img image.Image, extra map[string]interface{}, - ) ([]objectdetection.Detection, error) - // classification functions - ClassificationsFromCameraFunc func(ctx context.Context, cameraName string, - n int, extra map[string]interface{}) (classification.Classifications, error) - ClassificationsFunc func(ctx context.Context, img image.Image, - n int, extra map[string]interface{}) (classification.Classifications, error) - // segmentation functions - GetObjectPointCloudsFunc func(ctx context.Context, cameraName string, extra map[string]interface{}) ([]*viz.Object, error) - DoCommandFunc func(ctx context.Context, - cmd map[string]interface{}) (map[string]interface{}, error) - CloseFunc func(ctx context.Context) error -} - -// NewVisionService returns a new injected vision service. -func NewVisionService(name string) *VisionService { - return &VisionService{name: vision.Named(name)} -} - -// Name returns the name of the resource. -func (vs *VisionService) Name() resource.Name { - return vs.name -} - -// DetectionsFromCamera calls the injected DetectionsFromCamera or the real variant. -func (vs *VisionService) DetectionsFromCamera(ctx context.Context, cameraName string, extra map[string]interface{}, -) ([]objectdetection.Detection, error) { - if vs.DetectionsFunc == nil { - return vs.Service.DetectionsFromCamera(ctx, cameraName, extra) - } - return vs.DetectionsFromCameraFunc(ctx, cameraName, extra) -} - -// Detections calls the injected Detect or the real variant. -func (vs *VisionService) Detections(ctx context.Context, img image.Image, extra map[string]interface{}, -) ([]objectdetection.Detection, error) { - if vs.DetectionsFunc == nil { - return vs.Service.Detections(ctx, img, extra) - } - return vs.DetectionsFunc(ctx, img, extra) -} - -// ClassificationsFromCamera calls the injected Classifer or the real variant. -func (vs *VisionService) ClassificationsFromCamera(ctx context.Context, - cameraName string, n int, extra map[string]interface{}, -) (classification.Classifications, error) { - if vs.ClassificationsFromCameraFunc == nil { - return vs.Service.ClassificationsFromCamera(ctx, cameraName, n, extra) - } - return vs.ClassificationsFromCameraFunc(ctx, cameraName, n, extra) -} - -// Classifications calls the injected Classifier or the real variant. -func (vs *VisionService) Classifications(ctx context.Context, img image.Image, - n int, extra map[string]interface{}, -) (classification.Classifications, error) { - if vs.ClassificationsFunc == nil { - return vs.Service.Classifications(ctx, img, n, extra) - } - return vs.ClassificationsFunc(ctx, img, n, extra) -} - -// GetObjectPointClouds calls the injected GetObjectPointClouds or the real variant. -func (vs *VisionService) GetObjectPointClouds( - ctx context.Context, - cameraName string, extra map[string]interface{}, -) ([]*viz.Object, error) { - if vs.GetObjectPointCloudsFunc == nil { - return vs.Service.GetObjectPointClouds(ctx, cameraName, extra) - } - return vs.GetObjectPointCloudsFunc(ctx, cameraName, extra) -} - -// DoCommand calls the injected DoCommand or the real variant. -func (vs *VisionService) DoCommand(ctx context.Context, - cmd map[string]interface{}, -) (map[string]interface{}, error) { - if vs.DoCommandFunc == nil { - return vs.Service.DoCommand(ctx, cmd) - } - return vs.DoCommandFunc(ctx, cmd) -} - -// Close calls the injected Close or the real version. -func (vs *VisionService) Close(ctx context.Context) error { - if vs.CloseFunc == nil { - if vs.Service == nil { - return nil - } - return vs.Service.Close(ctx) - } - return vs.CloseFunc(ctx) -} diff --git a/utils/attribute_map_test.go b/utils/attribute_map_test.go deleted file mode 100644 index 0182d0b4ed2..00000000000 --- a/utils/attribute_map_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package utils - -import ( - "reflect" - "testing" - - "go.viam.com/test" -) - -var sampleAttributeMap = AttributeMap{ - "ok_boolean_false": false, - "ok_boolean_true": true, - "bad_boolean_false": 0, - "bad_boolean_true": "true", - "good_int_slice": []interface{}{1, 2, 3}, - "bad_int_slice": "this is not an int slice", - "bad_int_slice_2": []interface{}{1, 2, "3"}, - "good_string_slice": []interface{}{"1", "2", "3"}, - "bad_string_slice": 123, - "bad_string_slice_2": []interface{}{"1", "2", 3}, - "good_float64_slice": []interface{}{1.1, 2.2, 3.3}, - "bad_float64_slice": []interface{}{int(1), "2", 3.3}, - "good_boolean_slice": []interface{}{true, true, false}, - "bad_boolean_slice": []interface{}{"true", "F", false}, -} - -func TestAttributeMap(t *testing.T) { - // TODO: As a general revisit, perhaps AttributeMap should return - // errors rather than panicking? - - // AttributeMap.Bool tests - - // AttributeMap.Bool properly returns for boolean value of True - b := sampleAttributeMap.Bool("ok_boolean_true", false) - test.That(t, b, test.ShouldBeTrue) - // AttributeMap.Bool properly returns for boolean value of False - b = sampleAttributeMap.Bool("ok_boolean_false", false) - test.That(t, b, test.ShouldBeFalse) - // AttributeMap.Bool panics for non-boolean values - badTrueGetter := func() { - sampleAttributeMap.Bool("bad_boolean_true", false) - } - badFalseGetter := func() { - sampleAttributeMap.Bool("bad_boolean_false", false) - } - test.That(t, badTrueGetter, test.ShouldPanic) - test.That(t, badFalseGetter, test.ShouldPanic) - // AttributeMap.Bool provides default boolean value when key is missing - b = sampleAttributeMap.Bool("junk_key", false) - test.That(t, b, test.ShouldBeFalse) - b = sampleAttributeMap.Bool("junk_key", true) - test.That(t, b, test.ShouldBeTrue) - - // TODO: write tests for below functions - // AttributeMap.Float64 - // AttributeMap.Int - // AttributeMap.String - - // AttributeMap.IntSlice properly returns an int slice - iSlice := sampleAttributeMap.IntSlice("good_int_slice") - test.That(t, iSlice, test.ShouldResemble, []int{1, 2, 3}) - // AttributeMap.IntSlice panics when corresponding value is - // not a slice of all integers - badIntSliceGetter1 := func() { - sampleAttributeMap.IntSlice("bad_int_slice") - } - badIntSliceGetter2 := func() { - sampleAttributeMap.IntSlice("bad_int_slice_2") - } - test.That(t, badIntSliceGetter1, test.ShouldPanic) - test.That(t, badIntSliceGetter2, test.ShouldPanic) - - // AttributeMap.IntSlice properly returns an int slice - sSlice := sampleAttributeMap.StringSlice("good_string_slice") - test.That(t, sSlice, test.ShouldResemble, []string{"1", "2", "3"}) - // AttributeMap.IntSlice panics when corresponding value is - // not a slice of all integers - badStringSliceGetter1 := func() { - sampleAttributeMap.StringSlice("bad_string_slice") - } - badStringSliceGetter2 := func() { - sampleAttributeMap.StringSlice("bad_string_slice_2") - } - test.That(t, badStringSliceGetter1, test.ShouldPanic) - test.That(t, badStringSliceGetter2, test.ShouldPanic) - - // AttributeMap.Float64Slice properly returns a float64 slice - fSlice := sampleAttributeMap.Float64Slice("good_float64_slice") - test.That(t, fSlice, test.ShouldResemble, []float64{1.1, 2.2, 3.3}) - // AttributeMap.Float64Slice panics when corresponding value is - // not a slice of all float64s - badFloat64SliceGetter := func() { - sampleAttributeMap.Float64Slice("bad_float64_slice") - } - test.That(t, badFloat64SliceGetter, test.ShouldPanic) - - // AttributeMap.BoolSlice properly returns a boolean slice - bSlice := sampleAttributeMap.BoolSlice("good_boolean_slice", true) - test.That(t, bSlice, test.ShouldResemble, []bool{true, true, false}) - // AttributeMap.BoolSlice panics when corresponding value is - // not a slice of all booleans - badBoolSliceGetter := func() { - sampleAttributeMap.BoolSlice("bad_boolean_slice", false) - } - test.That(t, badBoolSliceGetter, test.ShouldPanic) -} - -func TestAttributeMapWalk(t *testing.T) { - type dataV struct { - Other string - } - type internalAttr struct { - StringValue string - StringPtrValue *string - StringArray []string - BoolValue bool - ByteArray []byte - Data dataV - DataPtr *dataV - DataMapStr map[string]*dataV - } - - stringVal := "some string val" - complexData := internalAttr{ - StringValue: "/some/path", - StringPtrValue: &stringVal, - StringArray: []string{"one", "two", "three"}, - BoolValue: true, - ByteArray: []byte("hello"), - Data: dataV{Other: "this is a string"}, - DataPtr: &dataV{Other: "/some/other/path"}, - DataMapStr: map[string]*dataV{ - "other1": {Other: "/its/another/path"}, - "other2": {Other: "hello2"}, - }, - } - - attributes := AttributeMap{ - "one": float64(1), - "file_path": "/this/is/a/path", - "data": complexData, - } - - newAttrs, err := attributes.Walk(&indirectVisitor{}) - test.That(t, err, test.ShouldBeNil) - - expectedAttrs := AttributeMap{ - "one": float64(1), - "file_path": "/this/is/a/path", - "data": map[string]interface{}{ - "StringValue": "/some/path", - "StringPtrValue": "some string val", - "StringArray": []interface{}{"one", "two", "three"}, - "BoolValue": true, - "ByteArray": []interface{}{byte('h'), byte('e'), byte('l'), byte('l'), byte('o')}, - "Data": map[string]interface{}{"Other": "this is a string"}, - "DataPtr": map[string]interface{}{"Other": "/some/other/path"}, - "DataMapStr": map[string]interface{}{ - "other1": map[string]interface{}{"Other": "/its/another/path"}, - "other2": map[string]interface{}{"Other": "hello2"}, - }, - }, - } - test.That(t, newAttrs, test.ShouldResemble, expectedAttrs) -} - -// indirectVisitor visits a type and if it's a pointer, it returns the value that the pointer points -// to. This makes testing easier since we can compare values with values. -type indirectVisitor struct{} - -func (v *indirectVisitor) Visit(data interface{}) (interface{}, error) { - val := reflect.ValueOf(data) - val = reflect.Indirect(val) - return val.Interface(), nil -} diff --git a/utils/average_test.go b/utils/average_test.go deleted file mode 100644 index 08f6a89bd55..00000000000 --- a/utils/average_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package utils - -import ( - "testing" - - "go.viam.com/test" -) - -func TestRolling1(t *testing.T) { - ra := NewRollingAverage(2) - ra.Add(5) - ra.Add(9) - test.That(t, ra.Average(), test.ShouldEqual, 7) - - ra.Add(11) - test.That(t, ra.Average(), test.ShouldEqual, 10) - - test.That(t, ra.NumSamples(), test.ShouldEqual, 2) -} diff --git a/utils/clf_test.go b/utils/clf_test.go deleted file mode 100644 index 42b267e79bc..00000000000 --- a/utils/clf_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package utils_test - -import ( - "os" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/utils" -) - -func TestCLF(t *testing.T) { - f, err := os.Open(artifact.MustPath("utils/aces_sample.clf")) - test.That(t, err, test.ShouldBeNil) - defer f.Close() - - clf := utils.NewCLFReader(f) - - numMessages := 0 - var haveAnOdom bool - err = clf.Process(func(message utils.CLFMessage) error { - numMessages++ - if message.Type() == utils.CLFMessageTypeOdometry { - haveAnOdom = true - } - return nil - }) - test.That(t, err, test.ShouldBeNil) - test.That(t, numMessages, test.ShouldEqual, 20) - test.That(t, haveAnOdom, test.ShouldBeTrue) -} diff --git a/utils/contextutils/context_test.go b/utils/contextutils/context_test.go deleted file mode 100644 index 865d4f28dfe..00000000000 --- a/utils/contextutils/context_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package contextutils - -import ( - "context" - "testing" - "time" - - "go.viam.com/test" -) - -func TestContextWithMetadata(t *testing.T) { - // nothing in context - ctx := context.Background() - mdFromContext := ctx.Value(MetadataContextKey) - test.That(t, mdFromContext, test.ShouldBeNil) - - // initialize metadata, empty at first - ctx, md := ContextWithMetadata(ctx) - test.That(t, md, test.ShouldBeEmpty) - test.That(t, ctx.Value(MetadataContextKey), test.ShouldBeEmpty) - - // add values to local metadata and show context metadata has updated - k, v := "hello", []string{"world"} - md[k] = v - mdFromContext = ctx.Value(MetadataContextKey) - mdMap, ok := mdFromContext.(map[string][]string) - test.That(t, ok, test.ShouldEqual, true) - test.That(t, mdMap[k], test.ShouldResemble, v) - - // after calling ContextWithMetadata second time, old metadata still there - ctx, md = ContextWithMetadata(ctx) - test.That(t, md[k], test.ShouldResemble, v) - - // if metadata value gets overwritten with non-metadata value, next call to - // ContextWithMetadata will add viam-metadata again, but will not be able to access old - // metadata - someString := "iamastring" - ctx = context.WithValue(ctx, MetadataContextKey, someString) - mdFromContext = ctx.Value(MetadataContextKey) - mdString, ok := mdFromContext.(string) - test.That(t, ok, test.ShouldEqual, true) - test.That(t, mdString, test.ShouldEqual, someString) - - ctx, md = ContextWithMetadata(ctx) - test.That(t, md, test.ShouldBeEmpty) - - mdFromContext = ctx.Value(MetadataContextKey) - mdMap, ok = mdFromContext.(map[string][]string) - test.That(t, ok, test.ShouldEqual, true) - test.That(t, mdMap, test.ShouldBeEmpty) - test.That(t, ctx.Value(MetadataContextKey), test.ShouldBeEmpty) -} - -func TestContextWithTimeoutIfNoDeadline(t *testing.T) { - // Test with no set deadline - noDeadlineCtx := context.Background() - noDeadlineCtxDeadline, _ := noDeadlineCtx.Deadline() - deadlineCtx, cancel := ContextWithTimeoutIfNoDeadline(noDeadlineCtx, time.Second) - defer cancel() - deadlineCtxDeadline, _ := deadlineCtx.Deadline() - test.That(t, deadlineCtxDeadline.After(noDeadlineCtxDeadline), test.ShouldBeTrue) - - // Test with prev set deadline - ctx, cancel := ContextWithTimeoutIfNoDeadline(deadlineCtx, 2*time.Second) - defer cancel() - ctxDeadline, _ := ctx.Deadline() - test.That(t, ctxDeadline, test.ShouldEqual, deadlineCtxDeadline) -} diff --git a/utils/distance_test.go b/utils/distance_test.go deleted file mode 100644 index ce0d6e7ce25..00000000000 --- a/utils/distance_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package utils - -import ( - "math" - "testing" - - "go.viam.com/test" -) - -func TestComputeDistance(t *testing.T) { - v1 := []float64{1, 0, 1} - d1Euclidean, err := ComputeDistance(v1, v1, Euclidean) - test.That(t, err, test.ShouldBeNil) - test.That(t, d1Euclidean, test.ShouldEqual, 0) - - d1Hamming, err := ComputeDistance(v1, v1, Hamming) - test.That(t, err, test.ShouldBeNil) - test.That(t, d1Hamming, test.ShouldEqual, 0) - - v2 := []float64{1, 1, 1} - - d2Euclidean, err := ComputeDistance(v1, v2, Euclidean) - test.That(t, err, test.ShouldBeNil) - test.That(t, d2Euclidean, test.ShouldEqual, 1) - - d2Hamming, err := ComputeDistance(v1, v2, Hamming) - test.That(t, err, test.ShouldBeNil) - test.That(t, d2Hamming, test.ShouldEqual, 1) - - v3 := []float64{1, 0, 0} - d3Euclidean, err := ComputeDistance(v2, v3, Euclidean) - test.That(t, err, test.ShouldBeNil) - test.That(t, d3Euclidean, test.ShouldEqual, math.Sqrt(2)) - - d3Hamming, err := ComputeDistance(v2, v3, Hamming) - test.That(t, err, test.ShouldBeNil) - test.That(t, d3Hamming, test.ShouldEqual, 2) -} - -func TestPairwiseDistance(t *testing.T) { - p1 := [][]float64{ - {1, 0, 1}, - {1, 1, 1}, - {1, 0, 0}, - } - - p2 := [][]float64{ - {1, 0, 0}, - {1, 1, 0}, - {1, 1, 0}, - {0, 0, 0}, - } - - distancesEuclidean, err := PairwiseDistance(p1, p2, Euclidean) - test.That(t, err, test.ShouldBeNil) - test.That(t, distancesEuclidean, test.ShouldNotBeNil) - m, n := distancesEuclidean.Dims() - test.That(t, m, test.ShouldEqual, 3) - test.That(t, n, test.ShouldEqual, 4) - test.That(t, distancesEuclidean.At(0, 0), test.ShouldEqual, 1) - - distancesHamming, err := PairwiseDistance(p1, p2, Hamming) - test.That(t, err, test.ShouldBeNil) - test.That(t, distancesHamming, test.ShouldNotBeNil) - m2, n2 := distancesHamming.Dims() - test.That(t, m2, test.ShouldEqual, 3) - test.That(t, n2, test.ShouldEqual, 4) - test.That(t, distancesHamming.At(0, 0), test.ShouldEqual, 1) - - minIdx := GetArgMinDistancesPerRow(distancesHamming) - test.That(t, len(minIdx), test.ShouldEqual, 3) - test.That(t, minIdx[0], test.ShouldEqual, 0) - test.That(t, minIdx[1], test.ShouldEqual, 1) - test.That(t, minIdx[2], test.ShouldEqual, 0) -} diff --git a/utils/errors_test.go b/utils/errors_test.go deleted file mode 100644 index a5fe3b9e06f..00000000000 --- a/utils/errors_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package utils - -import ( - "testing" - - "go.viam.com/test" -) - -func TestNewUnexpectedTypeError(t *testing.T) { - err := NewUnexpectedTypeError[string]("actual1") - test.That(t, err.Error(), test.ShouldContainSubstring, `expected string but got string`) - - err = NewUnexpectedTypeError[int]("actual2") - test.That(t, err.Error(), test.ShouldContainSubstring, `expected int but got string`) - - err = NewUnexpectedTypeError[*someStruct](3) - test.That(t, err.Error(), test.ShouldContainSubstring, `expected *utils.someStruct but got int`) - - err = NewUnexpectedTypeError[someStruct](4) - test.That(t, err.Error(), test.ShouldContainSubstring, `expected utils.someStruct but got int`) - - err = NewUnexpectedTypeError[someIfc](5) - test.That(t, err.Error(), test.ShouldContainSubstring, `expected utils.someIfc but got int`) - - err = NewUnexpectedTypeError[*someIfc](6) - test.That(t, err.Error(), test.ShouldContainSubstring, `expected *utils.someIfc but got int`) -} - -type ( - someStruct struct{} - someIfc interface{} -) diff --git a/utils/file_test.go b/utils/file_test.go deleted file mode 100644 index 613c384bd56..00000000000 --- a/utils/file_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package utils - -import ( - "bytes" - "os" - "testing" - - "go.viam.com/test" -) - -func TestResolveFile(t *testing.T) { - sentinel := "great" - _ = sentinel - resolved := ResolveFile("utils/file_test.go") - rd, err := os.ReadFile(resolved) - test.That(t, err, test.ShouldBeNil) - test.That(t, bytes.Contains(rd, []byte(`sentinel := "great"`)), test.ShouldBeTrue) -} diff --git a/utils/math_test.go b/utils/math_test.go deleted file mode 100644 index 3ead833d3f1..00000000000 --- a/utils/math_test.go +++ /dev/null @@ -1,356 +0,0 @@ -package utils - -import ( - "fmt" - "math" - "testing" - - "go.viam.com/test" -) - -func TestAbs1(t *testing.T) { - test.That(t, AbsInt(5), test.ShouldEqual, 5) - test.That(t, AbsInt(-5), test.ShouldEqual, 5) - test.That(t, AbsInt(0), test.ShouldEqual, 0) - - test.That(t, AbsInt64(5), test.ShouldEqual, int64(5)) - test.That(t, AbsInt64(-5), test.ShouldEqual, int64(5)) - test.That(t, AbsInt64(0), test.ShouldEqual, int64(0)) -} - -func TestCubeRoot(t *testing.T) { - test.That(t, CubeRoot(1.0), test.ShouldAlmostEqual, 1.0) - test.That(t, CubeRoot(8.0), test.ShouldAlmostEqual, 2.0) -} - -func TestSquare1(t *testing.T) { - test.That(t, Square(2.0), test.ShouldEqual, 4.0) - test.That(t, SquareInt(2), test.ShouldEqual, 4) -} - -func TestDegToRad(t *testing.T) { - test.That(t, DegToRad(0), test.ShouldEqual, 0) - test.That(t, DegToRad(5.625), test.ShouldEqual, math.Pi/32) - test.That(t, DegToRad(11.25), test.ShouldEqual, math.Pi/16) - test.That(t, DegToRad(22.5), test.ShouldEqual, math.Pi/8) - test.That(t, DegToRad(45), test.ShouldEqual, math.Pi/4) - test.That(t, DegToRad(90), test.ShouldEqual, math.Pi/2) - test.That(t, DegToRad(95.625), test.ShouldAlmostEqual, math.Pi/2+math.Pi/32) - test.That(t, DegToRad(101.25), test.ShouldAlmostEqual, math.Pi/2+math.Pi/16) - test.That(t, DegToRad(112.5), test.ShouldEqual, math.Pi/2+math.Pi/8) - test.That(t, DegToRad(135), test.ShouldEqual, math.Pi/2+math.Pi/4) - test.That(t, DegToRad(180), test.ShouldEqual, math.Pi) - test.That(t, DegToRad(360), test.ShouldEqual, 2*math.Pi) - test.That(t, DegToRad(720), test.ShouldEqual, 4*math.Pi) -} - -func TestRadToDeg(t *testing.T) { - test.That(t, 0, test.ShouldEqual, DegToRad(0)) - test.That(t, math.Pi/32, test.ShouldEqual, DegToRad(5.625)) - test.That(t, math.Pi/16, test.ShouldEqual, DegToRad(11.25)) - test.That(t, math.Pi/8, test.ShouldEqual, DegToRad(22.5)) - test.That(t, math.Pi/4, test.ShouldEqual, DegToRad(45)) - test.That(t, math.Pi/2, test.ShouldEqual, DegToRad(90)) - test.That(t, math.Pi/2+math.Pi/32, test.ShouldAlmostEqual, DegToRad(95.625)) - test.That(t, math.Pi/2+math.Pi/16, test.ShouldAlmostEqual, DegToRad(101.25)) - test.That(t, math.Pi/2+math.Pi/8, test.ShouldEqual, DegToRad(112.5)) - test.That(t, math.Pi/2+math.Pi/4, test.ShouldEqual, DegToRad(135)) - test.That(t, math.Pi, test.ShouldEqual, DegToRad(180)) - test.That(t, 2*math.Pi, test.ShouldEqual, DegToRad(360)) - test.That(t, 4*math.Pi, test.ShouldEqual, DegToRad(720)) -} - -func TestDegrees(t *testing.T) { - test.That(t, DegToRad(0), test.ShouldEqual, 0.0) - test.That(t, RadToDeg(DegToRad(0)), test.ShouldEqual, 0.0) - - test.That(t, DegToRad(180), test.ShouldEqual, math.Pi) - test.That(t, RadToDeg(DegToRad(180)), test.ShouldEqual, 180.0) -} - -func TestAngleDiffDeg(t *testing.T) { - for _, tc := range []struct { - a1, a2 float64 - expected float64 - }{ - {0, 0, 0}, - {0, 45, 45}, - {0, 90, 90}, - {0, 180, 180}, - {45, 0, 45}, - {90, 0, 90}, - {180, 0, 180}, - {0, 360, 0}, - {350, 20, 30}, - {20, 350, 30}, - } { - t.Run(fmt.Sprintf("|%f-%f|=%f", tc.a1, tc.a2, tc.expected), func(t *testing.T) { - test.That(t, AngleDiffDeg(tc.a1, tc.a2), test.ShouldEqual, tc.expected) - }) - } -} - -func TestAntiCWDeg(t *testing.T) { - test.That(t, AntiCWDeg(0), test.ShouldEqual, 0) - test.That(t, AntiCWDeg(360), test.ShouldEqual, 0) - test.That(t, AntiCWDeg(180), test.ShouldEqual, 180) - test.That(t, AntiCWDeg(1), test.ShouldEqual, 359) - test.That(t, AntiCWDeg(90), test.ShouldEqual, 270) - test.That(t, AntiCWDeg(270), test.ShouldEqual, 90) - test.That(t, AntiCWDeg(45), test.ShouldEqual, 315) -} - -func TestModAngDeg(t *testing.T) { - test.That(t, ModAngDeg(0-180), test.ShouldEqual, 180) - test.That(t, ModAngDeg(360+40), test.ShouldEqual, 40) - test.That(t, ModAngDeg(180+360), test.ShouldEqual, 180) - test.That(t, ModAngDeg(1-209), test.ShouldEqual, 152) - test.That(t, ModAngDeg(90-1), test.ShouldEqual, 89) - test.That(t, ModAngDeg(270+90), test.ShouldEqual, 0) - test.That(t, ModAngDeg(45), test.ShouldEqual, 45) -} - -func TestMedian(t *testing.T) { - for i, tc := range []struct { - values []float64 - expected float64 - }{ - { - []float64{}, - math.NaN(), - }, - { - []float64{1}, - 1, - }, - { - []float64{1, 2, 3}, - 2, - }, - { - []float64{3, 2, 1}, - 2, - }, - { - []float64{90, 90, 90}, - 90, - }, - { - []float64{90, 45, 80}, - 80, - }, - } { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - if math.IsNaN(tc.expected) { - test.That(t, math.IsNaN(Median(tc.values...)), test.ShouldBeTrue) - return - } - test.That(t, Median(tc.values...), test.ShouldAlmostEqual, tc.expected) - }) - } -} - -func TestMinMax(t *testing.T) { - test.That(t, MaxInt(0, 5), test.ShouldEqual, 5) - test.That(t, MaxInt(-12, 5), test.ShouldEqual, 5) - test.That(t, MaxInt(5, 4), test.ShouldEqual, 5) - - test.That(t, MinInt(0, 5), test.ShouldEqual, 0) - test.That(t, MinInt(-12, 5), test.ShouldEqual, -12) - test.That(t, MinInt(5, 4), test.ShouldEqual, 4) - - test.That(t, MaxUint8(0, 5), test.ShouldEqual, uint8(5)) - test.That(t, MaxUint8(1, 5), test.ShouldEqual, uint8(5)) - test.That(t, MaxUint8(5, 4), test.ShouldEqual, uint8(5)) - - test.That(t, MinUint8(0, 5), test.ShouldEqual, uint8(0)) - test.That(t, MinUint8(1, 5), test.ShouldEqual, uint8(1)) - test.That(t, MinUint8(5, 4), test.ShouldEqual, uint8(4)) -} - -func TestScaleByPct(t *testing.T) { - test.That(t, ScaleByPct(0, 0), test.ShouldEqual, 0) - test.That(t, ScaleByPct(255, 0), test.ShouldEqual, 0) - test.That(t, ScaleByPct(255, 1), test.ShouldEqual, 255) - test.That(t, ScaleByPct(255, .5), test.ShouldEqual, 127) - test.That(t, ScaleByPct(255, -2), test.ShouldEqual, 0) - test.That(t, ScaleByPct(255, 2), test.ShouldEqual, 255) -} - -func TestFloat64AlmostEqual(t *testing.T) { - test.That(t, Float64AlmostEqual(1, 1.001, 1e-4), test.ShouldBeFalse) - test.That(t, Float64AlmostEqual(1, 1.001, 1e-2), test.ShouldBeTrue) -} - -func TestClamp(t *testing.T) { - for i, tc := range []struct { - value float64 - min float64 - max float64 - expected float64 - }{ - { - 3, - 1, - 2, - 2, - }, - { - 1.5, - 1, - 2, - 1.5, - }, - { - 0.5, - 1, - 2, - 1, - }, - } { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - test.That(t, Clamp(tc.value, tc.min, tc.max), test.ShouldAlmostEqual, tc.expected) - }) - } -} - -func TestSampleNIntegersNormal(t *testing.T) { - samples1 := SampleNIntegersNormal(256, -7, 8) - test.That(t, len(samples1), test.ShouldEqual, 256) - mean1 := 0. - for _, sample := range samples1 { - mean1 += float64(sample) - test.That(t, sample, test.ShouldBeGreaterThanOrEqualTo, -7) - test.That(t, sample, test.ShouldBeLessThanOrEqualTo, 8) - } - mean1 /= float64(len(samples1)) - // mean1 should be approximately 0.5 - test.That(t, mean1, test.ShouldBeLessThanOrEqualTo, 1.6) - test.That(t, mean1, test.ShouldBeGreaterThanOrEqualTo, -0.6) - - nSample2 := 25000 - samples2 := SampleNIntegersNormal(nSample2, -16, 32) - test.That(t, len(samples2), test.ShouldEqual, nSample2) -} - -func TestCycleIntSlice(t *testing.T) { - s := []int{1, 2, 3, 4, 5, 6, 7} - res := CycleIntSliceByN(s, 0) - test.That(t, res, test.ShouldResemble, []int{1, 2, 3, 4, 5, 6, 7}) - res = CycleIntSliceByN(s, 7) - test.That(t, res, test.ShouldResemble, []int{1, 2, 3, 4, 5, 6, 7}) - res = CycleIntSliceByN(s, 14) - test.That(t, res, test.ShouldResemble, []int{1, 2, 3, 4, 5, 6, 7}) - res = CycleIntSliceByN(s, 1) - test.That(t, res, test.ShouldResemble, []int{2, 3, 4, 5, 6, 7, 1}) - res = CycleIntSliceByN(s, 15) - test.That(t, res, test.ShouldResemble, []int{2, 3, 4, 5, 6, 7, 1}) -} - -func TestNRegularlySpaced(t *testing.T) { - n := 13 - vMin, vMax := 0.0, 13.0 - res := SampleNRegularlySpaced(n, vMin, vMax) - test.That(t, res, test.ShouldResemble, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) - - vMin, vMax = -5.0, 20.0 - res = SampleNRegularlySpaced(n, vMin, vMax) - test.That(t, res, test.ShouldResemble, []int{-5, -3, -1, 1, 3, 5, 7, 8, 10, 12, 14, 16, 18}) - - vMin, vMax = -5.0, 5.0 - res = SampleNRegularlySpaced(n, vMin, vMax) - test.That(t, res, test.ShouldResemble, []int{-5, -4, -3, -3, -2, -1, 0, 0, 1, 2, 3, 3, 4}) - - vMin, vMax = 5.0, -5.0 - test.That(t, func() { SampleNRegularlySpaced(n, vMin, vMax) }, test.ShouldPanic) -} - -func TestSampleNIntegersUniform(t *testing.T) { - samples1 := SampleNIntegersUniform(256, -7, 8) - test.That(t, len(samples1), test.ShouldEqual, 256) - for _, sample := range samples1 { - test.That(t, sample, test.ShouldBeGreaterThanOrEqualTo, -7) - test.That(t, sample, test.ShouldBeLessThanOrEqualTo, 8) - } - - samples2 := SampleNIntegersUniform(16, -16, 32) - test.That(t, len(samples2), test.ShouldEqual, 16) - for _, sample := range samples2 { - test.That(t, sample, test.ShouldBeGreaterThanOrEqualTo, -16) - test.That(t, sample, test.ShouldBeLessThanOrEqualTo, 32) - } - - samples3 := SampleNIntegersUniform(1000, -4, 16) - // test number of samples - test.That(t, len(samples3), test.ShouldEqual, 1000) -} - -func TestVarToBytes(t *testing.T) { - var f32 float32 - var f64 float64 - var u32 uint32 - f32 = -6.2598534e18 - b := BytesFromFloat32LE(f32) - test.That(t, b, test.ShouldResemble, []byte{0xEF, 0xBE, 0xAD, 0xDE}) - b = BytesFromFloat32BE(f32) - test.That(t, b, test.ShouldResemble, []byte{0xDE, 0xAD, 0xBE, 0xEF}) - f32 = 6.2598534e18 - b = BytesFromFloat32LE(f32) - test.That(t, b, test.ShouldResemble, []byte{0xEF, 0xBE, 0xAD, 0x5E}) - b = BytesFromFloat32BE(f32) - test.That(t, b, test.ShouldResemble, []byte{0x5E, 0xAD, 0xBE, 0xEF}) - - f64 = -1.1885958550205170e+148 - b = BytesFromFloat64LE(f64) - test.That(t, b, test.ShouldResemble, - []byte{0x01, 0xEE, 0xFF, 0xC0, 0xEF, 0xBE, 0xAD, 0xDE}) - b = BytesFromFloat64BE(f64) - test.That(t, b, test.ShouldResemble, - []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xC0, 0xFF, 0xEE, 0x01}) - f64 = 1.1885958550205170e+148 - b = BytesFromFloat64LE(f64) - test.That(t, b, test.ShouldResemble, - []byte{0x01, 0xEE, 0xFF, 0xC0, 0xEF, 0xBE, 0xAD, 0x5E}) - b = BytesFromFloat64BE(f64) - test.That(t, b, test.ShouldResemble, - []byte{0x5E, 0xAD, 0xBE, 0xEF, 0xC0, 0xFF, 0xEE, 0x01}) - - u32 = 0x12345678 - - b = BytesFromUint32BE(u32) - test.That(t, b, test.ShouldResemble, - []byte{0x12, 0x34, 0x56, 0x78}) - b = BytesFromUint32LE(u32) - test.That(t, b, test.ShouldResemble, - []byte{0x78, 0x56, 0x34, 0x12}) -} - -func TestBytesToVar(t *testing.T) { - var f32 float32 - var f64 float64 - var u32 uint32 - var i16 int16 - f32 = -6.2598534e18 - v := Float32FromBytesLE([]byte{0xEF, 0xBE, 0xAD, 0xDE}) - test.That(t, v, test.ShouldEqual, f32) - v = Float32FromBytesBE([]byte{0xDE, 0xAD, 0xBE, 0xEF}) - test.That(t, v, test.ShouldEqual, f32) - - f64 = -1.1885958550205170e+148 - v64 := Float64FromBytesBE([]byte{0xDE, 0xAD, 0xBE, 0xEF, 0xC0, 0xFF, 0xEE, 0x01}) - test.That(t, v64, test.ShouldEqual, f64) - v64 = Float64FromBytesLE([]byte{0x01, 0xEE, 0xFF, 0xC0, 0xEF, 0xBE, 0xAD, 0xDE}) - test.That(t, v64, test.ShouldEqual, f64) - - u32 = 0x12345678 - vu32 := Uint32FromBytesLE([]byte{0x78, 0x56, 0x34, 0x12}) - test.That(t, vu32, test.ShouldEqual, u32) - vu32 = Uint32FromBytesBE([]byte{0x12, 0x34, 0x56, 0x78}) - test.That(t, vu32, test.ShouldEqual, u32) - - i16 = 0x1234 - vi16 := Int16FromBytesLE([]byte{0x34, 0x12}) - test.That(t, vi16, test.ShouldEqual, i16) - vi16 = Int16FromBytesBE([]byte{0x12, 0x34}) - test.That(t, vi16, test.ShouldEqual, i16) -} diff --git a/utils/matrix_test.go b/utils/matrix_test.go deleted file mode 100644 index e13110dfc69..00000000000 --- a/utils/matrix_test.go +++ /dev/null @@ -1,1153 +0,0 @@ -package utils - -import ( - "fmt" - "sort" - "testing" - - "go.viam.com/test" - "gonum.org/v1/gonum/mat" -) - -func TestRotateMatrixAbout(t *testing.T) { - var m mat.Dense - vec2M := (*Vec2Matrix)(&m) - rot := vec2M.RotateMatrixAbout(0, 0, 45) - test.That(t, rot, test.ShouldEqual, vec2M) -} - -func TestDistanceMSETo(t *testing.T) { - data := []float64{ - 0.6046602879796196, - 0.6645600532184904, - 0.4246374970712657, - 0.06563701921747622, - 0.09696951891448456, - 0.5152126285020654, - 0.21426387258237492, - 0.31805817433032985, - 0.28303415118044517, - 0.6790846759202163, - 0.20318687664732285, - 0.5706732760710226, - 0.29311424455385804, - 0.7525730355516119, - 0.865335013001561, - 0.5238203060500009, - 0.15832827774512764, - 0.9752416188605784, - 0.5948085976830626, - 0.692024587353112, - 0.17326623818270528, - 0.544155573000885, - 0.4231522015718281, - 0.2535405005150605, - 0.7886049150193449, - 0.8805431227416171, - 0.8943617293304537, - 0.9769168685862624, - 0.22228941700678773, - 0.24151508854715265, - 0.932846428518434, - 0.8010550426526613, - 0.18292491645390843, - 0.8969919575618727, - 0.9789293555766876, - 0.09083727535388708, - 0.9269868035744142, - 0.3479539636282229, - 0.7109071952999951, - 0.6494894605929404, - 0.7558235074915978, - 0.13065111702897217, - 0.8963417453962161, - 0.7211477651926741, - 0.08552050754191123, - 0.6227283173637045, - 0.2368225468054852, - 0.18724610140105305, - 0.6280981712183633, - 0.28133029380535923, - 0.43491247389145765, - 0.5501469205077233, - 0.7291807267342981, - 0.0005138155161213613, - 0.39998376285699544, - 0.6039781022829275, - 0.02967128127488647, - 0.0028430411748625642, - 0.5898341850049194, - 0.8154051709333606, - 0.4584424785756506, - 0.02626515060968944, - 0.24969320116349378, - 0.24746660783662855, - 0.5926237532124455, - 0.6938381365172095, - 0.539210105890946, - 0.7507630564795985, - 0.7531612777367586, - 0.35576726540923664, - 0.2318300419376769, - 0.4983943012759756, - 0.02519395979489504, - 0.5893830864007992, - 0.572086801443084, - 0.4117626883450162, - 0.49160739613162047, - 0.7972085409108028, - 0.7830349733960021, - 0.1304138461737918, - 0.7398257810158336, - 0.09838378898573259, - 0.09972966371993512, - 0.07619026230375504, - 0.15965092146489504, - 0.32261068286779754, - 0.5708516273454957, - 0.6841751300974551, - 0.524499759549865, - 0.7163683749016712, - 0.012825909106361078, - 0.098030874806305, - 0.826454125634742, - 0.3443150177263606, - 0.21647114665497036, - 0.4020708452718306, - 0.16867966833433606, - 0.8279280961505588, - 0.05792625966433577, - 0.411540363220476, - 0.7807540845584926, - 0.05349462449440764, - 0.2507622754291802, - 0.9738818740706728, - 0.021533783325605065, - 0.09297015549992493, - 0.31188554282705405, - 0.4872392485803695, - 0.6718291062346395, - 0.9002751472643116, - 0.31933126760711733, - 0.4004323171418129, - 0.6450388660194482, - 0.33959675138730994, - 0.23632747430436052, - 0.035754647436084384, - 0.6258366269581253, - 0.07244835679235131, - 0.8798448463057091, - 0.8475002622646813, - 0.24784452318699535, - 0.07503721013465342, - 0.6293316916453007, - 0.6320034337887998, - 0.014827369494876504, - 0.06875619520215474, - 0.6491884165984236, - 0.8348057602192125, - 0.6613931805833426, - 0.31051027622482125, - 0.967094341371773, - 0.3095484505273281, - 0.4173258421903824, - 0.40673677545039083, - 0.9581763626025936, - 0.7916723090820832, - 0.015181277223073395, - 0.9047762370657334, - 0.0429216421763342, - 0.34785904313005395, - 0.8399474211705598, - 0.12436351859954159, - 0.8349475064934941, - 0.007681206474088089, - 0.37026753645051064, - 0.6432040265702031, - 0.7911253356619845, - 0.34686388037925503, - 0.8220928971765717, - 0.599194252505881, - 0.41358098797631293, - 0.911613706791599, - 0.22891758676059876, - 0.35076512764716117, - 0.3038021298543657, - 0.6715265504608378, - 0.9631394012024704, - 0.807941080954808, - 0.9477602891945563, - 0.9532587542503518, - 0.1815908467575638, - 0.8312410355437677, - 0.8553103464769398, - 0.27349475629159786, - 0.9097612825191185, - 0.4986324518556019, - 0.9758952564996382, - 0.044990698677957075, - 0.9519061481203719, - 0.5357909989896142, - 0.8651749974832864, - 0.5785509024958203, - 0.5506157619831827, - 0.5098654204729554, - 0.49079401441435533, - 0.2624906626498994, - 0.3714866516582223, - 0.4157519697339963, - 0.9016276244796935, - 0.2739245433510268, - 0.8554484128929543, - 0.9891320920320221, - 0.1709460320883929, - 0.5334514497811548, - 0.8135907747765283, - 0.2572075574213995, - 0.33509436689927874, - 0.0048797826077860845, - 0.2295735886966574, - 0.9499374071687937, - 0.9626242011438862, - 0.7126626121646726, - 0.4075321048585774, - 0.34746838606940983, - 0.5975662051444081, - 0.8328558555773872, - 0.9367075689065375, - 0.7203131001891496, - 0.45015392507594826, - 0.472492052251115, - 0.36233147712894287, - 0.04786834817976424, - 0.7562101814022738, - 0.2209988520356824, - 0.39084244515115124, - 0.14968548168959894, - 0.7655885318342354, - 0.4554439687670013, - 0.08196946696466764, - 0.26638179337652607, - 0.09907017544018948, - 0.15920356466786856, - 0.6314765800673349, - 0.9675052460215976, - 0.3381810952188099, - 0.8374659240734459, - 0.502220808518761, - 0.46303950236617286, - 0.7440726180661482, - 0.24841698197784662, - 0.4237021994537764, - 0.6568482587078144, - 0.6367908945869619, - 0.3868614372344463, - 0.12636435926832457, - 0.5113391781716151, - 0.9946063668723257, - 0.7273189933105949, - 0.5140235687917202, - 0.5456026338512396, - 0.2932843538031215, - 0.1417787687869967, - 0.6313482346468529, - 0.0014115440248851888, - 0.8877920216386791, - 0.5961786585231289, - 0.8298384033630892, - 0.06593483186524836, - 0.33551802219817817, - 0.6190296776466547, - 0.32464410822828615, - 0.08418688550119664, - 0.6875681905604295, - 0.022131132163735048, - 0.07824573493651082, - 0.22857519266973128, - 0.27204487725166815, - 0.6618300541680365, - 0.968744565816411, - 0.36440006309012346, - 0.2061752842702243, - 0.7664312698045661, - 0.28790948593091775, - 0.4522039282394723, - 0.38905639165530426, - 0.007528449185090612, - 0.004489301491513168, - 0.23784091936527604, - 0.7931566165268344, - 0.44009472089005885, - 0.7840206765672978, - 0.10218788555665104, - 0.16385844085171358, - 0.12119745631121061, - 0.9983330793002129, - 0.10328868330084902, - 0.314590139237557, - 0.14326690380198182, - 0.6878038995433429, - 0.03281587367327066, - 0.33481839912544525, - 0.348580055442631, - 0.9147014220021253, - 0.15845527169775636, - 0.2631481339983502, - 0.1889547442836859, - 0.6413635631441661, - 0.6283967430668859, - 0.3369850819097053, - 0.4282551326736735, - 0.3467992512856615, - 0.8049930454838599, - 0.37729356339813386, - 0.44730317876928205, - 0.0988527767942791, - 0.026524679327150302, - 0.7834725514535928, - 0.9751350581531069, - 0.4426452337283921, - 0.6124367528672799, - 0.09649270985743633, - 0.23819146822340934, - 0.438007884259053, - 0.2054626561542727, - 0.47017894237198277, - 0.08955778686501577, - 0.018404037976305604, - 0.9508283197764046, - 0.5032641471128878, - 0.5453866093820224, - 0.2023124895049186, - 0.9511724352303262, - 0.9646698427464975, - 0.27157696612578064, - 0.2558710695632199, - 0.18927726966121622, - 0.3565376617022169, - 0.6154037086366806, - 0.7824416824349484, - 0.7642632047982512, - 0.1231719189615159, - 0.5350412597731126, - 0.2185698014148156, - 0.6279724526238374, - 0.45813301659344546, - 0.8669360972460193, - 0.4063892246730919, - 0.4636380185527241, - 0.6231305881147619, - 0.3321902082191424, - 0.7837430530219855, - 0.7845907471751309, - 0.30948957177885994, - 0.05276331263182054, - 0.38209306144499544, - 0.29179316954369106, - 0.42995718000771943, - 0.3250813569198517, - 0.41289957873852917, - 0.7728096346359248, - 0.13817455711021298, - 0.3344156725529401, - 0.6415758176907888, - 0.5408426827578644, - 0.4983312842605871, - 0.06920886569024445, - 0.41435144181497824, - 0.9323084953168621, - 0.7119294744517222, - 0.15061104326485164, - 0.6124508167986009, - 0.6100933314746595, - 0.08417161787544385, - 0.7999475091803873, - 0.10359242822311615, - 0.49823676228315233, - 0.944085605118216, - 0.7851150701963044, - 0.6135903952940208, - 0.6154847981358815, - 0.07226250660480299, - 0.9105083434669217, - 0.8606077665759194, - 0.8337966240141702, - 0.5781396810490688, - 0.07879841835011174, - 0.5504194817781458, - 0.4786594588667231, - 0.18038968874266706, - 0.6582011134231887, - 0.9405090880450124, - 0.4377141871869802, - 0.6868230728671094, - 0.15651925473279124, - 0.30091186058528707, - 0.8136399609900968, - 0.380657189299686, - 0.4688898449024232, - 0.29310185733681576, - 0.21855305259276428, - 0.360871416856906, - 0.8624914374478864, - 0.29708256355629153, - 0.2065826619136986, - 0.6967191657466347, - 0.028303083325889995, - 0.6072534395455154, - 0.07945362337387198, - 0.05912065131387529, - 0.30152268100656, - 0.5410998550087353, - 0.27850762181610883, - 0.5305857153507052, - 0.28208099496492467, - 0.3618054804803169, - 0.2971122606397708, - 0.09745461839911657, - 0.07429099894984302, - 0.6810783123925709, - 0.31152244431052484, - 0.741848959991823, - 0.7302314772948083, - 0.4283570818068078, - 0.6826534880132438, - 0.9222122589217269, - 0.4931419977048804, - 0.9549454404167818, - 0.6908388315056789, - 0.5637795958152644, - 0.5517650490127749, - 0.40380328579570035, - 0.9859647293402467, - 0.3220839705208817, - 0.6445397825093294, - 0.6695752976997745, - 0.3696928436398219, - 0.5352818906344061, - 0.2388407028053186, - 0.1267529293726013, - 0.41032284435628247, - 0.6250950283005304, - 0.6236088264529301, - 0.8305339189948062, - 0.7360686014954314, - 0.497868113342702, - 0.40961827788499267, - 0.0019038945142366389, - 0.915821314612957, - 0.559392449071014, - 0.8780117586524, - 0.6001655953233308, - 0.8458327872480417, - 0.641784290799583, - 0.17365584472313275, - 0.8143945509670211, - 0.03032254783300687, - 0.9756748149873165, - 0.2940063127950149, - 0.150964044979607, - 0.8319308529698163, - 0.6278346050000227, - 0.08983608926036683, - 0.3922161831540248, - 0.9296116354490302, - 0.5885763451434821, - 0.5525803898142438, - 0.9579539135375136, - 0.10738111282075208, - 0.3932509992288867, - 0.19003276633920804, - 0.6540414092312797, - 0.5203802857122278, - 0.15184340208190175, - 0.3152080853201245, - 0.13780406161952607, - 0.5390745170394794, - 0.5127817581110815, - 0.6530402051353608, - 0.654270134424146, - 0.6366442140381798, - 0.030682195787138565, - 0.3691117091643448, - 0.34768170859156955, - 0.2529998236478441, - 0.5550021356347942, - 0.5064970636764183, - 0.33136826030698385, - 0.7002878731458151, - 0.9991594902203332, - 0.11167463676480495, - 0.09211762444074219, - 0.7146958158916296, - 0.8486329209031569, - 0.21256094905031958, - 0.9451947603888259, - 0.6458333745239767, - 0.44846436394045647, - 0.08247967651135, - 0.40018828942364343, - 0.9498832061012532, - 0.4993854937524132, - 0.01980867032545194, - 0.42868843006993296, - 0.8874475008505005, - 0.7650082149332798, - 0.7275772560415229, - 0.5130875060878567, - 0.7242290584591684, - 0.9777634773577185, - 0.8321979381499331, - 0.9133990629364709, - 0.8351038011543529, - 0.7517405788967347, - 0.09693421323873166, - 0.5838347418625311, - 0.9982738110084945, - 0.9854655786332479, - 0.33205608571906026, - 0.9560206265966097, - 0.1843906940020268, - 0.8332418155281546, - 0.8058717675376446, - 0.7185304493527748, - 0.8958032677441458, - 0.018713221139656417, - 0.423553153885841, - 0.43269824007906393, - 0.8557044145748864, - 0.6590305330077544, - 0.5034867900486911, - 0.023109568410543832, - 0.2611756191882184, - 0.3148047959559753, - 0.8997501257175273, - 0.10019940926941497, - 0.7698890899830834, - 0.26238190747072776, - 0.21465371537694145, - 0.3511342996652132, - 0.5783512569311121, - 0.1198505089028631, - 0.05378558010574821, - 0.3241739630613883, - 0.34928874777426255, - 0.9687461599658117, - 0.2079431283731565, - 0.30220237504213365, - 0.1340841627502418, - 0.6408648211682538, - 0.8098742259339521, - 0.9427573715373733, - 0.4946804357820598, - 0.7107439118190982, - 0.4076328718919816, - 0.9443971387003045, - 0.28863831012730923, - 0.45258447627808124, - 0.31536198151820755, - 0.7515630824742361, - 0.6697145888351075, - 0.4588844539038894, - 0.48152982184966137, - 0.9506232438081543, - 0.7425147296618299, - 0.06615141487068936, - 0.9254679440779998, - 0.4094194000310731, - 0.09726159973653944, - 0.004444659798132893, - 0.10930666111680035, - 0.25705535663902546, - 0.9264114223681271, - 0.3038871248932524, - 0.17648961347647024, - 0.7051371238012589, - 0.25036892046498466, - 0.7512406316252606, - 0.8409932064362602, - 0.013285547727582237, - 0.8993714646570142, - 0.043000361954927006, - 0.05109409825821224, - 0.475697373996154, - 0.04071993871109642, - 0.26012467360309705, - 0.9604975052982179, - 0.2293202384473396, - 0.756482324268764, - 0.3389773883954362, - 0.9859943600081704, - 0.24965500730361836, - 0.7897172155299953, - 0.6574690455599712, - 0.6575053763818192, - 0.9192544999689806, - 0.5203920220601794, - 0.24818246016761236, - 0.6997891424274157, - 0.5122720329429057, - 0.6220016345973344, - 0.14057528768697503, - 0.9440150975942463, - 0.24579195449605964, - 0.5369806159703968, - 0.6716676406616031, - 0.7338548372033181, - 0.4234737796578878, - 0.9530446527485383, - 0.45518959189718156, - 0.7198855627833396, - 0.7425908573712917, - 0.682521575399965, - 0.24858283553819602, - 0.7699709528045788, - 0.2866377655128289, - 0.19856153537434532, - 0.6359532835253209, - 0.4123154430150975, - 0.1311899153799101, - 0.8450350165079077, - 0.7783038174399962, - 0.6543795234077353, - 0.39396524478154943, - 0.5658666144424519, - 0.4524828345917371, - 0.6428353371366816, - 0.8103801590743087, - 0.08147470507461664, - 0.49063473734720886, - 0.6276587359645659, - 0.7932145217370414, - 0.2543257200511509, - 0.4202173656755083, - 0.21699679248744047, - 0.5638443341393955, - 0.36757356661613383, - 0.900299766345919, - 0.4451299089920236, - 0.667613339955574, - 0.006626965546730929, - 0.782455054262922, - 0.8082416219927049, - 0.6099497178047082, - 0.22650959076197102, - 0.24686460113721356, - 0.15347570862157847, - 0.9092035473471718, - 0.3272630685304311, - 0.43029177967211024, - 0.09960947545721176, - 0.5220790936787113, - 0.8482870257410837, - 0.5654042126543889, - 0.27535449617052216, - 0.9292998432035774, - 0.21087109674193696, - 0.4724291506924252, - 0.37547840737864574, - 0.7879010097201398, - 0.0963890594348332, - 0.523107427839454, - 0.18027178584513548, - 0.36810106115736846, - 0.09101785536353409, - 0.3944866312415146, - 0.7558532255910314, - 0.3186824449914864, - 0.2784447833137586, - 0.5774681270476576, - 0.11049024076434935, - 0.3809836754593058, - 0.8830369117118917, - 0.17984592548596948, - 0.1893724701430174, - 0.1646351770617486, - 0.6511740108138305, - 0.183759416747548, - 0.7312381165996661, - 0.25125951839063504, - 0.022463358900934063, - 0.5000720043313008, - 0.2810097094575801, - 0.9154829851049563, - 0.702914827930527, - 0.9416776792342575, - 0.7692554102175037, - 0.09392256985342647, - 0.0632202608191323, - 0.797744931979739, - 0.40473685055146047, - 0.31816877565826934, - 0.7912070123859711, - 0.1660286029311165, - 0.6496509866575767, - 0.6164662013388623, - 0.96042849606088, - 0.9308120167007626, - 0.6755204214103049, - 0.06582839048524818, - 0.7241292930553304, - 0.8088521887446851, - 0.5455248138303446, - 0.050571327578438616, - 0.6189388463813191, - 0.472300900550716, - 0.5670387879048848, - 0.6080214484445662, - 0.8544671050335831, - 0.6947663381383937, - 0.6917797275262825, - 0.7888323936371067, - 0.852016155104462, - 0.027118954252390824, - 0.17814602079049136, - 0.7737291616975424, - 0.37308960874062175, - 0.21616470165894677, - 0.5012675271840176, - 0.2880752276001944, - 0.24183011539986346, - 0.5744830737306705, - 0.055145272584174884, - 0.17822265745369018, - 0.6531595287075836, - 0.2512089677286095, - 0.6554169687050234, - 0.25932343469081554, - 0.02679770375645185, - 0.8470474567561609, - 0.14798565561009658, - 0.7387964087793741, - 0.8605926769895941, - 0.4150779924271759, - 0.21126747625859899, - 0.2126205839936398, - 0.1627235387727422, - 0.8202179406328636, - 0.2447240722097798, - 0.2542048974310394, - 0.2043277928270828, - 0.525427196619863, - 0.46556409529180914, - 0.7835511023241203, - 0.8979519762576306, - 0.29421953857387145, - 0.9775977087121448, - 0.4242160149698978, - 0.05683202156480986, - 0.24076899686988937, - 0.4165327185093328, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - } - left := mat.NewDense(3, 360, data) - leftVec2 := (*Vec2Matrix)(left) - dist := leftVec2.DistanceMSETo(leftVec2) - test.That(t, dist, test.ShouldEqual, 0) - - rot := leftVec2.RotateMatrixAbout(0, 0, 0) - dist = leftVec2.DistanceMSETo(rot) - test.That(t, dist, test.ShouldEqual, 0) - - rot = leftVec2.RotateMatrixAbout(0, 0, 45) - dist = leftVec2.DistanceMSETo(rot) - test.That(t, dist, test.ShouldEqual, 0.7560178601305556) - - rot = leftVec2.RotateMatrixAbout(0, 0, 70) - dist = leftVec2.DistanceMSETo(rot) - test.That(t, dist, test.ShouldEqual, 0.7633847913096043) - - rot = leftVec2.RotateMatrixAbout(0, 0, 90) - dist = leftVec2.DistanceMSETo(rot) - test.That(t, dist, test.ShouldEqual, 0.7657411445029718) - - rot = leftVec2.RotateMatrixAbout(0, 0, 270) - dist = leftVec2.DistanceMSETo(rot) - test.That(t, dist, test.ShouldEqual, 1.207983341766853) - - rot = leftVec2.RotateMatrixAbout(0, 0, 360) - dist = leftVec2.DistanceMSETo(rot) - test.That(t, dist, test.ShouldEqual, 0) - - rot = leftVec2.RotateMatrixAbout(1, 1, 45) - dist = leftVec2.DistanceMSETo(rot) - test.That(t, dist, test.ShouldEqual, 0.4622326374440451) -} - -func TestSortVec2Fs(t *testing.T) { - for i, tc := range []struct { - values Vec2Fs - expected Vec2Fs - }{ - {nil, nil}, - {Vec2Fs{}, Vec2Fs{}}, - {Vec2Fs{[]float64{0, 0}}, Vec2Fs{[]float64{0, 0}}}, - {Vec2Fs{[]float64{0, 0}, []float64{1, 0}}, Vec2Fs{[]float64{0, 0}, []float64{1, 0}}}, - {Vec2Fs{[]float64{1, 0}, []float64{0, 0}}, Vec2Fs{[]float64{0, 0}, []float64{1, 0}}}, - {Vec2Fs{[]float64{0, 0}, []float64{1, 2}, []float64{1, 0}}, Vec2Fs{[]float64{0, 0}, []float64{1, 0}, []float64{1, 2}}}, - } { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - sort.Sort(tc.values) - test.That(t, tc.values, test.ShouldResemble, tc.expected) - }) - } -} diff --git a/utils/meshgrid_test.go b/utils/meshgrid_test.go deleted file mode 100644 index 8c05a4241b5..00000000000 --- a/utils/meshgrid_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package utils - -import ( - "testing" - - "go.viam.com/test" -) - -func TestSingle(t *testing.T) { - x := make([]float64, 3) - x[0] = 0 - x[1] = 1 - x[2] = 2 - mesh := Single(2, x) - test.That(t, len(mesh), test.ShouldEqual, 9) - test.That(t, len(mesh[0]), test.ShouldEqual, 2) - test.That(t, mesh[0][0], test.ShouldEqual, 0) - test.That(t, mesh[0][1], test.ShouldEqual, 0) - test.That(t, mesh[8][0], test.ShouldEqual, 2) - test.That(t, mesh[8][1], test.ShouldEqual, 2) -} diff --git a/utils/parallel_test.go b/utils/parallel_test.go deleted file mode 100644 index abe66a3b941..00000000000 --- a/utils/parallel_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package utils - -import ( - "context" - "errors" - "testing" - "time" - - "go.viam.com/test" - gutils "go.viam.com/utils" -) - -// This test may be flaky due to timing-based tests. -func TestRunInParallel(t *testing.T) { - wait200ms := func(ctx context.Context) error { - gutils.SelectContextOrWait(ctx, 200*time.Millisecond) - return ctx.Err() - } - - elapsed, err := RunInParallel(context.Background(), []SimpleFunc{wait200ms, wait200ms}) - test.That(t, err, test.ShouldBeNil) - test.That(t, elapsed, test.ShouldBeLessThan, 300*time.Millisecond) - test.That(t, elapsed, test.ShouldBeGreaterThan, 180*time.Millisecond) - - errFunc := func(ctx context.Context) error { - return errors.New("bad") - } - - elapsed, err = RunInParallel(context.Background(), []SimpleFunc{wait200ms, wait200ms, errFunc}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, elapsed, test.ShouldBeLessThan, 50*time.Millisecond) - - panicFunc := func(ctx context.Context) error { - panic(1) - } - - _, err = RunInParallel(context.Background(), []SimpleFunc{panicFunc}) - test.That(t, err, test.ShouldNotBeNil) -} - -func TestGroupWorkParallel(t *testing.T) { - // Add one slice of ones with another slice of twos and see if the sum of the entire result is correct. - N := 5001 - sliceA := make([]int, N) - sliceB := make([]int, N) - for i := 0; i < N; i++ { - sliceA[i] = 1 - sliceB[i] = 2 - } - var results []int - err := GroupWorkParallel( - context.Background(), - N, - func(numGroups int) { - results = make([]int, numGroups) - }, - func(groupNum, groupSize, from, to int) (MemberWorkFunc, GroupWorkDoneFunc) { - mySum := 0 - return func(memberNum, workNum int) { - a := sliceA[workNum] - b := sliceB[workNum] - mySum += a + b - }, func() { - results[groupNum] = mySum - } - }, - ) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(results), test.ShouldEqual, ParallelFactor) - total := 0 - for _, ans := range results { - total += ans - } - test.That(t, total, test.ShouldEqual, 3*N) -} diff --git a/utils/trusted_env_test.go b/utils/trusted_env_test.go deleted file mode 100644 index 706f8f45a6f..00000000000 --- a/utils/trusted_env_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "testing" - - "go.viam.com/test" -) - -func TestTrustedEnvironment(t *testing.T) { - test.That(t, IsTrustedEnvironment(context.Background()), test.ShouldBeTrue) - - newCtx, err := WithTrustedEnvironment(context.Background(), true) - test.That(t, err, test.ShouldBeNil) - test.That(t, IsTrustedEnvironment(newCtx), test.ShouldBeTrue) - - newCtx, err = WithTrustedEnvironment(context.Background(), false) - test.That(t, err, test.ShouldBeNil) - test.That(t, IsTrustedEnvironment(newCtx), test.ShouldBeFalse) - - newCtx, err = WithTrustedEnvironment(newCtx, false) - test.That(t, err, test.ShouldBeNil) - test.That(t, IsTrustedEnvironment(newCtx), test.ShouldBeFalse) - - newCtx, err = WithTrustedEnvironment(newCtx, true) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, "elevate") - test.That(t, newCtx, test.ShouldBeNil) -} diff --git a/utils/value_test.go b/utils/value_test.go deleted file mode 100644 index e0f37ae30f6..00000000000 --- a/utils/value_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package utils - -import ( - "math/rand" - "os/exec" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" -) - -func TestAssertType(t *testing.T) { - one := 1 - _, err := AssertType[string](one) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, NewUnexpectedTypeError[string](one)) - - _, err = AssertType[myAssertIfc](one) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeError, NewUnexpectedTypeError[myAssertIfc](one)) - - asserted, err := AssertType[myAssertIfc](myAssertInt(one)) - test.That(t, err, test.ShouldBeNil) - test.That(t, asserted.method1(), test.ShouldBeError, errors.New("cool 8)")) -} - -type myAssertIfc interface { - method1() error -} - -type myAssertInt int - -func (m myAssertInt) method1() error { - return errors.New("cool 8)") -} - -func TestFilterMap(t *testing.T) { - ret := FilterMap(map[string]int{"x": 1, "y": 2}, func(_ string, val int) bool { return val > 1 }) - test.That(t, ret, test.ShouldResemble, map[string]int{"y": 2}) -} - -func TestTesting(t *testing.T) { - test.That(t, Testing(), test.ShouldBeTrue) - cmd := exec.Command("go", "run", "./test_detector") - cmd.Start() - cmd.Wait() - test.That(t, cmd.ProcessState.ExitCode(), test.ShouldEqual, 0) -} - -func TestSafeRand(t *testing.T) { - instance := SafeTestingRand() - source := rand.New(rand.NewSource(0)) - test.That(t, instance.Float64(), test.ShouldEqual, source.Float64()) -} - -func TestFindInSlice(t *testing.T) { - filtered := FindInSlice([]int{1, 2, 3}, func(x int) bool { return x > 2 }) - test.That(t, filtered, test.ShouldNotBeNil) - test.That(t, *filtered, test.ShouldEqual, 3) -} diff --git a/utils/variables_test.go b/utils/variables_test.go deleted file mode 100644 index 50d926277a3..00000000000 --- a/utils/variables_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package utils - -import ( - "fmt" - "strings" - "testing" - - "go.viam.com/test" -) - -func TestJSONTags(t *testing.T) { - testStruct := struct { - FirstVar int `json:"int_var"` - SecondVar float64 `json:"float_var"` - ThirdVar string `json:"string_var"` - FourthVar bool `json:"bool_var,omitempty"` - FifthVar int `json:"-"` - SixthVar float64 - SeventhVar string `json:""` - EigthVar string `json:",omitempty"` - }{} - expectedNames := []TypedName{ - {"int_var", "int"}, - {"float_var", "float64"}, - {"string_var", "string"}, - {"bool_var", "bool"}, - {"SixthVar", "float64"}, - {"SeventhVar", "string"}, - {"EigthVar", "string"}, - } - tagNames := JSONTags(testStruct) - test.That(t, tagNames, test.ShouldHaveLength, 7) - test.That(t, tagNames, test.ShouldResemble, expectedNames) -} - -func TestNameValidations(t *testing.T) { - tests := []struct { - name string - shouldContainErr string - }{ - {name: "a"}, - {name: "1"}, - {name: "justLetters"}, - {name: "numbersAndLetters1"}, - {name: "letters-and-dashes"}, - {name: "letters_and_underscores"}, - {name: "1number"}, - {name: "a!", shouldContainErr: "must only contain"}, - {name: "s p a c e s", shouldContainErr: "must only contain"}, - {name: "period.", shouldContainErr: "must only contain"}, - {name: "emoji👿", shouldContainErr: "must only contain"}, - {name: "-dashstart", shouldContainErr: "must only contain"}, - {name: strings.Repeat("a", 201), shouldContainErr: "or fewer"}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.shouldContainErr == "" { - test.That(t, ValidateResourceName(tc.name), test.ShouldBeNil) - test.That(t, ValidateModuleName(tc.name), test.ShouldBeNil) - test.That(t, ValidatePackageName(tc.name), test.ShouldBeNil) - test.That(t, ValidateRemoteName(tc.name), test.ShouldBeNil) - } else { - test.That(t, fmt.Sprint(ValidateResourceName(tc.name)), test.ShouldContainSubstring, tc.shouldContainErr) - test.That(t, fmt.Sprint(ValidateModuleName(tc.name)), test.ShouldContainSubstring, tc.shouldContainErr) - test.That(t, fmt.Sprint(ValidatePackageName(tc.name)), test.ShouldContainSubstring, tc.shouldContainErr) - test.That(t, fmt.Sprint(ValidateRemoteName(tc.name)), test.ShouldContainSubstring, tc.shouldContainErr) - } - }) - } - // test differences between the validation functions - name := strings.Repeat("a", 61) - test.That(t, fmt.Sprint(ValidateResourceName(name)), test.ShouldContainSubstring, "or fewer") - test.That(t, fmt.Sprint(ValidateRemoteName(name)), test.ShouldContainSubstring, "or fewer") - test.That(t, ValidateModuleName(name), test.ShouldBeNil) - test.That(t, ValidatePackageName(name), test.ShouldBeNil) -} diff --git a/utils/verify_main_test.go b/utils/verify_main_test.go deleted file mode 100644 index 939f4b7b76e..00000000000 --- a/utils/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package utils - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/utils/walk_test.go b/utils/walk_test.go deleted file mode 100644 index 58373cdb0e9..00000000000 --- a/utils/walk_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package utils - -import ( - "fmt" - "testing" - - "go.viam.com/test" -) - -func TestWalk1(t *testing.T) { - m := map[string]int{} - - Walk(1, 1, 1, func(x, y int) error { - s := fmt.Sprintf("%d,%d", x, y) - old := m[s] - m[s] = old + 1 - return nil - }) - - test.That(t, m, test.ShouldHaveLength, 9) - - for k, v := range m { - t.Run(k, func(t *testing.T) { - test.That(t, v, test.ShouldEqual, 1) - }) - } -} diff --git a/vision/classification/classification_utils.go b/vision/classification/classification_utils.go deleted file mode 100644 index 158c5d3274d..00000000000 --- a/vision/classification/classification_utils.go +++ /dev/null @@ -1,32 +0,0 @@ -package classification - -import ( - "fmt" - "image" - "image/color" - - "github.com/fogleman/gg" - - "go.viam.com/rdk/rimage" -) - -// Overlay returns a color image with the classification labels and confidence scores overlaid on -// the original image. -func Overlay(img image.Image, classifications Classifications) (image.Image, error) { - gimg := gg.NewContextForImage(img) - x := 30 - y := 30 - for _, classification := range classifications { - // Skip unknown labels generated by Viam-trained models. - if classification.Label() == "VIAM_UNKNOWN" { - continue - } - rimage.DrawString(gimg, - fmt.Sprintf("%v: %.2f", classification.Label(), classification.Score()), - image.Point{x, y}, - color.NRGBA{255, 0, 0, 255}, - 30) - y += 30 - } - return gimg.Image(), nil -} diff --git a/vision/classification/classifier.go b/vision/classification/classifier.go deleted file mode 100644 index a407a285dee..00000000000 --- a/vision/classification/classifier.go +++ /dev/null @@ -1,56 +0,0 @@ -// Package classification implements a classifier for use as a visModel in the vision service -package classification - -import ( - "context" - "fmt" - "image" - "sort" -) - -// Classification returns a confidence score of the classification and a label of the class. -type Classification interface { - Score() float64 - Label() string -} - -// Classifications is a list of the Classification object. -type Classifications []Classification - -// TopN finds the N Classifications with the highest confidence scores. -func (cc Classifications) TopN(n int) (Classifications, error) { - if len(cc) < n { - n = len(cc) - } - sort.Slice(cc, func(i, j int) bool { return cc[i].Score() > cc[j].Score() }) - return cc[0:n], nil -} - -// A Classifier is defined as a function from an image to a list of Classifications. -type Classifier func(context.Context, image.Image) (Classifications, error) - -// NewClassification creates a simple 2D classification. -func NewClassification(score float64, label string) Classification { - return &classification2D{score, label} -} - -// classificationD is a simple struct for storing 2D classifications. -type classification2D struct { - score float64 - label string -} - -// Score returns a confidence score of the classification between 0.0 and 1.0. -func (c *classification2D) Score() float64 { - return c.score -} - -// Label returns the class label of the object in the bounding box. -func (c *classification2D) Label() string { - return c.label -} - -// String turns the classification into a string. -func (c *classification2D) String() string { - return fmt.Sprintf("Label: %s, Score: %.2f", c.label, c.score) -} diff --git a/vision/classification/postprocessor.go b/vision/classification/postprocessor.go deleted file mode 100644 index c0a4f4f81b6..00000000000 --- a/vision/classification/postprocessor.go +++ /dev/null @@ -1,61 +0,0 @@ -package classification - -import "strings" - -// Postprocessor defines a function that filters/modifies on an incoming array of Classifications. -type Postprocessor func(Classifications) Classifications - -// NewScoreFilter returns a function that filters out classifications below a certain confidence -// score. -func NewScoreFilter(conf float64) Postprocessor { - return func(in Classifications) Classifications { - out := make(Classifications, 0, len(in)) - for _, c := range in { - if c.Score() >= conf { - out = append(out, c) - } - } - return out - } -} - -// NewLabelFilter returns a function that filters out classifications without one of the chosen labels. -// Does not filter when input is empty. -func NewLabelFilter(labels map[string]interface{}) Postprocessor { - return func(in Classifications) Classifications { - if len(labels) < 1 { - return in - } - out := make(Classifications, 0, len(in)) - for _, c := range in { - if _, ok := labels[strings.ToLower(c.Label())]; ok { - out = append(out, c) - } - } - return out - } -} - -// NewLabelConfidenceFilter returns a function that filters out classifications based on label map. -// Does not filter when input is empty. -func NewLabelConfidenceFilter(labels map[string]float64) Postprocessor { - // ensure all the label names are lower case - theLabels := make(map[string]float64) - for name, conf := range labels { - theLabels[strings.ToLower(name)] = conf - } - return func(in Classifications) Classifications { - if len(theLabels) < 1 { - return in - } - out := make(Classifications, 0, len(in)) - for _, c := range in { - if conf, ok := theLabels[strings.ToLower(c.Label())]; ok { - if c.Score() >= conf { - out = append(out, c) - } - } - } - return out - } -} diff --git a/vision/classification/postprocessor_test.go b/vision/classification/postprocessor_test.go deleted file mode 100644 index e36175483ba..00000000000 --- a/vision/classification/postprocessor_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package classification - -import ( - "testing" - - "go.viam.com/test" -) - -func TestLabelConfidencePostprocessor(t *testing.T) { - d := []Classification{ - NewClassification(0.5, "A"), - NewClassification(0.1, "a"), - NewClassification(0.1, "B"), - NewClassification(0.6, "b"), - NewClassification(1, "C"), - NewClassification(0.8773934448, "D"), - } - - postNoFilter := NewLabelConfidenceFilter(nil) // no filtering - results := postNoFilter(d) - test.That(t, len(results), test.ShouldEqual, len(d)) - - label := map[string]float64{"a": 0.5, "B": 0.5} - postFilter := NewLabelConfidenceFilter(label) - results = postFilter(d) - test.That(t, len(results), test.ShouldEqual, 2) - labelList := make([]string, 2) - for _, g := range results { - labelList = append(labelList, g.Label()) - } - test.That(t, labelList, test.ShouldContain, "A") - test.That(t, labelList, test.ShouldContain, "b") -} diff --git a/vision/delaunay/convex_hull.go b/vision/delaunay/convex_hull.go deleted file mode 100644 index 5631e01f4ea..00000000000 --- a/vision/delaunay/convex_hull.go +++ /dev/null @@ -1,62 +0,0 @@ -package delaunay - -import "sort" - -func cross2D(p, a, b Point) float64 { - return (a.X-p.X)*(b.Y-p.Y) - (a.Y-p.Y)*(b.X-p.X) -} - -// ConvexHull returns the convex hull of the provided points. -func ConvexHull(points []Point) []Point { - // copy points - pointsCopy := make([]Point, len(points)) - copy(pointsCopy, points) - points = pointsCopy - - // sort points - sort.Slice(points, func(i, j int) bool { - a := points[i] - b := points[j] - if a.X != b.X { - return a.X < b.X - } - return a.Y < b.Y - }) - - // filter nearly-duplicate points - distinctPoints := points[:0] - for i, p := range points { - if i > 0 && p.squaredDistance(points[i-1]) < eps { - continue - } - distinctPoints = append(distinctPoints, p) - } - points = distinctPoints - - // find upper and lower portions - var U, L []Point - for _, p := range points { - for len(U) > 1 && cross2D(U[len(U)-2], U[len(U)-1], p) > 0 { - U = U[:len(U)-1] - } - for len(L) > 1 && cross2D(L[len(L)-2], L[len(L)-1], p) < 0 { - L = L[:len(L)-1] - } - U = append(U, p) - L = append(L, p) - } - - // reverse upper portion - for i, j := 0, len(U)-1; i < j; i, j = i+1, j-1 { - U[i], U[j] = U[j], U[i] - } - - // construct complete hull - if len(U) > 0 { - U = U[:len(U)-1] - } - if len(L) > 0 { - L = L[:len(L)-1] - } - return append(L, U...) -} diff --git a/vision/delaunay/delaunay_point.go b/vision/delaunay/delaunay_point.go deleted file mode 100644 index 844e8ac4e82..00000000000 --- a/vision/delaunay/delaunay_point.go +++ /dev/null @@ -1,24 +0,0 @@ -package delaunay - -import ( - "math" - - "github.com/golang/geo/r2" -) - -// Point is a float64 2d point used in Delaunay triangulation. -type Point r2.Point - -func (a Point) squaredDistance(b Point) float64 { - dx := a.X - b.X - dy := a.Y - b.Y - return dx*dx + dy*dy -} - -func (a Point) distance(b Point) float64 { - return math.Hypot(a.X-b.X, a.Y-b.Y) -} - -func (a Point) sub(b Point) Point { - return Point{a.X - b.X, a.Y - b.Y} -} diff --git a/vision/delaunay/delaunay_test.go b/vision/delaunay/delaunay_test.go deleted file mode 100644 index d57973e15c4..00000000000 --- a/vision/delaunay/delaunay_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package delaunay - -import ( - "math" - "math/rand" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" -) - -// uniform samples points from a uniform 2D distribution. -func uniform(n int, rnd *rand.Rand) []Point { - points := make([]Point, n) - for i := range points { - x := rnd.Float64() - y := rnd.Float64() - points[i] = Point{x, y} - } - return points -} - -// normal samples 2d points from a 2d normal distribution. -func normal(n int, rnd *rand.Rand) []Point { - points := make([]Point, n) - for i := range points { - x := rnd.NormFloat64() - y := rnd.NormFloat64() - points[i] = Point{x, y} - } - return points -} - -// grid samples 2d points on a 2d grid. -func grid(n int) []Point { - side := int(math.Floor(math.Sqrt(float64(n)))) - n = side * side - points := make([]Point, 0, n) - for y := 0; y < side; y++ { - for x := 0; x < side; x++ { - p := Point{float64(x), float64(y)} - points = append(points, p) - } - } - return points -} - -// circle samples 2d point on a circle. -func circle(n int) []Point { - points := make([]Point, n) - for i := range points { - t := float64(i) / float64(n) - x := math.Cos(t) - y := math.Sin(t) - points[i] = Point{x, y} - } - return points -} - -func TestConvexHull(t *testing.T) { - // test square + point at the centroid - points := []Point{{0, 0}, {1, 1}, {0, 1}, {1, 0}, {0.5, 0.5}} - hull := ConvexHull(points) - test.That(t, hull, test.ShouldNotBeNil) - test.That(t, len(hull), test.ShouldEqual, 4) - test.That(t, hull[0], test.ShouldResemble, Point{0, 0}) - test.That(t, hull[1], test.ShouldResemble, Point{1, 0}) - test.That(t, hull[2], test.ShouldResemble, Point{1, 1}) - test.That(t, hull[3], test.ShouldResemble, Point{0, 1}) - // test segment - points2 := []Point{{0, 0}, {1, 1}} - hull2 := ConvexHull(points2) - test.That(t, len(hull2), test.ShouldEqual, len(points2)) - test.That(t, hull2[0], test.ShouldResemble, points2[0]) - test.That(t, hull2[1], test.ShouldResemble, points2[1]) - // test one point: empty convex hull - points1 := []Point{{1, 1}} - hull1 := ConvexHull(points1) - test.That(t, len(hull1), test.ShouldEqual, 0) -} - -func TestTricky(t *testing.T) { - var points []Point - rnd := rand.New(rand.NewSource(99)) - for len(points) < 100000 { - x := rnd.NormFloat64() * 0.5 - y := rnd.NormFloat64() * 0.5 - points = append(points, Point{x, y}) - nx := rnd.Intn(4) - for i := 0; i < nx; i++ { - x = math.Nextafter(x, x+1) - } - ny := rnd.Intn(4) - for i := 0; i < ny; i++ { - y = math.Nextafter(y, y+1) - } - points = append(points, Point{x, y}) - } - tri, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(tri.Points), test.ShouldEqual, 100000) - test.That(t, len(tri.Triangles), test.ShouldEqual, 299958) -} - -func TestCases(t *testing.T) { - // test one point - tri1, err := Triangulate([]Point{{0, 0}}) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, len(tri1.Points), test.ShouldEqual, 1) - test.That(t, len(tri1.Triangles), test.ShouldEqual, 0) - - // test 3 times same point - tri2, err := Triangulate([]Point{{0, 0}, {0, 0}, {0, 0}}) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, len(tri2.Points), test.ShouldEqual, 3) - test.That(t, len(tri1.Triangles), test.ShouldEqual, 0) - - // test 2 points - tri3, err := Triangulate([]Point{{0, 0}, {1, 0}}) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, len(tri3.Points), test.ShouldEqual, 2) - test.That(t, len(tri1.Triangles), test.ShouldEqual, 0) - - // test collinear points - tri4, err := Triangulate([]Point{{0, 0}, {1, 0}, {2, 0}, {3, 0}}) - - test.That(t, err, test.ShouldNotBeNil) - test.That(t, len(tri4.Points), test.ShouldEqual, 4) - test.That(t, len(tri1.Triangles), test.ShouldEqual, 0) - - // test should work - points := []Point{ - {516, 661}, - {369, 793}, - {426, 539}, - {273, 525}, - {204, 694}, - {747, 750}, - {454, 390}, - } - tri5, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(tri5.Points), test.ShouldEqual, 7) - test.That(t, len(tri5.Triangles), test.ShouldEqual, 21) -} - -func TestUniform(t *testing.T) { - rnd := rand.New(rand.NewSource(99)) - points := uniform(100000, rnd) - tri, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(tri.Points), test.ShouldEqual, 100000) - test.That(t, len(tri.Triangles), test.ShouldEqual, 599910) - test.That(t, len(tri.ConvexHull), test.ShouldEqual, 28) -} - -func TestNormal(t *testing.T) { - rnd := rand.New(rand.NewSource(99)) - points := normal(100000, rnd) - tri, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(tri.Points), test.ShouldEqual, 100000) - test.That(t, len(tri.Triangles), test.ShouldEqual, 599943) - test.That(t, len(tri.ConvexHull), test.ShouldEqual, 17) -} - -func TestGrid(t *testing.T) { - points := grid(100000) - tri, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(tri.Points), test.ShouldEqual, 99856) - test.That(t, len(tri.Triangles), test.ShouldEqual, 595350) - test.That(t, len(tri.ConvexHull), test.ShouldEqual, 1260) - - // test that parallelograms on a grid have an area of 1 - ts := tri.Triangles - for i := 0; i < len(ts); i += 3 { - p0 := points[ts[i+0]] - p1 := points[ts[i+1]] - p2 := points[ts[i+2]] - a := area(p0, p1, p2) - test.That(t, a, test.ShouldEqual, 1) - } -} - -func TestCircle(t *testing.T) { - points := circle(10000) - tri, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(tri.Points), test.ShouldEqual, 10000) - test.That(t, len(tri.ConvexHull), test.ShouldEqual, 10000) -} - -func TestGetTriangleIdsMap(t *testing.T) { - points := circle(10000) - tri, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - triangles := tri.Triangles - test.That(t, len(triangles)%3, test.ShouldEqual, 0) - idMap := tri.GetTrianglesPointsMap() - // test number of triangles - test.That(t, len(idMap), test.ShouldEqual, len(triangles)/3) - // test that every triangle has 3 and only 3 points - for _, v := range idMap { - test.That(t, len(v), test.ShouldEqual, 3) - } -} - -func TestGetTriangles(t *testing.T) { - points := circle(10000) - points3D := make([]r3.Vector, len(points)) - for i, pt := range points { - pt3d := r3.Vector{pt.X, pt.Y, 1} - points3D[i] = pt3d - } - tri, err := Triangulate(points) - test.That(t, err, test.ShouldBeNil) - triangles := tri.Triangles - test.That(t, len(triangles)%3, test.ShouldEqual, 0) - triangles3D := tri.GetTriangles(points3D) - // test number of triangles - test.That(t, len(triangles3D), test.ShouldEqual, len(triangles)/3) - // test that every triangle has 3 and only 3 points - for _, v := range triangles3D { - test.That(t, len(v), test.ShouldEqual, 3) - test.That(t, v[0].Z, test.ShouldEqual, 1) - } -} - -func BenchmarkUniform(b *testing.B) { - rnd := rand.New(rand.NewSource(99)) - points := uniform(b.N, rnd) - Triangulate(points) -} - -func BenchmarkNormal(b *testing.B) { - rnd := rand.New(rand.NewSource(99)) - points := normal(b.N, rnd) - Triangulate(points) -} - -func BenchmarkGrid(b *testing.B) { - points := grid(b.N) - Triangulate(points) -} diff --git a/vision/delaunay/node.go b/vision/delaunay/node.go deleted file mode 100644 index bcf35912c96..00000000000 --- a/vision/delaunay/node.go +++ /dev/null @@ -1,30 +0,0 @@ -package delaunay - -type node struct { - i int - t int - prev *node - next *node -} - -func newNode(nodes []node, i int, prev *node) *node { - n := &nodes[i] - n.i = i - if prev == nil { - n.prev = n - n.next = n - } else { - n.next = prev.next - n.prev = prev - prev.next.prev = n - prev.next = n - } - return n -} - -func (n *node) remove() *node { - n.prev.next = n.next - n.next.prev = n.prev - n.i = -1 - return n.prev -} diff --git a/vision/delaunay/triangulation.go b/vision/delaunay/triangulation.go deleted file mode 100644 index 53fcc59089d..00000000000 --- a/vision/delaunay/triangulation.go +++ /dev/null @@ -1,107 +0,0 @@ -// Package delaunay implements 2d Delaunay triangulation -package delaunay - -import ( - "math" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" -) - -// Triangulation stores the points, convex hull, triangles and half edges from a Delaunay triangulation. -type Triangulation struct { - Points []Point - ConvexHull []Point - Triangles []int - Halfedges []int -} - -// Triangulate returns a Delaunay triangulation of the provided points. -func Triangulate(points []Point) (*Triangulation, error) { - t := newTriangulator(points) - err := t.triangulate() - return &Triangulation{points, t.convexHull(), t.triangles, t.halfedges}, err -} - -func (t *Triangulation) area() float64 { - var result float64 - points := t.Points - ts := t.Triangles - for i := 0; i < len(ts); i += 3 { - p0 := points[ts[i+0]] - p1 := points[ts[i+1]] - p2 := points[ts[i+2]] - result += area(p0, p1, p2) - } - return result / 2 -} - -// Validate performs several sanity checks on the Triangulation to check for -// potential errors. Returns nil if no issues were found. You normally -// shouldn't need to call this function but it can be useful for debugging. -func (t *Triangulation) Validate() error { - // verify halfedges - for i1, i2 := range t.Halfedges { - if i1 != -1 && t.Halfedges[i1] != i2 { - return errors.New("invalid halfedge connection") - } - if i2 != -1 && t.Halfedges[i2] != i1 { - return errors.New("invalid halfedge connection") - } - } - - // verify convex hull area vs sum of triangle areas - hull1 := t.ConvexHull - hull2 := ConvexHull(t.Points) - area1 := polygonArea(hull1) - area2 := polygonArea(hull2) - area3 := t.area() - if math.Abs(area1-area2) > 1e-9 || math.Abs(area1-area3) > 1e-9 { - return errors.New("hull areas disagree") - } - - // verify convex hull perimeter - perimeter1 := polygonPerimeter(hull1) - perimeter2 := polygonPerimeter(hull2) - if math.Abs(perimeter1-perimeter2) > 1e-9 { - return errors.New("hull perimeters disagree") - } - - return nil -} - -// GetTrianglesPointsMap returns a map that had triangle ID as key, and points IDs as value. -func (t *Triangulation) GetTrianglesPointsMap() map[int][]int { - triangles := make(map[int][]int) - ts := t.Triangles - currentTriangleID := 0 - for i := 0; i < len(t.Triangles); i += 3 { - id0 := ts[i] - id1 := ts[i+1] - id2 := ts[i+2] - triangles[currentTriangleID] = []int{id0, id1, id2} - // update current triangle ID - currentTriangleID++ - } - return triangles -} - -// GetTriangles returns a slice that has triangle ID as index, and points IDs as value. -func (t *Triangulation) GetTriangles(pts3d []r3.Vector) [][]r3.Vector { - triangles := make([][]r3.Vector, 0, len(t.Triangles)) - ts := t.Triangles - currentTriangleID := 0 - for i := 0; i < len(t.Triangles); i += 3 { - id0 := ts[i] - id1 := ts[i+1] - id2 := ts[i+2] - pt0 := pts3d[id0] - pt1 := pts3d[id1] - pt2 := pts3d[id2] - currentTriangle := []r3.Vector{pt0, pt1, pt2} - triangles = append(triangles, currentTriangle) - // update current triangle ID - currentTriangleID++ - } - return triangles -} diff --git a/vision/delaunay/triangulator.go b/vision/delaunay/triangulator.go deleted file mode 100644 index 74d6a65a53a..00000000000 --- a/vision/delaunay/triangulator.go +++ /dev/null @@ -1,360 +0,0 @@ -package delaunay - -import ( - "errors" - "math" - "sort" -) - -type triangulator struct { - points []Point - squaredDistances []float64 - ids []int - center Point - triangles []int - halfedges []int - trianglesLen int - hull *node - hash []*node -} - -func newTriangulator(points []Point) *triangulator { - return &triangulator{points: points} -} - -// sorting a triangulator sorts the `ids` such that the referenced points -// are in order by their distance to `center` - -// Len computes length of points. -func (tri *triangulator) Len() int { - return len(tri.points) -} - -func (tri *triangulator) Swap(i, j int) { - tri.ids[i], tri.ids[j] = tri.ids[j], tri.ids[i] -} - -func (tri *triangulator) Less(i, j int) bool { - d1 := tri.squaredDistances[tri.ids[i]] - d2 := tri.squaredDistances[tri.ids[j]] - if d1 != d2 { - return d1 < d2 - } - p1 := tri.points[tri.ids[i]] - p2 := tri.points[tri.ids[j]] - if p1.X != p2.X { - return p1.X < p2.X - } - return p1.Y < p2.Y -} - -func (tri *triangulator) triangulate() error { - points := tri.points - - n := len(points) - if n == 0 { - return nil - } - - tri.ids = make([]int, n) - - // compute bounds - x0 := points[0].X - y0 := points[0].Y - x1 := points[0].X - y1 := points[0].Y - for i, p := range points { - if p.X < x0 { - x0 = p.X - } - if p.X > x1 { - x1 = p.X - } - if p.Y < y0 { - y0 = p.Y - } - if p.Y > y1 { - y1 = p.Y - } - tri.ids[i] = i - } - - var i0, i1, i2 int - - // pick a seed point close to midpoint - m := Point{(x0 + x1) / 2, (y0 + y1) / 2} - minDist := infinity - for i, p := range points { - d := p.squaredDistance(m) - if d < minDist { - i0 = i - minDist = d - } - } - - // find point closest to seed point - minDist = infinity - for i, p := range points { - if i == i0 { - continue - } - d := p.squaredDistance(points[i0]) - if d > 0 && d < minDist { - i1 = i - minDist = d - } - } - - // find the third point which forms the smallest circumcircle - minRadius := infinity - for i, p := range points { - if i == i0 || i == i1 { - continue - } - r := circumRadius(points[i0], points[i1], p) - if r < minRadius { - i2 = i - minRadius = r - } - } - if minRadius == infinity { - return errors.New("no Delaunay triangulation exists for this input") - } - - // swap the order of the seed points for counter-clockwise orientation - if area(points[i0], points[i1], points[i2]) < 0 { - i1, i2 = i2, i1 - } - - tri.center = circumcenter(points[i0], points[i1], points[i2]) - - // sort the points by distance from the seed triangle circumcenter - tri.squaredDistances = make([]float64, n) - for i, p := range points { - tri.squaredDistances[i] = p.squaredDistance(tri.center) - } - sort.Sort(tri) - - // initialize a hash table for storing edges of the advancing convex hull - hashSize := int(math.Ceil(math.Sqrt(float64(n)))) - tri.hash = make([]*node, hashSize) - - // initialize a circular doubly-linked list that will hold an advancing convex hull - nodes := make([]node, n) - - e := newNode(nodes, i0, nil) - e.t = 0 - tri.hashEdge(e) - - e = newNode(nodes, i1, e) - e.t = 1 - tri.hashEdge(e) - - e = newNode(nodes, i2, e) - e.t = 2 - tri.hashEdge(e) - - tri.hull = e - - maxTriangles := 2*n - 5 - tri.triangles = make([]int, maxTriangles*3) - tri.halfedges = make([]int, maxTriangles*3) - - tri.addTriangle(i0, i1, i2, -1, -1, -1) - - pp := Point{infinity, infinity} - for k := 0; k < n; k++ { - i := tri.ids[k] - p := points[i] - - // skip nearly-duplicate points - if p.squaredDistance(pp) < eps { - continue - } - pp = p - - // skip seed triangle points - if i == i0 || i == i1 || i == i2 { - continue - } - - // find a visible edge on the convex hull using edge hash - var start *node - key := tri.hashKey(p) - for j := 0; j < len(tri.hash); j++ { - start = tri.hash[key] - if start != nil && start.i >= 0 { - break - } - key++ - if key >= len(tri.hash) { - key = 0 - } - } - start = start.prev - - e := start - for area(p, points[e.i], points[e.next.i]) >= 0 { - e = e.next - if e == start { - e = nil - break - } - } - if e == nil { - // likely a near-duplicate point; skip it - continue - } - walkBack := e == start - - // add the first triangle from the point - t := tri.addTriangle(e.i, i, e.next.i, -1, -1, e.t) - e.t = t // keep track of boundary triangles on the hull - e = newNode(nodes, i, e) - - // recursively flip triangles from the point until they satisfy the Delaunay condition - e.t = tri.legalize(t + 2) - - // walk forward through the hull, adding more triangles and flipping recursively - q := e.next - for area(p, points[q.i], points[q.next.i]) < 0 { - t = tri.addTriangle(q.i, i, q.next.i, q.prev.t, -1, q.t) - q.prev.t = tri.legalize(t + 2) - tri.hull = q.remove() - q = q.next - } - - if walkBack { - // walk backward from the other side, adding more triangles and flipping - q := e.prev - for area(p, points[q.prev.i], points[q.i]) < 0 { - t = tri.addTriangle(q.prev.i, i, q.i, -1, q.t, q.prev.t) - tri.legalize(t + 2) - q.prev.t = t - tri.hull = q.remove() - q = q.prev - } - } - - // save the two new edges in the hash table - tri.hashEdge(e) - tri.hashEdge(e.prev) - } - - tri.triangles = tri.triangles[:tri.trianglesLen] - tri.halfedges = tri.halfedges[:tri.trianglesLen] - - return nil -} - -func (tri *triangulator) hashKey(point Point) int { - d := point.sub(tri.center) - return int(pseudoAngle(d.X, d.Y) * float64(len(tri.hash))) -} - -func (tri *triangulator) hashEdge(e *node) { - tri.hash[tri.hashKey(tri.points[e.i])] = e -} - -// addTriangle add a triangle to the triangulation. -func (tri *triangulator) addTriangle(i0, i1, i2, a, b, c int) int { - i := tri.trianglesLen - tri.triangles[i] = i0 - tri.triangles[i+1] = i1 - tri.triangles[i+2] = i2 - tri.link(i, a) - tri.link(i+1, b) - tri.link(i+2, c) - tri.trianglesLen += 3 - return i -} - -func (tri *triangulator) link(a, b int) { - tri.halfedges[a] = b - if b >= 0 { - tri.halfedges[b] = a - } -} - -func (tri *triangulator) legalize(a int) int { - //nolint:dupword - // if the pair of triangles doesn'tri satisfy the Delaunay condition - // (p1 is inside the circumcircle of [p0, pl, pr]), flip them, - // then do the same check/flip recursively for the new pair of triangles - // - // pl pl - // /||\ / \ - // al/ || \bl al/ \a - // / || \ / \ - // / a||b \ flip /___ar___\ - // p0\ || /p1 => p0\---bl---/p1 - // \ || / \ / - // ar\ || /br b\ /br - // \||/ \ / - // pr pr - - b := tri.halfedges[a] - - a0 := a - a%3 - b0 := b - b%3 - - al := a0 + (a+1)%3 - ar := a0 + (a+2)%3 - bl := b0 + (b+2)%3 - - if b < 0 { - return ar - } - - p0 := tri.triangles[ar] - pr := tri.triangles[a] - pl := tri.triangles[al] - p1 := tri.triangles[bl] - - illegal := inCircle(tri.points[p0], tri.points[pr], tri.points[pl], tri.points[p1]) - - if illegal { - tri.triangles[a] = p1 - tri.triangles[b] = p0 - - // edge swapped on the other side of the hull (rare) - // fix the halfedge reference - if tri.halfedges[bl] == -1 { - e := tri.hull - for { - if e.t == bl { - e.t = a - break - } - e = e.next - if e == tri.hull { - break - } - } - } - - tri.link(a, tri.halfedges[bl]) - tri.link(b, tri.halfedges[ar]) - tri.link(ar, bl) - - br := b0 + (b+1)%3 - - tri.legalize(a) - return tri.legalize(br) - } - - return ar -} - -func (tri *triangulator) convexHull() []Point { - var result []Point - e := tri.hull - for e != nil { - result = append(result, tri.points[e.i]) - e = e.prev - if e == tri.hull { - break - } - } - return result -} diff --git a/vision/delaunay/utils.go b/vision/delaunay/utils.go deleted file mode 100644 index f037614c7b3..00000000000 --- a/vision/delaunay/utils.go +++ /dev/null @@ -1,96 +0,0 @@ -package delaunay - -import "math" - -var eps = math.Nextafter(1, 2) - 1 - -var infinity = math.Inf(1) - -func pseudoAngle(dx, dy float64) float64 { - p := dx / (math.Abs(dx) + math.Abs(dy)) - if dy > 0 { - p = (3 - p) / 4 - } else { - p = (1 + p) / 4 - } - return math.Max(0, math.Min(1-eps, p)) -} - -func area(a, b, c Point) float64 { - return (b.Y-a.Y)*(c.X-b.X) - (b.X-a.X)*(c.Y-b.Y) -} - -func inCircle(a, b, c, p Point) bool { - dx := a.X - p.X - dy := a.Y - p.Y - ex := b.X - p.X - ey := b.Y - p.Y - fx := c.X - p.X - fy := c.Y - p.Y - - ap := dx*dx + dy*dy - bp := ex*ex + ey*ey - cp := fx*fx + fy*fy - - return dx*(ey*cp-bp*fy)-dy*(ex*cp-bp*fx)+ap*(ex*fy-ey*fx) < 0 -} - -func circumRadius(a, b, c Point) float64 { - dx := b.X - a.X - dy := b.Y - a.Y - ex := c.X - a.X - ey := c.Y - a.Y - - bl := dx*dx + dy*dy - cl := ex*ex + ey*ey - d := dx*ey - dy*ex - - x := (ey*bl - dy*cl) * 0.5 / d - y := (dx*cl - ex*bl) * 0.5 / d - - r := x*x + y*y - - if bl == 0 || cl == 0 || d == 0 || r == 0 { - return infinity - } - - return r -} - -func circumcenter(a, b, c Point) Point { - dx := b.X - a.X - dy := b.Y - a.Y - ex := c.X - a.X - ey := c.Y - a.Y - - bl := dx*dx + dy*dy - cl := ex*ex + ey*ey - d := dx*ey - dy*ex - - x := a.X + (ey*bl-dy*cl)*0.5/d - y := a.Y + (dx*cl-ex*bl)*0.5/d - - return Point{x, y} -} - -func polygonArea(points []Point) float64 { - var result float64 - for i, p := range points { - q := points[(i+1)%len(points)] - result += (p.X - q.X) * (p.Y + q.Y) - } - return result / 2 -} - -func polygonPerimeter(points []Point) float64 { - if len(points) == 0 { - return 0 - } - var result float64 - q := points[len(points)-1] - for _, p := range points { - result += p.distance(q) - q = p - } - return result -} diff --git a/vision/keypoints/brief.json b/vision/keypoints/brief.json deleted file mode 100644 index 110cb1c929c..00000000000 --- a/vision/keypoints/brief.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "n": 512, - "sampling": 0, - "use_orientation": true, - "patch_size": 24 -} diff --git a/vision/keypoints/briefdesc.go b/vision/keypoints/briefdesc.go deleted file mode 100644 index aedecffaacd..00000000000 --- a/vision/keypoints/briefdesc.go +++ /dev/null @@ -1,159 +0,0 @@ -package keypoints - -import ( - "encoding/json" - "image" - "math" - "os" - "path/filepath" - - uts "go.viam.com/utils" - - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/utils" -) - -// SamplingType stores 0 if a sampling of image points for BRIEF is uniform, 1 if gaussian. -type SamplingType int - -const ( - uniform SamplingType = iota // 0 - normal // 1 - fixed // 2 -) - -// SamplePairs are N pairs of points used to create the BRIEF Descriptors of a patch. -type SamplePairs struct { - P0 []image.Point - P1 []image.Point - N int -} - -// GenerateSamplePairs generates n samples for a patch size with the chosen Sampling Type. -func GenerateSamplePairs(dist SamplingType, n, patchSize int) *SamplePairs { - // sample positions - var xs0, ys0, xs1, ys1 []int - if dist == fixed { - xs0 = sampleIntegers(patchSize, n, dist) - ys0 = sampleIntegers(patchSize, n, dist) - xs1 = sampleIntegers(patchSize, n, dist) - for i := 0; i < n; i++ { - ys1 = append(ys1, -ys0[i]) - if i%2 == 0 { - xs0[i] = 2 * xs0[i] / 3 - xs1[i] = -2 * xs1[i] / 3 - ys1[i] = ys0[i] - } - } - } else { - xs0 = sampleIntegers(patchSize, n, dist) - ys0 = sampleIntegers(patchSize, n, dist) - xs1 = sampleIntegers(patchSize, n, dist) - ys1 = sampleIntegers(patchSize, n, dist) - } - p0 := make([]image.Point, 0, n) - p1 := make([]image.Point, 0, n) - for i := 0; i < n; i++ { - p0 = append(p0, image.Point{X: xs0[i], Y: ys0[i]}) - p1 = append(p1, image.Point{X: xs1[i], Y: ys1[i]}) - } - - return &SamplePairs{P0: p0, P1: p1, N: n} -} - -func sampleIntegers(patchSize, n int, sampling SamplingType) []int { - vMin := math.Round(-(float64(patchSize) - 2) / 2.) - vMax := math.Round(float64(patchSize) / 2.) - switch sampling { - case uniform: - return utils.SampleNIntegersUniform(n, vMin, vMax) - case normal: - return utils.SampleNIntegersNormal(n, vMin, vMax) - case fixed: - return utils.SampleNRegularlySpaced(n, vMin, vMax) - default: - return utils.SampleNIntegersUniform(n, vMin, vMax) - } -} - -// BRIEFConfig stores the parameters. -type BRIEFConfig struct { - N int `json:"n"` // number of samples taken - Sampling SamplingType `json:"sampling"` - UseOrientation bool `json:"use_orientation"` - PatchSize int `json:"patch_size"` -} - -// LoadBRIEFConfiguration loads a BRIEFConfig from a json file. -func LoadBRIEFConfiguration(file string) *BRIEFConfig { - var config BRIEFConfig - filePath := filepath.Clean(file) - configFile, err := os.Open(filePath) - defer uts.UncheckedErrorFunc(configFile.Close) - if err != nil { - return nil - } - jsonParser := json.NewDecoder(configFile) - err = jsonParser.Decode(&config) - if err != nil { - return nil - } - return &config -} - -// ComputeBRIEFDescriptors computes BRIEF descriptors on image img at keypoints kps. -func ComputeBRIEFDescriptors(img *image.Gray, sp *SamplePairs, kps *FASTKeypoints, cfg *BRIEFConfig) ([]Descriptor, error) { - // blur image - kernel := rimage.GetGaussian5() - normalized := kernel.Normalize() - blurred, err := rimage.ConvolveGray(img, normalized, image.Point{2, 2}, 0) - if err != nil { - return nil, err - } - // compute descriptors - - descs := make([]Descriptor, len(kps.Points)) - bnd := blurred.Bounds() - halfSize := cfg.PatchSize / 2 - for k, kp := range kps.Points { - p1 := image.Point{kp.X + halfSize, kp.Y + halfSize} - p2 := image.Point{kp.X + halfSize, kp.Y - halfSize} - p3 := image.Point{kp.X - halfSize, kp.Y + halfSize} - p4 := image.Point{kp.X - halfSize, kp.Y - halfSize} - // Divide by 64 since we store a descriptor as a uint64 array. - descriptor := make([]uint64, sp.N/64) - if !p1.In(bnd) || !p2.In(bnd) || !p3.In(bnd) || !p4.In(bnd) { - descs[k] = descriptor - continue - } - cosTheta := 1.0 - sinTheta := 0.0 - // if use orientation and keypoints are oriented, compute rotation matrix - if cfg.UseOrientation && kps.Orientations != nil { - angle := kps.Orientations[k] - cosTheta = math.Cos(angle) - sinTheta = math.Sin(angle) - } - for i := 0; i < sp.N; i++ { - x0, y0 := float64(sp.P0[i].X), float64(sp.P0[i].Y) - x1, y1 := float64(sp.P1[i].X), float64(sp.P1[i].Y) - // compute rotated sampled coordinates (Identity matrix if no orientation s) - outx0 := int(math.Round(cosTheta*x0 - sinTheta*y0)) - outy0 := int(math.Round(sinTheta*x0 + cosTheta*y0)) - outx1 := int(math.Round(cosTheta*x1 - sinTheta*y1)) - outy1 := int(math.Round(sinTheta*x1 + cosTheta*y1)) - // fill BRIEF descriptor - p0Val := blurred.GrayAt(kp.X+outx0, kp.Y+outy0).Y - p1Val := blurred.GrayAt(kp.X+outx1, kp.Y+outy1).Y - if p0Val > p1Val { - // Casting to an int truncates the float, which is what we want. - descriptorIndex := int64(i / 64) - numPos := i % 64 - // This flips the bit at numPos to 1. - descriptor[descriptorIndex] |= (1 << numPos) - } - } - descs[k] = descriptor - } - return descs, nil -} diff --git a/vision/keypoints/briefdesc_test.go b/vision/keypoints/briefdesc_test.go deleted file mode 100644 index accee1aa446..00000000000 --- a/vision/keypoints/briefdesc_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package keypoints - -import ( - "testing" - - "github.com/fogleman/gg" - "go.viam.com/test" - - "go.viam.com/rdk/logging" -) - -func TestGenerateSamplePairs(t *testing.T) { - logger := logging.NewTestLogger(t) - patchSize := 250 - descSize := 128 - offset := (patchSize / 2) - 1 - tempDir := t.TempDir() - logger.Infof("writing sample points to %s", tempDir) - // create plotter - plotTmpImage := func(fileName string, sp *SamplePairs) { - dc := gg.NewContext(patchSize, patchSize) - dc.SetRGBA(0, 1, 0, 0.5) - for i := 0; i < sp.N; i++ { - dc.SetLineWidth(1.25) - dc.DrawLine( - float64(sp.P0[i].X+offset), float64(sp.P0[i].Y+offset), - float64(sp.P1[i].X+offset), float64(sp.P1[i].Y+offset), - ) - dc.Stroke() - } - dc.SavePNG(tempDir + "/" + fileName) - } - // uniform distribution - uniformDist := GenerateSamplePairs(uniform, descSize, patchSize) - test.That(t, uniformDist.N, test.ShouldEqual, descSize) - test.That(t, len(uniformDist.P0), test.ShouldEqual, descSize) - test.That(t, len(uniformDist.P1), test.ShouldEqual, descSize) - plotTmpImage("uniform_dist.png", uniformDist) - // fixed distribution - fixedDist := GenerateSamplePairs(fixed, descSize, patchSize) - test.That(t, fixedDist.N, test.ShouldEqual, descSize) - test.That(t, len(fixedDist.P0), test.ShouldEqual, descSize) - test.That(t, len(fixedDist.P1), test.ShouldEqual, descSize) - plotTmpImage("fixed_dist.png", fixedDist) -} diff --git a/vision/keypoints/descriptors.go b/vision/keypoints/descriptors.go deleted file mode 100644 index fab44a9f008..00000000000 --- a/vision/keypoints/descriptors.go +++ /dev/null @@ -1,51 +0,0 @@ -package keypoints - -import "errors" - -// Descriptor is an alias for a slice of uint64. -type Descriptor = []uint64 - -// DescriptorsHammingDistance computes the pairwise distances between 2 descriptor arrays. -func DescriptorsHammingDistance(descs1, descs2 []Descriptor) ([][]int, error) { - var m int - var n int - m = len(descs1) - n = len(descs2) - - // Instantiate distances array. - distances := make([][]int, m) - for i := range distances { - distances[i] = make([]int, n) - } - - for i := 0; i < m; i++ { - for j := 0; j < n; j++ { - d, err := descHammingDistance(descs1[i], descs2[j]) - if err != nil { - return nil, err - } - distances[i][j] = d - } - } - return distances, nil -} - -func descHammingDistance(desc1, desc2 Descriptor) (int, error) { - if len(desc1) != len(desc2) { - return 0, errors.New("descriptors must have same length") - } - var x uint64 - var y uint64 - var dist int - for i := 0; i < len(desc1); i++ { - x = desc1[i] - y = desc2[i] - // ^= is bitwise XOR - x ^= y - for x > 0 { - dist++ - x &= x - 1 - } - } - return dist, nil -} diff --git a/vision/keypoints/descriptors_test.go b/vision/keypoints/descriptors_test.go deleted file mode 100644 index ec09a04f018..00000000000 --- a/vision/keypoints/descriptors_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package keypoints - -import ( - "image" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/rimage" -) - -func TestComputeBRIEFDescriptors(t *testing.T) { - // load config - cfg := LoadFASTConfiguration("kpconfig.json") - test.That(t, cfg, test.ShouldNotBeNil) - // load image from artifacts and convert to gray image - im, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess3.jpg")) - test.That(t, err, test.ShouldBeNil) - // Convert to grayscale image - bounds := im.Bounds() - w, h := bounds.Max.X, bounds.Max.Y - imGray := image.NewGray(image.Rect(0, 0, w, h)) - for x := 0; x < w; x++ { - for y := 0; y < h; y++ { - imGray.Set(x, y, im.At(x, y)) - } - } - fastKps := NewFASTKeypointsFromImage(imGray, cfg) - test.That(t, len(fastKps.Points), test.ShouldEqual, 28) - test.That(t, len(fastKps.Orientations), test.ShouldEqual, 28) - isOriented1 := fastKps.IsOriented() - test.That(t, isOriented1, test.ShouldBeTrue) - - // load BRIEF cfg - cfgBrief := LoadBRIEFConfiguration("brief.json") - test.That(t, cfgBrief, test.ShouldNotBeNil) - samplePoints := GenerateSamplePairs(cfgBrief.Sampling, cfgBrief.N, cfgBrief.PatchSize) - briefDescriptors, err := ComputeBRIEFDescriptors(imGray, samplePoints, fastKps, cfgBrief) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(briefDescriptors), test.ShouldEqual, len(fastKps.Points)) -} diff --git a/vision/keypoints/fastkp.go b/vision/keypoints/fastkp.go deleted file mode 100644 index 3f8cf8c3bcf..00000000000 --- a/vision/keypoints/fastkp.go +++ /dev/null @@ -1,285 +0,0 @@ -package keypoints - -import ( - "encoding/json" - "image" - "math" - "os" - "path/filepath" - - "go.viam.com/utils" -) - -// Implementation of FAST keypoint detector -// From paper : -// Rosten, Edward; Tom Drummond (2005). Fusing points and lines for high performance tracking. -// IEEE International Conference on Computer Vision. Vol. 2. pp. 1508–1511. -// Available at: http://www.edwardrosten.com/work/rosten_2005_tracking.pdf -// Resources: -// - https://medium.com/data-breach/introduction-to-fast-features-from-accelerated-segment-test-4ed33dde6d65 -// - https://homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/AV1011/AV1FeaturefromAcceleratedSegmentTest.pdf - -// FASTConfig holds the parameters necessary to compute the FAST keypoints. -type FASTConfig struct { - NMatchesCircle int `json:"n_matches"` - NMSWinSize int `json:"nms_win_size_px"` - Threshold int `json:"threshold"` // between 0 and 255, represents a unit of grayscale intensity of the pixel - Oriented bool `json:"oriented"` - Radius int `json:"radius_px"` -} - -// PixelType stores 0 if a pixel is darker than center pixel, and 1 if brighter. -type PixelType int - -const ( - darker PixelType = iota // 0 - brighter // 1 -) - -// FASTPixel stores coordinates of an image point in a neighborhood and its PixelType. -type FASTPixel struct { - Point image.Point - Type PixelType -} - -// FASTKeypoints stores keypoint locations and orientations (nil if not oriented). -type FASTKeypoints OrientedKeypoints - -// NewFASTKeypointsFromImage returns a pointer to a FASTKeypoints struct containing keypoints locations and -// orientations if Oriented is set to true in the configuration. -func NewFASTKeypointsFromImage(img *image.Gray, cfg *FASTConfig) *FASTKeypoints { - kps := ComputeFAST(img, cfg) - var orientations []float64 - if cfg.Oriented { - orientations = ComputeKeypointsOrientations(img, kps, cfg.Radius) - } - return &FASTKeypoints{ - kps, - orientations, - } -} - -// IsOriented returns true if FASTKeypoints contains orientations. -func (kps *FASTKeypoints) IsOriented() bool { - return !(kps.Orientations == nil) -} - -var ( - // CrossIdx contains the neighbors coordinates in a 3-cross neighborhood. - CrossIdx = []image.Point{{0, 3}, {3, 0}, {0, -3}, {-3, 0}} - // CircleIdx contains the neighbors coordinates in a circle of radius 3 neighborhood. - CircleIdx = []image.Point{ - {0, -3}, - {1, -3}, - {2, -2}, - {3, -1}, - {3, 0}, - {3, 1}, - {2, 2}, - {1, 3}, - {0, 3}, - {-1, 3}, - {-2, 2}, - {-3, 1}, - {-3, 0}, - {-3, -1}, - {-2, -2}, - {-1, -3}, - } -) - -// LoadFASTConfiguration loads a FASTConfig from a json file. -func LoadFASTConfiguration(file string) *FASTConfig { - var config FASTConfig - filePath := filepath.Clean(file) - configFile, err := os.Open(filePath) - defer utils.UncheckedErrorFunc(configFile.Close) - if err != nil { - return nil - } - jsonParser := json.NewDecoder(configFile) - err = jsonParser.Decode(&config) - if err != nil { - return nil - } - return &config -} - -// GetPointValuesInNeighborhood returns a slice of floats containing the values of neighborhood pixels in image img. -func GetPointValuesInNeighborhood(img *image.Gray, coords image.Point, neighborhood []image.Point) []float64 { - vals := make([]float64, len(neighborhood)) - for i := 0; i < len(neighborhood); i++ { - c := img.GrayAt(coords.X+neighborhood[i].X, coords.Y+neighborhood[i].Y).Y - vals[i] = float64(c) - } - return vals -} - -func isValidSliceVals(vals []float64, n int) bool { - cnt := 0 - for _, val := range vals { - // if value is positive, increment count of consecutive darker or brighter pixels in neighborhood - if val > 0 { - cnt++ - } else { - // otherwise, reset count - cnt = 0 - } - if cnt > n { - return true - } - } - return false -} - -func sumOfPositiveValuesSlice(s []float64) float64 { - sum := 0. - for _, it := range s { - if it > 0 { - sum += it - } - } - return sum -} - -func sumOfNegativeValuesSlice(s []float64) float64 { - sum := 0. - for _, it := range s { - if it < 0 { - sum += it - } - } - return sum -} - -func computeNMSScore(img *image.Gray, pix FASTPixel) float64 { - val := float64(img.GrayAt(pix.Point.X, pix.Point.Y).Y) - circleValues := GetPointValuesInNeighborhood(img, pix.Point, CircleIdx) - diffValues := make([]float64, len(circleValues)) - for i, v := range circleValues { - diffValues[i] = v - val - } - if pix.Type == brighter { - return sumOfPositiveValuesSlice(diffValues) - } - return -1. * sumOfNegativeValuesSlice(diffValues) -} - -func canDeleteNMS(pix FASTPixel, pix2Score map[image.Point]float64, winSize int) bool { - for dx := -winSize; dx < winSize+1; dx++ { - for dy := -winSize; dy < winSize+1; dy++ { - neighbor := image.Point{pix.Point.X + dx, pix.Point.Y + dy} - if neighborScore, ok := pix2Score[neighbor]; ok && neighborScore > pix2Score[pix.Point] { - return true - } - } - } - - return false -} - -// nonMaximumSuppression returns maximal keypoints in a window of size 2*winSize+1 around keypoints. -func nonMaximumSuppression(img *image.Gray, kps []FASTPixel, winSize int) KeyPoints { - // compute score map - pix2score := make(map[image.Point]float64) - for _, kp := range kps { - pix2score[kp.Point] = computeNMSScore(img, kp) - } - // initialize keypoints - nmsKps := make([]image.Point, 0) - for _, kp := range kps { - if !canDeleteNMS(kp, pix2score, winSize) { - nmsKps = append(nmsKps, kp.Point) - } - } - return nmsKps -} - -func computeAngle(img *image.Gray, kp image.Point, radius int, halfWidthMax []int) float64 { - w, h := img.Bounds().Max.X, img.Bounds().Max.Y - m10, m01 := 0, 0 - for y := -radius + 1; y < radius; y++ { - if y+kp.Y < 0 || y+kp.Y >= h { - continue - } - currentWidth := halfWidthMax[int(math.Abs(float64(y)))] - for x := 0; x < currentWidth; x++ { - if x+kp.X < w { - m10 += x * int(img.GrayAt(x+kp.X, y+kp.Y).Y) - m01 += y * int(img.GrayAt(x+kp.X, y+kp.Y).Y) - } - } - } - return math.Atan2(float64(m01), float64(m10)) -} - -// ComputeKeypointsOrientations compute keypoints orientations in image. -func ComputeKeypointsOrientations(img *image.Gray, kps KeyPoints, radius int) []float64 { - halfWidthMax := make([]int, radius) - for i := 0; i < radius; i++ { - halfWidthMax[i] = int(math.Sqrt(float64(radius*radius - i*i))) - } - orientations := make([]float64, len(kps)) - for i, kp := range kps { - orientations[i] = computeAngle(img, kp, radius, halfWidthMax) - } - return orientations -} - -func getBrighterValues(s []float64, t float64) []float64 { - brighterValues := make([]float64, len(s)) - for i, v := range s { - if v > t { - brighterValues[i] = 1 - } else { - brighterValues[i] = 0 - } - } - return brighterValues -} - -func getDarkerValues(s []float64, t float64) []float64 { - darkerValues := make([]float64, len(s)) - for i, v := range s { - if v < t { - darkerValues[i] = 1 - } else { - darkerValues[i] = 0 - } - } - return darkerValues -} - -// ComputeFAST computes the location of FAST keypoints. -// The configuration should contain the following parameters -// - nMatchCircle - Minimum number of consecutive pixels out of 16 pixels on the -// circle that should all be either brighter or darker w.r.t -// test-pixel. A point c on the circle is darker w.r.t test pixel p -// if “Ic < Ip - threshold“ and brighter if -// “Ic > Ip + threshold“. -// - nmsWin - int, size of window to perform non-maximum suppression -// - threshold - int, Threshold used to decide whether the pixels on the -// circle are brighter, darker or similar w.r.t. the test pixel. Given in absolute units. -// Decrease the threshold when more corners are desired and -// vice-versa. -func ComputeFAST(img *image.Gray, cfg *FASTConfig) KeyPoints { - kps := make([]FASTPixel, 0) - w, h := img.Bounds().Max.X, img.Bounds().Max.Y - for y := 3; y < h-3; y++ { - for x := 3; x < w-3; x++ { - pixel := float64(img.GrayAt(x, y).Y) - circleValues := GetPointValuesInNeighborhood(img, image.Point{x, y}, CircleIdx) - brighterValues := getBrighterValues(circleValues, pixel+float64(cfg.Threshold)) - darkerValues := getDarkerValues(circleValues, pixel-float64(cfg.Threshold)) - if isValidSliceVals(brighterValues, cfg.NMatchesCircle) { - kps = append(kps, FASTPixel{image.Point{x, y}, brighter}) - } else if isValidSliceVals(darkerValues, cfg.NMatchesCircle) { - kps = append(kps, FASTPixel{image.Point{x, y}, darker}) - } - } - } - // nonMaximumSuppression - nmsKps := nonMaximumSuppression(img, kps, cfg.NMSWinSize) - - return nmsKps -} diff --git a/vision/keypoints/fastkp_test.go b/vision/keypoints/fastkp_test.go deleted file mode 100644 index 773c5008630..00000000000 --- a/vision/keypoints/fastkp_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package keypoints - -import ( - "image" - "image/color" - "image/draw" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/rimage" -) - -func createTestImage() *image.Gray { - rectImage := image.NewGray(image.Rect(0, 0, 300, 200)) - whiteRect := image.Rect(50, 30, 100, 150) - white := color.Gray{255} - black := color.Gray{0} - draw.Draw(rectImage, rectImage.Bounds(), &image.Uniform{black}, image.Point{0, 0}, draw.Src) - draw.Draw(rectImage, whiteRect, &image.Uniform{white}, image.Point{0, 0}, draw.Src) - return rectImage -} - -func TestLoadFASTConfiguration(t *testing.T) { - cfg := LoadFASTConfiguration("kpconfig.json") - test.That(t, cfg, test.ShouldNotBeNil) - test.That(t, cfg.Threshold, test.ShouldEqual, 20) - test.That(t, cfg.NMatchesCircle, test.ShouldEqual, 9) - test.That(t, cfg.NMSWinSize, test.ShouldEqual, 7) -} - -func TestGetPointValuesInNeighborhood(t *testing.T) { - // create test image - rectImage := createTestImage() - // testing cross neighborhood - vals := GetPointValuesInNeighborhood(rectImage, image.Point{50, 30}, CrossIdx) - // test length - test.That(t, len(vals), test.ShouldEqual, 4) - // test values at a corner of the rectangle - test.That(t, vals[0], test.ShouldEqual, 255) - test.That(t, vals[1], test.ShouldEqual, 255) - test.That(t, vals[2], test.ShouldEqual, 0) - test.That(t, vals[3], test.ShouldEqual, 0) - // testing circle neighborhood - valsCircle := GetPointValuesInNeighborhood(rectImage, image.Point{50, 30}, CircleIdx) - // test length - test.That(t, len(valsCircle), test.ShouldEqual, 16) - // test values at a corner of the rectangle - test.That(t, valsCircle[0], test.ShouldEqual, 0) - test.That(t, valsCircle[1], test.ShouldEqual, 0) - test.That(t, valsCircle[2], test.ShouldEqual, 0) - test.That(t, valsCircle[3], test.ShouldEqual, 0) - test.That(t, valsCircle[4], test.ShouldEqual, 255) - test.That(t, valsCircle[5], test.ShouldEqual, 255) - test.That(t, valsCircle[6], test.ShouldEqual, 255) - test.That(t, valsCircle[7], test.ShouldEqual, 255) - test.That(t, valsCircle[8], test.ShouldEqual, 255) - for i := 9; i < len(valsCircle); i++ { - test.That(t, valsCircle[i], test.ShouldEqual, 0) - } -} - -func TestIsValidSlice(t *testing.T) { - tests := []struct { - s []float64 - n int - expected bool - }{ - {[]float64{0, 0, 0, 0, 0}, 9, false}, - {[]float64{1, 1, 1, 1, 1, 1, 1}, 3, true}, - {[]float64{0, 1, 1, 1, 0, 1, 1}, 2, true}, - {[]float64{0, 1, 1, 0, 0, 1, 0}, 2, false}, - } - for _, tst := range tests { - test.That(t, isValidSliceVals(tst.s, tst.n), test.ShouldEqual, tst.expected) - } -} - -func TestSumPositiveValues(t *testing.T) { - tests := []struct { - s []float64 - expected float64 - }{ - {[]float64{0, 0, 0, 0, 0}, 0}, - {[]float64{1, -1, -1, 0, 1, 1, 1}, 4}, - {[]float64{-1, -1, -1, 0, -1, -1, -1}, 0}, - } - for _, tst := range tests { - test.That(t, sumOfPositiveValuesSlice(tst.s), test.ShouldEqual, tst.expected) - } -} - -func TestSumNegativeValues(t *testing.T) { - tests := []struct { - s []float64 - expected float64 - }{ - {[]float64{0, 0, 0, 0, 0}, 0}, - {[]float64{1, -1, -1, 0, 1, 1, 1}, -2}, - {[]float64{-1, -1, -1, 0, -1, -1, -1}, -6}, - } - for _, tst := range tests { - test.That(t, sumOfNegativeValuesSlice(tst.s), test.ShouldEqual, tst.expected) - } -} - -func TestGetBrighterValues(t *testing.T) { - tests := []struct { - s []float64 - t float64 - expected []float64 - }{ - {[]float64{1, 10, 3, 1, 20, 11}, 10, []float64{0, 0, 0, 0, 1, 1}}, - {[]float64{1, 1, 1, 1}, 1, []float64{0, 0, 0, 0}}, - } - for _, tst := range tests { - test.That(t, getBrighterValues(tst.s, tst.t), test.ShouldResemble, tst.expected) - } -} - -func TestGetDarkerValues(t *testing.T) { - tests := []struct { - s []float64 - t float64 - expected []float64 - }{ - {[]float64{1, 10, 3, 1, 20, 11}, 10, []float64{1, 0, 1, 1, 0, 0}}, - {[]float64{1, 1, 1, 1}, 1, []float64{0, 0, 0, 0}}, - } - for _, tst := range tests { - test.That(t, getDarkerValues(tst.s, tst.t), test.ShouldResemble, tst.expected) - } -} - -func TestComputeFAST(t *testing.T) { - // load config - cfg := LoadFASTConfiguration("kpconfig.json") - test.That(t, cfg, test.ShouldNotBeNil) - // load image from artifacts and convert to gray image - im, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess3.jpg")) - test.That(t, err, test.ShouldBeNil) - // Convert to grayscale image - bounds := im.Bounds() - w, h := bounds.Max.X, bounds.Max.Y - imGray := image.NewGray(image.Rect(0, 0, w, h)) - for x := 0; x < w; x++ { - for y := 0; y < h; y++ { - imGray.Set(x, y, im.At(x, y)) - } - } - // compute kps - kpsChess := ComputeFAST(imGray, cfg) - test.That(t, len(kpsChess), test.ShouldEqual, 28) - keyImg := PlotKeypoints(imGray, kpsChess) - test.That(t, keyImg, test.ShouldNotBeNil) - // test with rectangle image - rectImage := createTestImage() - kps := ComputeFAST(rectImage, cfg) - test.That(t, len(kps), test.ShouldEqual, 2) - test.That(t, kps[0], test.ShouldResemble, image.Point{50, 149}) - test.That(t, kps[1], test.ShouldResemble, image.Point{99, 149}) -} - -func TestNewFASTKeypointsFromImage(t *testing.T) { - // load config - cfg := LoadFASTConfiguration("kpconfig.json") - test.That(t, cfg, test.ShouldNotBeNil) - // load image from artifacts and convert to gray image - im, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess3.jpg")) - test.That(t, err, test.ShouldBeNil) - // Convert to grayscale image - bounds := im.Bounds() - w, h := bounds.Max.X, bounds.Max.Y - imGray := image.NewGray(image.Rect(0, 0, w, h)) - for x := 0; x < w; x++ { - for y := 0; y < h; y++ { - imGray.Set(x, y, im.At(x, y)) - } - } - fastKps := NewFASTKeypointsFromImage(imGray, cfg) - test.That(t, len(fastKps.Points), test.ShouldEqual, 28) - test.That(t, len(fastKps.Orientations), test.ShouldEqual, 28) - isOriented1 := fastKps.IsOriented() - test.That(t, isOriented1, test.ShouldBeTrue) - - // test no orientation - cfg.Oriented = false - fastKpsNoOrientation := NewFASTKeypointsFromImage(imGray, cfg) - test.That(t, len(fastKpsNoOrientation.Points), test.ShouldEqual, 28) - test.That(t, fastKpsNoOrientation.Orientations, test.ShouldBeNil) - isOriented2 := fastKpsNoOrientation.IsOriented() - test.That(t, isOriented2, test.ShouldBeFalse) -} diff --git a/vision/keypoints/imagepyramid.go b/vision/keypoints/imagepyramid.go deleted file mode 100644 index 542bbd63fa8..00000000000 --- a/vision/keypoints/imagepyramid.go +++ /dev/null @@ -1,91 +0,0 @@ -package keypoints - -import ( - "image" - "math" - - "github.com/pkg/errors" - - "go.viam.com/rdk/utils" -) - -// ImagePyramid contains a slice of an image and its downscaled images as well as -// the corresponding scales wrt the original image. -type ImagePyramid struct { - Images []*image.Gray - Scales []int -} - -// downscaleNearestGrayImage downscales an image.Gray by factor. -func downscaleNearestGrayImage(img *image.Gray, factor float64) (*image.Gray, error) { - if img == nil { - return nil, errors.New("input image is nil") - } - imgRect := img.Bounds() - newRect := image.Rectangle{ - image.Point{0, 0}, - image.Point{ - int(float64(imgRect.Max.X) / factor), - int(float64(imgRect.Max.Y) / factor), - }, - } - downsized := image.NewGray(newRect) - utils.ParallelForEachPixel(newRect.Max, func(x, y int) { - origXTemp := float64(x) * factor - var origX int - // round original float coordinates to the closest int coordinates - if fraction := origXTemp - float64(int(origXTemp)); fraction >= 0.5 { - origX = int(origXTemp + 1) - } else { - origX = int(origXTemp) - } - origYTemp := float64(y) * factor - var origY int - if fraction := origYTemp - float64(int(origYTemp)); fraction >= 0.5 { - origY = int(origYTemp + 1) - } else { - origY = int(origYTemp) - } - downsized.SetGray(x, y, img.GrayAt(origX, origY)) - }) - return downsized, nil -} - -// GetNumberOctaves returns the number of scales (or Octaves) in the Pyramid computed from the image size. -func GetNumberOctaves(imgSize image.Point) int { - approxNumber := math.Log(math.Min(float64(imgSize.X), float64(imgSize.Y)))/math.Log(2.) - 1 - // handle the case where image is too small to be downscaled - if approxNumber < 0 { - approxNumber = 1. - } - - return int(math.Round(approxNumber)) -} - -// GetImagePyramid return the images in the pyramid as well as the scales corresponding to each image in the -// ImagePyramid struct. -func GetImagePyramid(img *image.Gray) (*ImagePyramid, error) { - imgSize := img.Bounds().Max - // compute number of scales - nOctaves := GetNumberOctaves(imgSize) - // create image and scale slices - scales := make([]int, nOctaves) - images := make([]*image.Gray, nOctaves) - // set first element - images[0] = img - scales[0] = 1 - for i := 1; i < nOctaves; i++ { - im := images[i-1] - downsized, err := downscaleNearestGrayImage(im, 2.) - if err != nil { - return nil, err - } - images[i] = downsized - scales[i] = int(math.Pow(2, float64(i))) - } - pyramid := ImagePyramid{ - Images: images, - Scales: scales, - } - return &pyramid, nil -} diff --git a/vision/keypoints/imagepyramid_test.go b/vision/keypoints/imagepyramid_test.go deleted file mode 100644 index 0b80e8d230a..00000000000 --- a/vision/keypoints/imagepyramid_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package keypoints - -import ( - "image" - "image/color" - "image/draw" - "testing" - - "go.viam.com/test" -) - -func TestDownscaleImage(t *testing.T) { - // create test image - rectImage := image.NewGray(image.Rect(0, 0, 300, 200)) - whiteRect := image.Rect(50, 30, 100, 150) - white := color.Gray{255} - black := color.Gray{0} - draw.Draw(rectImage, rectImage.Bounds(), &image.Uniform{black}, image.Point{0, 0}, draw.Src) - draw.Draw(rectImage, whiteRect, &image.Uniform{white}, image.Point{0, 0}, draw.Src) - - // downsize image - downsized, err := downscaleNearestGrayImage(rectImage, 2.) - // test no error - test.That(t, err, test.ShouldBeNil) - // test new size - newSize := downsized.Bounds().Max - test.That(t, newSize.X, test.ShouldEqual, rectImage.Rect.Max.X/2) - test.That(t, newSize.Y, test.ShouldEqual, rectImage.Rect.Max.Y/2) - // test values around white rect border in downscaled image - test.That(t, downsized.At(25, 15).(color.Gray).Y, test.ShouldEqual, 255) - test.That(t, downsized.At(49, 74).(color.Gray).Y, test.ShouldEqual, 255) - test.That(t, downsized.At(24, 15).(color.Gray).Y, test.ShouldEqual, 0) - test.That(t, downsized.At(50, 75).(color.Gray).Y, test.ShouldEqual, 0) -} - -func TestGetNumberOctaves(t *testing.T) { - tests := []struct { - imgSize image.Point - want int - }{ - {image.Point{200, 400}, 7}, - {image.Point{16, 8}, 2}, - {image.Point{2, 8}, 0}, - } - for _, tst := range tests { - nOct := GetNumberOctaves(tst.imgSize) - test.That(t, nOct, test.ShouldEqual, tst.want) - } -} - -func TestGetImagePyramid(t *testing.T) { - // create test image - rectImage := image.NewGray(image.Rect(0, 0, 300, 200)) - whiteRect := image.Rect(50, 30, 100, 150) - white := color.Gray{255} - black := color.Gray{0} - draw.Draw(rectImage, rectImage.Bounds(), &image.Uniform{black}, image.Point{0, 0}, draw.Src) - draw.Draw(rectImage, whiteRect, &image.Uniform{white}, image.Point{0, 0}, draw.Src) - - pyramid, err := GetImagePyramid(rectImage) - // test no error - test.That(t, err, test.ShouldBeNil) - // test number scales / octaves - test.That(t, len(pyramid.Images), test.ShouldEqual, 7) - test.That(t, len(pyramid.Scales), test.ShouldEqual, 7) - test.That(t, len(pyramid.Scales), test.ShouldEqual, len(pyramid.Scales)) - // test image sizes - imgSize1 := pyramid.Images[1].Bounds().Max - test.That(t, imgSize1.X, test.ShouldEqual, rectImage.Rect.Max.X/2) - test.That(t, imgSize1.Y, test.ShouldEqual, rectImage.Rect.Max.Y/2) - imgSize2 := pyramid.Images[2].Bounds().Max - test.That(t, imgSize2.X, test.ShouldEqual, rectImage.Rect.Max.X/4) - test.That(t, imgSize2.Y, test.ShouldEqual, rectImage.Rect.Max.Y/4) -} diff --git a/vision/keypoints/keypoints.go b/vision/keypoints/keypoints.go deleted file mode 100644 index cdf1340208e..00000000000 --- a/vision/keypoints/keypoints.go +++ /dev/null @@ -1,86 +0,0 @@ -// Package keypoints contains the implementation of keypoints in an image. For now: -// - FAST keypoints -package keypoints - -import ( - "image" - - "github.com/fogleman/gg" - - "go.viam.com/rdk/utils" -) - -type ( - // KeyPoint is an image.Point that contains coordinates of a kp. - KeyPoint image.Point // keypoint type - // KeyPoints is a slice of image.Point that contains several kps. - KeyPoints []image.Point // set of keypoints type -) - -// OrientedKeypoints stores keypoint locations and orientations (nil if not oriented). -type OrientedKeypoints struct { - Points KeyPoints - Orientations []float64 -} - -// PlotKeypoints plots keypoints on image. -func PlotKeypoints(img *image.Gray, kps []image.Point) image.Image { - w, h := img.Bounds().Max.X, img.Bounds().Max.Y - - dc := gg.NewContext(w, h) - dc.DrawImage(img, 0, 0) - - // draw keypoints on image - dc.SetRGBA(0, 0, 1, 0.5) - for _, p := range kps { - dc.DrawCircle(float64(p.X), float64(p.Y), float64(3.0)) - dc.Fill() - } - return dc.Image() -} - -// PlotMatchedLines plots matched keypoints on both images. vertical is true if the images should be stacked on top of each other. -func PlotMatchedLines(im1, im2 image.Image, kps1, kps2 []image.Point, vertical bool) image.Image { - w, h := im1.Bounds().Max.X+im2.Bounds().Max.X, utils.MaxInt(im1.Bounds().Max.Y, im2.Bounds().Max.Y) - if vertical { - w, h = utils.MaxInt(im1.Bounds().Max.X, im2.Bounds().Max.X), im1.Bounds().Max.Y+im2.Bounds().Max.Y - } - - dc := gg.NewContext(w, h) - dc.DrawImage(im1, 0, 0) - if vertical { - dc.DrawImage(im2, 0, im1.Bounds().Max.Y) - } else { - dc.DrawImage(im2, im1.Bounds().Max.X, 0) - } - - // draw keypoint matches on image - dc.SetRGBA(0, 1, 0, 0.5) - for i, p1 := range kps1 { - // Plot every other dot so the lines are decipherable. - if i%2 == 0 { - continue - } - p2 := kps2[i] - dc.SetLineWidth(1.25) - if vertical { - dc.DrawLine(float64(p1.X), float64(p1.Y), float64(p2.X), float64(im1.Bounds().Max.Y+p2.Y)) - } else { - dc.DrawLine(float64(p1.X), float64(p1.Y), float64(im1.Bounds().Max.X+p2.X), float64(p2.Y)) - } - dc.Stroke() - } - return dc.Image() -} - -// RescaleKeypoints rescales given keypoints wrt scaleFactor. -func RescaleKeypoints(kps KeyPoints, scaleFactor int) KeyPoints { - nKeypoints := len(kps) - rescaledKeypoints := make(KeyPoints, nKeypoints) - for i := 0; i < nKeypoints; i++ { - currentKp := kps[i] - rescaled := image.Point{currentKp.X * scaleFactor, currentKp.Y * scaleFactor} - rescaledKeypoints[i] = rescaled - } - return rescaledKeypoints -} diff --git a/vision/keypoints/keypoints_test.go b/vision/keypoints/keypoints_test.go deleted file mode 100644 index 765b7c4a438..00000000000 --- a/vision/keypoints/keypoints_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package keypoints - -import ( - "image" - "math/rand" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/utils" -) - -func generateRandomKeypoint(max int) image.Point { - x := utils.AbsInt(rand.Intn(max)) - y := utils.AbsInt(rand.Intn(max)) - return image.Point{x, y} -} - -func TestRescaleKeypoints(t *testing.T) { - kps := make(KeyPoints, 10) - rescaledKeypoints := RescaleKeypoints(kps, 2) - test.That(t, rescaledKeypoints[0], test.ShouldResemble, kps[0]) - - // test on slice of random keypoints - kps2 := make(KeyPoints, 2) - kps2[0] = generateRandomKeypoint(320) - kps2[1] = generateRandomKeypoint(320) - rescaledKeypoints1 := RescaleKeypoints(kps2, 1) - test.That(t, rescaledKeypoints1[0], test.ShouldResemble, kps2[0]) - test.That(t, rescaledKeypoints1[1], test.ShouldResemble, kps2[1]) - rescaledKeypoints2 := RescaleKeypoints(kps2, 2) - test.That(t, rescaledKeypoints2[0].X, test.ShouldEqual, kps2[0].X*2) - test.That(t, rescaledKeypoints2[0].Y, test.ShouldEqual, kps2[0].Y*2) - test.That(t, rescaledKeypoints2[1].X, test.ShouldEqual, kps2[1].X*2) - test.That(t, rescaledKeypoints2[1].Y, test.ShouldEqual, kps2[1].Y*2) -} diff --git a/vision/keypoints/kpconfig.json b/vision/keypoints/kpconfig.json deleted file mode 100644 index c85ef6ba8e3..00000000000 --- a/vision/keypoints/kpconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "n_matches": 9, - "nms_win_size_px": 7, - "threshold": 20, - "oriented": true, - "radius_px": 16 -} diff --git a/vision/keypoints/matching.go b/vision/keypoints/matching.go deleted file mode 100644 index 7e561ac775e..00000000000 --- a/vision/keypoints/matching.go +++ /dev/null @@ -1,129 +0,0 @@ -package keypoints - -import ( - "sort" - - "github.com/pkg/errors" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/utils" -) - -var logger = logging.NewLogger("matching") - -// rangeInt generates a sliced of integers from l to u-1, with step size step. -func rangeInt(u, l, step int) []int { - if u < l { - logger.Info("Upper bound u is lower than the lower bound l. Inverting u and l.") - u, l = l, u - } - n := (u - l) / step - out := make([]int, n) - current := l - out[0] = l - for i := 1; i < n; i++ { - current += step - out[i] = current - } - return out -} - -// MatchingConfig contains the parameters for matching descriptors. -type MatchingConfig struct { - DoCrossCheck bool `json:"do_cross_check"` - MaxDist int `json:"max_dist_bits"` -} - -// DescriptorMatch contains the index of a match in the first and second set of descriptors, and their score. -type DescriptorMatch struct { - Idx1 int - Idx2 int - Score int - Descriptor1 Descriptor - Descriptor2 Descriptor -} - -// MatchDescriptors takes 2 sets of descriptors and performs matching. -// Order orders: desc1 are being matched to desc2. -func MatchDescriptors(desc1, desc2 []Descriptor, cfg *MatchingConfig, logger logging.Logger) []DescriptorMatch { - distances, err := DescriptorsHammingDistance(desc1, desc2) - if err != nil { - return nil - } - indices1 := rangeInt(len(desc1), 0, 1) - matchedIn2 := utils.GetArgMinDistancesPerRowInt(distances) - // mask for valid indices - maskIdx := make([]int, len(indices1)) - for i := range maskIdx { - maskIdx[i] = 1 - } - if cfg.DoCrossCheck { - // transpose distances - distT := utils.Transpose(distances) - // compute argmin per rows on transposed mat - matchedIn1 := utils.GetArgMinDistancesPerRowInt(distT) - // create mask for indices in cross check - for _, idx := range indices1 { - if indices1[idx] == matchedIn1[matchedIn2[idx]] { - maskIdx[idx] *= 1 - } else { - maskIdx[idx] *= 0 - } - } - } - if cfg.MaxDist > 0 { - for _, idx := range indices1 { - if distances[indices1[idx]][matchedIn2[idx]] < cfg.MaxDist { - maskIdx[idx] *= 1 - } else { - maskIdx[idx] *= 0 - } - } - } - // get the reduced set of matched indices, which will be less than or equal to len(desc1) - dm := make([]DescriptorMatch, 0, len(desc1)) - for i := range desc1 { - if maskIdx[i] == 1 { - dm = append(dm, DescriptorMatch{ - Idx1: indices1[i], - Idx2: matchedIn2[i], - Score: distances[indices1[i]][matchedIn2[i]], - Descriptor1: desc1[indices1[i]], - Descriptor2: desc2[matchedIn2[i]], - }) - } - } - // sort by Score, highest to lowest - sort.Slice(dm, func(i, j int) bool { - return dm[j].Score < dm[i].Score - }) - // fill matches, skip over points in 1 that have already been matched - alreadyMatched := make([]bool, len(indices1)) - matches := make([]DescriptorMatch, 0, len(dm)) - for _, match := range dm { - if !alreadyMatched[match.Idx1] { - matches = append(matches, match) - alreadyMatched[match.Idx1] = true - } - } - return matches -} - -// GetMatchingKeyPoints takes the matches and the keypoints and returns the corresponding keypoints that are matched. -func GetMatchingKeyPoints(matches []DescriptorMatch, kps1, kps2 KeyPoints) (KeyPoints, KeyPoints, error) { - if len(kps1) < len(matches) { - err := errors.New("there are more matches than keypoints in first set") - return nil, nil, err - } - if len(kps2) < len(matches) { - err := errors.New("there are more matches than keypoints in second set") - return nil, nil, err - } - matchedKps1 := make(KeyPoints, len(matches)) - matchedKps2 := make(KeyPoints, len(matches)) - for i, match := range matches { - matchedKps1[i] = kps1[match.Idx1] - matchedKps2[i] = kps2[match.Idx2] - } - return matchedKps1, matchedKps2, nil -} diff --git a/vision/keypoints/matching_test.go b/vision/keypoints/matching_test.go deleted file mode 100644 index 4a00fa271cf..00000000000 --- a/vision/keypoints/matching_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package keypoints - -import ( - "image" - "image/draw" - "math" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" -) - -func TestRangeInt(t *testing.T) { - u1, l1 := 2, -5 - step1 := 1 - r1 := rangeInt(u1, l1, step1) - test.That(t, len(r1), test.ShouldEqual, 7) - test.That(t, r1[0], test.ShouldEqual, -5) - test.That(t, r1[6], test.ShouldEqual, 1) - - u2, l2 := 8, 2 - step2 := 2 - r2 := rangeInt(u2, l2, step2) - test.That(t, len(r2), test.ShouldEqual, 3) - test.That(t, r2[0], test.ShouldEqual, 2) - test.That(t, r2[1], test.ShouldEqual, 4) - test.That(t, r2[2], test.ShouldEqual, 6) - - // test u < l - u3, l3 := 2, 8 - step3 := 2 - r3 := rangeInt(u3, l3, step3) - test.That(t, len(r3), test.ShouldEqual, 3) - test.That(t, r3[0], test.ShouldEqual, 2) - test.That(t, r3[1], test.ShouldEqual, 4) - test.That(t, r3[2], test.ShouldEqual, 6) -} - -func TestMatchDescriptors(t *testing.T) { - logger := logging.NewTestLogger(t) - tempDir := t.TempDir() - - logger.Infof("writing sample points to %s", tempDir) - // load config - cfg := LoadFASTConfiguration("kpconfig.json") - // load image from artifacts and convert to gray image - im, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess3.jpg")) - test.That(t, err, test.ShouldBeNil) - // Convert to grayscale image - imGray := rimage.MakeGray(im) - fastKps := NewFASTKeypointsFromImage(imGray, cfg) - t.Logf("number of keypoints in img 1: %d", len(fastKps.Points)) - keyPtsImg := PlotKeypoints(imGray, fastKps.Points) - err = rimage.WriteImageToFile(tempDir+"/chessKps_1.png", keyPtsImg) - test.That(t, err, test.ShouldBeNil) - - // image 2 - // load image from artifacts and convert to gray image - im2, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess.jpg")) - test.That(t, err, test.ShouldBeNil) - // Convert to grayscale image - imGray2 := rimage.MakeGray(im2) - fastKps2 := NewFASTKeypointsFromImage(imGray2, cfg) - t.Logf("number of keypoints in img 2: %d", len(fastKps2.Points)) - keyPtsImg2 := PlotKeypoints(imGray2, fastKps2.Points) - err = rimage.WriteImageToFile(tempDir+"/chessKps_2.png", keyPtsImg2) - test.That(t, err, test.ShouldBeNil) - - // load BRIEF cfg - cfgBrief := LoadBRIEFConfiguration("brief.json") - samplePoints := GenerateSamplePairs(cfgBrief.Sampling, cfgBrief.N, cfgBrief.PatchSize) - - briefDescriptors, err := ComputeBRIEFDescriptors(imGray, samplePoints, fastKps, cfgBrief) - t.Logf("number of descriptors in img 1: %d", len(briefDescriptors)) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(briefDescriptors), test.ShouldEqual, len(fastKps.Points)) - - briefDescriptors2, err := ComputeBRIEFDescriptors(imGray2, samplePoints, fastKps2, cfgBrief) - t.Logf("number of descriptors in img 2: %d", len(briefDescriptors2)) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(briefDescriptors2), test.ShouldEqual, len(fastKps2.Points)) - // matches - cfgMatch := MatchingConfig{ - true, - 400, - } - // test matches with itself - matches := MatchDescriptors(briefDescriptors, briefDescriptors, &cfgMatch, logger) - t.Logf("number of matches in img 1: %d", len(matches)) - matchedKps1, matchedKps2, err := GetMatchingKeyPoints(matches, fastKps.Points, fastKps.Points) - test.That(t, err, test.ShouldBeNil) - matchedLinesImg := PlotMatchedLines(imGray, imGray, matchedKps1, matchedKps2, false) - err = rimage.WriteImageToFile(tempDir+"/matched_chess.png", matchedLinesImg) - test.That(t, err, test.ShouldBeNil) - for _, match := range matches { - test.That(t, match.Idx1, test.ShouldEqual, match.Idx2) - } - // test matches with bigger image and cross-check; #matches <= #kps2 - matches = MatchDescriptors(briefDescriptors, briefDescriptors2, &cfgMatch, logger) - t.Logf("number of matches in img 1 vs img 2: %d", len(matches)) - test.That(t, len(matches), test.ShouldBeLessThanOrEqualTo, len(fastKps2.Points)) - matchedKps1, matchedKps2, err = GetMatchingKeyPoints(matches, fastKps.Points, fastKps2.Points) - test.That(t, err, test.ShouldBeNil) - matchedLinesImg = PlotMatchedLines(imGray, imGray2, matchedKps1, matchedKps2, false) - err = rimage.WriteImageToFile(tempDir+"/bigger_matched_chess.png", matchedLinesImg) - test.That(t, err, test.ShouldBeNil) -} - -func TestGetMatchingKeyPoints(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := LoadORBConfiguration("orbconfig.json") - test.That(t, err, test.ShouldBeNil) - test.That(t, cfg, test.ShouldNotBeNil) - // load image from artifacts and convert to gray image - im, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess3.jpg")) - test.That(t, err, test.ShouldBeNil) - // Convert to grayscale image - bounds := im.Bounds() - w, h := bounds.Max.X, bounds.Max.Y - imGray := image.NewGray(image.Rect(0, 0, w, h)) - draw.Draw(imGray, imGray.Bounds(), im, im.Bounds().Min, draw.Src) - samplePoints := GenerateSamplePairs(cfg.BRIEFConf.Sampling, cfg.BRIEFConf.N, cfg.BRIEFConf.PatchSize) - descs, kps, err := ComputeORBKeypoints(imGray, samplePoints, cfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(descs), test.ShouldEqual, 58) - test.That(t, len(kps), test.ShouldEqual, 58) - - // get matches - cfgMatch := MatchingConfig{ - true, - 1000, - } - // test matches with itself - matches := MatchDescriptors(descs, descs, &cfgMatch, logger) - - kps1, kps2, err := GetMatchingKeyPoints(matches, kps, kps) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(kps1), test.ShouldEqual, len(kps2)) - for i, pt1 := range kps1 { - pt2 := kps2[i] - test.That(t, math.Abs(float64(pt1.X-pt2.X)), test.ShouldBeLessThan, 1) - test.That(t, math.Abs(float64(pt1.Y-pt2.Y)), test.ShouldBeLessThan, 1) - } -} - -func TestOrbMatching(t *testing.T) { - logger := logging.NewTestLogger(t) - orbConf := &ORBConfig{ - Layers: 4, - DownscaleFactor: 2, - FastConf: &FASTConfig{ - NMatchesCircle: 9, - NMSWinSize: 7, - Threshold: 20, - Oriented: true, - Radius: 16, - }, - BRIEFConf: &BRIEFConfig{ - N: 512, - Sampling: 2, - UseOrientation: true, - PatchSize: 48, - }, - } - matchingConf := &MatchingConfig{ - DoCrossCheck: true, - MaxDist: 1000, - } - img1, err := rimage.NewImageFromFile(artifact.MustPath("vision/odometry/000001.png")) - test.That(t, err, test.ShouldBeNil) - img2, err := rimage.NewImageFromFile(artifact.MustPath("vision/odometry/000002.png")) - test.That(t, err, test.ShouldBeNil) - im1 := rimage.MakeGray(img1) - im2 := rimage.MakeGray(img2) - samplePoints := GenerateSamplePairs(orbConf.BRIEFConf.Sampling, orbConf.BRIEFConf.N, orbConf.BRIEFConf.PatchSize) - // image 1 - orb1, _, err := ComputeORBKeypoints(im1, samplePoints, orbConf) - test.That(t, err, test.ShouldBeNil) - // image 2 - orb2, _, err := ComputeORBKeypoints(im2, samplePoints, orbConf) - test.That(t, err, test.ShouldBeNil) - matches := MatchDescriptors(orb1, orb2, matchingConf, logger) - test.That(t, len(matches), test.ShouldBeGreaterThan, 300) - test.That(t, len(matches), test.ShouldBeLessThan, 350) -} diff --git a/vision/keypoints/orb.go b/vision/keypoints/orb.go deleted file mode 100644 index 73e9ad61eaa..00000000000 --- a/vision/keypoints/orb.go +++ /dev/null @@ -1,98 +0,0 @@ -package keypoints - -import ( - "encoding/json" - "errors" - "image" - "os" - "path/filepath" - - "go.viam.com/utils" - - "go.viam.com/rdk/resource" -) - -// ORBConfig contains the parameters / configs needed to compute ORB features. -type ORBConfig struct { - Layers int `json:"n_layers"` - DownscaleFactor int `json:"downscale_factor"` - FastConf *FASTConfig `json:"fast"` - BRIEFConf *BRIEFConfig `json:"brief"` -} - -// LoadORBConfiguration loads a ORBConfig from a json file. -func LoadORBConfiguration(file string) (*ORBConfig, error) { - var config ORBConfig - filePath := filepath.Clean(file) - configFile, err := os.Open(filePath) - defer utils.UncheckedErrorFunc(configFile.Close) - if err != nil { - return nil, err - } - jsonParser := json.NewDecoder(configFile) - err = jsonParser.Decode(&config) - if err != nil { - return nil, err - } - err = config.Validate(file) - if err != nil { - return nil, err - } - return &config, nil -} - -// Validate ensures all parts of the ORBConfig are valid. -func (config *ORBConfig) Validate(path string) error { - if config.Layers < 1 { - return resource.NewConfigValidationError(path, errors.New("n_layers should be >= 1")) - } - if config.DownscaleFactor <= 1 { - return resource.NewConfigValidationError(path, errors.New("downscale_factor should be greater than 1")) - } - if config.FastConf == nil { - return resource.NewConfigValidationFieldRequiredError(path, "fast") - } - if config.BRIEFConf == nil { - return resource.NewConfigValidationFieldRequiredError(path, "brief") - } - return nil -} - -// ComputeORBKeypoints compute ORB keypoints on gray image. -func ComputeORBKeypoints(im *image.Gray, sp *SamplePairs, cfg *ORBConfig) ([]Descriptor, KeyPoints, error) { - pyramid, err := GetImagePyramid(im) - if err != nil { - return nil, nil, err - } - if cfg.Layers <= 0 { - err = errors.New("number of layers should be > 0") - return nil, nil, err - } - if cfg.DownscaleFactor <= 1 { - err = errors.New("number of layers should be >= 2") - return nil, nil, err - } - if len(pyramid.Scales) < cfg.Layers { - err = errors.New("more layers than actual number of octaves in image pyramid") - return nil, nil, err - } - orbDescriptors := []Descriptor{} - orbPoints := make(KeyPoints, 0) - for i := 0; i < cfg.Layers; i++ { - currentImage := pyramid.Images[i] - currentScale := pyramid.Scales[i] - fastKps := NewFASTKeypointsFromImage(currentImage, cfg.FastConf) - rescaledKps := RescaleKeypoints(fastKps.Points, currentScale) - rescaledFASTKps := FASTKeypoints{ - Points: rescaledKps, - Orientations: fastKps.Orientations, - } - orbPoints = append(orbPoints, rescaledFASTKps.Points...) - descs, err := ComputeBRIEFDescriptors(currentImage, sp, &rescaledFASTKps, cfg.BRIEFConf) - if err != nil { - return nil, nil, err - } - orbDescriptors = append(orbDescriptors, descs...) - } - return orbDescriptors, orbPoints, nil -} diff --git a/vision/keypoints/orb_test.go b/vision/keypoints/orb_test.go deleted file mode 100644 index 4ad6a4d2901..00000000000 --- a/vision/keypoints/orb_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package keypoints - -import ( - "image" - "image/draw" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" -) - -func TestLoadORBConfiguration(t *testing.T) { - cfg, err := LoadORBConfiguration("orbconfig.json") - test.That(t, err, test.ShouldBeNil) - test.That(t, cfg, test.ShouldNotBeNil) - test.That(t, cfg.Layers, test.ShouldEqual, 4) - test.That(t, cfg.DownscaleFactor, test.ShouldEqual, 2) - test.That(t, cfg.FastConf.Threshold, test.ShouldEqual, 20) - test.That(t, cfg.FastConf.NMatchesCircle, test.ShouldEqual, 9) - test.That(t, cfg.FastConf.NMSWinSize, test.ShouldEqual, 7) - - // test config validation - cfg1 := &ORBConfig{ - Layers: 0, - } - err = cfg1.Validate("") - test.That(t, err, test.ShouldBeError) - test.That(t, err.Error(), test.ShouldEqual, "Error validating. Path: \"\" Error: n_layers should be >= 1") - - cfg2 := &ORBConfig{ - Layers: 2, - DownscaleFactor: 1, - } - err = cfg2.Validate("") - test.That(t, err, test.ShouldBeError) - test.That(t, err.Error(), test.ShouldEqual, "Error validating. Path: \"\" Error: downscale_factor should be greater than 1") - - cfg3 := &ORBConfig{ - Layers: 4, - DownscaleFactor: 2, - } - err = cfg3.Validate("") - test.That(t, err, test.ShouldBeError) -} - -func TestComputeORBKeypoints(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := LoadORBConfiguration("orbconfig.json") - test.That(t, err, test.ShouldBeNil) - test.That(t, cfg, test.ShouldNotBeNil) - // load image from artifacts and convert to gray image - im, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess3.jpg")) - test.That(t, err, test.ShouldBeNil) - // Convert to grayscale image - bounds := im.Bounds() - w, h := bounds.Max.X, bounds.Max.Y - imGray := image.NewGray(image.Rect(0, 0, w, h)) - draw.Draw(imGray, imGray.Bounds(), im, im.Bounds().Min, draw.Src) - samplePoints := GenerateSamplePairs(cfg.BRIEFConf.Sampling, cfg.BRIEFConf.N, cfg.BRIEFConf.PatchSize) - descs, kps, err := ComputeORBKeypoints(imGray, samplePoints, cfg) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(descs), test.ShouldEqual, 58) - test.That(t, len(kps), test.ShouldEqual, 58) - // save the output image in a temp file - tempDir := t.TempDir() - logger.Infof("writing orb keypoint files to %s", tempDir) - keyImg := PlotKeypoints(imGray, kps) - test.That(t, keyImg, test.ShouldNotBeNil) - err = rimage.WriteImageToFile(tempDir+"/orb_keypoints.png", keyImg) - test.That(t, err, test.ShouldBeNil) -} - -func TestMatchingWithRotation(t *testing.T) { - logger := logging.NewTestLogger(t) - cfg, err := LoadORBConfiguration("orbconfig.json") - test.That(t, err, test.ShouldBeNil) - test.That(t, cfg, test.ShouldNotBeNil) - // load images from artifacts and convert to gray image - im, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess3_rotate.jpg")) - test.That(t, err, test.ShouldBeNil) - imGray := rimage.MakeGray(im) - imBig, err := rimage.NewImageFromFile(artifact.MustPath("vision/keypoints/chess.jpg")) - test.That(t, err, test.ShouldBeNil) - imBigGray := rimage.MakeGray(imBig) - // compute orb points for each image - samplePoints := GenerateSamplePairs(cfg.BRIEFConf.Sampling, cfg.BRIEFConf.N, cfg.BRIEFConf.PatchSize) - orb1, kps1, err := ComputeORBKeypoints(imGray, samplePoints, cfg) - test.That(t, err, test.ShouldBeNil) - orb2, kps2, err := ComputeORBKeypoints(imBigGray, samplePoints, cfg) - test.That(t, err, test.ShouldBeNil) - cfgMatch := &MatchingConfig{DoCrossCheck: true, MaxDist: 400} - matches := MatchDescriptors(orb1, orb2, cfgMatch, logger) - matchedKps1, matchedKps2, err := GetMatchingKeyPoints(matches, kps1, kps2) - test.That(t, err, test.ShouldBeNil) - matchedOrbPts1 := PlotKeypoints(imGray, matchedKps1) - matchedOrbPts2 := PlotKeypoints(imBigGray, matchedKps2) - matchedLines := PlotMatchedLines(matchedOrbPts1, matchedOrbPts2, matchedKps1, matchedKps2, true) - test.That(t, matchedLines, test.ShouldNotBeNil) - // save the output image in a temp file - tempDir := t.TempDir() - logger.Infof("writing orb keypoint files to %s", tempDir) - err = rimage.WriteImageToFile(tempDir+"/rotated_chess_orb.png", matchedLines) - test.That(t, err, test.ShouldBeNil) -} diff --git a/vision/keypoints/orbconfig.json b/vision/keypoints/orbconfig.json deleted file mode 100644 index b3b2170d4ea..00000000000 --- a/vision/keypoints/orbconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "n_layers": 4, - "downscale_factor": 2, - "fast": { - "n_matches": 9, - "nms_win_size_px": 7, - "threshold": 20, - "oriented": true, - "radius_px": 16 - }, - "brief": { - "n": 512, - "sampling": 2, - "use_orientation": true, - "patch_size": 48 - } -} diff --git a/vision/object.go b/vision/object.go deleted file mode 100644 index a56bbc583ca..00000000000 --- a/vision/object.go +++ /dev/null @@ -1,62 +0,0 @@ -package vision - -import ( - "errors" - "math" - - commonpb "go.viam.com/api/common/v1" - - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/spatialmath" -) - -// Object extends PointCloud with respective metadata, like the center coordinate. -// NOTE(bh):Can potentially add category or pose information to this struct. -type Object struct { - pc.PointCloud - Geometry spatialmath.Geometry -} - -// NewObject creates a new vision.Object from a point cloud with an empty label. -func NewObject(cloud pc.PointCloud) (*Object, error) { - return NewObjectWithLabel(cloud, "", nil) -} - -// NewObjectWithLabel creates a new vision.Object from a point cloud with the given label. -func NewObjectWithLabel(cloud pc.PointCloud, label string, geometry *commonpb.Geometry) (*Object, error) { - if cloud == nil { - return NewEmptyObject(), nil - } - if geometry == nil { - box, err := pc.BoundingBoxFromPointCloudWithLabel(cloud, label) - if err != nil { - return nil, err - } - return &Object{cloud, box}, nil - } - if label != "" { // will override geometry proto label with given label (unless empty) - geometry.Label = label - } - geom, err := spatialmath.NewGeometryFromProto(geometry) - if err != nil { - return nil, err - } - return &Object{PointCloud: cloud, Geometry: geom}, nil -} - -// NewEmptyObject creates a new empty point cloud with metadata. -func NewEmptyObject() *Object { - cloud := pc.New() - return &Object{PointCloud: cloud} -} - -// Distance calculates and returns the distance from the center point of the object to the origin. -func (o *Object) Distance() (float64, error) { - if o.Geometry == nil { - return -1, errors.New("no geometry object defined for distance formula to be applied") - } - point := o.Geometry.Pose().Point() - dist := math.Pow(point.X, 2) + math.Pow(point.Y, 2) + math.Pow(point.Z, 2) - dist = math.Sqrt(dist) - return dist, nil -} diff --git a/vision/object_test.go b/vision/object_test.go deleted file mode 100644 index f3e5628ef96..00000000000 --- a/vision/object_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package vision - -import ( - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/spatialmath" -) - -func TestObjectCreation(t *testing.T) { - // test empty objects - obj, err := NewObject(nil) - test.That(t, err, test.ShouldBeNil) - obj2 := NewEmptyObject() - test.That(t, obj, test.ShouldResemble, obj2) - - // create from point cloud - pc := pointcloud.New() - err = pc.Set(pointcloud.NewVector(0, 0, 0), nil) - test.That(t, err, test.ShouldBeNil) - err = pc.Set(pointcloud.NewVector(0, 1, 0), nil) - test.That(t, err, test.ShouldBeNil) - err = pc.Set(pointcloud.NewVector(1, 0, 0), nil) - test.That(t, err, test.ShouldBeNil) - err = pc.Set(pointcloud.NewVector(1, 1, 0), nil) - test.That(t, err, test.ShouldBeNil) - obj, err = NewObject(pc) - test.That(t, err, test.ShouldBeNil) - test.That(t, obj.PointCloud, test.ShouldResemble, pc) - expectedBox, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(r3.Vector{0.5, 0.5, 0}), r3.Vector{1, 1, 0}, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.GeometriesAlmostEqual(obj.Geometry, expectedBox), test.ShouldBeTrue) -} - -func TestObjectCreationWithLabel(t *testing.T) { - // test that object created "withlabel" maintains label - - geomLabel := "blah" - providedLabel := "notBlah" - - // create point cloud - pc := pointcloud.New() - err := pc.Set(pointcloud.NewVector(0, 0, 200), nil) - test.That(t, err, test.ShouldBeNil) - - // create labelled and unlabelled Geometries - geom := spatialmath.NewPoint(r3.Vector{0, 0, 200}, geomLabel) - geom2 := spatialmath.NewPoint(r3.Vector{0, 0, 200}, "") - pbGeomWithLabel := geom.ToProtobuf() - pbGeomNoLabel := geom2.ToProtobuf() - - // Test that a providedLabel will overwrite the geometry label - obj, err := NewObjectWithLabel(pc, "", pbGeomWithLabel) - test.That(t, err, test.ShouldBeNil) - test.That(t, obj.Geometry.Label(), test.ShouldResemble, geomLabel) - - obj, err = NewObjectWithLabel(pc, providedLabel, pbGeomWithLabel) - test.That(t, err, test.ShouldBeNil) - test.That(t, obj.Geometry.Label(), test.ShouldResemble, providedLabel) - - // Test that with no geometry label, the providedLabel persists - obj2, err := NewObjectWithLabel(pc, "", pbGeomNoLabel) - test.That(t, err, test.ShouldBeNil) - test.That(t, obj2.Geometry.Label(), test.ShouldResemble, "") - - obj2, err = NewObjectWithLabel(pc, providedLabel, pbGeomNoLabel) - test.That(t, err, test.ShouldBeNil) - test.That(t, obj2.Geometry.Label(), test.ShouldResemble, providedLabel) -} - -func TestObjectDistance(t *testing.T) { - pc := pointcloud.New() - err := pc.Set(pointcloud.NewVector(0, 0, 0), nil) - test.That(t, err, test.ShouldBeNil) - obj, err := NewObject(pc) - test.That(t, err, test.ShouldBeNil) - dist, err := obj.Distance() - test.That(t, err, test.ShouldBeNil) - test.That(t, dist, test.ShouldEqual, 0) - - err = pc.Set(pointcloud.NewVector(0, 1, 0), nil) - test.That(t, err, test.ShouldBeNil) - err = pc.Set(pointcloud.NewVector(1, 0, 0), nil) - test.That(t, err, test.ShouldBeNil) - err = pc.Set(pointcloud.NewVector(1, 1, 0), nil) - test.That(t, err, test.ShouldBeNil) - obj, err = NewObject(pc) - test.That(t, err, test.ShouldBeNil) - dist, err = obj.Distance() - test.That(t, err, test.ShouldBeNil) - test.That(t, dist, test.ShouldEqual, math.Sqrt(math.Pow(0.5, 2)+math.Pow(0.5, 2))) - - err = pc.Set(pointcloud.NewVector(0, 0, -3), nil) - test.That(t, err, test.ShouldBeNil) - obj, err = NewObject(pc) - test.That(t, err, test.ShouldBeNil) - dist, err = obj.Distance() - test.That(t, err, test.ShouldBeNil) - test.That(t, dist, test.ShouldEqual, math.Sqrt(math.Pow(0.4, 2)+math.Pow(0.4, 2)+math.Pow(0.6, 2))) - - obj = NewEmptyObject() - _, err = obj.Distance() - test.That(t, err.Error(), test.ShouldContainSubstring, "no geometry object") -} diff --git a/vision/objectdetection/color_detector.go b/vision/objectdetection/color_detector.go deleted file mode 100644 index 03ededbbdcd..00000000000 --- a/vision/objectdetection/color_detector.go +++ /dev/null @@ -1,153 +0,0 @@ -package objectdetection - -import ( - "image" - - "github.com/pkg/errors" - - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" -) - -// ColorDetectorConfig specifies the fields necessary for creating a color detector. -type ColorDetectorConfig struct { - resource.TriviallyValidateConfig - SegmentSize int `json:"segment_size_px"` - HueTolerance float64 `json:"hue_tolerance_pct"` - SaturationCutoff float64 `json:"saturation_cutoff_pct,omitempty"` - ValueCutoff float64 `json:"value_cutoff_pct,omitempty"` - DetectColorString string `json:"detect_color"` // hex string "#RRGGBB" - Label string `json:"label,omitempty"` -} - -// NewColorDetector is a detector that identifies objects based on color. -// It takes in a hue value between 0 and 360, and then defines a valid range around the hue of that color -// based on the tolerance. The color is considered valid if the pixel is between (hue - tol) <= color <= (hue + tol) -// and if the saturation and value level are above their cutoff points. -func NewColorDetector(cfg *ColorDetectorConfig) (Detector, error) { - col, err := rimage.NewColorFromHex(cfg.DetectColorString) - if err != nil { - return nil, err - } - hue, s, v := col.HsvNormal() - if s == 0 { // color detector cannot detect black/white/grayscale - return nil, errors.New("the chosen color to detect has a saturation of 0. " + - "The color detector cannot detect black, white or grayscale colors.") - } - tol := cfg.HueTolerance - sat := cfg.SaturationCutoff - if sat == 0 { - sat = 0.2 // saturation less than .2 look very washed out and grayscale - } - val := cfg.ValueCutoff - if val == 0 { - val = 0.3 // values less than .3 look very dark and hard to distinguish from black - } - - if tol > 1.0 || tol <= 0.0 { - return nil, errors.Errorf("hue_tolerance_pct must be between 0.0 and 1.0. Got %.5f", tol) - } - if sat > 1.0 || sat < 0.0 { - return nil, errors.Errorf("saturation_cutoff_pct must be between 0.0 and 1.0. Got %.5f", sat) - } - if val > 1.0 || val < 0.0 { - return nil, errors.Errorf("value_cutoff_pct must be between 0.0 and 1.0. Got %.5f", val) - } - if s < sat { - return nil, errors.Errorf("the chosen color to detect has a saturation of %.5f which is less than saturation_cutoff_pct %.5f", s, sat) - } - if v < val { - return nil, errors.Errorf("the chosen color to detect has a value of %.5f which is less than value_cutoff_pct %.5f", v, val) - } - - var valid validPixelFunc - if tol == 1.0 { - valid = makeValidColorFunction(0, 360, sat, val) - } else { - tol = (tol / 2.) * 360.0 // change from percent to degrees - hiValid := hue + tol - if hiValid >= 360. { - hiValid -= 360. - } - loValid := hue - tol - if loValid < 0. { - loValid += 360. - } - valid = makeValidColorFunction(loValid, hiValid, sat, val) - } - label := cfg.Label - if label == "" { - label = hueToString(hue) - } - cd := connectedComponentDetector{valid, label} - // define the filter - segmentSize := 5000 // default value - if cfg.SegmentSize != 0 { - segmentSize = cfg.SegmentSize - } - filt := NewAreaFilter(segmentSize) - // build the detector pipeline - det, err := Build(nil, cd.Inference, filt) - if err != nil { - return nil, err - } - sortedDet, err := Build(nil, det, SortByArea()) - if err != nil { - return nil, err - } - - return sortedDet, nil -} - -func hueToString(hue float64) string { - hueInt := int(hue) % 360 - switch { - case hueInt < 15 || hueInt >= 345: - return "red" - case hueInt >= 15 && hueInt < 45: - return "orange" - case hueInt >= 45 && hueInt < 75: - return "yellow" - case hueInt >= 75 && hueInt < 105: - return "lime-green" - case hueInt >= 105 && hueInt < 135: - return "green" - case hueInt >= 135 && hueInt < 165: - return "green-blue" - case hueInt >= 165 && hueInt < 195: - return "cyan" - case hueInt >= 195 && hueInt < 225: - return "light-blue" - case hueInt >= 225 && hueInt < 255: - return "blue" - case hueInt >= 255 && hueInt < 285: - return "violet" - case hueInt >= 285 && hueInt < 315: - return "magenta" - case hueInt >= 315 && hueInt < 345: - return "rose" - default: - return "impossible" - } -} - -func makeValidColorFunction(loValid, hiValid, sat, val float64) validPixelFunc { - valid := func(v float64) bool { return v == loValid } - if hiValid > loValid { - valid = func(v float64) bool { return v <= hiValid && v >= loValid } - } else if loValid > hiValid { - valid = func(v float64) bool { return v <= hiValid || v >= loValid } - } - // create the ValidPixel function - return func(img image.Image, pt image.Point) bool { - c := rimage.NewColorFromColor(img.At(pt.X, pt.Y)) - h, s, v := c.HsvNormal() - if s < sat { - return false - } - if v < val { - return false - } - return valid(h) - } -} diff --git a/vision/objectdetection/color_detector_test.go b/vision/objectdetection/color_detector_test.go deleted file mode 100644 index 9214f607cca..00000000000 --- a/vision/objectdetection/color_detector_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package objectdetection - -import ( - "context" - "image" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/rimage" -) - -const colorHexString = "#4F3815" // an orange color - -func TestColorDetector(t *testing.T) { - // make the original source - img, err := rimage.NewImageFromFile(artifact.MustPath("vision/objectdetection/detection_test.jpg")) - test.That(t, err, test.ShouldBeNil) - ctx := context.Background() - // detector with error - cfg := &ColorDetectorConfig{ - SegmentSize: 150000, - HueTolerance: 8.0, - DetectColorString: colorHexString, - } - _, err = NewColorDetector(cfg) - test.That(t, err, test.ShouldBeError, errors.New("hue_tolerance_pct must be between 0.0 and 1.0. Got 8.00000")) - - cfg.HueTolerance = 1. - cfg.SaturationCutoff = 8 - _, err = NewColorDetector(cfg) - test.That(t, err, test.ShouldBeError, errors.New("saturation_cutoff_pct must be between 0.0 and 1.0. Got 8.00000")) - - cfg.SaturationCutoff = 1. - cfg.ValueCutoff = 8 - _, err = NewColorDetector(cfg) - test.That(t, err, test.ShouldBeError, errors.New("value_cutoff_pct must be between 0.0 and 1.0. Got 8.00000")) - - cfg.ValueCutoff = 1. - _, err = NewColorDetector(cfg) - test.That(t, err, test.ShouldBeError, - errors.New("the chosen color to detect has a saturation of 0.73333 which is less than saturation_cutoff_pct 1.00000"), - ) - - cfg.SaturationCutoff = 0.2 - _, err = NewColorDetector(cfg) - test.That(t, err, test.ShouldBeError, - errors.New("the chosen color to detect has a value of 0.30980 which is less than value_cutoff_pct 1.00000"), - ) - - cfg.ValueCutoff = 0.3 - cfg.DetectColorString = "#000000" // black - _, err = NewColorDetector(cfg) - test.That(t, err.Error(), test.ShouldContainSubstring, - "the chosen color to detect has a saturation of 0", - ) - - cfg.DetectColorString = colorHexString - det, err := NewColorDetector(cfg) - test.That(t, err, test.ShouldBeNil) - result, err := det(ctx, img) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldHaveLength, 1) - test.That(t, result[0].BoundingBox().Min, test.ShouldResemble, image.Point{0, 336}) - - // Try with default cutoff values - cfg.SaturationCutoff = 0 - cfg.ValueCutoff = 0 - det, err = NewColorDetector(cfg) - test.That(t, err, test.ShouldBeNil) - result, err = det(ctx, img) - test.That(t, err, test.ShouldBeNil) - test.That(t, result, test.ShouldHaveLength, 1) - test.That(t, result[0].BoundingBox().Min, test.ShouldResemble, image.Point{0, 336}) -} - -func TestHueToString(t *testing.T) { - theColor := rimage.Red - hue, _, _ := theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "red") - theColor = rimage.NewColor(255, 125, 0) // orange - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "orange") - theColor = rimage.Yellow - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "yellow") - theColor = rimage.NewColor(125, 255, 0) // lime green - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "lime-green") - theColor = rimage.Green - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "green") - theColor = rimage.NewColor(0, 255, 125) // green-blue - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "green-blue") - theColor = rimage.Cyan - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "cyan") - theColor = rimage.NewColor(0, 125, 255) // light-blue - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "light-blue") - theColor = rimage.Blue - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "blue") - theColor = rimage.NewColor(125, 0, 255) // violet - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "violet") - theColor = rimage.NewColor(255, 0, 255) // magenta - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "magenta") - theColor = rimage.NewColor(255, 0, 125) // rose - hue, _, _ = theColor.HsvNormal() - test.That(t, hueToString(hue), test.ShouldEqual, "rose") -} diff --git a/vision/objectdetection/connected_components_detector.go b/vision/objectdetection/connected_components_detector.go deleted file mode 100644 index 021913c5d1c..00000000000 --- a/vision/objectdetection/connected_components_detector.go +++ /dev/null @@ -1,79 +0,0 @@ -package objectdetection - -import ( - "context" - "image" -) - -// validPixelFunc is a function that returns true if a pixel in an image.Image passes a certain criteria. -type validPixelFunc func(image.Image, image.Point) bool - -// connectedComponentDetector identifies objects in an image by merging neighbors that share similar properties. -// Based on some valid criteria, it will group the pixel into the current segment. -type connectedComponentDetector struct { - valid validPixelFunc - label string -} - -// Inference takes in an image frame and returns the Detections found in the image. -func (ccd *connectedComponentDetector) Inference(ctx context.Context, img image.Image) ([]Detection, error) { - width, height := img.Bounds().Dx(), img.Bounds().Dy() - seen := make([]bool, width*height) - queue := []image.Point{} - detections := []Detection{} - for i := 0; i < width; i++ { - for j := 0; j < height; j++ { - pt := image.Point{i, j} - indx := pt.Y*width + pt.X - if seen[indx] { - continue - } - if !ccd.valid(img, pt) { - seen[indx] = true - continue - } - queue = append(queue, pt) - x0, y0, x1, y1 := pt.X, pt.Y, pt.X, pt.Y // the bounding box of the segment - for len(queue) != 0 { - newPt := queue[0] - newIndx := newPt.Y*width + newPt.X - seen[newIndx] = true - queue = queue[1:] - if newPt.X < x0 { - x0 = newPt.X - } - if newPt.X > x1 { - x1 = newPt.X - } - if newPt.Y < y0 { - y0 = newPt.Y - } - if newPt.Y > y1 { - y1 = newPt.Y - } - neighbors := ccd.getNeighbors(newPt, img, seen) - queue = append(queue, neighbors...) - } - d := &detection2D{image.Rect(x0, y0, x1, y1), 1.0, ccd.label} - detections = append(detections, d) - } - } - return detections, nil -} - -func (ccd *connectedComponentDetector) getNeighbors(pt image.Point, img image.Image, seen []bool) []image.Point { - bounds := img.Bounds() - neighbors := make([]image.Point, 0, 4) - fourPoints := []image.Point{{pt.X, pt.Y - 1}, {pt.X, pt.Y + 1}, {pt.X - 1, pt.Y}, {pt.X + 1, pt.Y}} - for _, p := range fourPoints { - indx := p.Y*bounds.Dx() + p.X - if !p.In(bounds) || seen[indx] { - continue - } - if ccd.valid(img, p) { - neighbors = append(neighbors, p) - } - seen[indx] = true - } - return neighbors -} diff --git a/vision/objectdetection/detection_test.go b/vision/objectdetection/detection_test.go deleted file mode 100644 index 303b29fd2fe..00000000000 --- a/vision/objectdetection/detection_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package objectdetection - -import ( - "context" - "image" - "testing" - - "github.com/pkg/errors" - "go.viam.com/test" - - "go.viam.com/rdk/rimage" -) - -func TestBuildFunc(t *testing.T) { - img := rimage.NewImage(400, 400) - _, err := Build(nil, nil, nil) - test.That(t, err.Error(), test.ShouldContainSubstring, "must have a Detector") - // detector that creates an error - det := func(context.Context, image.Image) ([]Detection, error) { - return nil, errors.New("detector error") - } - ctx := context.Background() - pipeline, err := Build(nil, det, nil) - test.That(t, err, test.ShouldBeNil) - _, err = pipeline(ctx, img) - test.That(t, err.Error(), test.ShouldEqual, "detector error") - // make simple detector - det = func(context.Context, image.Image) ([]Detection, error) { - return []Detection{&detection2D{}}, nil - } - pipeline, err = Build(nil, det, nil) - test.That(t, err, test.ShouldBeNil) - res, err := pipeline(ctx, img) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldHaveLength, 1) - // make simple filter - filt := func(d []Detection) []Detection { - return []Detection{} - } - pipeline, err = Build(nil, det, filt) - test.That(t, err, test.ShouldBeNil) - res, err = pipeline(ctx, img) - test.That(t, err, test.ShouldBeNil) - test.That(t, res, test.ShouldHaveLength, 0) -} diff --git a/vision/objectdetection/detection_utils.go b/vision/objectdetection/detection_utils.go deleted file mode 100644 index d51ac56c58d..00000000000 --- a/vision/objectdetection/detection_utils.go +++ /dev/null @@ -1,40 +0,0 @@ -package objectdetection - -import ( - "fmt" - "image" - "image/color" - - "github.com/fogleman/gg" - "github.com/pkg/errors" - - "go.viam.com/rdk/rimage" -) - -// Overlay returns a color image with the bounding boxes overlaid on the original image. -func Overlay(img image.Image, dets []Detection) (image.Image, error) { - gimg := gg.NewContextForImage(img) - for _, det := range dets { - if !det.BoundingBox().In(img.Bounds()) { - return nil, errors.Errorf("bounding box (%v) does not fit in image (%v)", det.BoundingBox(), img.Bounds()) - } - drawDetection(gimg, det) - } - return gimg.Image(), nil -} - -// drawDetection overlays text of the image label and score in the upper left hand of the bounding box. -func drawDetection(img *gg.Context, d Detection) { - red := &color.NRGBA{255, 0, 0, 255} - box := d.BoundingBox() - rimage.DrawRectangleEmpty(img, *box, red, 2.0) - text := fmt.Sprintf("%s: %.2f", d.Label(), d.Score()) - rimage.DrawString(img, text, image.Point{box.Min.X, box.Min.Y}, red, 30) -} - -// OverlayText writes a string in the top of the image. -func OverlayText(img image.Image, text string) image.Image { - gimg := gg.NewContextForImage(img) - rimage.DrawString(gimg, text, image.Point{30, 30}, color.NRGBA{255, 0, 0, 255}, 30) - return gimg.Image() -} diff --git a/vision/objectdetection/detector.go b/vision/objectdetection/detector.go deleted file mode 100644 index 1d0e0b63eb2..00000000000 --- a/vision/objectdetection/detector.go +++ /dev/null @@ -1,74 +0,0 @@ -// Package objectdetection defines a functional way to create object detection pipelines by feeding in -// images from a gostream.VideoSource source. -package objectdetection - -import ( - "context" - "fmt" - "image" - - "github.com/pkg/errors" -) - -// Detector returns a slice of object detections from an input image. -type Detector func(context.Context, image.Image) ([]Detection, error) - -// Build zips up a preprocessor-detector-postprocessor stream into a detector. -func Build(prep Preprocessor, det Detector, post Postprocessor) (Detector, error) { - if det == nil { - return nil, errors.New("must have a Detector to build a detection pipeline") - } - if prep == nil { - prep = func(img image.Image) image.Image { return img } - } - if post == nil { - post = func(inp []Detection) []Detection { return inp } - } - return func(ctx context.Context, img image.Image) ([]Detection, error) { - preprocessed := prep(img) - detections, err := det(ctx, preprocessed) - if err != nil { - return nil, err - } - return post(detections), nil - }, nil -} - -// Detection returns a bounding box around the object and a confidence score of the detection. -type Detection interface { - BoundingBox() *image.Rectangle - Score() float64 - Label() string -} - -// NewDetection creates a simple 2D detection. -func NewDetection(boundingBox image.Rectangle, score float64, label string) Detection { - return &detection2D{boundingBox, score, label} -} - -// detection2D is a simple struct for storing 2D detections. -type detection2D struct { - boundingBox image.Rectangle - score float64 - label string -} - -// BoundingBox returns a bounding box around the detected object. -func (d *detection2D) BoundingBox() *image.Rectangle { - return &d.boundingBox -} - -// Score returns a confidence score of the detection between 0.0 and 1.0. -func (d *detection2D) Score() float64 { - return d.score -} - -// Label returns the class label of the object in the bounding box. -func (d *detection2D) Label() string { - return d.label -} - -// String turns the detection into a string. -func (d *detection2D) String() string { - return fmt.Sprintf("Label: %s, Score: %.2f, Box: %v", d.label, d.score, d.boundingBox) -} diff --git a/vision/objectdetection/postprocessor.go b/vision/objectdetection/postprocessor.go deleted file mode 100644 index e135e8f0471..00000000000 --- a/vision/objectdetection/postprocessor.go +++ /dev/null @@ -1,86 +0,0 @@ -package objectdetection - -import ( - "sort" - "strings" -) - -// Postprocessor defines a function that filters/modifies on an incoming array of Detections. -type Postprocessor func([]Detection) []Detection - -// NewAreaFilter returns a function that filters out detections below a certain area. -func NewAreaFilter(area int) Postprocessor { - return func(in []Detection) []Detection { - out := make([]Detection, 0, len(in)) - for _, d := range in { - if d.BoundingBox().Dx()*d.BoundingBox().Dy() >= area { - out = append(out, d) - } - } - return out - } -} - -// NewScoreFilter returns a function that filters out detections below a certain confidence. -func NewScoreFilter(conf float64) Postprocessor { - return func(in []Detection) []Detection { - out := make([]Detection, 0, len(in)) - for _, d := range in { - if d.Score() >= conf { - out = append(out, d) - } - } - return out - } -} - -// NewLabelFilter returns a function that filters out detections without one of the chosen labels. -// Does not filter when input is empty. -func NewLabelFilter(labels map[string]interface{}) Postprocessor { - return func(in []Detection) []Detection { - if len(labels) < 1 { - return in - } - out := make([]Detection, 0, len(in)) - for _, d := range in { - if _, ok := labels[strings.ToLower(d.Label())]; ok { - out = append(out, d) - } - } - return out - } -} - -// NewLabelConfidenceFilter returns a function that filters out detections based on label map. -// Does not filter when input is empty. -func NewLabelConfidenceFilter(labels map[string]float64) Postprocessor { - // ensure all the label names are lower case - theLabels := make(map[string]float64) - for name, conf := range labels { - theLabels[strings.ToLower(name)] = conf - } - return func(in []Detection) []Detection { - if len(theLabels) < 1 { - return in - } - out := make([]Detection, 0, len(in)) - for _, d := range in { - if conf, ok := theLabels[strings.ToLower(d.Label())]; ok { - if d.Score() >= conf { - out = append(out, d) - } - } - } - return out - } -} - -// SortByArea returns a function that sorts the list of detections by area (largest first). -func SortByArea() Postprocessor { - return func(in []Detection) []Detection { - sort.Slice(in, func(i, j int) bool { - return in[i].BoundingBox().Dx()*in[i].BoundingBox().Dy() > in[j].BoundingBox().Dx()*in[j].BoundingBox().Dy() - }) - return in - } -} diff --git a/vision/objectdetection/postprocessor_test.go b/vision/objectdetection/postprocessor_test.go deleted file mode 100644 index 8aadf2a474a..00000000000 --- a/vision/objectdetection/postprocessor_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package objectdetection - -import ( - "image" - "testing" - - "go.viam.com/test" -) - -func TestLabelConfidencePostprocessor(t *testing.T) { - d := []Detection{ - NewDetection(image.Rect(0, 0, 30, 30), 0.5, "A"), - NewDetection(image.Rect(0, 0, 30, 30), 0.1, "a"), - NewDetection(image.Rect(0, 0, 300, 300), 0.1, "B"), - NewDetection(image.Rect(0, 0, 300, 300), 0.6, "b"), - NewDetection(image.Rect(150, 150, 310, 310), 1, "C"), - NewDetection(image.Rect(50, 50, 200, 200), 0.8773934448, "D"), - } - - postNoFilter := NewLabelConfidenceFilter(nil) // no filtering - results := postNoFilter(d) - test.That(t, len(results), test.ShouldEqual, len(d)) - - label := map[string]float64{"a": 0.5, "B": 0.5} - postFilter := NewLabelConfidenceFilter(label) - results = postFilter(d) - test.That(t, len(results), test.ShouldEqual, 2) - labelList := make([]string, 2) - for _, g := range results { - labelList = append(labelList, g.Label()) - } - test.That(t, labelList, test.ShouldContain, "A") - test.That(t, labelList, test.ShouldContain, "b") -} - -func TestPostprocessors(t *testing.T) { - d := []Detection{ - NewDetection(image.Rect(0, 0, 30, 30), 0.5, "A"), - NewDetection(image.Rect(0, 0, 300, 300), 0.6, "B"), - NewDetection(image.Rect(150, 150, 310, 310), 1, "C"), - NewDetection(image.Rect(50, 50, 200, 200), 0.8773934448, "D"), - } - sorter := SortByArea() - got := sorter(d) - test.That(t, got[0].Label(), test.ShouldEqual, "B") - test.That(t, got[1].Label(), test.ShouldEqual, "C") - test.That(t, got[2].Label(), test.ShouldEqual, "D") - test.That(t, got[3].Label(), test.ShouldEqual, "A") - - areaFilt := NewAreaFilter(1000) - got = areaFilt(d) - labelList := make([]string, 4) - for _, g := range got { - labelList = append(labelList, g.Label()) - } - test.That(t, len(got), test.ShouldEqual, 3) - test.That(t, labelList, test.ShouldNotContain, "A") - test.That(t, labelList, test.ShouldContain, "B") - test.That(t, labelList, test.ShouldContain, "C") - test.That(t, labelList, test.ShouldContain, "D") - - scoreFilt := NewScoreFilter(0.7) - got = scoreFilt(d) - labelList = make([]string, 4) - for _, g := range got { - labelList = append(labelList, g.Label()) - } - test.That(t, len(got), test.ShouldEqual, 2) - test.That(t, labelList, test.ShouldNotContain, "A") - test.That(t, labelList, test.ShouldNotContain, "B") - test.That(t, labelList, test.ShouldContain, "C") - test.That(t, labelList, test.ShouldContain, "D") -} diff --git a/vision/objectdetection/preprocessor.go b/vision/objectdetection/preprocessor.go deleted file mode 100644 index b0dfd06179b..00000000000 --- a/vision/objectdetection/preprocessor.go +++ /dev/null @@ -1,66 +0,0 @@ -package objectdetection - -import ( - "image" - - "github.com/pkg/errors" - - "go.viam.com/rdk/rimage" -) - -// Preprocessor will apply processing to an input image before feeding it into the detector. -type Preprocessor func(image.Image) image.Image - -// ComposePreprocessors takes in a slice of Preprocessors and returns one Preprocessor function. -func ComposePreprocessors(pSlice []Preprocessor) Preprocessor { - return func(img image.Image) image.Image { - for _, p := range pSlice { - img = p(img) - } - return img - } -} - -// RemoveColorChannel will set the requested channel color to 0 in every picture. only "R", "G", and "B" are allowed. -func RemoveColorChannel(col string) (Preprocessor, error) { - switch col { - case "R", "r": - return func(img image.Image) image.Image { - rimg := rimage.ConvertImage(img) - for y := 0; y < rimg.Height(); y++ { - for x := 0; x < rimg.Width(); x++ { - c := rimg.GetXY(x, y) - _, g, b := c.RGB255() - rimg.SetXY(x, y, rimage.NewColor(0, g, b)) - } - } - return rimg - }, nil - case "G", "g": - return func(img image.Image) image.Image { - rimg := rimage.ConvertImage(img) - for y := 0; y < rimg.Height(); y++ { - for x := 0; x < rimg.Width(); x++ { - c := rimg.GetXY(x, y) - r, _, b := c.RGB255() - rimg.SetXY(x, y, rimage.NewColor(r, 0, b)) - } - } - return rimg - }, nil - case "B", "b": - return func(img image.Image) image.Image { - rimg := rimage.ConvertImage(img) - for y := 0; y < rimg.Height(); y++ { - for x := 0; x < rimg.Width(); x++ { - c := rimg.GetXY(x, y) - r, g, _ := c.RGB255() - rimg.SetXY(x, y, rimage.NewColor(r, g, 0)) - } - } - return rimg - }, nil - default: - return nil, errors.Errorf("do not know channel %q, only valid channels are 'r', 'g', or 'b'", col) - } -} diff --git a/vision/segmentation/chunks_test.go b/vision/segmentation/chunks_test.go deleted file mode 100644 index 908b57efe4c..00000000000 --- a/vision/segmentation/chunks_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package segmentation - -import ( - "context" - "image" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" -) - -type chunkImageDebug struct{} - -func (cid *chunkImageDebug) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - imgraw, img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - img := rimage.ConvertImage(imgraw) - dm, _ := rimage.ConvertImageToDepthMap(context.Background(), img2) // DepthMap is optional, ok if nil. - - type AShape struct { - Start image.Point - PixelRange []int - BadPoints []image.Point - } - - type imgConfig struct { - Shapes []AShape - } - - cfg := imgConfig{} - err := pCtx.CurrentImgConfig(&cfg) - if err != nil { - return err - } - - if true { - out := img.InterestingPixels(.2) - pCtx.GotDebugImage(out, "t") - } - - if true { - starts := []image.Point{} - - for _, s := range cfg.Shapes { - starts = append(starts, s.Start) - } - - if true { - // this shows things with the cleaning, is it useful, not sure - out, err := ShapeWalkMultiple(img, dm, starts, ShapeWalkOptions{SkipCleaning: true}, logger) - if err != nil { - return err - } - pCtx.GotDebugImage(out, "shapes-noclean") - } - - out, err := ShapeWalkMultiple(img, dm, starts, ShapeWalkOptions{}, logger) - if err != nil { - return err - } - - pCtx.GotDebugImage(out, "shapes") - - for idx, s := range cfg.Shapes { - numPixels := out.PixelsInSegmemnt(idx + 1) - - reRun := false - - if numPixels < s.PixelRange[0] || numPixels > s.PixelRange[1] { - reRun = true - t.Errorf("out of pixel range %s %v %d", fn, s, numPixels) - } - - for _, badPoint := range s.BadPoints { - if out.GetSegment(badPoint) == idx+1 { - reRun = true - t.Errorf("point %v was in cluster %v but should not have been", badPoint, idx+1) - } - } - - if reRun { - // run again with debugging on - _, err := ShapeWalkMultiple(img, dm, []image.Point{s.Start}, ShapeWalkOptions{Debug: true}, logger) - if err != nil { - return err - } - } - } - } - - if true { - out, err := ShapeWalkEntireDebug(img, dm, ShapeWalkOptions{}, logger) - if err != nil { - return err - } - pCtx.GotDebugImage(out, "entire") - } - - if dm != nil { - x := dm.ToPrettyPicture(0, 0) - pCtx.GotDebugImage(x, "depth") - - x2 := dm.InterestingPixels(2) - pCtx.GotDebugImage(x2, "depth-interesting") - - pp := transform.ParallelProjection{} - pc, err := pp.RGBDToPointCloud(img, dm) - if err != nil { - t.Fatal(err) - } - - plane, removed, err := SegmentPlane(context.Background(), pc, 3000, 5) - if err != nil { - t.Fatal(err) - } - - planePc, err := plane.PointCloud() - if err != nil { - t.Fatal(err) - } - pCtx.GotDebugPointCloud(planePc, "only-plane") - pCtx.GotDebugPointCloud(removed, "plane-removed") - } - - return nil -} - -func TestChunk1(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "segmentation/test1/color", "*.png", "segmentation/test1/depth") - err := d.Process(t, &chunkImageDebug{}) - test.That(t, err, test.ShouldBeNil) -} diff --git a/vision/segmentation/color_objects.go b/vision/segmentation/color_objects.go deleted file mode 100644 index 60e38f6cbbb..00000000000 --- a/vision/segmentation/color_objects.go +++ /dev/null @@ -1,83 +0,0 @@ -package segmentation - -import ( - "fmt" - - "github.com/go-viper/mapstructure/v2" - "github.com/pkg/errors" - - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/objectdetection" -) - -// ColorObjectsConfig specifies the necessary parameters for the color detection and transformation to 3D objects. -type ColorObjectsConfig struct { - HueTolerance float64 `json:"hue_tolerance_pct"` - SaturationCutoff float64 `json:"saturation_cutoff_pct,omitempty"` - ValueCutoff float64 `json:"value_cutoff_pct,omitempty"` - Color string `json:"detect_color"` // form #RRGGBB - MeanK int `json:"mean_k"` // used for StatisticalFilter - Sigma float64 `json:"sigma"` // used for StatisticalFilter - MinSegmentSize int `json:"min_points_in_segment"` - Label string `json:"label,omitempty"` -} - -// CheckValid checks to see in the input values are valid. -func (csc *ColorObjectsConfig) CheckValid() error { - if csc.HueTolerance < 0.0 || csc.HueTolerance > 1.0 { - return errors.Errorf("tolerance must be between 0.0 and 1.0, got %v", csc.HueTolerance) - } - var r, g, b uint8 - n, err := fmt.Sscanf(csc.Color, "#%02x%02x%02x", &r, &g, &b) - if n != 3 || err != nil { - return errors.Wrapf(err, "couldn't parse hex (%s) n: %d", csc.Color, n) - } - if csc.Sigma <= 0 { - return errors.Errorf("sigma, the std dev used for filtering, must be greater than 0, got %v", csc.Sigma) - } - if csc.MinSegmentSize < 0 { - return errors.Errorf("min_points_in_segment cannot be less than 0, got %v", csc.MinSegmentSize) - } - return nil -} - -// ConvertAttributes changes the AttributeMap input into a ColorObjectsConfig. -func (csc *ColorObjectsConfig) ConvertAttributes(am utils.AttributeMap) error { - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: csc}) - if err != nil { - return err - } - err = decoder.Decode(am) - if err == nil { - err = csc.CheckValid() - } - return err -} - -// ColorObjects returns a Segmenter that turns the bounding boxes found by the ColorDetector into 3D objects. -func ColorObjects(params utils.AttributeMap) (Segmenter, error) { - cfg := &ColorObjectsConfig{} - err := cfg.ConvertAttributes(params) - if err != nil { - return nil, err - } - // get info from config to build color detector - detCfg := &objectdetection.ColorDetectorConfig{ - SegmentSize: cfg.MinSegmentSize, - HueTolerance: cfg.HueTolerance, - SaturationCutoff: cfg.SaturationCutoff, - ValueCutoff: cfg.ValueCutoff, - DetectColorString: cfg.Color, - Label: cfg.Label, - } - detector, err := objectdetection.NewColorDetector(detCfg) - if err != nil { - return nil, err - } - // turn the detector into a segmentor - segmenter, err := DetectionSegmenter(detector, cfg.MeanK, cfg.Sigma, 1.) - if err != nil { - return nil, err - } - return segmenter, nil -} diff --git a/vision/segmentation/color_objects_test.go b/vision/segmentation/color_objects_test.go deleted file mode 100644 index aeb7be47d3c..00000000000 --- a/vision/segmentation/color_objects_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package segmentation_test - -import ( - "context" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/components/camera/videosource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/segmentation" -) - -func TestColorObjects(t *testing.T) { - t.Parallel() - // create camera - img, err := rimage.NewImageFromFile(artifact.MustPath("segmentation/aligned_intel/color/desktop2.png")) - test.That(t, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("segmentation/aligned_intel/depth/desktop2.png")) - test.That(t, err, test.ShouldBeNil) - params, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - c := &videosource.StaticSource{ColorImg: img, DepthImg: dm, Proj: ¶ms.ColorCamera} - src, err := camera.NewVideoSourceFromReader( - context.Background(), - c, - &transform.PinholeCameraModel{PinholeCameraIntrinsics: ¶ms.ColorCamera}, - camera.DepthStream, - ) - test.That(t, err, test.ShouldBeNil) - // create config - expectedLabel := "test_label" - cfg := utils.AttributeMap{ - "hue_tolerance_pct": 0.025, - "detect_color": "#6D2814", - "mean_k": 50, - "sigma": 1.5, - "min_points_in_segment": 1000, - "label": expectedLabel, - } - // run segmenter - segmenter, err := segmentation.ColorObjects(cfg) - test.That(t, err, test.ShouldBeNil) - objects, err := segmenter(context.Background(), src) - test.That(t, err, test.ShouldBeNil) - test.That(t, objects, test.ShouldHaveLength, 1) - test.That(t, objects[0].Geometry.Label(), test.ShouldEqual, expectedLabel) - // create config with no mean_k filtering - cfg = utils.AttributeMap{ - "hue_tolerance_pct": 0.025, - "detect_color": "#6D2814", - "mean_k": -1, - "sigma": 1.5, - "min_points_in_segment": 1000, - "label": expectedLabel, - } - // run segmenter - segmenter, err = segmentation.ColorObjects(cfg) - test.That(t, err, test.ShouldBeNil) - objects, err = segmenter(context.Background(), src) - test.That(t, err, test.ShouldBeNil) - test.That(t, objects, test.ShouldHaveLength, 1) - test.That(t, objects[0].Geometry.Label(), test.ShouldEqual, expectedLabel) -} - -func TestColorObjectsValidate(t *testing.T) { - cfg := segmentation.ColorObjectsConfig{} - // tolerance value too big - cfg.HueTolerance = 10. - err := cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "tolerance must be between 0.0 and 1.0") - // not a valid color - cfg.HueTolerance = 1. - cfg.Color = "#GGGGGG" - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "couldn't parse hex") - // not a valid sigma - cfg.Color = "#123456" - cfg.MeanK = 5 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "must be greater than 0") - // not a valid min segment size - cfg.Sigma = 1 - cfg.MinSegmentSize = -1 - t.Logf("conf: %v", cfg) - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "min_points_in_segment cannot be less than 0") - // valid - cfg.MinSegmentSize = 5 - err = cfg.CheckValid() - test.That(t, err, test.ShouldBeNil) -} diff --git a/vision/segmentation/data/gripper_combo_parameters.json b/vision/segmentation/data/gripper_combo_parameters.json deleted file mode 100644 index 17e0a358f01..00000000000 --- a/vision/segmentation/data/gripper_combo_parameters.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "color_intrinsic_parameters": { - "height_px": 342, - "width_px": 448, - "fx": 522.9379789, - "fy": 373.3617853, - "ppx": 217.946713, - "ppy": 159.288377, - "distortion": { - "rk1": 0.0, - "rk2": 0.0, - "rk3": 0.0, - "tp1": 0.0, - "tp2": 0.0 - } - }, - "depth_intrinsic_parameters": { - "height_px": 171, - "width_px": 224, - "fx": 0.0, - "fy": 0.0, - "ppx": 0.0, - "ppy": 0.0, - "distortion": { - "rk1": 0.0, - "rk2": 0.0, - "rk3": 0.0, - "tp1": 0.0, - "tp2": 0.0 - } - }, - "depth_to_color_extrinsic_parameters": { - "rotation_rads": [ - 1.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 1.0 - ], - "translation_mm": [ - 0.0, - 0.0, - 0.0 - ] - } -} diff --git a/vision/segmentation/data/intel515_parameters.json b/vision/segmentation/data/intel515_parameters.json deleted file mode 100644 index bd02d2a333a..00000000000 --- a/vision/segmentation/data/intel515_parameters.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "color_intrinsic_parameters": { - "height_px": 720, - "width_px": 1280, - "fx": 900.538000, - "fy": 900.818000, - "ppx": 648.934000, - "ppy": 367.736000, - "distortion": { - "rk1": 0.158701, - "rk2": -0.485405, - "rk3": 0.435342, - "tp1": -0.00143327, - "tp2": -0.000705919 - } - }, - "depth_intrinsic_parameters": { - "height_px": 768, - "width_px": 1024, - "fx": 734.938, - "fy": 735.516, - "ppx": 542.078, - "ppy": 398.016, - "distortion": { - "rk1": 0.0, - "rk2": 0.0, - "rk3": 0.0, - "tp1": 0.0, - "tp2": 0.0 - } - }, - "depth_to_color_extrinsic_parameters": { - "rotation_rads": [ - 0.999958, - -0.00838489, - 0.00378392, - 0.00824708, - 0.999351, - 0.0350734, - -0.00407554, - -0.0350407, - 0.999378 - ], - "translation_mm": [ - -0.000828434, - 0.0139185, - -0.0033418 - ] - } -} diff --git a/vision/segmentation/debug_object_segmentation_test.go b/vision/segmentation/debug_object_segmentation_test.go deleted file mode 100644 index 11e99250ddd..00000000000 --- a/vision/segmentation/debug_object_segmentation_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package segmentation_test - -import ( - "context" - "image" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/segmentation" -) - -var ( - gripperComboParamsPath = utils.ResolveFile("vision/segmentation/data/gripper_combo_parameters.json") - intel515ParamsPath = utils.ResolveFile("vision/segmentation/data/intel515_parameters.json") -) - -// Test finding the objects in an aligned intel image. -type segmentObjectTestHelper struct { - cameraParams *transform.DepthColorIntrinsicsExtrinsics -} - -// Process creates a segmentation using raw PointClouds and then VoxelGrids. -func (h *segmentObjectTestHelper) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - img, img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - var err error - im := rimage.ConvertImage(img) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), img2) - test.That(t, err, test.ShouldBeNil) - test.That(t, h.cameraParams, test.ShouldNotBeNil) - - pCtx.GotDebugImage(rimage.Overlay(im, dm), "overlay") - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "depth-fixed") - - cloud, err := h.cameraParams.RGBDToPointCloud(im, dm) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(cloud, "intel-full-pointcloud") - injectCamera := &inject.Camera{} - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return cloud, nil - } - - objConfig := utils.AttributeMap{ - "min_points_in_plane": 50000, - "min_points_in_segment": 500, - "clustering_radius_mm": 10.0, - "mean_k_filtering": 50, - } - - // Do object segmentation with point clouds - segmenter, err := segmentation.NewRadiusClustering(objConfig) - test.That(t, err, test.ShouldBeNil) - segments, err := segmenter(context.Background(), injectCamera) - test.That(t, err, test.ShouldBeNil) - - objectClouds := []pc.PointCloud{} - for _, seg := range segments { - objectClouds = append(objectClouds, seg.PointCloud) - } - coloredSegments, err := pc.MergePointCloudsWithColor(objectClouds) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(coloredSegments, "intel-segments-pointcloud") - - segImage, err := segmentation.PointCloudSegmentsToMask(h.cameraParams.ColorCamera, objectClouds) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(segImage, "segmented-pointcloud-image-with-depth") - - return nil -} - -func TestObjectSegmentationAlignedIntel(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "segmentation/aligned_intel/color", "*.png", "segmentation/aligned_intel/depth") - aligner, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - - err = d.Process(t, &segmentObjectTestHelper{aligner}) - test.That(t, err, test.ShouldBeNil) -} - -// Test finding objects in images from the gripper camera. -type gripperSegmentTestHelper struct { - cameraParams *transform.DepthColorIntrinsicsExtrinsics -} - -func (h *gripperSegmentTestHelper) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - img, img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - var err error - im := rimage.ConvertImage(img) - dm, _ := rimage.ConvertImageToDepthMap(context.Background(), img2) - test.That(t, h.cameraParams, test.ShouldNotBeNil) - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "gripper-depth") - - // Pre-process the depth map to smooth the noise out and fill holes - dm, err = rimage.PreprocessDepthMap(dm, im) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "gripper-depth-filled") - - // Get the point cloud - cloud, err := h.cameraParams.RGBDToPointCloud(im, dm) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(cloud, "gripper-pointcloud") - injectCamera := &inject.Camera{} - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return cloud, nil - } - - // Do object segmentation with point clouds - objConfig := utils.AttributeMap{ - "min_points_in_plane": 15000, - "min_points_in_segment": 100, - "clustering_radius_mm": 10.0, - "mean_k_filtering": 10, - } - - // Do object segmentation with point clouds - segmenter, err := segmentation.NewRadiusClustering(objConfig) - test.That(t, err, test.ShouldBeNil) - segments, err := segmenter(context.Background(), injectCamera) - test.That(t, err, test.ShouldBeNil) - - objectClouds := []pc.PointCloud{} - for _, seg := range segments { - objectClouds = append(objectClouds, seg.PointCloud) - } - - coloredSegments, err := pc.MergePointCloudsWithColor(objectClouds) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(coloredSegments, "gripper-segments-pointcloud") - - segImage, err := segmentation.PointCloudSegmentsToMask(h.cameraParams.ColorCamera, objectClouds) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(segImage, "gripper-segmented-pointcloud-image-with-depth") - - return nil -} - -func TestGripperObjectSegmentation(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "segmentation/gripper/color", "*.png", "segmentation/gripper/depth") - camera, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(gripperComboParamsPath) - test.That(t, err, test.ShouldBeNil) - - err = d.Process(t, &gripperSegmentTestHelper{camera}) - test.That(t, err, test.ShouldBeNil) -} diff --git a/vision/segmentation/debug_object_segmentation_voxel_test.go b/vision/segmentation/debug_object_segmentation_voxel_test.go deleted file mode 100644 index d3e4c2ff6ba..00000000000 --- a/vision/segmentation/debug_object_segmentation_voxel_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package segmentation_test - -import ( - "context" - "image" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/logging" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/segmentation" -) - -// Test finding objects in images from the gripper camera in voxel. -type gripperVoxelSegmentTestHelper struct { - cameraParams *transform.DepthColorIntrinsicsExtrinsics -} - -func (h *gripperVoxelSegmentTestHelper) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - img, img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - var err error - im := rimage.ConvertImage(img) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), img2) - test.That(t, err, test.ShouldBeNil) - test.That(t, h.cameraParams, test.ShouldNotBeNil) - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "gripper-depth") - - // Pre-process the depth map to smooth the noise out and fill holes - dm, err = rimage.PreprocessDepthMap(dm, im) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "gripper-depth-filled") - - // Get the point cloud - cloud, err := h.cameraParams.RGBDToPointCloud(im, dm) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(cloud, "gripper-pointcloud") - cam := &inject.Camera{} - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return cloud, nil - } - - // Do voxel segmentation - voxObjConfig := utils.AttributeMap{ - "voxel_size": 5.0, - "lambda": 1.0, - "min_points_in_plane": 15000, - "min_points_in_segment": 100, - "clustering_radius_mm": 7.5, - "weight_threshold": 0.9, - "angle_threshold": 30, - "cosine_threshold": 0.1, - "distance_threshold": 0.1, - } - - segmenter, err := segmentation.NewRadiusClusteringFromVoxels(voxObjConfig) - test.That(t, err, test.ShouldBeNil) - voxSegments, err := segmenter(context.Background(), cam) - test.That(t, err, test.ShouldBeNil) - - voxObjectClouds := []pc.PointCloud{} - for _, seg := range voxSegments { - voxObjectClouds = append(voxObjectClouds, seg.PointCloud) - } - voxColoredSegments, err := pc.MergePointCloudsWithColor(voxObjectClouds) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(voxColoredSegments, "gripper-segments-voxels") - - voxSegImage, err := segmentation.PointCloudSegmentsToMask(h.cameraParams.ColorCamera, voxObjectClouds) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(voxSegImage, "gripper-segmented-voxels-image-with-depth") - - return nil -} - -func TestGripperVoxelObjectSegmentation(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "segmentation/gripper/color", "*.png", "segmentation/gripper/depth") - camera, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(gripperComboParamsPath) - test.That(t, err, test.ShouldBeNil) - - err = d.Process(t, &gripperVoxelSegmentTestHelper{camera}) - test.That(t, err, test.ShouldBeNil) -} diff --git a/vision/segmentation/debug_plane_segmentation_test.go b/vision/segmentation/debug_plane_segmentation_test.go deleted file mode 100644 index b262a78b697..00000000000 --- a/vision/segmentation/debug_plane_segmentation_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package segmentation - -import ( - "context" - "image" - "testing" - - "go.viam.com/test" - - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -var ( - gripperComboParamsPath = utils.ResolveFile("vision/segmentation/data/gripper_combo_parameters.json") - intelJSONPath = utils.ResolveFile("vision/segmentation/data/intel.json") - intel515ParamsPath = utils.ResolveFile("vision/segmentation/data/intel515_parameters.json") -) - -// Test finding the planes in an image with depth. -func TestPlaneSegmentImageAndDepthMap(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "segmentation/planes/color", "*.png", "segmentation/planes/depth") - logger := logging.NewTestLogger(t) - config, err := config.Read(context.Background(), intelJSONPath, logger) - test.That(t, err, test.ShouldBeNil) - - c := config.FindComponent("front") - test.That(t, c, test.ShouldNotBeNil) - - aligner, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - - err = d.Process(t, &segmentTestHelper{c.Attributes, aligner}) - test.That(t, err, test.ShouldBeNil) -} - -type segmentTestHelper struct { - attrs utils.AttributeMap - cameraParams *transform.DepthColorIntrinsicsExtrinsics -} - -func (h *segmentTestHelper) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - img, img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - var err error - im := rimage.ConvertImage(img) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), img2) - test.That(t, err, test.ShouldBeNil) - - test.That(t, h.cameraParams, test.ShouldNotBeNil) - - fixedColor, fixedDepth, err := h.cameraParams.AlignColorAndDepthImage(im, dm) - test.That(t, err, test.ShouldBeNil) - fixedDepth, err = rimage.PreprocessDepthMap(fixedDepth, fixedColor) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(rimage.Overlay(fixedColor, fixedDepth), "overlay") - - pCtx.GotDebugImage(fixedDepth.ToPrettyPicture(0, rimage.MaxDepth), "depth-fixed") - - cloud, err := h.cameraParams.RGBDToPointCloud(fixedColor, fixedDepth) - test.That(t, err, test.ShouldBeNil) - - // create an image where all the planes in the point cloud are color-coded - planeSegCloud := NewPointCloudPlaneSegmentation(cloud, 50, 150000) // feed the parameters for the plane segmentation - planes, nonPlane, err := planeSegCloud.FindPlanes(context.Background()) // returns slice of planes and point cloud of non plane points - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planes), test.ShouldBeGreaterThan, 0) - segments := make([]pc.PointCloud, 0, len(planes)+1) // make a slice for all plane-pointclouds and the non-plane pointcloud - segments = append(segments, nonPlane) // non-plane point cloud gets added first - for _, plane := range planes { - test.That(t, plane, test.ShouldNotBeNil) - planeCloud, err := plane.PointCloud() - test.That(t, err, test.ShouldBeNil) - segments = append(segments, planeCloud) // append the point clouds of each plane to the slice - } - segImage, err := PointCloudSegmentsToMask(h.cameraParams.ColorCamera, segments) // color-coded image of all the planes - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(segImage, "from-pointcloud") - - // Informational histograms for voxel grid creation. This is useful for determining which lambda - // value to choose for the voxel grid plane segmentation. - voxelSize := 20. - lam := 8.0 - vg := pc.NewVoxelGridFromPointCloud(cloud, voxelSize, lam) - histPt, err := vg.VoxelHistogram(h.cameraParams.ColorCamera.Width, h.cameraParams.ColorCamera.Height, "points") - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(histPt, "voxel-histograms") - histWt, err := vg.VoxelHistogram(h.cameraParams.ColorCamera.Width, h.cameraParams.ColorCamera.Height, "weights") - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(histWt, "weight-histograms") - histRes, err := vg.VoxelHistogram(h.cameraParams.ColorCamera.Width, h.cameraParams.ColorCamera.Height, "residuals") - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugImage(histRes, "residual-histograms") - - // voxel grid plane segmentation -- do the same thing as above but using the voxel grid method to get the planes. - voxelConfig := VoxelGridPlaneConfig{ - WeightThresh: 0.9, - AngleThresh: 80, - CosineThresh: 0.30, - DistanceThresh: voxelSize * 0.5, - } - planeSegVoxel := NewVoxelGridPlaneSegmentation(vg, voxelConfig) - planesVox, nonPlaneVox, err := planeSegVoxel.FindPlanes(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planesVox), test.ShouldBeGreaterThan, 0) - t.Logf("number of planes: %d", len(planesVox)) - - voxSegments := make([]pc.PointCloud, 0, len(planes)+1) - voxSegments = append(voxSegments, nonPlaneVox) - for _, plane := range planesVox { - planeCloud, err := plane.PointCloud() - test.That(t, err, test.ShouldBeNil) - voxSegments = append(voxSegments, planeCloud) - } - voxSegImage, err := PointCloudSegmentsToMask(h.cameraParams.ColorCamera, voxSegments) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(voxSegImage, "from-segmented-pointcloud") - - return nil -} - -// testing out gripper plane segmentation. -func TestGripperPlaneSegmentation(t *testing.T) { - d := rimage.NewMultipleImageTestDebugger(t, "segmentation/gripper/color", "*.png", "segmentation/gripper/depth") - camera, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(gripperComboParamsPath) - test.That(t, err, test.ShouldBeNil) - - err = d.Process(t, &gripperPlaneTestHelper{camera}) - test.That(t, err, test.ShouldBeNil) -} - -type gripperPlaneTestHelper struct { - cameraParams *transform.DepthColorIntrinsicsExtrinsics -} - -func (h *gripperPlaneTestHelper) Process( - t *testing.T, - pCtx *rimage.ProcessorContext, - fn string, - img, img2 image.Image, - logger logging.Logger, -) error { - t.Helper() - var err error - im := rimage.ConvertImage(img) - dm, err := rimage.ConvertImageToDepthMap(context.Background(), img2) - test.That(t, err, test.ShouldBeNil) - test.That(t, h.cameraParams, test.ShouldNotBeNil) - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "gripper-depth") - - // Pre-process the depth map to smooth the noise out and fill holes - dm, err = rimage.PreprocessDepthMap(dm, im) - test.That(t, err, test.ShouldBeNil) - - pCtx.GotDebugImage(dm.ToPrettyPicture(0, rimage.MaxDepth), "gripper-depth-filled") - - // Get the point cloud - cloud, err := h.cameraParams.RGBDToPointCloud(im, dm) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(cloud, "gripper-pointcloud") - - // voxel grid plane segmentation - voxelConfig := VoxelGridPlaneConfig{ - WeightThresh: 0.9, - AngleThresh: 30, - CosineThresh: 0.1, - DistanceThresh: 0.1, - } - vg := pc.NewVoxelGridFromPointCloud(cloud, 8.0, 0.1) - planeSegVoxel := NewVoxelGridPlaneSegmentation(vg, voxelConfig) - planesVox, _, err := planeSegVoxel.FindPlanes(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planesVox), test.ShouldBeGreaterThan, 0) - t.Logf("number of planes: %d", len(planesVox)) - - // point cloud plane segmentation - planeSeg := NewPointCloudPlaneSegmentation(cloud, 10, 15000) - planes, nonPlane, err := planeSeg.FindPlanes(context.Background()) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(planes), test.ShouldBeGreaterThan, 0) - - // find the planes, and only keep points above the biggest found plane - above, _, err := SplitPointCloudByPlane(nonPlane, planes[0]) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(above, "gripper-above-pointcloud") - heightLimit, err := ThresholdPointCloudByPlane(above, planes[0], 100.0) - test.That(t, err, test.ShouldBeNil) - - // color the segmentation - segments, err := segmentPointCloudObjects(heightLimit, 10.0, 5) - test.That(t, err, test.ShouldBeNil) - coloredSegments, err := pc.MergePointCloudsWithColor(segments) - test.That(t, err, test.ShouldBeNil) - pCtx.GotDebugPointCloud(coloredSegments, "gripper-segments-pointcloud") - - return nil -} diff --git a/vision/segmentation/detections_to_objects.go b/vision/segmentation/detections_to_objects.go deleted file mode 100644 index a471580596c..00000000000 --- a/vision/segmentation/detections_to_objects.go +++ /dev/null @@ -1,132 +0,0 @@ -package segmentation - -import ( - "context" - "image" - - "github.com/go-viper/mapstructure/v2" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" - "go.viam.com/rdk/vision/objectdetection" -) - -// DetectionSegmenterConfig are the optional parameters to turn a detector into a segmenter. -type DetectionSegmenterConfig struct { - resource.TriviallyValidateConfig - DetectorName string `json:"detector_name"` - ConfidenceThresh float64 `json:"confidence_threshold_pct"` - MeanK int `json:"mean_k"` - Sigma float64 `json:"sigma"` -} - -// ConvertAttributes changes the AttributeMap input into a DetectionSegmenterConfig. -func (dsc *DetectionSegmenterConfig) ConvertAttributes(am utils.AttributeMap) error { - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: dsc}) - if err != nil { - return err - } - return decoder.Decode(am) -} - -// DetectionSegmenter will take an objectdetector.Detector and turn it into a Segementer. -// The params for the segmenter are "mean_k" and "sigma" for the statistical filter on the point clouds. -func DetectionSegmenter(detector objectdetection.Detector, meanK int, sigma, confidenceThresh float64) (Segmenter, error) { - var err error - if detector == nil { - return nil, errors.New("detector cannot be nil") - } - filter := func(pc pointcloud.PointCloud) (pointcloud.PointCloud, error) { - return pc, nil - } - if meanK > 0 && sigma > 0.0 { - filter, err = pointcloud.StatisticalOutlierFilter(meanK, sigma) - if err != nil { - return nil, err - } - } - // return the segmenter - seg := func(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - proj, err := src.Projector(ctx) - if err != nil { - return nil, err - } - // get the 3D detections, and turn them into 2D image and depthmap - imgs, _, err := src.Images(ctx) - if err != nil { - return nil, errors.Wrapf(err, "detection segmenter") - } - var img *rimage.Image - var dmimg image.Image - for _, i := range imgs { - thisI := i - if i.SourceName == "color" { - img = rimage.ConvertImage(thisI.Image) - } - if i.SourceName == "depth" { - dmimg = thisI.Image - } - } - if img == nil || dmimg == nil { - return nil, errors.New("source camera's getImages method did not have 'color' and 'depth' images") - } - dm, err := rimage.ConvertImageToDepthMap(ctx, dmimg) - if err != nil { - return nil, err - } - im := rimage.CloneImage(img) - dets, err := detector(ctx, im) // detector may modify the input image - if err != nil { - return nil, err - } - - objects := make([]*vision.Object, 0, len(dets)) - for _, d := range dets { - if d.Score() < confidenceThresh { - continue - } - // TODO(bhaney): Is there a way to just project the detection boxes themselves? - pc, err := detectionToPointCloud(d, img, dm, proj) - if err != nil { - return nil, err - } - pc, err = filter(pc) - if err != nil { - return nil, err - } - // if object was filtered away, skip it - if pc.Size() == 0 { - continue - } - obj, err := vision.NewObjectWithLabel(pc, d.Label(), nil) - if err != nil { - return nil, err - } - objects = append(objects, obj) - } - return objects, nil - } - return seg, nil -} - -func detectionToPointCloud( - d objectdetection.Detection, - im *rimage.Image, dm *rimage.DepthMap, - proj transform.Projector, -) (pointcloud.PointCloud, error) { - bb := d.BoundingBox() - if bb == nil { - return nil, errors.New("detection bounding box cannot be nil") - } - pc, err := proj.RGBDToPointCloud(im, dm, *bb) - if err != nil { - return nil, err - } - return pc, nil -} diff --git a/vision/segmentation/er_ccl_clustering.go b/vision/segmentation/er_ccl_clustering.go deleted file mode 100644 index 987c7ceb792..00000000000 --- a/vision/segmentation/er_ccl_clustering.go +++ /dev/null @@ -1,338 +0,0 @@ -package segmentation - -import ( - "context" - "math" - - "github.com/go-viper/mapstructure/v2" - "github.com/golang/geo/r3" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/camera" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" -) - -// MaxCCLIterations is a value to stop the CCL algo from going on for too long. -const ( - MaxCCLIterations = 300000 - GridSize = 200 - MinPtsInPlaneDefault = 500 - MaxDistFromPlaneDefault = 100 - AngleToleranceDefault = 30.0 - ClusteringRadiusDefault = 5 - ClusteringStrictnessDefault = 1 -) - -// ErCCLConfig specifies the necessary parameters to apply the -// connected components based clustering algo. -type ErCCLConfig struct { - resource.TriviallyValidateConfig - MinPtsInPlane int `json:"min_points_in_plane"` - MinPtsInSegment int `json:"min_points_in_segment"` - MaxDistFromPlane float64 `json:"max_dist_from_plane_mm"` - NormalVec r3.Vector `json:"ground_plane_normal_vec"` - AngleTolerance float64 `json:"ground_angle_tolerance_degs"` - ClusteringRadius int `json:"clustering_radius"` - ClusteringStrictness float64 `json:"clustering_strictness"` -} - -type node struct { - i, j int - label int - minHeight, maxHeight float64 - // could be implemented without i,j - // label -1 means no cluster, otherwise labeled according to index -} - -// CheckValid checks to see in the input values are valid. -func (erCCL *ErCCLConfig) CheckValid() error { - // min_points_in_plane - if erCCL.MinPtsInPlane == 0 { - erCCL.MinPtsInPlane = MinPtsInPlaneDefault - } - if erCCL.MinPtsInPlane <= 0 { - return errors.Errorf("min_points_in_plane must be greater than 0, got %v", erCCL.MinPtsInPlane) - } - // min_points_in_segment - if erCCL.MinPtsInSegment < 0 { - return errors.Errorf("min_points_in_segment must be greater than or equal to 0, got %v", erCCL.MinPtsInSegment) - } - // max_dist_from_plane_mm - if erCCL.MaxDistFromPlane == 0 { - erCCL.MaxDistFromPlane = MaxDistFromPlaneDefault - } - if erCCL.MaxDistFromPlane <= 0 { - return errors.Errorf("max_dist_from_plane must be greater than 0, got %v", erCCL.MaxDistFromPlane) - } - // ground_plane_normal_vec - // going to have to add that the ground plane's normal vec has to be {0, 1, 0} or {0, 0, 1} - if !erCCL.NormalVec.IsUnit() { - return errors.Errorf("ground_plane_normal_vec should be a unit vector, got %v", erCCL.NormalVec) - } - if erCCL.NormalVec.Norm2() == 0 { - erCCL.NormalVec = r3.Vector{X: 0, Y: 0, Z: 1} - } - // ground_angle_tolerance_degs - if erCCL.AngleTolerance == 0.0 { - erCCL.AngleTolerance = AngleToleranceDefault - } - if erCCL.AngleTolerance > 180 || erCCL.AngleTolerance < 0 { - return errors.Errorf("max_angle_of_plane must between 0 & 180 (inclusive), got %v", erCCL.AngleTolerance) - } - // clustering_radius - if erCCL.ClusteringRadius == 0 { - erCCL.ClusteringRadius = ClusteringRadiusDefault - } - if erCCL.ClusteringRadius < 0 { - return errors.Errorf("radius must be greater than 0, got %v", erCCL.ClusteringRadius) - } - // clustering_strictness - if erCCL.ClusteringStrictness == 0 { - erCCL.ClusteringStrictness = ClusteringStrictnessDefault - } - if erCCL.ClusteringStrictness < 0 { - return errors.Errorf("clustering_strictness must be greater than 0, got %v", erCCL.ClusteringStrictness) - } - return nil -} - -// ConvertAttributes changes the AttributeMap input into an ErCCLConfig. -func (erCCL *ErCCLConfig) ConvertAttributes(am utils.AttributeMap) error { - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: erCCL}) - if err != nil { - return err - } - err = decoder.Decode(am) - if err == nil { - err = erCCL.CheckValid() - } - return err -} - -// NewERCCLClustering returns a Segmenter that removes the ground plane and returns a segmentation -// of the objects in a point cloud using a connected components clustering algo described in the paper -// "A Fast Spatial Clustering Method for Sparse LiDAR Point Clouds Using GPU Programming" by Tian et al. 2020. -func NewERCCLClustering(params utils.AttributeMap) (Segmenter, error) { - // convert attributes to appropriate struct - if params == nil { - return nil, errors.New("config for ER-CCL segmentation cannot be nil") - } - cfg := &ErCCLConfig{} - err := cfg.ConvertAttributes(params) - if err != nil { - return nil, err - } - return cfg.ErCCLAlgorithm, nil -} - -// ErCCLAlgorithm applies the connected components clustering algorithm to a VideoSource. -func (erCCL *ErCCLConfig) ErCCLAlgorithm(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - // get next point cloud - cloud, err := src.NextPointCloud(ctx) - if err != nil { - return nil, err - } - return ApplyERCCLToPointCloud(ctx, cloud, erCCL) -} - -// ApplyERCCLToPointCloud clusters a point cloud according to the ER-CCL algorithm. -func ApplyERCCLToPointCloud(ctx context.Context, cloud pc.PointCloud, cfg *ErCCLConfig) ([]*vision.Object, error) { - // run ransac, get pointcloud without ground plane - ps := NewPointCloudGroundPlaneSegmentation(cloud, cfg.MaxDistFromPlane, cfg.MinPtsInPlane, cfg.AngleTolerance, cfg.NormalVec) - // if there are found planes, remove them, and keep all the non-plane points - _, nonPlane, err := ps.FindGroundPlane(ctx) - if err != nil { - return nil, err - } - - // need to figure out coordinate system - // if height is not y, then height is going to be z - heightIsY := cfg.NormalVec.Y != 0 - - // calculating s value, want GridSize x GridSize graph - resolution := math.Ceil((nonPlane.MetaData().MaxX - nonPlane.MetaData().MinX) / GridSize) - if heightIsY { - resolution = math.Ceil((math.Ceil((nonPlane.MetaData().MaxZ-nonPlane.MetaData().MinZ)/GridSize) + resolution) / 2) - } else { - resolution = math.Ceil((math.Ceil((nonPlane.MetaData().MaxY-nonPlane.MetaData().MinY)/GridSize) + resolution) / 2) - } - - // create obstacle flag map, return that 2d slice of nodes - labelMap := pcProjection(nonPlane, resolution, heightIsY) - - // actually run erCCLL - // iterate through every box, searching down and right r distance - // run calculations to meet similarity threshold - // if similar enough update to initial label value (will also be smallest) - // iterate through pointcloud - - err = LabelMapUpdate(labelMap, cfg.ClusteringRadius, 0.9, cfg.ClusteringStrictness, resolution) - if err != nil { - return nil, err - } - - // look up label value of point by looking at 2d array and seeing what label inside that struct - // set this label - var iterateErr error - segments := make(map[int]pc.PointCloud) - nonPlane.Iterate(0, 0, func(p r3.Vector, d pc.Data) bool { - i := int(math.Ceil((p.X - nonPlane.MetaData().MinX) / resolution)) - j := int(math.Ceil((p.Z - nonPlane.MetaData().MinZ) / resolution)) - if !heightIsY { - j = int(math.Ceil((p.Y - nonPlane.MetaData().MinY) / resolution)) - } - _, ok := segments[labelMap[i][j].label] - if !ok { - segments[labelMap[i][j].label] = pc.New() - } - err := segments[labelMap[i][j].label].Set(p, d) - if err != nil { - iterateErr = err - return false - } - return true - }) - if iterateErr != nil { - return nil, iterateErr - } - // prune smaller clusters. Default minimum number of points determined by size of original point cloud. - minPtsInSegment := int(math.Max(float64(nonPlane.Size())/float64(GridSize), 10.0)) - if cfg.MinPtsInSegment != 0 { - minPtsInSegment = cfg.MinPtsInSegment - } - validObjects := make([]*vision.Object, 0, len(segments)) - for _, cloud := range segments { - if cloud.Size() >= minPtsInSegment { - obj, err := vision.NewObject(cloud) - if err != nil { - return nil, err - } - validObjects = append(validObjects, obj) - } - } - return validObjects, nil -} - -// LabelMapUpdate updates the label map until it converges or errors. -func LabelMapUpdate(labelMap [][]node, r int, alpha, beta, s float64) error { - i := 0 - continueRunning := true - for continueRunning { - // 0.9 is alpha - continueRunning := minimumSearch(labelMap, r, 0.9, beta, s) - if !continueRunning { - break - } - - if i > MaxCCLIterations { // arbitrary cutoff for iterations - return errors.New("could not converge, change parameters") - } - i++ - } - return nil -} - -func pcProjection(cloud pc.PointCloud, s float64, heightIsY bool) [][]node { - h := int(math.Ceil((cloud.MetaData().MaxX-cloud.MetaData().MinX)/s)) + 1 - w := int(math.Ceil((cloud.MetaData().MaxZ-cloud.MetaData().MinZ)/s)) + 1 - if !heightIsY { - w = int(math.Ceil((cloud.MetaData().MaxY-cloud.MetaData().MinY)/s)) + 1 - } - retVal := make([][]node, h) - for i := range retVal { - retVal[i] = make([]node, w) - for j, curNode := range retVal[i] { - curNode.label = -1 - curNode.minHeight = 0 - curNode.maxHeight = 0 - curNode.i = i - curNode.j = j - retVal[i][j] = curNode - } - } - cloud.Iterate(0, 0, func(p r3.Vector, d pc.Data) bool { - i := int(math.Ceil((p.X - cloud.MetaData().MinX) / s)) - j := int(math.Ceil((p.Z - cloud.MetaData().MinZ) / s)) - var curNode node - if heightIsY { - curNode = retVal[i][j] - curNode.maxHeight = math.Max(curNode.maxHeight, p.Y) - curNode.minHeight = math.Min(curNode.minHeight, p.Y) - } else { - j = int(math.Ceil((p.Y - cloud.MetaData().MinY) / s)) - curNode = retVal[i][j] - curNode.maxHeight = math.Max(curNode.maxHeight, p.Z) - curNode.minHeight = math.Min(curNode.minHeight, p.Z) - } - curNode.label = i*w + j - retVal[i][j] = curNode - return true - }) - return retVal -} - -// minimumSearch updates the label map 'once' meaning it searches from every cell once. -func minimumSearch(labelMap [][]node, r int, alpha, beta, s float64) bool { - mapChanged := false - - for i, curNodeSlice := range labelMap { - for j, curNode := range curNodeSlice { - if curNode.label == -1 { - // skip if no points at cell - continue - } - minLabel := curNode.label - neighbors := make([]node, 0) - // finding neighbors + finding min label value - for x := 0; x < r; x++ { - newI := i + x - if newI >= len(labelMap) { - break - } - for y := 0; y < r; y++ { - newJ := j + y - if newJ >= len(curNodeSlice) { - break - } - if newI == i && newJ == j { - continue // might be able to remove this because original should be in neighbors list - } - neighborNode := labelMap[newI][newJ] - if similarEnough(curNode, neighborNode, r, alpha, beta, s) { - neighbors = append(neighbors, neighborNode) - minLabel = int(math.Min(float64(minLabel), float64(neighborNode.label))) - } - } - } - if minLabel != curNode.label { - mapChanged = true - labelMap[curNode.i][curNode.j].label = minLabel - } - for _, neighbor := range neighbors { - if neighbor.label != minLabel { - mapChanged = true - labelMap[neighbor.i][neighbor.j].label = minLabel - } - } - } - } - return mapChanged -} - -// similarEnough takes in two nodes and tries to see if they meet some similarity threshold -// there are three components, first calculate distance between nodes, then height difference between points -// use these values to then calculate a score for similarity and if it exceeds a threshold calculated from the -// search radius and clustering strictness value. -func similarEnough(curNode, neighbor node, r int, alpha, beta, s float64) bool { - // trying to avoid math.pow since these are ints and math.pow is slow - if neighbor.label == -1 { - return false - } - d := s * math.Sqrt(float64(((curNode.i-neighbor.i)*(curNode.i-neighbor.i) + (curNode.j-neighbor.j)*(curNode.j-neighbor.j)))) - h := math.Abs(curNode.maxHeight-neighbor.maxHeight) + math.Abs(curNode.minHeight-neighbor.minHeight) - ecc := alpha*math.Exp(-d) + (1-alpha)*math.Exp(-h) - return ecc >= beta*math.Exp(float64(-r)) -} diff --git a/vision/segmentation/er_ccl_clustering_test.go b/vision/segmentation/er_ccl_clustering_test.go deleted file mode 100644 index b22122c030a..00000000000 --- a/vision/segmentation/er_ccl_clustering_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package segmentation_test - -import ( - "context" - "os" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" - "go.viam.com/rdk/vision/segmentation" -) - -func TestERCCL(t *testing.T) { - // Setting a global in utils is unsafe, and is likely to cause races. Cannot be used with t.Parallel() - origParallelFactor := utils.ParallelFactor - utils.ParallelFactor = 1 - defer func() { - utils.ParallelFactor = origParallelFactor - }() - logger := logging.NewTestLogger(t) - injectCamera := &inject.Camera{} - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - return pointcloud.NewFromFile(artifact.MustPath("pointcloud/intel_d435_pointcloud_424.pcd"), logger) - } - - objConfig := utils.AttributeMap{ - // lidar config - // "min_points_in_plane": 1500, - // "max_dist_from_plane_mm": 100.0, - // "min_points_in_segment": 150, - // "ground_angle_tolerance_degs": 20, - // "ground_plane_normal_vec": r3.Vector{0, 0, 1}, - // "clustering_radius": 5, - // "clustering_granularity": 2, - - // realsense config - "min_points_in_plane": 2000, - "max_dist_from_plane_mm": 12.0, - "min_points_in_segment": 500, - "ground_angle_tolerance_degs": 20, - "ground_plane_normal_vec": r3.Vector{0, -1, 0}, - "clustering_radius": 10, - "clustering_strictness": 0.00000001, - } - - segmenter, err := segmentation.NewERCCLClustering(objConfig) - test.That(t, err, test.ShouldBeNil) - objects, err := segmenter(context.Background(), injectCamera) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(objects), test.ShouldEqual, 3) - - pcs := make([]pointcloud.PointCloud, len(objects)) - for i, pc := range objects { - pcs[i] = pc.PointCloud - } - mergedPc, err := pointcloud.MergePointCloudsWithColor(pcs) - test.That(t, err, test.ShouldBeNil) - tempPCD, err := os.CreateTemp(t.TempDir(), "*.pcd") - test.That(t, err, test.ShouldBeNil) - defer os.Remove(tempPCD.Name()) - err = pointcloud.ToPCD(mergedPc, tempPCD, pointcloud.PCDBinary) - test.That(t, err, test.ShouldBeNil) -} - -func BenchmarkERCCL(b *testing.B) { - params := &transform.PinholeCameraIntrinsics{ // D435 intrinsics for 424x240 - Width: 424, - Height: 240, - Fx: 304.1299133300781, - Fy: 304.2772216796875, - Ppx: 213.47967529296875, - Ppy: 124.63351440429688, - } - // create the fake camera - injectCamera := &inject.Camera{} - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pointcloud.PointCloud, error) { - img, err := rimage.NewImageFromFile(artifact.MustPath("pointcloud/the_color_image_intel_424.jpg")) - test.That(b, err, test.ShouldBeNil) - dm, err := rimage.NewDepthMapFromFile(context.Background(), artifact.MustPath("pointcloud/the_depth_image_intel_424.png")) - test.That(b, err, test.ShouldBeNil) - return params.RGBDToPointCloud(img, dm) - } - var pts []*vision.Object - // do segmentation - objConfig := utils.AttributeMap{ - // lidar config - // "min_points_in_plane": 1500, - // "max_dist_from_plane_mm": 100.0, - // "min_points_in_segment": 150, - // "ground_angle_tolerance_degs": 20, - // "ground_plane_normal_vec": r3.Vector{0, 0, 1}, - // "clustering_radius": 5, - // "clustering_strictness": 2, - - // realsense config - "min_points_in_plane": 2000, - "max_dist_from_plane_mm": 12.0, - "min_points_in_segment": 500, - "ground_angle_tolerance_degs": 20, - "ground_plane_normal_vec": r3.Vector{0, -1, 0}, - "clustering_radius": 10, - "clustering_strictness": 0.00000001, - } - - segmenter, err := segmentation.NewERCCLClustering(objConfig) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - pts, err = segmenter(context.Background(), injectCamera) - } - // to prevent vars from being optimized away - if pts == nil || err != nil { - panic("segmenter didn't work") - } -} diff --git a/vision/segmentation/plane_segmentation.go b/vision/segmentation/plane_segmentation.go deleted file mode 100644 index 7a25c2a6261..00000000000 --- a/vision/segmentation/plane_segmentation.go +++ /dev/null @@ -1,471 +0,0 @@ -// Package segmentation implements object segmentation algorithms. -package segmentation - -import ( - "context" - "image" - "math" - "math/rand" - "sort" - "sync" - - "github.com/golang/geo/r3" - "github.com/pkg/errors" - - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/transform" - "go.viam.com/rdk/utils" -) - -// Setting a global here is dangerous and doesn't work in parallel. -// This is only ever set for testing, and tests need to be careful to revert it to false afterwards. -var sortPositions bool - -// GetPointCloudPositions extracts the positions of the points from the pointcloud into a Vec3 slice. -func GetPointCloudPositions(cloud pc.PointCloud) ([]r3.Vector, []pc.Data) { - positions := make(pc.Vectors, 0, cloud.Size()) - data := make([]pc.Data, 0, cloud.Size()) - cloud.Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - positions = append(positions, pt) - data = append(data, d) - return true - }) - if sortPositions { - sort.Sort(positions) - } - return positions, data -} - -func distance(equation [4]float64, pt r3.Vector) float64 { - norm := math.Sqrt(equation[0]*equation[0] + equation[1]*equation[1] + equation[2]*equation[2]) - return (equation[0]*pt.X + equation[1]*pt.Y + equation[2]*pt.Z + equation[3]) / norm -} - -// pointCloudSplit return two point clouds, one with points found in a map of point positions, and the other with those not in the map. -func pointCloudSplit(cloud pc.PointCloud, inMap map[r3.Vector]bool) (pc.PointCloud, pc.PointCloud, error) { - mapCloud := pc.New() - nonMapCloud := pc.New() - var err error - seen := make(map[r3.Vector]bool) - cloud.Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - if _, ok := inMap[pt]; ok { - seen[pt] = true - err = mapCloud.Set(pt, d) - } else { - err = nonMapCloud.Set(pt, d) - } - if err != nil { - err = errors.Wrapf(err, "error setting point (%v, %v, %v) in point cloud", pt.X, pt.Y, pt.Z) - return false - } - return true - }) - if err != nil { - return nil, nil, err - } - if len(seen) != len(inMap) { - err = errors.New("map of points contains invalid points not found in the point cloud") - return nil, nil, err - } - return mapCloud, nonMapCloud, nil -} - -// SegmentPlaneWRTGround segments the biggest 'ground' plane in the 3D Pointcloud. -// nIterations is the number of iteration for ransac -// nIter to choose? nIter = log(1-p)/log(1-(1-e)^s), where p is prob of success, e is outlier ratio, s is subset size (3 for plane). -// dstThreshold is the float64 value for the maximum allowed distance to the found plane for a point to belong to it -// This function returns a Plane struct, as well as the remaining points in a pointcloud -// It also returns the equation of the found plane: [0]x + [1]y + [2]z + [3] = 0. -// angleThrehold is the maximum acceptable angle between the groundVec and angle of the plane, -// if the plane is at a larger angle than maxAngle, it will not be considered for segmentation, if set to 0 then not considered -// normalVec is the normal vector of the plane representing the ground. -func SegmentPlaneWRTGround(ctx context.Context, cloud pc.PointCloud, nIterations int, angleThreshold, - dstThreshold float64, normalVec r3.Vector, -) (pc.Plane, pc.PointCloud, error) { - if cloud.Size() <= 3 { // if point cloud does not have even 3 points, return original cloud with no planes - return pc.NewEmptyPlane(), cloud, nil - } - //nolint:gosec - r := rand.New(rand.NewSource(1)) - pts, data := GetPointCloudPositions(cloud) - nPoints := cloud.Size() - - // First get all equations - equations := make([][4]float64, 0, nIterations) - for i := 0; i < nIterations; i++ { - // sample 3 Points from the slice of 3D Points - n1, n2, n3 := utils.SampleRandomIntRange(1, nPoints-1, r), - utils.SampleRandomIntRange(1, nPoints-1, r), - utils.SampleRandomIntRange(1, nPoints-1, r) - p1, p2, p3 := pts[n1], pts[n2], pts[n3] - - // get 2 vectors that are going to define the plane - v1 := p2.Sub(p1) - v2 := p3.Sub(p1) - // cross product to get the normal unit vector to the plane (v1, v2) - cross := v1.Cross(v2) - planeVec := cross.Normalize() - // find current plane equation denoted as: - // cross[0]*x + cross[1]*y + cross[2]*z + d = 0 - // to find d, we just need to pick a point and deduce d from the plane equation (vec orth to p1, p2, p3) - d := -planeVec.Dot(p2) - - currentEquation := [4]float64{planeVec.X, planeVec.Y, planeVec.Z, d} - - if angleThreshold != 0 { - if math.Acos(normalVec.Dot(planeVec)) <= angleThreshold*math.Pi/180.0 { - equations = append(equations, currentEquation) - } - } else { - equations = append(equations, currentEquation) - } - } - return findBestEq(ctx, cloud, len(equations), equations, pts, data, dstThreshold) -} - -// SegmentPlane segments the biggest plane in the 3D Pointcloud. -// nIterations is the number of iteration for ransac -// nIter to choose? nIter = log(1-p)/log(1-(1-e)^s), where p is prob of success, e is outlier ratio, s is subset size (3 for plane). -// threshold is the float64 value for the maximum allowed distance to the found plane for a point to belong to it -// This function returns a Plane struct, as well as the remaining points in a pointcloud -// It also returns the equation of the found plane: [0]x + [1]y + [2]z + [3] = 0. -func SegmentPlane(ctx context.Context, cloud pc.PointCloud, nIterations int, threshold float64) (pc.Plane, pc.PointCloud, error) { - if cloud.Size() <= 3 { // if point cloud does not have even 3 points, return original cloud with no planes - return pc.NewEmptyPlane(), cloud, nil - } - //nolint:gosec - r := rand.New(rand.NewSource(1)) - pts, data := GetPointCloudPositions(cloud) - nPoints := cloud.Size() - - // First get all equations - equations := make([][4]float64, 0, nIterations) - for i := 0; i < nIterations; i++ { - // sample 3 Points from the slice of 3D Points - n1, n2, n3 := utils.SampleRandomIntRange(1, nPoints-1, r), - utils.SampleRandomIntRange(1, nPoints-1, r), - utils.SampleRandomIntRange(1, nPoints-1, r) - p1, p2, p3 := pts[n1], pts[n2], pts[n3] - - // get 2 vectors that are going to define the plane - v1 := p2.Sub(p1) - v2 := p3.Sub(p1) - // cross product to get the normal unit vector to the plane (v1, v2) - cross := v1.Cross(v2) - vec := cross.Normalize() - // find current plane equation denoted as: - // cross[0]*x + cross[1]*y + cross[2]*z + d = 0 - // to find d, we just need to pick a point and deduce d from the plane equation (vec orth to p1, p2, p3) - d := -vec.Dot(p2) - - // current plane equation - currentEquation := [4]float64{vec.X, vec.Y, vec.Z, d} - equations = append(equations, currentEquation) - } - - return findBestEq(ctx, cloud, nIterations, equations, pts, data, threshold) -} - -func findBestEq(ctx context.Context, cloud pc.PointCloud, nIterations int, equations [][4]float64, - pts []r3.Vector, data []pc.Data, threshold float64, -) (pc.Plane, pc.PointCloud, error) { - // Then find the best equation in parallel. It ends up being faster to loop - // by equations (iterations) and then points due to what I (erd) think is - // memory locality exploitation. - var bestEquation [4]float64 - type bestResult struct { - equation [4]float64 - inliers int - } - var bestResults []bestResult - var bestResultsMu sync.Mutex - if err := utils.GroupWorkParallel( - ctx, - nIterations, - func(numGroups int) { - bestResults = make([]bestResult, numGroups) - }, - func(groupNum, groupSize, from, to int) (utils.MemberWorkFunc, utils.GroupWorkDoneFunc) { - var groupMu sync.Mutex - bestEquation := [4]float64{} - bestInliers := 0 - - return func(memberNum, workNum int) { - currentInliers := 0 - currentEquation := equations[workNum] - // store all the Points that are below a certain distance to the plane - for _, pt := range pts { - dist := distance(currentEquation, pt) - if math.Abs(dist) < threshold { - currentInliers++ - } - } - // if the current plane contains more pixels than the previously stored one, save this one as the biggest plane - groupMu.Lock() - defer groupMu.Unlock() - if currentInliers > bestInliers { - bestEquation = currentEquation - bestInliers = currentInliers - } - }, func() { - bestResultsMu.Lock() - defer bestResultsMu.Unlock() - bestResults[groupNum] = bestResult{bestEquation, bestInliers} - } - }, - ); err != nil { - return nil, nil, err - } - - bestIdx := 0 - bestInliers := 0 - for i, result := range bestResults { - if result.inliers > bestInliers { - bestIdx = i - } - } - bestEquation = bestResults[bestIdx].equation - - nPoints := cloud.Size() - - planeCloud := pc.NewWithPrealloc(bestInliers) - nonPlaneCloud := pc.NewWithPrealloc(nPoints - bestInliers) - planeCloudCenter := r3.Vector{} - for i, pt := range pts { - dist := distance(bestEquation, pt) - var err error - if math.Abs(dist) < threshold { - planeCloudCenter = planeCloudCenter.Add(pt) - err = planeCloud.Set(pt, data[i]) - } else { - err = nonPlaneCloud.Set(pt, data[i]) - } - if err != nil { - return nil, nil, errors.Wrapf(err, "error setting point (%v, %v, %v) in point cloud", pt.X, pt.Y, pt.Z) - } - } - - if planeCloud.Size() != 0 { - planeCloudCenter = planeCloudCenter.Mul(1. / float64(planeCloud.Size())) - } - - plane := pc.NewPlaneWithCenter(planeCloud, bestEquation, planeCloudCenter) - return plane, nonPlaneCloud, nil -} - -// PlaneSegmentation is an interface used to find geometric planes in a 3D space. -type PlaneSegmentation interface { - FindPlanes(ctx context.Context) ([]pc.Plane, pc.PointCloud, error) - FindGroundPlane(ctx context.Context) (pc.Plane, pc.PointCloud, error) -} - -type pointCloudPlaneSegmentation struct { - cloud pc.PointCloud - distanceThreshold float64 - minPoints int - nIterations int - angleThreshold float64 - normalVec r3.Vector -} - -// NewPointCloudPlaneSegmentation initializes the plane segmentation with the necessary parameters to find the planes -// threshold is the float64 value for the maximum allowed distance to the found plane for a point to belong to it. -// minPoints is the minimum number of points necessary to be considered a plane. -func NewPointCloudPlaneSegmentation(cloud pc.PointCloud, threshold float64, minPoints int) PlaneSegmentation { - return &pointCloudPlaneSegmentation{ - cloud: cloud, - distanceThreshold: threshold, - minPoints: minPoints, - nIterations: 2000, - angleThreshold: 0, - normalVec: r3.Vector{X: 0, Y: 0, Z: 1}, - } -} - -// NewPointCloudGroundPlaneSegmentation initializes the plane segmentation with the necessary parameters to find -// ground like planes, meaning they are less than angleThreshold away from the plane corresponding to normaLVec -// distanceThreshold is the float64 value for the maximum allowed distance to the found plane for a -// point to belong to it. -// minPoints is the minimum number of points necessary to be considered a plane. -func NewPointCloudGroundPlaneSegmentation(cloud pc.PointCloud, distanceThreshold float64, minPoints int, - angleThreshold float64, normalVec r3.Vector, -) PlaneSegmentation { - return &pointCloudPlaneSegmentation{cloud, distanceThreshold, minPoints, 2000, angleThreshold, normalVec} -} - -// FindPlanes takes in a point cloud and outputs an array of the planes and a point cloud of the leftover points. -func (pcps *pointCloudPlaneSegmentation) FindPlanes(ctx context.Context) ([]pc.Plane, pc.PointCloud, error) { - planes := make([]pc.Plane, 0) - var err error - plane, nonPlaneCloud, err := SegmentPlane(ctx, pcps.cloud, pcps.nIterations, pcps.distanceThreshold) - if err != nil { - return nil, nil, err - } - planeCloud, err := plane.PointCloud() - if err != nil { - return nil, nil, err - } - if planeCloud.Size() <= pcps.minPoints { - return planes, pcps.cloud, nil - } - planes = append(planes, plane) - var lastNonPlaneCloud pc.PointCloud - for { - lastNonPlaneCloud = nonPlaneCloud - smallerPlane, smallerNonPlaneCloud, err := SegmentPlane(ctx, nonPlaneCloud, pcps.nIterations, pcps.distanceThreshold) - if err != nil { - return nil, nil, err - } - planeCloud, err := smallerPlane.PointCloud() - if err != nil { - return nil, nil, err - } - if planeCloud.Size() <= pcps.minPoints { - // this cloud is not valid so revert to last - nonPlaneCloud = lastNonPlaneCloud - break - } - nonPlaneCloud = smallerNonPlaneCloud - planes = append(planes, smallerPlane) - } - return planes, nonPlaneCloud, nil -} - -// FindGroundPlane takes in a point cloud and outputs an array of a ground like plane and a point cloud of the leftover points. -func (pcps *pointCloudPlaneSegmentation) FindGroundPlane(ctx context.Context) (pc.Plane, pc.PointCloud, error) { - var err error - plane, nonPlaneCloud, err := SegmentPlaneWRTGround(ctx, pcps.cloud, pcps.nIterations, - pcps.angleThreshold, pcps.distanceThreshold, pcps.normalVec) - if err != nil { - return nil, nil, err - } - planeCloud, err := plane.PointCloud() - if err != nil { - return nil, nil, err - } - if planeCloud.Size() <= pcps.minPoints { - return nil, pcps.cloud, nil - } - return plane, nonPlaneCloud, nil -} - -// VoxelGridPlaneConfig contains the parameters needed to create a Plane from a VoxelGrid. -type VoxelGridPlaneConfig struct { - WeightThresh float64 `json:"weight_threshold"` - AngleThresh float64 `json:"angle_threshold_degs"` - CosineThresh float64 `json:"cosine_threshold"` // between -1 and 1, the value after evaluating Cosine(theta) - DistanceThresh float64 `json:"distance_threshold_mm"` -} - -// CheckValid checks to see in the inputs values are valid. -func (vgpc *VoxelGridPlaneConfig) CheckValid() error { - if vgpc.WeightThresh < 0 { - return errors.Errorf("weight_threshold cannot be less than 0, got %v", vgpc.WeightThresh) - } - if vgpc.AngleThresh < -360 || vgpc.AngleThresh > 360 { - return errors.Errorf("angle_threshold must be in degrees, between -360 and 360, got %v", vgpc.AngleThresh) - } - if vgpc.CosineThresh < -1 || vgpc.CosineThresh > 1 { - return errors.Errorf("cosine_threshold must be between -1 and 1, got %v", vgpc.CosineThresh) - } - if vgpc.DistanceThresh < 0 { - return errors.Errorf("distance_threshold cannot be less than 0, got %v", vgpc.DistanceThresh) - } - return nil -} - -type voxelGridPlaneSegmentation struct { - *pc.VoxelGrid - config VoxelGridPlaneConfig -} - -// NewVoxelGridPlaneSegmentation initializes the necessary parameters needed to do plane segmentation on a voxel grid. -func NewVoxelGridPlaneSegmentation(vg *pc.VoxelGrid, config VoxelGridPlaneConfig) PlaneSegmentation { - return &voxelGridPlaneSegmentation{vg, config} -} - -// FindPlanes takes in a point cloud and outputs an array of the planes and a point cloud of the leftover points. -func (vgps *voxelGridPlaneSegmentation) FindPlanes(ctx context.Context) ([]pc.Plane, pc.PointCloud, error) { - vgps.SegmentPlanesRegionGrowing(vgps.config.WeightThresh, vgps.config.AngleThresh, vgps.config.CosineThresh, vgps.config.DistanceThresh) - planes, nonPlaneCloud, err := vgps.GetPlanesFromLabels() - if err != nil { - return nil, nil, err - } - return planes, nonPlaneCloud, nil -} - -// FindGroundPlane is yet to be implemented. -func (vgps *voxelGridPlaneSegmentation) FindGroundPlane(ctx context.Context) (pc.Plane, pc.PointCloud, error) { - return nil, nil, errors.New("function not yet implemented") -} - -// SplitPointCloudByPlane divides the point cloud in two point clouds, given the equation of a plane. -// one point cloud will have all the points above the plane and the other with all the points below the plane. -// Points exactly on the plane are not included! -func SplitPointCloudByPlane(cloud pc.PointCloud, plane pc.Plane) (pc.PointCloud, pc.PointCloud, error) { - aboveCloud, belowCloud := pc.New(), pc.New() - var err error - cloud.Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - dist := plane.Distance(pt) - if plane.Equation()[2] > 0.0 { - dist = -dist - } - if dist > 0.0 { - err = aboveCloud.Set(pt, d) - } else if dist < 0.0 { - err = belowCloud.Set(pt, d) - } - return err == nil - }) - if err != nil { - return nil, nil, err - } - return aboveCloud, belowCloud, nil -} - -// ThresholdPointCloudByPlane returns a pointcloud with the points less than or equal to a given distance from a given plane. -func ThresholdPointCloudByPlane(cloud pc.PointCloud, plane pc.Plane, threshold float64) (pc.PointCloud, error) { - thresholdCloud := pc.New() - var err error - cloud.Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - dist := plane.Distance(pt) - if math.Abs(dist) <= threshold { - err = thresholdCloud.Set(pt, d) - } - return err == nil - }) - if err != nil { - return nil, err - } - return thresholdCloud, nil -} - -// PointCloudSegmentsToMask takes in an instrinsic camera matrix and a slice of pointclouds and projects -// each pointcloud down to an image. -func PointCloudSegmentsToMask(params transform.PinholeCameraIntrinsics, segments []pc.PointCloud) (*SegmentedImage, error) { - img := newSegmentedImage(rimage.NewImage(params.Width, params.Height)) - visitedPoints := make(map[r3.Vector]bool) - var err error - for i, cloud := range segments { - cloud.Iterate(0, 0, func(pos r3.Vector, d pc.Data) bool { - if seen := visitedPoints[pos]; seen { - err = errors.Errorf("point clouds in array must be distinct, have already seen point (%v,%v,%v)", pos.X, pos.Y, pos.Z) - return false - } - visitedPoints[pos] = true - px, py := params.PointToPixel(pos.X, pos.Y, pos.Z) - px, py = math.Round(px), math.Round(py) - x, y := int(px), int(py) - if x >= 0 && x < params.Width && y >= 0 && y < params.Height { - img.set(image.Point{x, y}, i+1) - } - return true - }) - if err != nil { - return nil, err - } - } - img.createPalette() - return img, nil -} diff --git a/vision/segmentation/plane_segmentation_test.go b/vision/segmentation/plane_segmentation_test.go deleted file mode 100644 index 3e1b6eacf00..00000000000 --- a/vision/segmentation/plane_segmentation_test.go +++ /dev/null @@ -1,288 +0,0 @@ -//go:build !no_cgo - -package segmentation - -import ( - "context" - "image" - "image/color" - "math" - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - "go.viam.com/utils/artifact" - - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/rimage/depthadapter" - "go.viam.com/rdk/rimage/transform" -) - -func TestPlaneConfig(t *testing.T) { - // Setting a global, so cannot use t.Parallel() - sortPositions = true - defer func() { - sortPositions = false - }() - - cfg := VoxelGridPlaneConfig{} - // invalid weight threshold - cfg.WeightThresh = -2. - err := cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "weight_threshold cannot be less than 0") - // invalid angle threshold - cfg.WeightThresh = 1. - cfg.AngleThresh = 1000. - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "angle_threshold must be in degrees, between -360 and 360") - // invalid cosine threshold - cfg.AngleThresh = 30. - cfg.CosineThresh = 2. - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "cosine_threshold must be between -1 and 1") - // invalid distance threshold - cfg.CosineThresh = 0.2 - cfg.DistanceThresh = -5 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "distance_threshold cannot be less than 0") - // valid - cfg.DistanceThresh = 5 - err = cfg.CheckValid() - test.That(t, err, test.ShouldBeNil) -} - -func TestSegmentPlaneWRTGround(t *testing.T) { - // Setting a global, so cannot use t.Parallel() - sortPositions = true - defer func() { - sortPositions = false - }() - - // get depth map - d, err := rimage.NewDepthMapFromFile( - context.Background(), - artifact.MustPath("vision/segmentation/pointcloudsegmentation/align-test-1615172036.png")) - test.That(t, err, test.ShouldBeNil) - - // Pixel to Meter - sensorParams, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - depthIntrinsics := &sensorParams.DepthCamera - cloud := depthadapter.ToPointCloud(d, depthIntrinsics) - test.That(t, err, test.ShouldBeNil) - // Segment Plane - nIter := 3000 - groundNormVec := r3.Vector{0, 1, 0} - angleThresh := 30.0 - plane, _, err := SegmentPlaneWRTGround(context.Background(), cloud, nIter, angleThresh, 0.5, groundNormVec) - eq := plane.Equation() - test.That(t, err, test.ShouldBeNil) - - p1 := r3.Vector{-eq[3] / eq[0], 0, 0} - p2 := r3.Vector{0, -eq[3] / eq[1], 0} - p3 := r3.Vector{0, 0, -eq[3] / eq[2]} - - v1 := p2.Sub(p1).Normalize() - v2 := p3.Sub(p1).Normalize() - - planeNormVec := v1.Cross(v2) - planeNormVec = planeNormVec.Normalize() - test.That(t, math.Acos(planeNormVec.Dot(groundNormVec)), test.ShouldBeLessThanOrEqualTo, angleThresh*math.Pi/180) -} - -func TestSegmentPlane(t *testing.T) { - // Setting a global, so cannot use t.Parallel() - sortPositions = true - defer func() { - sortPositions = false - }() - - // Intel Sensor Extrinsic data from manufacturer - // Intel sensor depth 1024x768 to RGB 1280x720 - // Translation Vector : [-0.000828434,0.0139185,-0.0033418] - // Rotation Matrix : [0.999958,-0.00838489,0.00378392] - // : [0.00824708,0.999351,0.0350734] - // : [-0.00407554,-0.0350407,0.999378] - // Intel sensor RGB 1280x720 to depth 1024x768 - // Translation Vector : [0.000699992,-0.0140336,0.00285468] - // Rotation Matrix : [0.999958,0.00824708,-0.00407554] - // : [-0.00838489,0.999351,-0.0350407] - // : [0.00378392,0.0350734,0.999378] - // Intel sensor depth 1024x768 intrinsics - // Principal Point : 542.078, 398.016 - // Focal Length : 734.938, 735.516 - // get depth map - d, err := rimage.NewDepthMapFromFile( - context.Background(), - artifact.MustPath("vision/segmentation/pointcloudsegmentation/align-test-1615172036.png")) - test.That(t, err, test.ShouldBeNil) - - // Pixel to Meter - sensorParams, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - depthIntrinsics := &sensorParams.DepthCamera - cloud := depthadapter.ToPointCloud(d, depthIntrinsics) - test.That(t, err, test.ShouldBeNil) - // Segment Plane - nIter := 3000 - plane, _, err := SegmentPlane(context.Background(), cloud, nIter, 0.5) - eq := plane.Equation() - test.That(t, err, test.ShouldBeNil) - // assign gt plane equation - obtained from open3d library with the same parameters - gtPlaneEquation := make([]float64, 4) - // gtPlaneEquation = 0.02x + 1.00y + 0.09z + -1.12 = 0, obtained from Open3D - gtPlaneEquation[0] = 0.02 - gtPlaneEquation[1] = 1.0 - gtPlaneEquation[2] = 0.09 - gtPlaneEquation[3] = -1.12 - - dot := eq[0]*gtPlaneEquation[0] + eq[1]*gtPlaneEquation[1] + eq[2]*gtPlaneEquation[2] - tol := 0.75 - test.That(t, math.Abs(dot), test.ShouldBeGreaterThanOrEqualTo, tol) -} - -func TestDepthMapToPointCloud(t *testing.T) { - // Setting a global, so cannot use t.Parallel() - sortPositions = true - defer func() { - sortPositions = false - }() - - d, err := rimage.NewDepthMapFromFile( - context.Background(), - artifact.MustPath("vision/segmentation/pointcloudsegmentation/align-test-1615172036.png")) - test.That(t, err, test.ShouldBeNil) - sensorParams, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - depthIntrinsics := &sensorParams.DepthCamera - pc := depthadapter.ToPointCloud(d, depthIntrinsics) - test.That(t, err, test.ShouldBeNil) - test.That(t, pc.Size(), test.ShouldEqual, 456370) -} - -func TestProjectPlane3dPointsToRGBPlane(t *testing.T) { - // Setting a global, so cannot use t.Parallel() - sortPositions = true - defer func() { - sortPositions = false - }() - - rgb, err := rimage.NewImageFromFile(artifact.MustPath("vision/segmentation/pointcloudsegmentation/align-test-1615172036_color.png")) - test.That(t, err, test.ShouldBeNil) - d, err := rimage.NewDepthMapFromFile( - context.Background(), - artifact.MustPath("vision/segmentation/pointcloudsegmentation/align-test-1615172036.png")) - test.That(t, err, test.ShouldBeNil) - h, w := rgb.Height(), rgb.Width() - - // Get 3D Points - sensorParams, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(t, err, test.ShouldBeNil) - pts := depthadapter.ToPointCloud(d, &sensorParams.DepthCamera) - test.That(t, err, test.ShouldBeNil) - // Get rigid body transform between Depth and RGB sensor - // Apply RBT - transformedPoints, err := sensorParams.ApplyRigidBodyTransform(pts) - test.That(t, err, test.ShouldBeNil) - // Re-project 3D Points in RGB Plane - pixel2meter := 0.001 - coordinatesRGB, err := transform.ProjectPointCloudToRGBPlane(transformedPoints, h, w, sensorParams.ColorCamera, pixel2meter) - test.That(t, err, test.ShouldBeNil) - // fill image - upLeft := image.Point{0, 0} - lowRight := image.Point{w, h} - - img := image.NewGray16(image.Rectangle{upLeft, lowRight}) - coordinatesRGB.Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - if pt.Z > -1.0 { - img.Set(int(pt.X), int(pt.Y), color.Gray16{uint16(pt.Z / pixel2meter)}) - } - return true - }) - - maxPt := img.Bounds().Max - test.That(t, maxPt.X, test.ShouldEqual, rgb.Width()) - test.That(t, maxPt.Y, test.ShouldEqual, rgb.Height()) -} - -func BenchmarkPlaneSegmentPointCloud(b *testing.B) { - // Setting a global, so cannot use t.Parallel() - sortPositions = true - defer func() { - sortPositions = false - }() - - d, err := rimage.NewDepthMapFromFile( - context.Background(), - artifact.MustPath("vision/segmentation/pointcloudsegmentation/align-test-1615172036.png")) - test.That(b, err, test.ShouldBeNil) - - // Pixel to Meter - sensorParams, err := transform.NewDepthColorIntrinsicsExtrinsicsFromJSONFile(intel515ParamsPath) - test.That(b, err, test.ShouldBeNil) - depthIntrinsics := &sensorParams.DepthCamera - test.That(b, err, test.ShouldBeNil) - pts := depthadapter.ToPointCloud(d, depthIntrinsics) - test.That(b, err, test.ShouldBeNil) - for i := 0; i < b.N; i++ { - // Segment Plane - _, _, err := SegmentPlane(context.Background(), pts, 2500, 0.0025) - test.That(b, err, test.ShouldBeNil) - } -} - -func TestPointCloudSplit(t *testing.T) { - // Setting a global, so cannot use t.Parallel() - sortPositions = true - defer func() { - sortPositions = false - }() - - // make a simple point cloud - cloud := pc.New() - var err error - err = cloud.Set(pc.NewVector(1, 1, 1), pc.NewColoredData(color.NRGBA{255, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(1, 0, -1), pc.NewColoredData(color.NRGBA{0, 255, 0, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(-1, -2, -1), pc.NewColoredData(color.NRGBA{0, 0, 255, 255})) - test.That(t, err, test.ShouldBeNil) - err = cloud.Set(pc.NewVector(0, 0, 0), pc.NewColoredData(color.NRGBA{0, 0, 0, 255})) - test.That(t, err, test.ShouldBeNil) - // 2-2 split map - map1 := map[r3.Vector]bool{ - {1, 1, 1}: true, - {0, 0, 0}: true, - } - mapCloud, nonMapCloud, err := pointCloudSplit(cloud, map1) - test.That(t, err, test.ShouldBeNil) - test.That(t, mapCloud.Size(), test.ShouldEqual, 2) - test.That(t, nonMapCloud.Size(), test.ShouldEqual, 2) - // map of all points - map2 := map[r3.Vector]bool{ - {1, 1, 1}: true, - {0, 0, 0}: true, - {-1, -2, -1}: true, - {1, 0, -1}: true, - } - mapCloud, nonMapCloud, err = pointCloudSplit(cloud, map2) - test.That(t, err, test.ShouldBeNil) - test.That(t, mapCloud.Size(), test.ShouldEqual, 4) - test.That(t, nonMapCloud.Size(), test.ShouldEqual, 0) - // empty map - map3 := map[r3.Vector]bool{} - mapCloud, nonMapCloud, err = pointCloudSplit(cloud, map3) - test.That(t, err, test.ShouldBeNil) - test.That(t, mapCloud.Size(), test.ShouldEqual, 0) - test.That(t, nonMapCloud.Size(), test.ShouldEqual, 4) - // map with invalid points - map4 := map[r3.Vector]bool{ - {1, 1, 1}: true, - {0, 2, 0}: true, - } - mapCloud, nonMapCloud, err = pointCloudSplit(cloud, map4) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, mapCloud, test.ShouldBeNil) - test.That(t, nonMapCloud, test.ShouldBeNil) -} diff --git a/vision/segmentation/radius_clustering.go b/vision/segmentation/radius_clustering.go deleted file mode 100644 index 588ab8f6596..00000000000 --- a/vision/segmentation/radius_clustering.go +++ /dev/null @@ -1,190 +0,0 @@ -package segmentation - -import ( - "context" - - "github.com/go-viper/mapstructure/v2" - "github.com/golang/geo/r3" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/camera" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/resource" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" -) - -// RadiusClusteringConfig specifies the necessary parameters to apply the -// radius based clustering algo. -type RadiusClusteringConfig struct { - resource.TriviallyValidateConfig - MinPtsInPlane int `json:"min_points_in_plane"` - MaxDistFromPlane float64 `json:"max_dist_from_plane_mm"` - NormalVec r3.Vector `json:"ground_plane_normal_vec"` - AngleTolerance float64 `json:"ground_angle_tolerance_degs"` - MinPtsInSegment int `json:"min_points_in_segment"` - ClusteringRadiusMm float64 `json:"clustering_radius_mm"` - MeanKFiltering int `json:"mean_k_filtering"` - Label string `json:"label,omitempty"` -} - -// CheckValid checks to see in the input values are valid. -func (rcc *RadiusClusteringConfig) CheckValid() error { - if rcc.MinPtsInPlane <= 0 { - return errors.Errorf("min_points_in_plane must be greater than 0, got %v", rcc.MinPtsInPlane) - } - if rcc.MinPtsInSegment <= 0 { - return errors.Errorf("min_points_in_segment must be greater than 0, got %v", rcc.MinPtsInSegment) - } - if rcc.ClusteringRadiusMm <= 0 { - return errors.Errorf("clustering_radius_mm must be greater than 0, got %v", rcc.ClusteringRadiusMm) - } - if rcc.MaxDistFromPlane == 0 { - rcc.MaxDistFromPlane = 100 - } - if rcc.MaxDistFromPlane <= 0 { - return errors.Errorf("max_dist_from_plane must be greater than 0, got %v", rcc.MaxDistFromPlane) - } - if rcc.AngleTolerance > 180 || rcc.AngleTolerance < 0 { - return errors.Errorf("max_angle_of_plane must between 0 & 180 (inclusive), got %v", rcc.AngleTolerance) - } - if rcc.NormalVec.Norm2() == 0 { - rcc.NormalVec = r3.Vector{X: 0, Y: 0, Z: 1} - } - if !rcc.NormalVec.IsUnit() { - return errors.Errorf("ground_plane_normal_vec should be a unit vector, got %v", rcc.NormalVec) - } - return nil -} - -// ConvertAttributes changes the AttributeMap input into a RadiusClusteringConfig. -func (rcc *RadiusClusteringConfig) ConvertAttributes(am utils.AttributeMap) error { - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: rcc}) - if err != nil { - return err - } - err = decoder.Decode(am) - if err == nil { - err = rcc.CheckValid() - } - return err -} - -// NewRadiusClustering returns a Segmenter that removes the planes (if any) and returns -// a segmentation of the objects in a point cloud using a radius based clustering algo -// described in the paper "A Clustering Method for Efficient Segmentation of 3D Laser Data" by Klasing et al. 2008. -func NewRadiusClustering(params utils.AttributeMap) (Segmenter, error) { - // convert attributes to appropriate struct - if params == nil { - return nil, errors.New("config for radius clustering segmentation cannot be nil") - } - cfg := &RadiusClusteringConfig{} - err := cfg.ConvertAttributes(params) - if err != nil { - return nil, err - } - return cfg.RadiusClustering, nil -} - -// RadiusClustering applies the radius clustering algorithm directly on a given point cloud. -func (rcc *RadiusClusteringConfig) RadiusClustering(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - // get next point cloud - cloud, err := src.NextPointCloud(ctx) - if err != nil { - return nil, err - } - ps := NewPointCloudGroundPlaneSegmentation(cloud, rcc.MaxDistFromPlane, rcc.MinPtsInPlane, rcc.AngleTolerance, rcc.NormalVec) - // if there are found planes, remove them, and keep all the non-plane points - _, nonPlane, err := ps.FindGroundPlane(ctx) - if err != nil { - return nil, err - } - // filter out the noise on the point cloud if mean K is greater than 0 - if rcc.MeanKFiltering > 0.0 { - filter, err := pc.StatisticalOutlierFilter(rcc.MeanKFiltering, 1.25) - if err != nil { - return nil, err - } - nonPlane, err = filter(nonPlane) - if err != nil { - return nil, err - } - } - // do the segmentation - segments, err := segmentPointCloudObjects(nonPlane, rcc.ClusteringRadiusMm, rcc.MinPtsInSegment) - if err != nil { - return nil, err - } - objects, err := NewSegmentsFromSlice(segments, rcc.Label) - if err != nil { - return nil, err - } - return objects.Objects, nil -} - -// segmentPointCloudObjects uses radius based nearest neighbors to segment the images, and then prunes away -// segments that do not pass a certain threshold of points. -func segmentPointCloudObjects(cloud pc.PointCloud, radius float64, nMin int) ([]pc.PointCloud, error) { - segments, err := radiusBasedNearestNeighbors(cloud, radius) - if err != nil { - return nil, err - } - segments = pc.PrunePointClouds(segments, nMin) - return segments, nil -} - -// radiusBasedNearestNeighbors partitions the pointcloud, grouping points within a given radius of each other. -func radiusBasedNearestNeighbors(cloud pc.PointCloud, radius float64) ([]pc.PointCloud, error) { - kdt, ok := cloud.(*pc.KDTree) - if !ok { - kdt = pc.ToKDTree(cloud) - } - var err error - clusters := NewSegments() - c := 0 - kdt.Iterate(0, 0, func(v r3.Vector, d pc.Data) bool { - // skip if point already is assigned cluster - if _, ok := clusters.Indices[v]; ok { - return true - } - // if not assigned, see if any of its neighbors are assigned a cluster - nn := kdt.RadiusNearestNeighbors(v, radius, false) - for _, neighbor := range nn { - nv := neighbor.P - ptIndex, ptOk := clusters.Indices[v] - neighborIndex, neighborOk := clusters.Indices[nv] - switch { - case ptOk && neighborOk: - if ptIndex != neighborIndex { - err = clusters.MergeClusters(ptIndex, neighborIndex) - } - case !ptOk && neighborOk: - err = clusters.AssignCluster(v, d, neighborIndex) - case ptOk && !neighborOk: - err = clusters.AssignCluster(neighbor.P, neighbor.D, ptIndex) - } - if err != nil { - return false - } - } - // if none of the neighbors were assigned a cluster, create a new cluster and assign all neighbors to it - if _, ok := clusters.Indices[v]; !ok { - err = clusters.AssignCluster(v, d, c) - if err != nil { - return false - } - for _, neighbor := range nn { - err = clusters.AssignCluster(neighbor.P, neighbor.D, c) - if err != nil { - return false - } - } - c++ - } - return true - }) - if err != nil { - return nil, err - } - return clusters.PointClouds(), nil -} diff --git a/vision/segmentation/radius_clustering_test.go b/vision/segmentation/radius_clustering_test.go deleted file mode 100644 index da7b247118e..00000000000 --- a/vision/segmentation/radius_clustering_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package segmentation_test - -import ( - "context" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" - "go.viam.com/rdk/vision/segmentation" -) - -func TestRadiusClusteringValidate(t *testing.T) { - cfg := segmentation.RadiusClusteringConfig{} - // invalid points in plane - err := cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "min_points_in_plane must be greater than 0") - // invalid points in segment - cfg.MinPtsInPlane = 5 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "min_points_in_segment must be greater than 0") - // invalid clustering radius - cfg.MinPtsInSegment = 5 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "clustering_radius_mm must be greater than 0") - // invalid angle from plane - cfg.ClusteringRadiusMm = 5 - cfg.AngleTolerance = 190 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "max_angle_of_plane must between 0 & 180 (inclusive)") - // valid - cfg.AngleTolerance = 180 - cfg.MeanKFiltering = 5 - cfg.MaxDistFromPlane = 4 - err = cfg.CheckValid() - test.That(t, err, test.ShouldBeNil) - - // cfg succeeds even without MaxDistFromPlane, AngleTolerance, NormalVec - cfg = segmentation.RadiusClusteringConfig{ - MinPtsInPlane: 10, - MinPtsInSegment: 10, - ClusteringRadiusMm: 10, - MeanKFiltering: 10, - } - err = cfg.CheckValid() - test.That(t, err, test.ShouldBeNil) -} - -// get a segmentation of a pointcloud and calculate each object's center. -func TestPixelSegmentation(t *testing.T) { - t.Parallel() - logger := logging.NewTestLogger(t) - injectCamera := &inject.Camera{} - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return pc.NewFromLASFile(artifact.MustPath("pointcloud/test.las"), logger) - } - // do segmentation - expectedLabel := "test_label" - objConfig := utils.AttributeMap{ - "min_points_in_plane": 50000, - "max_dist_from_plane": 10, - "min_points_in_segment": 500, - "clustering_radius_mm": 10.0, - "mean_k_filtering": 50.0, - "extra_uneeded_param": 4444, - "another_extra_one": "hey", - "label": expectedLabel, - } - segmenter, err := segmentation.NewRadiusClustering(objConfig) - test.That(t, err, test.ShouldBeNil) - segments, err := segmenter(context.Background(), injectCamera) - test.That(t, err, test.ShouldBeNil) - testSegmentation(t, segments, expectedLabel) -} - -func TestPixelSegmentationNoFiltering(t *testing.T) { - t.Parallel() - logger := logging.NewTestLogger(t) - injectCamera := &inject.Camera{} - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return pc.NewFromLASFile(artifact.MustPath("pointcloud/test.las"), logger) - } - // do segmentation with no mean k filtering - expectedLabel := "test_label" - objConfig := utils.AttributeMap{ - "min_points_in_plane": 9000, - "max_dist_from_plane": 110.0, - "min_points_in_segment": 350, - "max_angle_of_plane": 30, - "clustering_radius_mm": 600.0, - "mean_k_filtering": 0, - "extra_uneeded_param": 4444, - "another_extra_one": "hey", - "label": expectedLabel, - } - segmenter, err := segmentation.NewRadiusClustering(objConfig) - test.That(t, err, test.ShouldBeNil) - segments, err := segmenter(context.Background(), injectCamera) - test.That(t, err, test.ShouldBeNil) - testSegmentation(t, segments, expectedLabel) -} - -func testSegmentation(t *testing.T, segments []*vision.Object, expectedLabel string) { - t.Helper() - test.That(t, len(segments), test.ShouldBeGreaterThan, 0) - for _, seg := range segments { - box, err := pc.BoundingBoxFromPointCloudWithLabel(seg, seg.Geometry.Label()) - if seg.Size() == 0 { - test.That(t, box, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - continue - } - test.That(t, box, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.GeometriesAlmostEqual(box, seg.Geometry), test.ShouldBeTrue) - test.That(t, box.Label(), test.ShouldEqual, expectedLabel) - } -} - -func BenchmarkRadiusClustering(b *testing.B) { - injectCamera := &inject.Camera{} - injectCamera.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return pc.NewFromLASFile(artifact.MustPath("pointcloud/test.las"), nil) - } - var pts []*vision.Object - var err error - // do segmentation - objConfig := utils.AttributeMap{ - "min_points_in_plane": 9000, - "max_dist_from_plane": 110.0, - "min_points_in_segment": 350, - "max_angle_of_plane": 30, - "clustering_radius_mm": 600.0, - "mean_k_filtering": 0, - } - segmenter, _ := segmentation.NewRadiusClustering(objConfig) - for i := 0; i < b.N; i++ { - pts, err = segmenter(context.Background(), injectCamera) - } - // to prevent vars from being optimized away - if pts == nil || err != nil { - panic("segmenter didn't work") - } -} diff --git a/vision/segmentation/radius_clustering_voxel.go b/vision/segmentation/radius_clustering_voxel.go deleted file mode 100644 index 06966ba1001..00000000000 --- a/vision/segmentation/radius_clustering_voxel.go +++ /dev/null @@ -1,202 +0,0 @@ -package segmentation - -import ( - "context" - "fmt" - - "github.com/go-viper/mapstructure/v2" - "github.com/golang/geo/r3" - "github.com/pkg/errors" - - "go.viam.com/rdk/components/camera" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" -) - -// RadiusClusteringVoxelConfig specifies the necessary parameters for 3D object finding. -type RadiusClusteringVoxelConfig struct { - VoxelSize float64 `json:"voxel_size"` - Lambda float64 `json:"lambda"` // clustering parameter for making voxel planes - MinPtsInPlane int `json:"min_points_in_plane"` - MaxDistFromPlane float64 `json:"max_dist_from_plane"` - MinPtsInSegment int `json:"min_points_in_segment"` - ClusteringRadiusMm float64 `json:"clustering_radius_mm"` - WeightThresh float64 `json:"weight_threshold"` - AngleThresh float64 `json:"angle_threshold_degs"` - CosineThresh float64 `json:"cosine_threshold"` // between -1 and 1, the value after evaluating Cosine(theta) - DistanceThresh float64 `json:"distance_threshold_mm"` - Label string `json:"label,omitempty"` -} - -// CheckValid checks to see in the input values are valid. -func (rcc *RadiusClusteringVoxelConfig) CheckValid() error { - if rcc.VoxelSize <= 0 { - return errors.Errorf("voxel_size must be greater than 0, got %v", rcc.VoxelSize) - } - if rcc.Lambda <= 0 { - return errors.Errorf("lambda must be greater than 0, got %v", rcc.Lambda) - } - radiusClustering := RadiusClusteringConfig{ - MinPtsInPlane: rcc.MinPtsInPlane, - MaxDistFromPlane: rcc.MaxDistFromPlane, - MinPtsInSegment: rcc.MinPtsInSegment, - ClusteringRadiusMm: rcc.ClusteringRadiusMm, - MeanKFiltering: 50.0, - Label: rcc.Label, - } - err := radiusClustering.CheckValid() - if err != nil { - return err - } - voxelPlanes := VoxelGridPlaneConfig{rcc.WeightThresh, rcc.AngleThresh, rcc.CosineThresh, rcc.DistanceThresh} - err = voxelPlanes.CheckValid() - if err != nil { - return err - } - return nil -} - -// ConvertAttributes changes the AttributeMap input into a RadiusClusteringVoxelConfig. -func (rcc *RadiusClusteringVoxelConfig) ConvertAttributes(am utils.AttributeMap) error { - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{TagName: "json", Result: rcc}) - if err != nil { - return err - } - err = decoder.Decode(am) - if err != nil { - return err - } - return rcc.CheckValid() -} - -// NewRadiusClusteringFromVoxels removes the planes (if any) and returns a segmentation of the objects in a point cloud. -func NewRadiusClusteringFromVoxels(params utils.AttributeMap) (Segmenter, error) { - // convert attributes to appropriate struct - if params == nil { - return nil, errors.New("config for radius clustering segmentation cannot be nil") - } - cfg := &RadiusClusteringVoxelConfig{} - err := cfg.ConvertAttributes(params) - if err != nil { - return nil, err - } - return cfg.RadiusClusteringVoxels, nil -} - -// RadiusClusteringVoxels turns the cloud into a voxel grid and then does radius clustering to segment it. -func (rcc *RadiusClusteringVoxelConfig) RadiusClusteringVoxels(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) { - // get next point cloud and convert it to a VoxelGrid - // NOTE(bh): Maybe one day cameras will return voxel grids directly. - cloud, err := src.NextPointCloud(ctx) - if err != nil { - return nil, err - } - // turn the point cloud into a voxel grid - vg := pc.NewVoxelGridFromPointCloud(cloud, rcc.VoxelSize, rcc.Lambda) - planeConfig := VoxelGridPlaneConfig{rcc.WeightThresh, rcc.AngleThresh, rcc.CosineThresh, rcc.DistanceThresh} - ps := NewVoxelGridPlaneSegmentation(vg, planeConfig) - planes, nonPlane, err := ps.FindPlanes(ctx) - if err != nil { - return nil, err - } - // if there is a found plane in the scene, take the biggest plane, and only save the non-plane points above it - if len(planes) > 0 { - nonPlane, _, err = SplitPointCloudByPlane(nonPlane, planes[0]) - if err != nil { - return nil, err - } - } - objVoxGrid := pc.NewVoxelGridFromPointCloud(nonPlane, vg.VoxelSize(), vg.Lambda()) - objects, err := voxelBasedNearestNeighbors(objVoxGrid, rcc.ClusteringRadiusMm) - if err != nil { - return nil, err - } - objects = pc.PrunePointClouds(objects, rcc.MinPtsInSegment) - segments, err := NewSegmentsFromSlice(objects, rcc.Label) - if err != nil { - return nil, err - } - - return segments.Objects, nil -} - -func voxelBasedNearestNeighbors(vg *pc.VoxelGrid, radius float64) ([]pc.PointCloud, error) { - var err error - vSize := vg.VoxelSize() - clusters := NewSegments() - c := 0 - for coord, vox := range vg.Voxels { - v := r3.Vector{float64(coord.I), float64(coord.J), float64(coord.K)} - // skip if point already is assigned cluster - if _, ok := clusters.Indices[v]; ok { - continue - } - // if not assigned, see if any of its neighbors are assigned a cluster - n := int((radius - 0.5*vSize) / vSize) - if n < 1 { - return nil, fmt.Errorf("cannot use radius %v to cluster voxels of size %v", radius, vSize) - } - nn := findVoxelNeighborsInRadius(vg, coord, uint(n)) - for nv, neighborVox := range nn { - ptIndex, ptOk := clusters.Indices[v] - neighborIndex, neighborOk := clusters.Indices[nv] - switch { - case ptOk && neighborOk: - if ptIndex != neighborIndex { - err = clusters.MergeClusters(ptIndex, neighborIndex) - if err != nil { - return nil, err - } - } - case !ptOk && neighborOk: - clusters.Indices[v] = neighborIndex // label the voxel coordinate - for p, d := range vox.Points { - err = clusters.AssignCluster(p, d, neighborIndex) // label all points in the voxel - if err != nil { - return nil, err - } - } - case ptOk && !neighborOk: - clusters.Indices[nv] = ptIndex - for p, d := range neighborVox.Points { - err = clusters.AssignCluster(p, d, ptIndex) - if err != nil { - return nil, err - } - } - } - } - // if none of the neighbors were assigned a cluster, create a new cluster and assign all neighbors to it - if _, ok := clusters.Indices[v]; !ok { - clusters.Indices[v] = c - for p, d := range vox.Points { - err = clusters.AssignCluster(p, d, c) - if err != nil { - return nil, err - } - } - for nv, neighborVox := range nn { - clusters.Indices[nv] = c - for p, d := range neighborVox.Points { - err = clusters.AssignCluster(p, d, c) - if err != nil { - return nil, err - } - } - } - c++ - } - } - return clusters.PointClouds(), nil -} - -func findVoxelNeighborsInRadius(vg *pc.VoxelGrid, coord pc.VoxelCoords, n uint) map[r3.Vector]*pc.Voxel { - neighbors := make(map[r3.Vector]*pc.Voxel) - adjCoords := vg.GetNNearestVoxels(vg.Voxels[coord], n) - for _, c := range adjCoords { - loc := r3.Vector{float64(c.I), float64(c.J), float64(c.K)} - neighbors[loc] = vg.Voxels[c] - } - return neighbors -} diff --git a/vision/segmentation/radius_clustering_voxel_test.go b/vision/segmentation/radius_clustering_voxel_test.go deleted file mode 100644 index d0b9ce6ae0e..00000000000 --- a/vision/segmentation/radius_clustering_voxel_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package segmentation_test - -import ( - "context" - "testing" - - "go.viam.com/test" - "go.viam.com/utils/artifact" - - "go.viam.com/rdk/logging" - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/testutils/inject" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision/segmentation" -) - -func TestClusteringVoxelConfig(t *testing.T) { - // invalid voxel size - cfg := segmentation.RadiusClusteringVoxelConfig{} - err := cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "voxel_size must be greater than 0") - // invalid lambda - cfg.VoxelSize = 2.0 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "lambda must be greater than 0") - // invalid clustering - cfg.Lambda = 0.1 - cfg.MinPtsInSegment = 5 - cfg.ClusteringRadiusMm = 5 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "min_points_in_plane must be greater than 0") - // invalid plane config - cfg.MinPtsInPlane = 5 - cfg.WeightThresh = -1 - cfg.AngleThresh = 40 - cfg.CosineThresh = .1 - cfg.DistanceThresh = 44 - cfg.MaxDistFromPlane = 10 - err = cfg.CheckValid() - test.That(t, err.Error(), test.ShouldContainSubstring, "weight_threshold cannot be less than 0") - // valid - cfg.WeightThresh = 1 - err = cfg.CheckValid() - test.That(t, err, test.ShouldBeNil) -} - -func TestVoxelSegmentMeans(t *testing.T) { - logger := logging.NewTestLogger(t) - cam := &inject.Camera{} - cam.NextPointCloudFunc = func(ctx context.Context) (pc.PointCloud, error) { - return pc.NewFromLASFile(artifact.MustPath("pointcloud/test.las"), logger) - } - - // Do voxel segmentation - expectedLabel := "test_label" - voxObjConfig := utils.AttributeMap{ - "voxel_size": 1.0, - "lambda": 0.1, - "min_points_in_plane": 100, - "max_dist_from_plane": 10, - "min_points_in_segment": 25, - "clustering_radius_mm": 7.5, - "weight_threshold": 0.9, - "angle_threshold": 30, - "cosine_threshold": 0.1, - "distance_threshold": 0.1, - "label": expectedLabel, - } - - segmenter, err := segmentation.NewRadiusClusteringFromVoxels(voxObjConfig) - test.That(t, err, test.ShouldBeNil) - voxSegments, err := segmenter(context.Background(), cam) - test.That(t, err, test.ShouldBeNil) - testSegmentation(t, voxSegments, expectedLabel) -} diff --git a/vision/segmentation/segmented_image.go b/vision/segmentation/segmented_image.go deleted file mode 100644 index 16beb38e805..00000000000 --- a/vision/segmentation/segmented_image.go +++ /dev/null @@ -1,137 +0,0 @@ -package segmentation - -import ( - "image" - "image/color" - - "github.com/lucasb-eyer/go-colorful" - - "go.viam.com/rdk/rimage" -) - -// SegmentedImage TODO. -type SegmentedImage struct { - palette []color.Color - dots []int // a value of 0 means no segment, < 0 is transient, > 0 is the segment number - width int - height int -} - -func newSegmentedImage(img *rimage.Image) *SegmentedImage { - si := &SegmentedImage{ - width: img.Width(), - height: img.Height(), - } - si.dots = make([]int, si.width*si.height) - return si -} - -// Height TODO. -func (si *SegmentedImage) Height() int { - return si.height -} - -// Width TODO. -func (si *SegmentedImage) Width() int { - return si.width -} - -func (si *SegmentedImage) toK(p image.Point) int { - return (p.Y * si.width) + p.X -} - -func (si *SegmentedImage) fromK(k int) image.Point { - y := k / si.width - x := k - (y * si.width) - return image.Point{x, y} -} - -// GetSegment TODO. -func (si *SegmentedImage) GetSegment(p image.Point) int { - return si.get(p) -} - -func (si *SegmentedImage) get(p image.Point) int { - k := si.toK(p) - if k < 0 || k >= len(si.dots) { - return 0 - } - return si.dots[k] -} - -func (si *SegmentedImage) set(p image.Point, val int) { - k := si.toK(p) - if k < 0 || k >= len(si.dots) { - return - } - si.dots[k] = val -} - -// PixelsInSegmemnt TODO. -func (si *SegmentedImage) PixelsInSegmemnt(segment int) int { - num := 0 - for _, v := range si.dots { - if v == segment { - num++ - } - } - return num -} - -// ColorModel TODO. -func (si *SegmentedImage) ColorModel() color.Model { - return color.RGBAModel -} - -// Bounds TODO. -func (si *SegmentedImage) Bounds() image.Rectangle { - return image.Rect(0, 0, si.width, si.height) -} - -// At TODO. -func (si *SegmentedImage) At(x, y int) color.Color { - v := si.get(image.Point{x, y}) - if v <= 0 { - return color.RGBA{0, 0, 0, 0} - } - return si.palette[v-1] -} - -func (si *SegmentedImage) createPalette() { - max := 0 - for _, v := range si.dots { - if v > max { - max = v - } - } - - if max == 0 { - // no segments - return - } - - palette := colorful.FastWarmPalette(max) - - for _, p := range palette { - si.palette = append(si.palette, p) - } -} - -// NumInAnyCluster TODO. -func (si *SegmentedImage) NumInAnyCluster() int { - num := 0 - for _, v := range si.dots { - if v > 0 { - num++ - } - } - return num -} - -func (si *SegmentedImage) clearTransients() { - for k, v := range si.dots { - if v < 0 { - si.dots[k] = 0 - } - } -} diff --git a/vision/segmentation/segmenter.go b/vision/segmentation/segmenter.go deleted file mode 100644 index b856ea0a693..00000000000 --- a/vision/segmentation/segmenter.go +++ /dev/null @@ -1,11 +0,0 @@ -package segmentation - -import ( - "context" - - "go.viam.com/rdk/components/camera" - "go.viam.com/rdk/vision" -) - -// A Segmenter is a function that takes images/pointclouds from an input source and segments them into objects. -type Segmenter func(ctx context.Context, src camera.VideoSource) ([]*vision.Object, error) diff --git a/vision/segmentation/segments.go b/vision/segmentation/segments.go deleted file mode 100644 index 804497e9777..00000000000 --- a/vision/segmentation/segments.go +++ /dev/null @@ -1,132 +0,0 @@ -package segmentation - -import ( - "fmt" - - "github.com/golang/geo/r3" - - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/utils" - "go.viam.com/rdk/vision" -) - -// Segments is a struct for keeping track of the individual objects of a point cloud as they are being built. -// Objects is a slice of all the objects, and Indices is a map that assigns each point to the object index it is a part of. -type Segments struct { - Objects []*vision.Object - Indices map[r3.Vector]int -} - -// NewSegments creates an empty new Segments struct. -func NewSegments() *Segments { - segments := make([]*vision.Object, 0) - indices := make(map[r3.Vector]int) - return &Segments{segments, indices} -} - -// NewSegmentsFromSlice creates a Segments struct from a slice of point clouds. -func NewSegmentsFromSlice(clouds []pc.PointCloud, label string) (*Segments, error) { - segments := NewSegments() - for i, cloud := range clouds { - seg, err := vision.NewObjectWithLabel(cloud, label, nil) - if err != nil { - return nil, err - } - segments.Objects = append(segments.Objects, seg) - cloud.Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - segments.Indices[pt] = i - return true - }) - } - return segments, nil -} - -// N gives the number of objects in the partition of the point cloud. -func (c *Segments) N() int { - return len(c.Objects) -} - -// PointClouds returns the underlying array of pointclouds. -func (c *Segments) PointClouds() []pc.PointCloud { - clouds := make([]pc.PointCloud, c.N()) - for i := 0; i < c.N(); i++ { - clouds[i] = c.Objects[i] - } - return clouds -} - -// SelectPointCloudFromPoint takes a 3D point as input and outputs the point cloud of the segment that the point belongs to. -func (c *Segments) SelectPointCloudFromPoint(x, y, z float64) (pc.PointCloud, error) { - v := r3.Vector{x, y, z} - if segIndex, ok := c.Indices[v]; ok { - return c.Objects[segIndex], nil - } - return nil, fmt.Errorf("no segment found at point (%v, %v, %v)", x, y, z) -} - -// getPointCloudAndLabel returns the *vision.Object from `objs` at `idx` and the label from -// vision.Object.spatialmath.Geometry if the Geometry exists, or otherwise an empty string. -func getPointCloudAndLabel(objs []*vision.Object, idx int) (*vision.Object, string) { - obj := objs[idx] - if obj.Geometry != nil { - return obj, obj.Geometry.Label() - } - return obj, "" -} - -// AssignCluster assigns the given point to the cluster with the given index. -func (c *Segments) AssignCluster(point r3.Vector, data pc.Data, index int) error { - for index >= len(c.Objects) { - c.Objects = append(c.Objects, vision.NewEmptyObject()) - } - c.Indices[point] = index - err := c.Objects[index].Set(point, data) - if err != nil { - return err - } - if c.Objects[index].Size() == 0 { - return nil - } - idx, label := getPointCloudAndLabel(c.Objects, index) - c.Objects[index].Geometry, err = pc.BoundingBoxFromPointCloudWithLabel(idx, label) - if err != nil { - return err - } - return nil -} - -// MergeClusters moves all the points in index "from" to the segment at index "to". -func (c *Segments) MergeClusters(from, to int) error { - // ensure no out of bounds errors - index := utils.MaxInt(from, to) - for index >= len(c.Objects) { - c.Objects = append(c.Objects, vision.NewEmptyObject()) - } - - // if no objects are in the cluster to delete, just return - if c.Objects[from].Size() == 0 { - return nil - } - - // perform merge - var err error - c.Objects[from].Iterate(0, 0, func(v r3.Vector, d pc.Data) bool { - c.Indices[v] = to - err = c.Objects[to].Set(v, d) - return err == nil - }) - if err != nil { - return err - } - c.Objects[from] = vision.NewEmptyObject() - // because BoundingBoxFromPointCloudWithLabel takes a PointCloud interface as its first argument, only the - // vision.Object.pointcloud.PointCloud is passed to this method from our vision.Object, not the spatialmath.Geometry. - // Consequently, it cannot access spatialmath.Geometry.Label(). Therefore, we must pass the label here before this - // information is lost. - t, label := getPointCloudAndLabel(c.Objects, to) - c.Objects[to].Geometry, err = pc.BoundingBoxFromPointCloudWithLabel(t, label) - if err != nil { - return err - } - return nil -} diff --git a/vision/segmentation/segments_test.go b/vision/segmentation/segments_test.go deleted file mode 100644 index 38cc411e21d..00000000000 --- a/vision/segmentation/segments_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package segmentation - -import ( - "testing" - - "github.com/golang/geo/r3" - "go.viam.com/test" - - pc "go.viam.com/rdk/pointcloud" - "go.viam.com/rdk/spatialmath" - "go.viam.com/rdk/vision" -) - -func createPointClouds(t *testing.T) *Segments { - t.Helper() - clusters := make([]*vision.Object, 0) - cloudMap := make(map[r3.Vector]int) - clouds := make([]pc.PointCloud, 0) - for i := 0; i < 3; i++ { - clouds = append(clouds, pc.New()) - } - - // create 1st cloud - p00 := pc.NewVector(0, 0, 0) - cloudMap[p00] = 0 - test.That(t, clouds[0].Set(p00, nil), test.ShouldBeNil) - p01 := pc.NewVector(0, 0, 1) - cloudMap[p01] = 0 - test.That(t, clouds[0].Set(p01, nil), test.ShouldBeNil) - p02 := pc.NewVector(0, 1, 0) - cloudMap[p02] = 0 - test.That(t, clouds[0].Set(p02, nil), test.ShouldBeNil) - p03 := pc.NewVector(0, 1, 1) - cloudMap[p03] = 0 - test.That(t, clouds[0].Set(p03, nil), test.ShouldBeNil) - testPointCloudBoundingBox(t, clouds[0], r3.Vector{0, 0.5, 0.5}, r3.Vector{0, 1, 1}) - obj, err := vision.NewObject(clouds[0]) - test.That(t, err, test.ShouldBeNil) - clusters = append(clusters, obj) - - // create a 2nd cloud far away - p10 := pc.NewVector(30, 0, 0) - cloudMap[p10] = 1 - test.That(t, clouds[1].Set(p10, nil), test.ShouldBeNil) - p11 := pc.NewVector(30, 0, 1) - cloudMap[p11] = 1 - test.That(t, clouds[1].Set(p11, nil), test.ShouldBeNil) - p12 := pc.NewVector(30, 1, 0) - cloudMap[p12] = 1 - test.That(t, clouds[1].Set(p12, nil), test.ShouldBeNil) - p13 := pc.NewVector(30, 1, 1) - cloudMap[p13] = 1 - test.That(t, clouds[1].Set(p13, nil), test.ShouldBeNil) - testPointCloudBoundingBox(t, clouds[1], r3.Vector{30, 0.5, 0.5}, r3.Vector{0, 1, 1}) - obj, err = vision.NewObject(clouds[1]) - test.That(t, err, test.ShouldBeNil) - clusters = append(clusters, obj) - - // create 3rd cloud - p20 := pc.NewVector(0, 30, 0) - cloudMap[p20] = 2 - test.That(t, clouds[2].Set(p20, nil), test.ShouldBeNil) - p21 := pc.NewVector(0, 30, 1) - cloudMap[p21] = 2 - test.That(t, clouds[2].Set(p21, nil), test.ShouldBeNil) - p22 := pc.NewVector(1, 30, 0) - cloudMap[p22] = 2 - test.That(t, clouds[2].Set(p22, nil), test.ShouldBeNil) - p23 := pc.NewVector(1, 30, 1) - cloudMap[p23] = 2 - test.That(t, clouds[2].Set(p23, nil), test.ShouldBeNil) - p24 := pc.NewVector(0.5, 30, 0.5) - cloudMap[p24] = 2 - test.That(t, clouds[2].Set(p24, nil), test.ShouldBeNil) - testPointCloudBoundingBox(t, clouds[2], r3.Vector{0.5, 30, 0.5}, r3.Vector{1, 0, 1}) - obj, err = vision.NewObject(clouds[2]) - test.That(t, err, test.ShouldBeNil) - clusters = append(clusters, obj) - return &Segments{clusters, cloudMap} -} - -func TestAssignCluter(t *testing.T) { - clusters := createPointClouds(t) - test.That(t, clusters.N(), test.ShouldEqual, 3) - - // assign a new cluster - p30 := pc.NewVector(30, 30, 1) - test.That(t, clusters.AssignCluster(p30, nil, 3), test.ShouldBeNil) - test.That(t, clusters.N(), test.ShouldEqual, 4) - test.That(t, clusters.Indices[p30], test.ShouldEqual, 3) - testPointCloudBoundingBox(t, clusters.Objects[3], r3.Vector{30, 30, 1}, r3.Vector{}) - - // assign a new cluster with a large index - pNew := pc.NewVector(30, 30, 30) - test.That(t, clusters.AssignCluster(pNew, nil, 100), test.ShouldBeNil) - test.That(t, clusters.N(), test.ShouldEqual, 101) - test.That(t, clusters.Indices[pNew], test.ShouldEqual, 100) - testPointCloudBoundingBox(t, clusters.Objects[100], r3.Vector{30, 30, 30}, r3.Vector{}) -} - -func TestMergeCluster(t *testing.T) { - clusters := createPointClouds(t) - - // before merge - test.That(t, clusters.Objects[0].Size(), test.ShouldEqual, 4) - test.That(t, clusters.Objects[1].Size(), test.ShouldEqual, 4) - test.That(t, clusters.Objects[2].Size(), test.ShouldEqual, 5) - for i := 0; i < 2; i++ { - clusters.Objects[i].Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - test.That(t, clusters.Indices[pt], test.ShouldEqual, i) - return true - }) - } - - // merge - test.That(t, clusters.MergeClusters(0, 1), test.ShouldBeNil) - - // after merge - test.That(t, clusters.Objects[0].Size(), test.ShouldEqual, 0) - test.That(t, clusters.Objects[1].Size(), test.ShouldEqual, 8) - test.That(t, clusters.Objects[2].Size(), test.ShouldEqual, 5) - clusters.Objects[1].Iterate(0, 0, func(pt r3.Vector, d pc.Data) bool { - test.That(t, clusters.Indices[pt], test.ShouldEqual, 1) - return true - }) - test.That(t, clusters.Objects[0].Geometry, test.ShouldBeNil) - testPointCloudBoundingBox(t, clusters.Objects[1].PointCloud, r3.Vector{15, 0.5, 0.5}, r3.Vector{30, 1, 1}) - testPointCloudBoundingBox(t, clusters.Objects[2].PointCloud, r3.Vector{0.5, 30, 0.5}, r3.Vector{1, 0, 1}) - - // merge to new cluster - test.That(t, clusters.MergeClusters(2, 3), test.ShouldBeNil) - - // after merge - test.That(t, clusters.Objects[0].Size(), test.ShouldEqual, 0) - test.That(t, clusters.Objects[1].Size(), test.ShouldEqual, 8) - test.That(t, clusters.Objects[2].Size(), test.ShouldEqual, 0) - test.That(t, clusters.Objects[3].Size(), test.ShouldEqual, 5) - test.That(t, clusters.Objects[0].Geometry, test.ShouldBeNil) - testPointCloudBoundingBox(t, clusters.Objects[1].PointCloud, r3.Vector{15, 0.5, 0.5}, r3.Vector{30, 1, 1}) - test.That(t, clusters.Objects[2].Geometry, test.ShouldBeNil) - testPointCloudBoundingBox(t, clusters.Objects[3].PointCloud, r3.Vector{0.5, 30, 0.5}, r3.Vector{1, 0, 1}) -} - -func testPointCloudBoundingBox(t *testing.T, cloud pc.PointCloud, center, dims r3.Vector) { - t.Helper() - box, err := pc.BoundingBoxFromPointCloud(cloud) - if cloud.Size() == 0 { - test.That(t, box, test.ShouldBeNil) - test.That(t, err, test.ShouldNotBeNil) - } else { - test.That(t, box, test.ShouldNotBeNil) - test.That(t, err, test.ShouldBeNil) - boxExpected, err := spatialmath.NewBox(spatialmath.NewPoseFromPoint(center), dims, "") - test.That(t, err, test.ShouldBeNil) - test.That(t, spatialmath.GeometriesAlmostEqual(box, boxExpected), test.ShouldBeTrue) - } -} diff --git a/vision/segmentation/shapewalk.go b/vision/segmentation/shapewalk.go deleted file mode 100644 index cdbc739af44..00000000000 --- a/vision/segmentation/shapewalk.go +++ /dev/null @@ -1,528 +0,0 @@ -package segmentation - -import ( - "image" - "math" - - "github.com/pkg/errors" - - "go.viam.com/rdk/logging" - "go.viam.com/rdk/rimage" - "go.viam.com/rdk/utils" -) - -// TODO. -const ( - DefaultColorThreshold = 1.0 - DefaultLookback = 15 // how many pixels do we ensure are similar - DefaultLookbackScaling = 6.0 // the bigger the number, the tighter the threshold - DefaultInterestingThreshold = .45 - DefaultInterestingRange = 5 - DefaultAverageColorDistanceWeight = .5 -) - -// ShapeWalkOptions TODO. -type ShapeWalkOptions struct { - Debug bool - MaxRadius int // 0 means no max - SkipCleaning bool - ThresholdMod float64 // 0 means no modification > 0 means more things will match - Diffs *rimage.ColorDiffs -} - -type walkState struct { - img *rimage.Image - dots *SegmentedImage - options ShapeWalkOptions - threshold float64 - - originalColor rimage.Color - originalPoint image.Point - originalInterestingPixelDensity float64 - - interestingPixels *image.Gray - - depth *rimage.DepthMap - depthMin, depthMax rimage.Depth - depthRange float64 - - logger logging.Logger -} - -func (ws *walkState) initIfNot() { - if ws.interestingPixels == nil { - ws.interestingPixels = ws.img.InterestingPixels(.2) - if ws.depth != nil { - ws.depthMin, ws.depthMax = ws.depth.MinMax() - ws.depthRange = float64(ws.depthMax) - float64(ws.depthMin) - if ws.options.Debug { - ws.logger.Debugf("depthRange %v", ws.depthRange) - } - } - } -} - -func (ws *walkState) interestingPixelDensity(p image.Point) float64 { - total := 0.0 - interesting := 0.0 - - err := utils.Walk(p.X, p.Y, DefaultInterestingRange, - func(x, y int) error { - if x < 0 || x >= ws.img.Width() || y < 0 || y >= ws.img.Height() { - return nil - } - - total++ - - k := ws.interestingPixels.PixOffset(x, y) - if ws.interestingPixels.Pix[k] > 0 { - interesting++ - } - - return nil - }, - ) - if err != nil { - panic(err) // impossible - } - - return interesting / total -} - -func (ws *walkState) valid(p image.Point) bool { - return p.X >= 0 && p.X < ws.img.Width() && p.Y >= 0 && p.Y < ws.img.Height() -} - -/* - func (ws *walkState) towardsCenter(p image.Point, amount int) image.Point { - xd := p.X - ws.originalPoint.X - yd := p.Y - ws.originalPoint.Y - - ret := p - - if xd < 0 { - ret.X += utils.MinInt(amount, utils.AbsInt(xd)) - } else if xd > 0 { - ret.X -= utils.MinInt(amount, utils.AbsInt(xd)) - } - - if yd < 0 { - ret.Y += utils.MinInt(amount, utils.AbsInt(yd)) - } else if yd > 0 { - ret.Y -= utils.MinInt(amount, utils.AbsInt(yd)) - } - - return ret - }. -*/ -func (ws *walkState) isPixelIsCluster(p image.Point, clusterNumber int, path []image.Point) bool { - v := ws.dots.get(p) - if v == 0 { - good := ws.computeIfPixelIsCluster(p, path) - if good { - v = clusterNumber - // } else { - // v = -1 // TODO(erh): remove clearTransients if i don't put this back - } - ws.dots.set(p, v) - } - - return v == clusterNumber -} - -func (ws *walkState) computeIfPixelIsCluster(p image.Point, path []image.Point) bool { - if !ws.valid(p) { - return false - } - - if len(path) == 0 { - if p.X == ws.originalPoint.X && p.Y == ws.originalPoint.Y { - return true - } - panic("wtf") - } - - if ws.options.MaxRadius > 0 { - d1 := utils.AbsInt(p.X - ws.originalPoint.X) - d2 := utils.AbsInt(p.Y - ws.originalPoint.Y) - if d1 > ws.options.MaxRadius || d2 > ws.options.MaxRadius { - return false - } - } - - myColor := ws.img.Get(p) - - myInterestingPixelDensity := ws.interestingPixelDensity(p) - myInterestingPixelDensity = math.Abs(myInterestingPixelDensity - ws.originalInterestingPixelDensity) - - if ws.options.Debug { - ws.logger.Debugf("\t %v %v myInterestingPixelDensity: %v", p, myColor.Hex(), myInterestingPixelDensity) - } - - if myInterestingPixelDensity > DefaultInterestingThreshold { - if ws.options.Debug { - ws.logger.Debug("\t\t blocked b/c density") - } - return false - } - - lookback := DefaultLookback - toLookAt := path[utils.MaxInt(0, len(path)-lookback):] - for idx, prev := range toLookAt { - prevColor := ws.img.Get(prev) - d := prevColor.Distance(myColor) - if ws.options.Diffs != nil && d > .1 { - ws.options.Diffs.AddD(prevColor, myColor, d) - } - - threshold := ws.threshold + (float64(lookback-idx) / (float64(lookback) * DefaultLookbackScaling)) - depthThreshold := 0.0 - - if idx == len(toLookAt)-1 && ws.depth != nil { - // only look at the last point - myZ := ws.depth.Get(p) - prevZ := ws.depth.Get(prev) - if myZ > 0 && prevZ > 0 { - depthThreshold = math.Abs(float64(myZ) - float64(prevZ)) - // in mm right now - - // first we scale to 0 -> 1 based on the data in the image - depthThreshold /= ws.depthRange - - depthThreshold = ((depthThreshold - .01) * 50) - depthThreshold *= -1 - - if ws.options.Debug { - ws.logger.Debugf("\t\t\t XXX %v %v %v", myZ, prevZ, depthThreshold) - } - } else if myZ > 0 || prevZ > 0 { - // this means one of the points had good data and one didn't - // this usually means it's an edge or something - // so make the threshold a bit smaller - depthThreshold = -1.1 - } - } - - threshold += depthThreshold - - good := d < threshold - - if ws.options.Debug { - ws.logger.Debugf("\t\t %v %v %v %0.3f threshold: %0.3f depthThreshold: %0.3f", prev, good, prevColor.Hex(), d, threshold, depthThreshold) - if !good && d-threshold < .2 { - ws.logger.Debugf("\t\t\t http://www.viam.com/color.html?#1=%s&2=%s", myColor.Hex()[1:], prevColor.Hex()[1:]) - } - } - - if !good { - return false - } - } - - return true -} - -// return the number of pieces added. -func (ws *walkState) pieceWalk(start image.Point, clusterNumber int, path []image.Point, quadrant image.Point) int { - if !ws.valid(start) { - return 0 - } - - if len(path) > 0 && ws.dots.get(start) != 0 { - // don't recompute a spot - return 0 - } - - if !ws.isPixelIsCluster(start, clusterNumber, path) { - return 0 - } - - total := 0 - if len(path) > 0 { - // the original pixel is special and is counted in the main piece function - total++ - } - - total += ws.pieceWalk(image.Point{start.X + quadrant.X, start.Y + quadrant.Y}, clusterNumber, append(path, start), quadrant) - if quadrant.Y != 0 && quadrant.X != 0 { - total += ws.pieceWalk(image.Point{start.X, start.Y + quadrant.Y}, clusterNumber, append(path, start), quadrant) - total += ws.pieceWalk(image.Point{start.X + quadrant.X, start.Y}, clusterNumber, append(path, start), quadrant) - } - - return total -} - -var allDirections = []image.Point{ - {1, 1}, - {1, 0}, - {1, -1}, - - {-1, 1}, - {-1, 0}, - {-1, -1}, - - {0, 1}, - {1, 0}, -} - -// return the number of pieces in the cell. -func (ws *walkState) piece(start image.Point, clusterNumber int) int { - if ws.options.Debug { - ws.logger.Debugf("segmentation.piece start: %v", start) - } - - ws.initIfNot() - - // ws.originalColor = ws.img.ColorHSV(start) - ws.originalPoint = start - ws.originalInterestingPixelDensity = ws.interestingPixelDensity(start) - - temp, averageColorDistance := ws.img.AverageColorAndStats(start, 1) - ws.originalColor = temp - - //nolint:ifshort,nolintlint - oldThreshold := ws.threshold - defer func() { - ws.threshold = oldThreshold - }() - - ws.threshold += averageColorDistance * DefaultAverageColorDistanceWeight - ws.threshold += ws.originalInterestingPixelDensity - - if ws.options.Debug { - ws.logger.Debugf("\t\t averageColorDistance: %v originalInterestingPixelDensity: %v threshold: %v -> %v", - averageColorDistance, - ws.originalInterestingPixelDensity, - oldThreshold, ws.threshold, - ) - } - - origTotal := 1 // we count the original pixel here - - for _, dir := range allDirections { - origTotal += ws.pieceWalk(start, clusterNumber, []image.Point{}, dir) - } - - total := origTotal - if !ws.options.SkipCleaning { - total = ws.lookForWeirdShapes(clusterNumber) - } - - if ws.options.Debug && total != origTotal { - ws.logger.Debugf("shape walk did %v -> %v", origTotal, total) - } - - ws.dots.clearTransients() - - return total -} - -func (ws *walkState) countOut(start image.Point, clusterNumber int, dir image.Point) int { - total := 0 - for { - start = image.Point{start.X + dir.X, start.Y + dir.Y} - if ws.dots.get(start) != clusterNumber { - break - } - total++ - } - - return total -} - -func (ws *walkState) lookForWeirdShapesReset(start image.Point, clusterNumber int, dir image.Point, depth int) int { - old := ws.dots.get(start) - - good := (old == clusterNumber*-1) || (old == clusterNumber && depth == 0) - if !good { - return 0 - } - - ws.dots.set(start, clusterNumber) - - total := 0 - - if depth > 0 { - total++ - } - - total += ws.lookForWeirdShapesReset(image.Point{start.X + dir.X, start.Y + dir.Y}, clusterNumber, dir, depth+1) - if dir.X != 0 && dir.Y != 0 { - total += ws.lookForWeirdShapesReset(image.Point{start.X + dir.X, start.Y}, clusterNumber, dir, depth+1) - total += ws.lookForWeirdShapesReset(image.Point{start.X, start.Y + dir.Y}, clusterNumber, dir, depth+1) - } - - return total -} - -func (ws *walkState) lookForWeirdShapes(clusterNumber int) int { - for k, v := range ws.dots.dots { - if v != clusterNumber { - continue - } - - start := ws.dots.fromK(k) - - for _, dir := range []image.Point{{1, 0}, {1, 1}, {0, 1}, {-1, 1}} { - r := ws.countOut(start, clusterNumber, dir) - r += ws.countOut(start, clusterNumber, image.Point{dir.X * -1, dir.Y * -1}) - if r < 3 { - if ws.options.Debug { - ws.logger.Debugf("removing %v b/c radius: %d for direction: %v", start, r, dir) - } - ws.dots.dots[k] = 0 - break - } - } - } - - for k, v := range ws.dots.dots { - if v != clusterNumber { - continue - } - - ws.dots.dots[k] = -1 * clusterNumber - } - - total := 1 - - ws.dots.set(ws.originalPoint, clusterNumber) - for _, dir := range allDirections { - total += ws.lookForWeirdShapesReset(ws.originalPoint, clusterNumber, dir, 0) - } - - return total -} - -// ShapeWalk TODO. -func ShapeWalk(img *rimage.Image, dm *rimage.DepthMap, start image.Point, options ShapeWalkOptions, logger logging.Logger, -) (*SegmentedImage, error) { - return ShapeWalkMultiple(img, dm, []image.Point{start}, options, logger) -} - -// ShapeWalkMultiple TODO. -func ShapeWalkMultiple( - img *rimage.Image, dm *rimage.DepthMap, - starts []image.Point, - options ShapeWalkOptions, - logger logging.Logger, -) (*SegmentedImage, error) { - ws := walkState{ - img: img, - depth: dm, - dots: newSegmentedImage(img), - options: options, - threshold: DefaultColorThreshold + options.ThresholdMod, - logger: logger, - } - - for idx, start := range starts { - ws.piece(start, idx+1) - } - - ws.dots.createPalette() - - return ws.dots, nil -} - -// MyWalkError TODO. -type MyWalkError struct { - pos image.Point -} - -// Error TODO. -func (e MyWalkError) Error() string { - return "MyWalkError" -} - -// ShapeWalkEntireDebug TODO. -func ShapeWalkEntireDebug(img *rimage.Image, dm *rimage.DepthMap, options ShapeWalkOptions, logger logging.Logger, -) (*SegmentedImage, error) { - var si *SegmentedImage - var err error - - for extra := 0.0; extra < .7; extra += .2 { - si, err = shapeWalkEntireDebugOnePass(img, dm, options, extra, logger) - if err != nil { - return nil, err - } - - if true { - // TODO(erh): is this idea worth exploring - break - } - - if float64(si.NumInAnyCluster()) > float64(img.Width()*img.Height())*.9 { - break - } - } - - return si, err -} - -func shapeWalkEntireDebugOnePass( - img *rimage.Image, dm *rimage.DepthMap, - options ShapeWalkOptions, - extraThreshold float64, - logger logging.Logger, -) (*SegmentedImage, error) { - ws := walkState{ - img: img, - depth: dm, - dots: newSegmentedImage(img), - options: options, - threshold: DefaultColorThreshold + options.ThresholdMod + extraThreshold, - logger: logger, - } - - radius := 10 - nextColor := 1 - - middleX := img.Width() / 2 - middleY := img.Height() / 2 - - xStep := img.Width() / (radius * 2) - yStep := img.Height() / (radius * 2) - - err := utils.Walk(0, 0, radius, func(x, y int) error { - startX := middleX + (x * xStep) - startY := middleY + (y * yStep) - - found := utils.Walk(startX, startY, img.Width(), - func(x, y int) error { - if x < 0 || x >= img.Width() || y < 0 || y >= img.Height() { - return nil - } - - if ws.dots.get(image.Point{x, y}) != 0 { - return nil - } - return MyWalkError{image.Point{x, y}} - }) - - if found == nil { - return nil - } - - var walkErr MyWalkError - if !errors.As(found, &walkErr) { - return errors.Wrapf(found, "expected %T but got", walkErr) - } - start := walkErr.pos - numPixels := ws.piece(start, nextColor) - if options.Debug && numPixels < 10 { - ws.logger.Debugf("only found %d pixels in the cluster @ %v", numPixels, start) - } - - nextColor++ - - return nil - }) - if err != nil { - return nil, err - } - - ws.dots.createPalette() - - return ws.dots, nil -} diff --git a/vision/segmentation/verify_main_test.go b/vision/segmentation/verify_main_test.go deleted file mode 100644 index a781591c747..00000000000 --- a/vision/segmentation/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package segmentation - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/vision/training.go b/vision/training.go deleted file mode 100644 index 0d6d8b56777..00000000000 --- a/vision/training.go +++ /dev/null @@ -1,164 +0,0 @@ -// Package vision implements computer vision algorithms. -package vision - -import ( - "bytes" - "context" - "image" - "image/png" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - - "go.viam.com/rdk/rimage" -) - -// TrainingImage TODO. -type TrainingImage struct { - ID primitive.ObjectID `bson:"_id" json:"id,omitempty"` - Data []byte - Labels []string - MetaData map[string]interface{} -} - -// ImageTrainingStore TODO. -type ImageTrainingStore struct { - theClient *mongo.Client - theCollection *mongo.Collection -} - -// NewImageTrainingStore TODO. -func NewImageTrainingStore(ctx context.Context, mongoURI, db, collection string) (*ImageTrainingStore, error) { - client, err := mongo.NewClient(options.Client().ApplyURI(mongoURI)) - if err != nil { - return nil, err - } - - err = client.Connect(ctx) - if err != nil { - return nil, err - } - - return &ImageTrainingStore{client, client.Database(db).Collection(collection)}, nil -} - -func (its *ImageTrainingStore) reset(ctx context.Context) error { - err := its.theCollection.Drop(ctx) - if err != nil { - return err - } - return its.BuildIndexes(ctx) -} - -// Close TODO. -func (its *ImageTrainingStore) Close(ctx context.Context) error { - return its.theClient.Disconnect(ctx) -} - -// BuildIndexes TODO. -func (its *ImageTrainingStore) BuildIndexes(ctx context.Context) error { - // TODO(erh): build indexes - return nil -} - -// StoreImageFromDisk TODO. -func (its *ImageTrainingStore) StoreImageFromDisk(ctx context.Context, fn string, labels []string) (primitive.ObjectID, error) { - img, err := rimage.NewImageFromFile(fn) - if err != nil { - return primitive.ObjectID{}, err - } - md := map[string]interface{}{"filename": fn} - return its.StoreImage(ctx, img, md, labels) -} - -// StoreImage TODO. -func (its *ImageTrainingStore) StoreImage( - ctx context.Context, - img image.Image, - metaData map[string]interface{}, - labels []string, -) (primitive.ObjectID, error) { - ti := TrainingImage{} - ti.ID = primitive.NewObjectID() - - bb := bytes.Buffer{} - err := png.Encode(&bb, img) - if err != nil { - return ti.ID, err - } - ti.Data = bb.Bytes() - ti.Labels = labels - ti.MetaData = metaData - - _, err = its.theCollection.InsertOne(ctx, ti) - return ti.ID, err -} - -// GetImage TODO. -func (its *ImageTrainingStore) GetImage(ctx context.Context, id primitive.ObjectID) (TrainingImage, error) { - ti := TrainingImage{} - err := its.theCollection.FindOne(ctx, bson.M{"_id": id}).Decode(&ti) - return ti, err -} - -// SetLabelsForImage TODO. -func (its *ImageTrainingStore) SetLabelsForImage(ctx context.Context, id primitive.ObjectID, labels []string) error { - panic(1) -} - -// GetImagesForLabel TODO. -func (its *ImageTrainingStore) GetImagesForLabel(ctx context.Context, label string) ([]primitive.ObjectID, error) { - cursor, err := its.theCollection.Find(ctx, bson.M{"labels": label}, options.Find().SetProjection(bson.M{"_id": 1})) - if err != nil { - return nil, err - } - - all := []TrainingImage{} - err = cursor.All(ctx, &all) - if err != nil { - return nil, err - } - - res := []primitive.ObjectID{} - for _, i := range all { - res = append(res, i.ID) - } - - return res, nil -} - -// GetLabels TODO. -func (its *ImageTrainingStore) GetLabels(ctx context.Context) (map[string]int, error) { - agg := mongo.Pipeline{ - bson.D{{"$unwind", "$labels"}}, - bson.D{{"$group", bson.D{ - {"_id", "$labels"}, - {"num", bson.D{{"$sum", 1}}}, - }}}, - } - - cursor, err := its.theCollection.Aggregate(ctx, agg) - if err != nil { - return nil, err - } - - type T struct { - ID string `bson:"_id"` - Num int - } - - var results []T - err = cursor.All(ctx, &results) - if err != nil { - return nil, err - } - - res := map[string]int{} - for _, t := range results { - res[t.ID] = t.Num - } - - return res, nil -} diff --git a/vision/training_test.go b/vision/training_test.go deleted file mode 100644 index 9ee318ab841..00000000000 --- a/vision/training_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package vision - -import ( - "context" - "testing" - "time" - - "go.viam.com/test" - "go.viam.com/utils/artifact" -) - -func TestTraining1(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - store, err := NewImageTrainingStore(ctx, "mongodb://127.0.0.1/", "test", "trainingtest") - if err != nil { - t.Skipf("cannot run TestTraining1 because no mongo: %s\n", err) - return - } - defer func() { - test.That(t, store.Close(context.Background()), test.ShouldBeNil) - }() - err = store.reset(ctx) - if err != nil { - t.Skipf("couldn't reset training collection %s", err) - return - } - - w1, err := store.StoreImageFromDisk(ctx, artifact.MustPath("vision/white1.png"), []string{"white"}) - test.That(t, err, test.ShouldBeNil) - - w2, err := store.StoreImageFromDisk(ctx, artifact.MustPath("vision/white2.png"), []string{"white"}) - test.That(t, err, test.ShouldBeNil) - - b1, err := store.StoreImageFromDisk(ctx, artifact.MustPath("vision/black1.png"), []string{"black"}) - test.That(t, err, test.ShouldBeNil) - - b2, err := store.StoreImageFromDisk(ctx, artifact.MustPath("vision/black2.png"), []string{"black"}) - test.That(t, err, test.ShouldBeNil) - - t.Logf("%s %s %s %s\n", w1, w2, b1, b2) - - temp, err := store.GetImage(ctx, w1) - test.That(t, err, test.ShouldBeNil) - test.That(t, temp.Labels, test.ShouldHaveLength, 1) - test.That(t, temp.Labels[0], test.ShouldEqual, "white") - - labels, err := store.GetLabels(ctx) - test.That(t, err, test.ShouldBeNil) - - test.That(t, labels, test.ShouldHaveLength, 2) - test.That(t, labels["white"], test.ShouldEqual, 2) - test.That(t, labels["black"], test.ShouldEqual, 2) - - ws, err := store.GetImagesForLabel(ctx, "white") - test.That(t, err, test.ShouldBeNil) - test.That(t, ws, test.ShouldHaveLength, 2) -} diff --git a/vision/verify_main_test.go b/vision/verify_main_test.go deleted file mode 100644 index 807ac775c31..00000000000 --- a/vision/verify_main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package vision - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -} diff --git a/web/server/entrypoint.go b/web/server/entrypoint.go index 6c137f13259..25d731830c0 100644 --- a/web/server/entrypoint.go +++ b/web/server/entrypoint.go @@ -22,7 +22,6 @@ import ( "go.viam.com/utils/perf" "go.viam.com/utils/rpc" - vlogging "go.viam.com/rdk/components/camera/videosource/logging" "go.viam.com/rdk/config" "go.viam.com/rdk/logging" "go.viam.com/rdk/resource" @@ -115,12 +114,6 @@ func RunServer(ctx context.Context, args []string, _ logging.Logger) (err error) defer pprof.StopCPUProfile() } - if argsParsed.Logging { - if err := vlogging.GLoggerCamComp.Start(ctx); err != nil { - logger.Debug(err) - } - } - // Read the config from disk and use it to initialize the remote logger. initialReadCtx, cancel := context.WithTimeout(ctx, time.Second*5) cfgFromDisk, err := config.ReadLocalConfig(initialReadCtx, argsParsed.ConfigFile, logger) @@ -353,12 +346,7 @@ func (s *robotServer) serveWeb(ctx context.Context, cfg *config.Config) (err err }) } - robotOptions := createRobotOptions() - if s.args.RevealSensitiveConfigDiffs { - robotOptions = append(robotOptions, robotimpl.WithRevealSensitiveConfigDiffs()) - } - - myRobot, err := robotimpl.New(ctx, processedConfig, s.logger, robotOptions...) + myRobot, err := robotimpl.New(ctx, processedConfig, s.logger, nil) if err != nil { cancel() return err diff --git a/web/server/entrypoint_android.go b/web/server/entrypoint_android.go deleted file mode 100644 index 7062eea3bba..00000000000 --- a/web/server/entrypoint_android.go +++ /dev/null @@ -1,12 +0,0 @@ -package server - -import ( - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec/x264" -) - -func makeStreamConfig() gostream.StreamConfig { - var streamConfig gostream.StreamConfig - streamConfig.VideoEncoderFactory = x264.NewEncoderFactory() - return streamConfig -} diff --git a/web/server/entrypoint_cgo.go b/web/server/entrypoint_cgo.go deleted file mode 100644 index 40413a30c87..00000000000 --- a/web/server/entrypoint_cgo.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !no_cgo || android - -package server - -import ( - robotimpl "go.viam.com/rdk/robot/impl" - "go.viam.com/rdk/robot/web" -) - -func createRobotOptions() []robotimpl.Option { - return []robotimpl.Option{robotimpl.WithWebOptions(web.WithStreamConfig(makeStreamConfig()))} -} diff --git a/web/server/entrypoint_linux.go b/web/server/entrypoint_linux.go deleted file mode 100644 index af90af72176..00000000000 --- a/web/server/entrypoint_linux.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !no_cgo && !android - -package server - -import ( - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec/opus" - "go.viam.com/rdk/gostream/codec/x264" -) - -func makeStreamConfig() gostream.StreamConfig { - var streamConfig gostream.StreamConfig - streamConfig.AudioEncoderFactory = opus.NewEncoderFactory() - streamConfig.VideoEncoderFactory = x264.NewEncoderFactory() - return streamConfig -} diff --git a/web/server/entrypoint_test.go b/web/server/entrypoint_test.go deleted file mode 100644 index 38588734092..00000000000 --- a/web/server/entrypoint_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// Package server implements the entry point for running a robot web server. -package server_test - -import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "testing" - - "github.com/invopop/jsonschema" - robotpb "go.viam.com/api/robot/v1" - "go.viam.com/test" - goutils "go.viam.com/utils" - - _ "go.viam.com/rdk/components/register" - "go.viam.com/rdk/config" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/resource" - _ "go.viam.com/rdk/services/register" - "go.viam.com/rdk/testutils" - "go.viam.com/rdk/testutils/robottestutils" - "go.viam.com/rdk/utils" -) - -// numResources is the # of resources in /etc/configs/fake.json + the 2 -// expected builtin resources. -const numResources = 21 - -func TestEntrypoint(t *testing.T) { - if runtime.GOARCH == "arm" { - t.Skip("skipping on 32-bit ARM, subprocess build warnings cause failure") - } - - t.Run("number of resources", func(t *testing.T) { - logger, logObserver := logging.NewObservedTestLogger(t) - cfgFilename := utils.ResolveFile("/etc/configs/fake.json") - cfg, err := config.Read(context.Background(), cfgFilename, logger) - test.That(t, err, test.ShouldBeNil) - - var port int - var success bool - for portTryNum := 0; portTryNum < 10; portTryNum++ { - p, err := goutils.TryReserveRandomPort() - port = p - test.That(t, err, test.ShouldBeNil) - - cfg.Network.BindAddress = fmt.Sprintf(":%d", port) - cfgFilename, err = robottestutils.MakeTempConfig(t, cfg, logger) - test.That(t, err, test.ShouldBeNil) - - server := robottestutils.ServerAsSeparateProcess(t, cfgFilename, logger) - - err = server.Start(context.Background()) - test.That(t, err, test.ShouldBeNil) - - if success = robottestutils.WaitForServing(logObserver, port); success { - defer func() { - test.That(t, server.Stop(), test.ShouldBeNil) - }() - break - } - logger.Infow("Port in use. Restarting on new port.", "port", port, "err", err) - server.Stop() - continue - } - test.That(t, success, test.ShouldBeTrue) - - conn, err := robottestutils.Connect(port) - test.That(t, err, test.ShouldBeNil) - defer func() { - test.That(t, conn.Close(), test.ShouldBeNil) - }() - rc := robotpb.NewRobotServiceClient(conn) - - resourceNames, err := rc.ResourceNames(context.Background(), &robotpb.ResourceNamesRequest{}) - test.That(t, err, test.ShouldBeNil) - - test.That(t, len(resourceNames.Resources), test.ShouldEqual, numResources) - }) - t.Run("dump resource registrations", func(t *testing.T) { - tempDir := t.TempDir() - outputFile := filepath.Join(tempDir, "resources.json") - serverPath := testutils.BuildTempModule(t, "web/cmd/server/") - command := exec.Command(serverPath, "--dump-resources", outputFile) - err := command.Run() - test.That(t, err, test.ShouldBeNil) - type registration struct { - Model string `json:"model"` - API string `json:"API"` - Schema *jsonschema.Schema `json:"attribute_schema"` - } - outputBytes, err := os.ReadFile(outputFile) - test.That(t, err, test.ShouldBeNil) - registrations := []registration{} - err = json.Unmarshal(outputBytes, ®istrations) - test.That(t, err, test.ShouldBeNil) - test.That(t, len(registrations), test.ShouldBeGreaterThan, 0) // to protect against misreading resource registrations - test.That(t, registrations, test.ShouldHaveLength, len(resource.RegisteredResources())) - for _, reg := range registrations { - test.That(t, reg.API, test.ShouldNotBeEmpty) - test.That(t, reg.Model, test.ShouldNotBeEmpty) - test.That(t, reg.Schema, test.ShouldNotBeNil) - } - }) -} diff --git a/web/server/entrypoint_unix.go b/web/server/entrypoint_unix.go index 2c46c390207..abb4e431abd 100644 --- a/web/server/entrypoint_unix.go +++ b/web/server/entrypoint_unix.go @@ -1,16 +1 @@ -//go:build (darwin || android) && !no_cgo - package server - -import ( - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec/opus" - "go.viam.com/rdk/gostream/codec/x264" -) - -func makeStreamConfig() gostream.StreamConfig { - var streamConfig gostream.StreamConfig - streamConfig.AudioEncoderFactory = opus.NewEncoderFactory() - streamConfig.VideoEncoderFactory = x264.NewEncoderFactory() - return streamConfig -} diff --git a/web/server/entrypoint_windows.go b/web/server/entrypoint_windows.go deleted file mode 100644 index 2d6303198b1..00000000000 --- a/web/server/entrypoint_windows.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build windows - -package server - -import ( - "go.viam.com/rdk/gostream" - "go.viam.com/rdk/gostream/codec/opus" -) - -func makeStreamConfig() gostream.StreamConfig { - var streamConfig gostream.StreamConfig - // TODO(RSDK-1771): support video on windows - streamConfig.AudioEncoderFactory = opus.NewEncoderFactory() - return streamConfig -} diff --git a/web/server/verify_main_test.go b/web/server/verify_main_test.go deleted file mode 100644 index 76eefe0e425..00000000000 --- a/web/server/verify_main_test.go +++ /dev/null @@ -1,13 +0,0 @@ -// Package server implements the entry point for running a robot web server. -package server - -import ( - "testing" - - testutilsext "go.viam.com/utils/testutils/ext" -) - -// TestMain is used to control the execution of all tests run within this package (including _test packages). -func TestMain(m *testing.M) { - testutilsext.VerifyTestMain(m) -}