From 4d18bb64d6e84d3bd2e6255382e186511d706a9e Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 19:35:04 +0000 Subject: [PATCH 01/26] Add custom .yamllint configuration for flexible YAML linting rules - Extend default rules to accommodate longer lines for URLs and specific contexts. - Adjust indentation rules to allow for consistent 2-space indentation. - Permit truthy values commonly found in configuration files. - Disable document start marker requirement for all files. - Allow empty values, which are frequent in Helm templates. - Relax comment spacing requirements and disable comments indentation. - Enable Helm template syntax with braces. - Disable checks for new line at end of file and allow various line endings. - Allow empty lines at the start of files and be permissive with trailing spaces and comma spacing. --- .pre-commit-config.yaml | 29 + .secrets.baseline | 1974 +++++++++++++++++++++++++++++++++++++++ .yamllint | 56 ++ 3 files changed, 2059 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 .secrets.baseline create mode 100644 .yamllint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..751b4c4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.0 + hooks: + - id: go-fmt + - id: go-imports + - id: no-go-testing + - id: golangci-lint + - id: go-unit-tests + + - repo: https://github.com/adrienverge/yamllint + rev: v1.35.1 + hooks: + - id: yamllint + args: [--config-file=.yamllint] + + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ["--baseline", ".secrets.baseline"] diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..9098edb --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,1974 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + "README.md": [ + { + "type": "Hex High Entropy String", + "filename": "README.md", + "hashed_secret": "4d56211d01d64fdacf65fc1caef3a12237dbd30a", + "is_verified": false, + "line_number": 97, + "is_secret": false + } + ], + "cmd/http_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "cmd/http_test.go", + "hashed_secret": "4d56211d01d64fdacf65fc1caef3a12237dbd30a", + "is_verified": false, + "line_number": 48, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "cmd/http_test.go", + "hashed_secret": "44bf1f7c931a410503db9759de7d3758c84c6e0f", + "is_verified": false, + "line_number": 165, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "cmd/http_test.go", + "hashed_secret": "116acfdb39846d90401e995a76003aba8b664352", + "is_verified": false, + "line_number": 208, + "is_secret": false + } + ], + "pkg/common/helpers_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/common/helpers_test.go", + "hashed_secret": "4d56211d01d64fdacf65fc1caef3a12237dbd30a", + "is_verified": false, + "line_number": 88, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/common/helpers_test.go", + "hashed_secret": "07621926332a7aa61d6240420968fff07444731a", + "is_verified": false, + "line_number": 104, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/common/helpers_test.go", + "hashed_secret": "687acbd024cc117b6d625c7f2c139a081537323f", + "is_verified": false, + "line_number": 110, + "is_secret": false + } + ], + "pkg/decoder/nomadxs/v1/decoder_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "218f9326549a5e0ec8a072eeb758272a96774874", + "is_verified": false, + "line_number": 25, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "a59ad97f3bbbb4e60da4510a9b2a8616f2368785", + "is_verified": false, + "line_number": 51, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "ad093615db8d44ced68edd64572e0f45cef791f0", + "is_verified": false, + "line_number": 83, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "6892b5e88d93ed1713d8c77ef7d1daa72d76ce61", + "is_verified": false, + "line_number": 107, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "3fd6318be4ad86b004c399338c8bbce579f2bf99", + "is_verified": false, + "line_number": 221, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "07f1d11e4cf3ca31fdbf70a4a84ea9ab6ef6fae9", + "is_verified": false, + "line_number": 226, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "21c19a2d5a95036e9150bec34d76b3dc8fa389c3", + "is_verified": false, + "line_number": 231, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "82378f805a7817ebf8382eaac06877e4a26decd9", + "is_verified": false, + "line_number": 236, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "d2f7046d13f23d7c940891a6cd6f702a444712e4", + "is_verified": false, + "line_number": 241, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "7d937ed124a6721f46742b87ae13f5175dc737d6", + "is_verified": false, + "line_number": 246, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "bd4967e4a46d45e53cb5d2e240f1250110f716eb", + "is_verified": false, + "line_number": 251, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "5a2fb47e02909b408e43d6463ddfd3df3cb9e307", + "is_verified": false, + "line_number": 256, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "f9e132ead483a20cc19e52832c1824b139caf6b1", + "is_verified": false, + "line_number": 261, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/nomadxs/v1/decoder_test.go", + "hashed_secret": "43f3b28c71181026691d3d851d3398e41ae205b0", + "is_verified": false, + "line_number": 266, + "is_secret": false + } + ], + "pkg/decoder/smartlabel/v1/decoder_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "2969e12a864a2091be4082d99c1767a6d225ef9f", + "is_verified": false, + "line_number": 170, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "18911f680f30a3d441a7314a5f4131ccdf5a291d", + "is_verified": false, + "line_number": 193, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "03799cbbbde9a26987fc61ee9d1c7607efa5b6c3", + "is_verified": false, + "line_number": 216, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "d85e5644e91feb126f202af26abefd4aadde571e", + "is_verified": false, + "line_number": 239, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "9db4104f44fd40f26bce7072ad15bc10f1e833d6", + "is_verified": false, + "line_number": 259, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "517accb9519da7da9ae0f3ee9eac83543169ee17", + "is_verified": false, + "line_number": 269, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", + "is_verified": false, + "line_number": 279, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "07cb4aff970c7271688f1e8eaf214a516d3970e3", + "is_verified": false, + "line_number": 299, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "bc4afa2f3630480ca72a32c5c8421b39f3ce1a71", + "is_verified": false, + "line_number": 332, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "395900609a0d94e6b75d0f0cb6b647d1d554c442", + "is_verified": false, + "line_number": 343, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "81de30f2e09afabf90f74a4addd1ddbc354277d4", + "is_verified": false, + "line_number": 356, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "a158f37d59e2f1ea505e92294149581814a5ffe3", + "is_verified": false, + "line_number": 373, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/decoder_test.go", + "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", + "is_verified": false, + "line_number": 500, + "is_secret": false + } + ], + "pkg/decoder/smartlabel/v1/input.json": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/input.json", + "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", + "is_verified": false, + "line_number": 7, + "is_secret": false + } + ], + "pkg/decoder/smartlabel/v1/response.json": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/smartlabel/v1/response.json", + "hashed_secret": "85afdb46df38392f5aa3b520d74e4a40f6904964", + "is_verified": false, + "line_number": 36, + "is_secret": false + } + ], + "pkg/decoder/tagsl/v1/decoder_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4d56211d01d64fdacf65fc1caef3a12237dbd30a", + "is_verified": false, + "line_number": 26, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "aee9b5963d186701a30e5f458d0ae7bd92440623", + "is_verified": false, + "line_number": 64, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "76bceaf8e015ef80ad2e4f13f60f6138adf893fb", + "is_verified": false, + "line_number": 84, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "bd559560db885a0a6be3236fc11d8a3d1ba06e4f", + "is_verified": false, + "line_number": 181, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "880b7b2e83776fc4fce15f888f50c9f639652e36", + "is_verified": false, + "line_number": 192, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "f9af07356d79806c477386122315490bae72aab6", + "is_verified": false, + "line_number": 203, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "65932e64fcc5ff9a73e0caad1a106670d6adf400", + "is_verified": false, + "line_number": 218, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "b5155189d0445febca625c6443a355297d37f7fe", + "is_verified": false, + "line_number": 322, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "45dcdf72ad07c05ecf291970881d4e4da4b045be", + "is_verified": false, + "line_number": 326, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "9efb937dc6fc6fed7cf6fbb07a13247438a0cb66", + "is_verified": false, + "line_number": 332, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "95ee7ce7785840997fca4c543af8f8e5ecad3e47", + "is_verified": false, + "line_number": 336, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "2aed0433683ce7e167f5d1352ff13ef86b8482d2", + "is_verified": false, + "line_number": 346, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "15bdecc414877451c0ac5fb7010c119ad05c1341", + "is_verified": false, + "line_number": 350, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "5c311f41abeb1f8d14d09e891d810baf89b2a622", + "is_verified": false, + "line_number": 364, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "f5a1f8254f974172e74560ab8eb3151ef17beb30", + "is_verified": false, + "line_number": 387, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "58e1b13efdd15138317388b2f77415d1efe74239", + "is_verified": false, + "line_number": 432, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "db867a42517a4706bf81caaec0f5605a1ea3e34e", + "is_verified": false, + "line_number": 455, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4034f222358e7a6eabf4c57fd8d2eceec1cff41a", + "is_verified": false, + "line_number": 479, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "0e262d812b6ca1565c5015c83c491496af7793bb", + "is_verified": false, + "line_number": 492, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "be729162560f0e9701f705cd1614ed78e008a658", + "is_verified": false, + "line_number": 505, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4dcdff0bce20bc74c7e0f59f9ead20e3c77096de", + "is_verified": false, + "line_number": 520, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "e96414e2ceb73830ebba40a14d9e0bea29858eb0", + "is_verified": false, + "line_number": 551, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "b5950857a05765aaec2390c6fb6e6d178fde38ec", + "is_verified": false, + "line_number": 562, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "cb7e80b2f321795df274f509805b20ed3c6751d4", + "is_verified": false, + "line_number": 576, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "bba62a3bbbeaf8bed3956c6c192236d906751da1", + "is_verified": false, + "line_number": 591, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "31a99520b7eba697c7444952c3c2371989f5d302", + "is_verified": false, + "line_number": 612, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "3dd088d254d2263b613e999eb38c2d082d060b39", + "is_verified": false, + "line_number": 687, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "83ab17724e29464aa19eb62a3ec320f693c4199f", + "is_verified": false, + "line_number": 709, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "c95a61ad80326b82fa49aad0fcf425a3d7ead2b7", + "is_verified": false, + "line_number": 729, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "22b1e759001ce9defffd8fbacead6a71d5f7f644", + "is_verified": false, + "line_number": 773, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "ef678205593788329ff416ce5c65fa04f33a05bd", + "is_verified": false, + "line_number": 783, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "a2eff15d0e3abb066344af61ca597e5575312b7d", + "is_verified": false, + "line_number": 804, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "9d4748082c8c11aff4001b76aba0b30460ce1228", + "is_verified": false, + "line_number": 823, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "2dd04c4f36f29f0c2107a88d67667be9bad5da0b", + "is_verified": false, + "line_number": 837, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "fc3a44706f4ed1d7561d64278ce5c722589b22ab", + "is_verified": false, + "line_number": 849, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "feeecd63c147195b4105830d21993fece572fe7b", + "is_verified": false, + "line_number": 869, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "8a6c8572b99d521eef6b4a84df768e5e139ea5e4", + "is_verified": false, + "line_number": 889, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "619b165cf1665fc690f71c9021635667c1a9d925", + "is_verified": false, + "line_number": 928, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "2f8702eac3aa3dcb3ce596ee88152f0c448fdc71", + "is_verified": false, + "line_number": 949, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "97d57d0610498aa317e06c23f304e6f8784112d9", + "is_verified": false, + "line_number": 969, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "5142cefc1dc327827e8f7ae7ba801bf970c7f524", + "is_verified": false, + "line_number": 989, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "07325f0b872a1751c51a06497a8c112430cd0ea5", + "is_verified": false, + "line_number": 1011, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "52cfed0797142f778d87452cc415db81143ac365", + "is_verified": false, + "line_number": 1034, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "82b0c946a02c6653237a78c65b906791530b3ad4", + "is_verified": false, + "line_number": 1058, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "fbc4795ed89ae9cadb6fe7b23fb911b98a50573d", + "is_verified": false, + "line_number": 1072, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "16fb1ef9606e1f3b0a4097bfa5084ff0c73adc0e", + "is_verified": false, + "line_number": 1086, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4fbd145a80b3844b4c0fa1891018f112d8a4cfd9", + "is_verified": false, + "line_number": 1104, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "5148a3d442475f3690c83686f1b86d3fb1ddf53f", + "is_verified": false, + "line_number": 1121, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "029b2bca56c1b3b4fa7976dcbbbadda7604ea46a", + "is_verified": false, + "line_number": 1135, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "b39091d2985983c65671abf5da4f358ca3204aac", + "is_verified": false, + "line_number": 1154, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "11c8b7a2a2a699880d93a67b89acc6284906956d", + "is_verified": false, + "line_number": 1179, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "626f9f8b8aa407b42e820cc313acd6db3295e9cc", + "is_verified": false, + "line_number": 1204, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "bc2ac0fe49674b7457213dc8e7dc417f7ef69469", + "is_verified": false, + "line_number": 1217, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4c5ebbbf2ba50eade3687c8fa6cc9891a572a97b", + "is_verified": false, + "line_number": 1254, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "22d0e5481e87ef019ec6e6e25e7aed04ba1eebce", + "is_verified": false, + "line_number": 1280, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "070a0884b834873dfa21639922378396a9e34661", + "is_verified": false, + "line_number": 1318, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "46b3cefa3082f56d07ac25f0d4c2ffcf6ed69b28", + "is_verified": false, + "line_number": 1337, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "d9e77b9592259eb596b50df9eb7ed9d2c643086d", + "is_verified": false, + "line_number": 1352, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "8a7e84e02bd69e1a76cf2a493fb63980ac1062fb", + "is_verified": false, + "line_number": 1360, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "f7e47b71351261ac247b71cab2cbf38b1b3333c6", + "is_verified": false, + "line_number": 1387, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "d9a081ac8ab847e1a2081b0fb6e0e7c08ba0d2eb", + "is_verified": false, + "line_number": 1415, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4dc873cc09613e992ebe16bc31121bc224073521", + "is_verified": false, + "line_number": 1457, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "78dbc4eafb03113e02d0119c5405be293e34a116", + "is_verified": false, + "line_number": 1509, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "c0d70edfee3d9fe4436b75dab4321b46c1ec678d", + "is_verified": false, + "line_number": 1519, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "60a5e86e9110d68b06c4781b12da9902abe94a63", + "is_verified": false, + "line_number": 1529, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "ce09bde1b0182afd6bc649891b02786b4afd6968", + "is_verified": false, + "line_number": 1549, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "0c6e1135f0868600daecede0164f01f7b2fb9cc6", + "is_verified": false, + "line_number": 1569, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "ecba67730af51c1fe53055da71e66c4b18bec47d", + "is_verified": false, + "line_number": 1616, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "243124ba42ea803ccaf83f33c46cd88e1bf8ad6f", + "is_verified": false, + "line_number": 1621, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "e680eb6ac0887cbf8e323d968e7bbfdb6e5a408f", + "is_verified": false, + "line_number": 1626, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "d354f0f33e04038f12672f9f9d2454d869f5ec5c", + "is_verified": false, + "line_number": 1631, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "8418b8a11e15da64f413edf42370d697fefae446", + "is_verified": false, + "line_number": 1636, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "845b0d08338a9355922067acc1423a2b4b7fe364", + "is_verified": false, + "line_number": 1641, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "1747d5a1c2efc5371fb745304ad69886652cfa6c", + "is_verified": false, + "line_number": 1646, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "410383290d2c4621c71350224217673a2eacf916", + "is_verified": false, + "line_number": 1656, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "8136636c3e1320795f72ba46dfd58d38e13ff8e6", + "is_verified": false, + "line_number": 1661, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "3fa0dd7c64ebebf981f4b92a292eee37664106e6", + "is_verified": false, + "line_number": 1666, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "c4ede4aa512277d5913681c850e094ab3d224002", + "is_verified": false, + "line_number": 1671, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "dfe09dd42d5ab49ee9abeca2c87a6633cdbd8c72", + "is_verified": false, + "line_number": 1676, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "8e3af6a1ed46b78a31aa1197dba812bf9c3451c5", + "is_verified": false, + "line_number": 1681, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "6a0f25adc054155a674e91a7f78e81cf31c74bf1", + "is_verified": false, + "line_number": 1701, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "9eaa692c674262fc8cfb06580b513fbc5a93134e", + "is_verified": false, + "line_number": 1706, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "abc00733eb25276faac611e31dfe95d708870e03", + "is_verified": false, + "line_number": 1711, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "fd1eb27a5fbba10b5b4ff0bc407d44ac1b24167e", + "is_verified": false, + "line_number": 1716, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "a2ae827d799199d3d1baa7d393e5c3e31c9bc8e6", + "is_verified": false, + "line_number": 1721, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "0c91c5cb9d56cc0171a991e56bc6427daacd92a1", + "is_verified": false, + "line_number": 1726, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "30a0b09e7a4e94e87f2a9f4cf7f7dac4d6fc7ab1", + "is_verified": false, + "line_number": 1731, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "f5d5837942ad0336f9eff696554a6090cea37c91", + "is_verified": false, + "line_number": 1736, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "82728cf0efb858a97903276062482f8f397d0e91", + "is_verified": false, + "line_number": 1741, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "144d1655f91880a3592b8e38153a120fec994302", + "is_verified": false, + "line_number": 1746, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "ec3ee07679aeb94dee708e6afe0bffa59b968f6e", + "is_verified": false, + "line_number": 1751, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4972105006e0b08837d0f184732f7a06aba70302", + "is_verified": false, + "line_number": 1756, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "d41b66bd74ac59e64dcc8832eabda42f62b6e180", + "is_verified": false, + "line_number": 1761, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "50cbc40d6ab2dc9f1ec48293fa0ba6f79dc96835", + "is_verified": false, + "line_number": 1766, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "4ead03bc45421378874317365e39aa873eb2b8bc", + "is_verified": false, + "line_number": 1771, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "0bc8f460e7c2185b28ff8b00d32e4a0d9740a2e5", + "is_verified": false, + "line_number": 1776, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "6f4dcdcec757c2894f45b29e43de6b31463d10d5", + "is_verified": false, + "line_number": 1781, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "1cc4ebae8c979ce47ebf1057dd8bec8e0f39e805", + "is_verified": false, + "line_number": 1786, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "b633a7193242cf7a0d95933db10b1b3c57b1d381", + "is_verified": false, + "line_number": 1791, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "78d8d4885ccf3358941670a153a54bcb89ab32df", + "is_verified": false, + "line_number": 1796, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "e51337283ba5632b9366cf0ed4a5f5a91a0651b9", + "is_verified": false, + "line_number": 1801, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "e0470066812b6eeb9427ea31d9ee31f62da62e85", + "is_verified": false, + "line_number": 1806, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "9c46efd9a7f86b5259f8414c58d40340b5842a04", + "is_verified": false, + "line_number": 1811, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "b77f33e98e53c50c42f934caf97a8886b89ee13f", + "is_verified": false, + "line_number": 1816, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "2e1dfca6cbf9721a8bf013e00b46314bdca4bb6c", + "is_verified": false, + "line_number": 1821, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "6a75920be2331eb84c05a8c6816047a9130a39d2", + "is_verified": false, + "line_number": 1826, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "f7f06b05f4cd54c416acc62fdb84ac721dcb75c9", + "is_verified": false, + "line_number": 1831, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "a067ac4b430ab07dda6b76915143921e9c063960", + "is_verified": false, + "line_number": 1836, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "5ad7464d84954552692705df853c569a0e062579", + "is_verified": false, + "line_number": 1841, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "5634e40f52daaabfd9eaea0ca9d227913b369136", + "is_verified": false, + "line_number": 1909, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "c95e5ca7aa2290267fcf16e603343d109e80ee53", + "is_verified": false, + "line_number": 1933, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "44bf1f7c931a410503db9759de7d3758c84c6e0f", + "is_verified": false, + "line_number": 1952, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "116acfdb39846d90401e995a76003aba8b664352", + "is_verified": false, + "line_number": 1960, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "b4b606d84bd119aae19310a43b203c063f6df405", + "is_verified": false, + "line_number": 1976, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "de693c33aa7dc9f5cc5665454f8e30c070511634", + "is_verified": false, + "line_number": 2053, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "ee7bfc7e008cd63d0db92515aa217e47780471d6", + "is_verified": false, + "line_number": 2082, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "b40059438caac64f210382b0756b5e0fbcae100d", + "is_verified": false, + "line_number": 2098, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "917e65661cd4d85ccfde96243b052494f160b704", + "is_verified": false, + "line_number": 2106, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "32fe17aa9743d82d59af5c8081f44ac1cdf5a78c", + "is_verified": false, + "line_number": 2114, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagsl/v1/decoder_test.go", + "hashed_secret": "d76d7d922e691ced5b77d86c9c4282866a26f89f", + "is_verified": false, + "line_number": 2355, + "is_secret": false + } + ], + "pkg/decoder/tagxl/v1/decoder_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "ed2b7805cbf0feec0ab81d143f4df1125c57bf3f", + "is_verified": false, + "line_number": 107, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "555917d2175ab1600c6cd926d255df6ad502febc", + "is_verified": false, + "line_number": 220, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "6c45c0c1b339a7891ba24bf33ef5b40b040a86a0", + "is_verified": false, + "line_number": 240, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", + "is_verified": false, + "line_number": 330, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", + "is_verified": false, + "line_number": 357, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", + "is_verified": false, + "line_number": 366, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", + "is_verified": false, + "line_number": 376, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", + "is_verified": false, + "line_number": 387, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", + "is_verified": false, + "line_number": 399, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", + "is_verified": false, + "line_number": 407, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", + "is_verified": false, + "line_number": 417, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", + "is_verified": false, + "line_number": 429, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", + "is_verified": false, + "line_number": 443, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", + "is_verified": false, + "line_number": 631, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", + "is_verified": false, + "line_number": 643, + "is_secret": false + } + ], + "pkg/decoder/tagxl/v1/input.json": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/input.json", + "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", + "is_verified": false, + "line_number": 7, + "is_secret": false + } + ], + "pkg/decoder/tagxl/v1/response.json": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/response.json", + "hashed_secret": "85afdb46df38392f5aa3b520d74e4a40f6904964", + "is_verified": false, + "line_number": 36, + "is_secret": false + } + ], + "pkg/encoder/nomadxs/v1/encoder_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/nomadxs/v1/encoder_test.go", + "hashed_secret": "3855ff9d1832eb080b5ea5205b4b99251317e4cb", + "is_verified": false, + "line_number": 40, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/nomadxs/v1/encoder_test.go", + "hashed_secret": "764509e5f48f7c9f81a257839b5c5e1036e5f601", + "is_verified": false, + "line_number": 63, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/nomadxs/v1/encoder_test.go", + "hashed_secret": "6bc0146b932505c8fce60c3a019bc0d57a974cf1", + "is_verified": false, + "line_number": 86, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/nomadxs/v1/encoder_test.go", + "hashed_secret": "4635b54625c0e934de91c20285267d61f8f519db", + "is_verified": false, + "line_number": 109, + "is_secret": false + } + ], + "pkg/encoder/smartlabel/v1/encoder_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/smartlabel/v1/encoder_test.go", + "hashed_secret": "96e2fa89d5aed98d8ddc4a7e096e26ec7affb56b", + "is_verified": false, + "line_number": 100, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/smartlabel/v1/encoder_test.go", + "hashed_secret": "87eba1e4705b07dd7b56752c6b132675ff140acb", + "is_verified": false, + "line_number": 137, + "is_secret": false + } + ], + "pkg/encoder/tagsl/v1/encoder_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "8aa655577635c0111d8ef9baf1940cd0220f5f7a", + "is_verified": false, + "line_number": 33, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "3048c1f6138c0624556771c5e9221deabbb9bb56", + "is_verified": false, + "line_number": 49, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "6f8f943dde4d7e0c9c0bd0c5a139f28e68f57c58", + "is_verified": false, + "line_number": 96, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "7b622b85c46abbfaf179676865a48fc9dd6aa755", + "is_verified": false, + "line_number": 100, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "b2ad7468b046d5af10e9c2a7bc9bec48e0afdbeb", + "is_verified": false, + "line_number": 111, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "16cf9dd541cc0d3c05fe8bdb9f21b323fa48119f", + "is_verified": false, + "line_number": 124, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "a5f1d0a3bc6845395d5b09761aab8b79cb9f710c", + "is_verified": false, + "line_number": 139, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "700fc3487752ea604d61b07683d04c1cc55cfbda", + "is_verified": false, + "line_number": 156, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "dfaa0650875ef14a70c344e0148b42db96a3b826", + "is_verified": false, + "line_number": 175, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "b29bbe65508c8f4adb7b056a2853b88bf3bebcba", + "is_verified": false, + "line_number": 196, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "9b517cdb2b0a9f113eec968c7812b5586a71777a", + "is_verified": false, + "line_number": 216, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "1bbd31d965434aae5f4b149099a392b84a9628d4", + "is_verified": false, + "line_number": 220, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "2fd43faca100e94fb41f56f64d95a1287950afc6", + "is_verified": false, + "line_number": 232, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "a53c8539efbe60b7c7a3b617578935487cd2ccf7", + "is_verified": false, + "line_number": 246, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "5a19df446db91f41a36a77a08bc3882d4fc9151d", + "is_verified": false, + "line_number": 262, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "cffa250ba5535cec809c1791f0dc44c6de30469f", + "is_verified": false, + "line_number": 280, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "523d8770094449aaf7b539a619aed044ee1c69c7", + "is_verified": false, + "line_number": 300, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "5424dcbc6f1043b9a1fcfdbe02462f99a1b9d36b", + "is_verified": false, + "line_number": 315, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "cea2b9a520af68a878242460551ce974a6a6406f", + "is_verified": false, + "line_number": 330, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "a702c2c1e5c0eae0d28e07034098dfaeaf3a8686", + "is_verified": false, + "line_number": 345, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "4b94629eee78fddc1117f1289b49521f487cf4f1", + "is_verified": false, + "line_number": 360, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "57af8e1340f7bb0766b6538515a9d121255f6a7a", + "is_verified": false, + "line_number": 387, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "735b83d9b7d1bda9defc5b0a8f5a9d8bfac05dfd", + "is_verified": false, + "line_number": 391, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "233df4d0d3625b116ea7903270d64b882167f2c6", + "is_verified": false, + "line_number": 408, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "ec56e9970fb77b2f023f6b77c89ced7ee1f2d91b", + "is_verified": false, + "line_number": 427, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "d71bb44ee948f860502b7d32a2e2bca8039df7ee", + "is_verified": false, + "line_number": 448, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "71edee845d66812fa774248a9d75ae965afef7d5", + "is_verified": false, + "line_number": 465, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "18c045b874607274a5f88ae77db0d7ddc92ad59b", + "is_verified": false, + "line_number": 484, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "aa256d290dae26f1afd8b46339b20a788d50a6d1", + "is_verified": false, + "line_number": 505, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "b22951ddaa6879c05daf7367695b911cba73605f", + "is_verified": false, + "line_number": 528, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "45c2f04a34c854b4f5d7fcb0107f49719c493e94", + "is_verified": false, + "line_number": 539, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "16f903545b36a2e6af3ff2bd5fc31cca5a2c7c36", + "is_verified": false, + "line_number": 552, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "1b305d935061f260522cb6d86030004b037ad411", + "is_verified": false, + "line_number": 567, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "b3f7663551ae5bac73ac35adfb7d8baeca351672", + "is_verified": false, + "line_number": 584, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "cc5a23815a4ded17c6d583a933ae53c064fb8dae", + "is_verified": false, + "line_number": 603, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "7d49ee8289c9316454fcd24f786dfb14752a5f87", + "is_verified": false, + "line_number": 624, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "5d31ca43ca85f441739753ae8a20f6aae8a7686e", + "is_verified": false, + "line_number": 640, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "4b92412990e455a8b2b14bdaba8a054ff727fa21", + "is_verified": false, + "line_number": 656, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "cae771c3b29e43e7e307a0ed87888f20b5921b2b", + "is_verified": false, + "line_number": 672, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "9a15a601471e54a4e31abe24489661e9688a9b63", + "is_verified": false, + "line_number": 688, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "ca8e85d2c3e9f6934272b7e68e0830ce1283fd3c", + "is_verified": false, + "line_number": 809, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/encoder/tagsl/v1/encoder_test.go", + "hashed_secret": "dc496d565d617db2551a5b9c344d8b67f16b8a30", + "is_verified": false, + "line_number": 824, + "is_secret": false + } + ], + "pkg/solver/aws/aws_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/solver/aws/aws_test.go", + "hashed_secret": "40ac629780e89319e9600423a89a0d4e12cdd1e8", + "is_verified": false, + "line_number": 23, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/solver/aws/aws_test.go", + "hashed_secret": "232838a7a907736f5c1a2418cca2bc399b4c4e08", + "is_verified": false, + "line_number": 34, + "is_secret": false + } + ], + "pkg/solver/loracloud/loracloud_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/solver/loracloud/loracloud_test.go", + "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", + "is_verified": false, + "line_number": 228, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/solver/loracloud/loracloud_test.go", + "hashed_secret": "56a889aac5ddc1816be3e5604074cf3e50c70500", + "is_verified": false, + "line_number": 351, + "is_secret": false + } + ], + "test/tagsl_v1.hurl": [ + { + "type": "Hex High Entropy String", + "filename": "test/tagsl_v1.hurl", + "hashed_secret": "4d56211d01d64fdacf65fc1caef3a12237dbd30a", + "is_verified": false, + "line_number": 4, + "is_secret": false + } + ] + }, + "generated_at": "2025-09-04T19:34:13Z" +} diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..c105ade --- /dev/null +++ b/.yamllint @@ -0,0 +1,56 @@ +--- +extends: default + +ignore: | + .git/ + openapi.patched.yaml + openapi.yaml + +rules: + # Allow longer lines for URLs and certain contexts + line-length: + max: 300 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + + # Be more flexible with indentation in some contexts + indentation: + spaces: 2 + indent-sequences: consistent + + # Allow truthy values like "on", "off" which are common in configs + truthy: + allowed-values: ['true', 'false', 'on', 'off'] + + # Don't require document start marker for every file + document-start: disable + + # Allow empty values (common in Helm templates) + empty-values: disable + + # Be more permissive with comments + comments: + min-spaces-from-content: 0 + comments-indentation: disable + + # Allow Helm template syntax with braces + braces: + max-spaces-inside: 1 + min-spaces-inside: 0 + + # Allow different line endings (for templates) + new-line-at-end-of-file: disable + + # Allow different line ending types (Windows/Unix) + new-lines: disable + + # Allow empty lines at start of file + empty-lines: + max-start: 1 + max: 2 + + # Be permissive with trailing spaces (common in editors) + trailing-spaces: disable + + # Be permissive with comma spacing + commas: disable From 82858ca016bf6db9ebf105c7591e779e0dfbdfd3 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 19:36:44 +0000 Subject: [PATCH 02/26] Update CI workflow: Upgrade Go version to 1.24, enhance dependency checks, and add secret scanning steps --- .github/workflows/ci.yaml | 69 ++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2f0c73f..f68dd23 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,11 +18,43 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.22 - # Enable caching for Go modules + go-version: 1.24 cache: true - # Optional: Specify a cache-dependency path if your go.mod/go.sum are not at the root - # cache-dependency-path: go.sum + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + pip install detect-secrets yamllint + + - name: go mod tidy check + run: | + cp go.mod go.mod.prev + cp go.sum go.sum.prev + go mod tidy + diff -u go.mod.prev go.mod || (echo "::error file=go.mod::Run 'go mod tidy' and commit changes."; exit 1) + diff -u go.sum.prev go.sum || (echo "::error file=go.sum::Run 'go mod tidy' and commit changes."; exit 1) + + - name: go fmt (no diffs allowed) + run: | + CHANGED=$(gofmt -s -l . || true) + if [ -n "$CHANGED" ]; then + echo "::error ::Run 'gofmt -s -w .' to format:" + echo "$CHANGED" + exit 1 + fi + + - name: go vet + run: go vet ./... + + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@latest + + - name: staticcheck + run: $(go env GOPATH)/bin/staticcheck ./... - name: Lint uses: golangci/golangci-lint-action@v6 @@ -35,6 +67,30 @@ jobs: - name: Check Prometheus metrics run: make check-metrics + - name: Run yamllint + run: yamllint . + + - name: Check for uncommitted changes + run: | + git diff --exit-code || (echo "::error::Generated code is not up to date. Run 'make generate' and commit changes."; exit 1) + + - name: Detect secrets + run: | + echo "🔍 Scanning for secrets..." + if command -v detect-secrets >/dev/null 2>&1; then + detect-secrets scan --baseline .secrets.baseline --all-files || echo "Secret detection completed with findings" + if [ -f ".secrets.baseline" ]; then + detect-secrets audit .secrets.baseline --statistics || echo "Baseline audit completed" + fi + else + echo "detect-secrets not available, skipping secret scan (basic validation will still run)" + fi + + - name: Secret Scanning with TruffleHog + uses: trufflesecurity/trufflehog@main + with: + extra_args: --results=verified,unknown + test-coverage: name: Test & Coverage 🧪 runs-on: ubuntu-latest @@ -50,11 +106,8 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.22 - # Enable caching for Go modules + go-version: 1.24 cache: true - # Optional: Specify a cache-dependency path if your go.mod/go.sum are not at the root - # cache-dependency-path: go.sum - name: Generate test coverage # No explicit 'go mod download' needed if tests directly use modules, From 7468184743cdf2e461c41fa5eec058aaf4eca6a7 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 19:38:58 +0000 Subject: [PATCH 03/26] Add YAML workflow files to CI trigger paths --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f68dd23..e8ff244 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,6 +6,7 @@ on: - "**.go" - "**.md" - "**.mod" + - ".github/workflows/**.yaml" jobs: lint-and-format: From c3a11df64daaa3ef029410f9751c03a234789bb3 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 19:43:35 +0000 Subject: [PATCH 04/26] Remove unnecessary comments from test coverage step in CI workflow --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e8ff244..f6cf6c9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -111,8 +111,6 @@ jobs: cache: true - name: Generate test coverage - # No explicit 'go mod download' needed if tests directly use modules, - # as the 'go test' command will use the cached modules. run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... - name: Upload coverage reports to Codecov From 369d5c3f2ee7e32c7b03b07dcbc2e9ba0e094e4e Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 19:51:29 +0000 Subject: [PATCH 05/26] Refactor import statements to use aliasing for decoder packages --- pkg/encoder/nomadxs/v1/encoder.go | 2 +- pkg/encoder/smartlabel/v1/encoder.go | 2 +- pkg/encoder/tagsl/v1/encoder.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/encoder/nomadxs/v1/encoder.go b/pkg/encoder/nomadxs/v1/encoder.go index 7bec256..9a41cac 100644 --- a/pkg/encoder/nomadxs/v1/encoder.go +++ b/pkg/encoder/nomadxs/v1/encoder.go @@ -5,7 +5,7 @@ import ( "reflect" "github.com/truvami/decoder/pkg/common" - "github.com/truvami/decoder/pkg/decoder/nomadxs/v1" + nomadxs "github.com/truvami/decoder/pkg/decoder/nomadxs/v1" "github.com/truvami/decoder/pkg/encoder" ) diff --git a/pkg/encoder/smartlabel/v1/encoder.go b/pkg/encoder/smartlabel/v1/encoder.go index c81ebdc..108c5bf 100644 --- a/pkg/encoder/smartlabel/v1/encoder.go +++ b/pkg/encoder/smartlabel/v1/encoder.go @@ -5,7 +5,7 @@ import ( "reflect" "github.com/truvami/decoder/pkg/common" - "github.com/truvami/decoder/pkg/decoder/smartlabel/v1" + smartlabel "github.com/truvami/decoder/pkg/decoder/smartlabel/v1" "github.com/truvami/decoder/pkg/encoder" ) diff --git a/pkg/encoder/tagsl/v1/encoder.go b/pkg/encoder/tagsl/v1/encoder.go index ac6c0e4..f6cbcc2 100644 --- a/pkg/encoder/tagsl/v1/encoder.go +++ b/pkg/encoder/tagsl/v1/encoder.go @@ -5,7 +5,7 @@ import ( "reflect" "github.com/truvami/decoder/pkg/common" - "github.com/truvami/decoder/pkg/decoder/tagsl/v1" + tagsl "github.com/truvami/decoder/pkg/decoder/tagsl/v1" "github.com/truvami/decoder/pkg/encoder" ) From b61e5ae2f4f5ad7e2219b6bd13478265022c0a79 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 19:55:01 +0000 Subject: [PATCH 06/26] Refactor import statements to use aliasing for encoder packages --- pkg/encoder/nomadxs/v1/encoder_test.go | 2 +- pkg/encoder/smartlabel/v1/encoder_test.go | 2 +- pkg/encoder/tagsl/v1/encoder_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/encoder/nomadxs/v1/encoder_test.go b/pkg/encoder/nomadxs/v1/encoder_test.go index 3d3afd9..503138b 100644 --- a/pkg/encoder/nomadxs/v1/encoder_test.go +++ b/pkg/encoder/nomadxs/v1/encoder_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/truvami/decoder/pkg/common" - "github.com/truvami/decoder/pkg/decoder/nomadxs/v1" + nomadxs "github.com/truvami/decoder/pkg/decoder/nomadxs/v1" ) func TestEncode(t *testing.T) { diff --git a/pkg/encoder/smartlabel/v1/encoder_test.go b/pkg/encoder/smartlabel/v1/encoder_test.go index f0bf891..c52a1b2 100644 --- a/pkg/encoder/smartlabel/v1/encoder_test.go +++ b/pkg/encoder/smartlabel/v1/encoder_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/truvami/decoder/pkg/common" - "github.com/truvami/decoder/pkg/decoder/smartlabel/v1" + smartlabel "github.com/truvami/decoder/pkg/decoder/smartlabel/v1" ) func TestEncode(t *testing.T) { diff --git a/pkg/encoder/tagsl/v1/encoder_test.go b/pkg/encoder/tagsl/v1/encoder_test.go index 2e72798..de2c498 100644 --- a/pkg/encoder/tagsl/v1/encoder_test.go +++ b/pkg/encoder/tagsl/v1/encoder_test.go @@ -7,7 +7,7 @@ import ( "time" helpers "github.com/truvami/decoder/pkg/common" - "github.com/truvami/decoder/pkg/decoder/tagsl/v1" + tagsl "github.com/truvami/decoder/pkg/decoder/tagsl/v1" ) func TestEncode(t *testing.T) { From 6de082bc6218f1b23eee8628fef7267cac6b9622 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 19:56:53 +0000 Subject: [PATCH 07/26] Refactor deployment.yaml: standardize indentation and formatting for container configuration --- deployment/deployment.yaml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/deployment/deployment.yaml b/deployment/deployment.yaml index 0b6f030..4d50764 100644 --- a/deployment/deployment.yaml +++ b/deployment/deployment.yaml @@ -12,20 +12,20 @@ spec: app: decoder spec: containers: - - name: decoder - image: ghcr.io/truvami/decoder:latest # WARNING: never use latest in production - command: + - name: decoder + image: ghcr.io/truvami/decoder:latest # WARNING: never use latest in production + command: - decoder - args: - - 'http' - - '--host' - - '0.0.0.0' # Listen on all interfaces - - '--port' - - '8080' # Listen on port 8080 - - '-jd' # Enable JSON decoding and debugging - resources: - limits: - memory: "128Mi" - cpu: "500m" - ports: - - containerPort: 8080 + args: + - "http" + - "--host" + - "0.0.0.0" # Listen on all interfaces + - "--port" + - "8080" # Listen on port 8080 + - "-jd" # Enable JSON decoding and debugging + resources: + limits: + memory: "128Mi" + cpu: "500m" + ports: + - containerPort: 8080 From 4ab88fb02910547e09c683a233b42291e62bd97f Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 20:10:07 +0000 Subject: [PATCH 08/26] Remove .vscode directory from .gitignore and add tasks.json for pre-commit hook installation --- .gitignore | 1 - .vscode/tasks.json | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index 9985153..a777dd3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,3 @@ ^decoder$ dist/ -.vscode/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..14619ab --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Install pre-commit hooks", + "type": "shell", + "command": "pre-commit install", + "runOptions": { "runOn": "folderOpen" }, + "problemMatcher": [] + } + ] +} From 6451ef46027212f4e90265da3cb7e721ff0534bd Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 20:11:09 +0000 Subject: [PATCH 09/26] Fix metric label casing for consistency: change "devEUI" to "devEui" --- pkg/solver/loracloud/metrics.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/solver/loracloud/metrics.go b/pkg/solver/loracloud/metrics.go index e6e21eb..4e4dbc6 100644 --- a/pkg/solver/loracloud/metrics.go +++ b/pkg/solver/loracloud/metrics.go @@ -9,21 +9,21 @@ var ( loracloudPositionEstimateNoCapturedAtSetCounter = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "truvami_loracloud_position_estimate_no_captured_at_set_total", Help: "The total number of position estimate responses where the captured at (UTC) timestamp is not set", - }, []string{"devEUI"}) + }, []string{"devEui"}) loracloudPositionEstimateZeroCoordinatesSetCounter = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "truvami_loracloud_position_estimate_zero_coordinates_set_total", Help: "The total number of position estimate responses where the coordinates are set to 0", - }, []string{"devEUI"}) + }, []string{"devEui"}) loracloudPositionEstimateNoCapturedAtSetWithValidCoordinatesCounter = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "truvami_loracloud_position_estimate_no_captured_at_set_with_valid_coordinates_total", Help: "The total number of position estimate responses where the captured at (UTC) timestamp is not set and the coordinates are valid", - }, []string{"devEUI"}) + }, []string{"devEui"}) loracloudPositionEstimateValidCounter = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "truvami_loracloud_position_estimate_valid_total", Help: "The total number of position estimate responses where the captured at (UTC) timestamp is set and the coordinates are valid", - }, []string{"devEUI"}) + }, []string{"devEui"}) loracloudPositionEstimateInvalidCounter = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "truvami_loracloud_position_estimate_invalid_total", Help: "The total number of position estimate responses where the position resolution is invalid", - }, []string{"devEUI"}) + }, []string{"devEui"}) ) From 3457d460a8d470b3a9426fa288076b8f89da0f14 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Thu, 4 Sep 2025 20:16:08 +0000 Subject: [PATCH 10/26] Remove TruffleHog secret scanning step from CI workflow --- .github/workflows/ci.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f6cf6c9..5c9f405 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -87,11 +87,6 @@ jobs: echo "detect-secrets not available, skipping secret scan (basic validation will still run)" fi - - name: Secret Scanning with TruffleHog - uses: trufflesecurity/trufflehog@main - with: - extra_args: --results=verified,unknown - test-coverage: name: Test & Coverage 🧪 runs-on: ubuntu-latest From f74bd94a514bd7116c9e1df255717244fbceac5f Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Fri, 5 Sep 2025 09:00:25 +0000 Subject: [PATCH 11/26] Add support for Port 198 payload and enhance Port 197 payload structure - Introduced Port198Payload struct with versioning and movement indication. - Updated Port197Payload to include version and moving fields. - Enhanced decoder logic to handle new payload formats for both ports. - Added unit tests to validate new payload structures and behaviors. --- .secrets.baseline | 24 +-- pkg/decoder/tagxl/v1/decoder.go | 60 ++++++- pkg/decoder/tagxl/v1/decoder_test.go | 256 +++++++++++++++++++++------ pkg/decoder/tagxl/v1/port197.go | 48 +++-- pkg/decoder/tagxl/v1/port198.go | 103 +++++++++++ 5 files changed, 414 insertions(+), 77 deletions(-) create mode 100644 pkg/decoder/tagxl/v1/port198.go diff --git a/.secrets.baseline b/.secrets.baseline index 9098edb..9e85ede 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1429,7 +1429,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", "is_verified": false, - "line_number": 357, + "line_number": 359, "is_secret": false }, { @@ -1437,7 +1437,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 366, + "line_number": 370, "is_secret": false }, { @@ -1445,7 +1445,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 376, + "line_number": 382, "is_secret": false }, { @@ -1453,7 +1453,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 387, + "line_number": 395, "is_secret": false }, { @@ -1461,7 +1461,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 399, + "line_number": 409, "is_secret": false }, { @@ -1469,7 +1469,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 407, + "line_number": 419, "is_secret": false }, { @@ -1477,7 +1477,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 417, + "line_number": 431, "is_secret": false }, { @@ -1485,7 +1485,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 429, + "line_number": 445, "is_secret": false }, { @@ -1493,7 +1493,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 443, + "line_number": 461, "is_secret": false }, { @@ -1501,7 +1501,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 631, + "line_number": 787, "is_secret": false }, { @@ -1509,7 +1509,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 643, + "line_number": 799, "is_secret": false } ], @@ -1970,5 +1970,5 @@ } ] }, - "generated_at": "2025-09-04T19:34:13Z" + "generated_at": "2025-09-05T09:00:08Z" } diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index eecd5ce..8fa8e4b 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -166,9 +166,11 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon case 197: var version uint8 = payload[0] switch version { - case 0x00: + case Port197Version1: return common.PayloadConfig{ Fields: []common.FieldConfig{ + {Name: "Version", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysFalse}, {Name: "Mac1", Start: 1, Length: 6, Hex: true}, {Name: "Mac2", Start: 7, Length: 6, Optional: true, Hex: true}, {Name: "Mac3", Start: 13, Length: 6, Optional: true, Hex: true}, @@ -176,11 +178,13 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon {Name: "Mac5", Start: 25, Length: 6, Optional: true, Hex: true}, }, TargetType: reflect.TypeOf(Port197Payload{}), - Features: []decoder.Feature{decoder.FeatureWiFi}, + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving}, }, nil - case 0x01: + case Port197Version2: return common.PayloadConfig{ Fields: []common.FieldConfig{ + {Name: "Version", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysFalse}, {Name: "Rssi1", Start: 1, Length: 1}, {Name: "Mac1", Start: 2, Length: 6, Hex: true}, {Name: "Rssi2", Start: 8, Length: 1, Optional: true}, @@ -193,11 +197,51 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon {Name: "Mac5", Start: 30, Length: 6, Optional: true, Hex: true}, }, TargetType: reflect.TypeOf(Port197Payload{}), - Features: []decoder.Feature{decoder.FeatureWiFi}, + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving}, + }, nil + default: + return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) + } + case 198: + var version uint8 = payload[0] + switch version { + case Port198Version1: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Version", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysTrue}, + {Name: "Mac1", Start: 1, Length: 6, Hex: true}, + {Name: "Mac2", Start: 7, Length: 6, Optional: true, Hex: true}, + {Name: "Mac3", Start: 13, Length: 6, Optional: true, Hex: true}, + {Name: "Mac4", Start: 19, Length: 6, Optional: true, Hex: true}, + {Name: "Mac5", Start: 25, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port198Payload{}), + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving}, + }, nil + case Port198Version2: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Version", Start: 0, Length: 1}, + {Name: "Moving", Start: 0, Length: 1, Transform: alwaysTrue}, + {Name: "Rssi1", Start: 1, Length: 1}, + {Name: "Mac1", Start: 2, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 8, Length: 1, Optional: true}, + {Name: "Mac2", Start: 9, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 15, Length: 1, Optional: true}, + {Name: "Mac3", Start: 16, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 22, Length: 1, Optional: true}, + {Name: "Mac4", Start: 23, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 29, Length: 1, Optional: true}, + {Name: "Mac5", Start: 30, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port198Payload{}), + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving}, }, nil default: return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } + } return common.PayloadConfig{}, fmt.Errorf("%w: port %v not supported", common.ErrPortNotSupported, port) } @@ -247,3 +291,11 @@ func (t TagXLv1Decoder) Decode(ctx context.Context, data string, port uint8) (*d func timestamp(v any) any { return time.Unix(int64(common.BytesToUint32(v.([]byte))), 0).UTC() } + +func alwaysTrue(v any) any { + return true +} + +func alwaysFalse(v any) any { + return false +} diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index 4d0022f..d9bdfd1 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -348,110 +348,266 @@ func TestDecode(t *testing.T) { port: 197, payload: "003385f8ee30c2", expected: Port197Payload{ - Rssi1: nil, - Mac1: "3385f8ee30c2", + Rssi1: nil, + Mac1: "3385f8ee30c2", + Moving: false, + Version: Port197Version1, }, }, { port: 197, payload: "003385f8ee30c2a0382c2601db", expected: Port197Payload{ - Rssi1: nil, - Mac1: "3385f8ee30c2", - Mac2: helpers.StringPtr("a0382c2601db"), + Rssi1: nil, + Mac1: "3385f8ee30c2", + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: false, + Version: Port197Version1, }, }, { port: 197, payload: "00b5eded55a313a0b8b5e86e3194a765f3ad40", expected: Port197Payload{ - Rssi1: nil, - Mac1: "b5eded55a313", - Mac2: helpers.StringPtr("a0b8b5e86e31"), - Mac3: helpers.StringPtr("94a765f3ad40"), + Rssi1: nil, + Mac1: "b5eded55a313", + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: false, + Version: Port197Version1, }, }, { port: 197, payload: "006fbcfdd764347e7cbff22fc500dc0af60588010161302d9c", expected: Port197Payload{ - Rssi1: nil, - Mac1: "6fbcfdd76434", - Mac2: helpers.StringPtr("7e7cbff22fc5"), - Mac3: helpers.StringPtr("00dc0af60588"), - Mac4: helpers.StringPtr("010161302d9c"), + Rssi1: nil, + Mac1: "6fbcfdd76434", + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Mac3: helpers.StringPtr("00dc0af60588"), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: false, + Version: Port197Version1, }, }, { port: 197, payload: "00218f6c166fad59ea3bdec77df72faac81784263386a455d33592a063900b", expected: Port197Payload{ - Rssi1: nil, - Mac1: "218f6c166fad", - Mac2: helpers.StringPtr("59ea3bdec77d"), - Mac3: helpers.StringPtr("f72faac81784"), - Mac4: helpers.StringPtr("263386a455d3"), - Mac5: helpers.StringPtr("3592a063900b"), + Rssi1: nil, + Mac1: "218f6c166fad", + Mac2: helpers.StringPtr("59ea3bdec77d"), + Mac3: helpers.StringPtr("f72faac81784"), + Mac4: helpers.StringPtr("263386a455d3"), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: false, + Version: Port197Version1, }, }, { port: 197, payload: "01d63385f8ee30c2", expected: Port197Payload{ - Rssi1: helpers.Int8Ptr(-42), - Mac1: "3385f8ee30c2", + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Moving: false, + Version: Port197Version2, }, }, { port: 197, payload: "01d63385f8ee30c2d0a0382c2601db", expected: Port197Payload{ - Rssi1: helpers.Int8Ptr(-42), - Mac1: "3385f8ee30c2", - Rssi2: helpers.Int8Ptr(-48), - Mac2: helpers.StringPtr("a0382c2601db"), + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: false, + Version: Port197Version2, }, }, { port: 197, payload: "01c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", expected: Port197Payload{ - Rssi1: helpers.Int8Ptr(-56), - Mac1: "b5eded55a313", - Rssi2: helpers.Int8Ptr(-64), - Mac2: helpers.StringPtr("a0b8b5e86e31"), - Rssi3: helpers.Int8Ptr(-72), - Mac3: helpers.StringPtr("94a765f3ad40"), + Rssi1: helpers.Int8Ptr(-56), + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: false, + Version: Port197Version2, }, }, { port: 197, payload: "01bd6fbcfdd76434bb7e7cbff22fc5b900dc0af60588b7010161302d9c", expected: Port197Payload{ - Rssi1: helpers.Int8Ptr(-67), - Mac1: "6fbcfdd76434", - Rssi2: helpers.Int8Ptr(-69), - Mac2: helpers.StringPtr("7e7cbff22fc5"), - Rssi3: helpers.Int8Ptr(-71), - Mac3: helpers.StringPtr("00dc0af60588"), - Rssi4: helpers.Int8Ptr(-73), - Mac4: helpers.StringPtr("010161302d9c"), + Rssi1: helpers.Int8Ptr(-67), + Mac1: "6fbcfdd76434", + Rssi2: helpers.Int8Ptr(-69), + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Rssi3: helpers.Int8Ptr(-71), + Mac3: helpers.StringPtr("00dc0af60588"), + Rssi4: helpers.Int8Ptr(-73), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: false, + Version: Port197Version2, }, }, { port: 197, payload: "01b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", expected: Port197Payload{ - Rssi1: helpers.Int8Ptr(-73), - Mac1: "218f6c166fad", - Rssi2: helpers.Int8Ptr(-77), - Mac2: helpers.StringPtr("59ea3bdec77d"), - Rssi3: helpers.Int8Ptr(-81), - Mac3: helpers.StringPtr("f72faac81784"), - Rssi4: helpers.Int8Ptr(-85), - Mac4: helpers.StringPtr("263386a455d3"), - Rssi5: helpers.Int8Ptr(-89), - Mac5: helpers.StringPtr("3592a063900b"), + Rssi1: helpers.Int8Ptr(-73), + Mac1: "218f6c166fad", + Rssi2: helpers.Int8Ptr(-77), + Mac2: helpers.StringPtr("59ea3bdec77d"), + Rssi3: helpers.Int8Ptr(-81), + Mac3: helpers.StringPtr("f72faac81784"), + Rssi4: helpers.Int8Ptr(-85), + Mac4: helpers.StringPtr("263386a455d3"), + Rssi5: helpers.Int8Ptr(-89), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: false, + Version: Port197Version2, + }, + }, + { + port: 198, + payload: "ff", + expected: Port198Payload{}, + expectedErr: "port not supported: version 255 for port 198 not supported", + }, + { + port: 198, + payload: "003385f8ee30c2", + expected: Port198Payload{ + Rssi1: nil, + Mac1: "3385f8ee30c2", + Moving: true, + Version: Port197Version1, + }, + }, + { + port: 198, + payload: "003385f8ee30c2a0382c2601db", + expected: Port198Payload{ + Rssi1: nil, + Mac1: "3385f8ee30c2", + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: true, + Version: Port197Version1, + }, + }, + { + port: 198, + payload: "00b5eded55a313a0b8b5e86e3194a765f3ad40", + expected: Port198Payload{ + Rssi1: nil, + Mac1: "b5eded55a313", + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: true, + Version: Port197Version1, + }, + }, + { + port: 198, + payload: "006fbcfdd764347e7cbff22fc500dc0af60588010161302d9c", + expected: Port198Payload{ + Rssi1: nil, + Mac1: "6fbcfdd76434", + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Mac3: helpers.StringPtr("00dc0af60588"), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: true, + Version: Port197Version1, + }, + }, + { + port: 198, + payload: "00218f6c166fad59ea3bdec77df72faac81784263386a455d33592a063900b", + expected: Port198Payload{ + Rssi1: nil, + Mac1: "218f6c166fad", + Mac2: helpers.StringPtr("59ea3bdec77d"), + Mac3: helpers.StringPtr("f72faac81784"), + Mac4: helpers.StringPtr("263386a455d3"), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: true, + Version: Port197Version1, + }, + }, + { + port: 198, + payload: "01d63385f8ee30c2", + expected: Port198Payload{ + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Moving: true, + Version: Port197Version2, + }, + }, + { + port: 198, + payload: "01d63385f8ee30c2d0a0382c2601db", + expected: Port198Payload{ + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: true, + Version: Port197Version2, + }, + }, + { + port: 198, + payload: "01c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + expected: Port198Payload{ + Rssi1: helpers.Int8Ptr(-56), + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: true, + Version: Port197Version2, + }, + }, + { + port: 198, + payload: "01bd6fbcfdd76434bb7e7cbff22fc5b900dc0af60588b7010161302d9c", + expected: Port198Payload{ + Rssi1: helpers.Int8Ptr(-67), + Mac1: "6fbcfdd76434", + Rssi2: helpers.Int8Ptr(-69), + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Rssi3: helpers.Int8Ptr(-71), + Mac3: helpers.StringPtr("00dc0af60588"), + Rssi4: helpers.Int8Ptr(-73), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: true, + Version: Port197Version2, + }, + }, + { + port: 198, + payload: "01b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", + expected: Port198Payload{ + Rssi1: helpers.Int8Ptr(-73), + Mac1: "218f6c166fad", + Rssi2: helpers.Int8Ptr(-77), + Mac2: helpers.StringPtr("59ea3bdec77d"), + Rssi3: helpers.Int8Ptr(-81), + Mac3: helpers.StringPtr("f72faac81784"), + Rssi4: helpers.Int8Ptr(-85), + Mac4: helpers.StringPtr("263386a455d3"), + Rssi5: helpers.Int8Ptr(-89), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: true, + Version: Port197Version2, }, }, } diff --git a/pkg/decoder/tagxl/v1/port197.go b/pkg/decoder/tagxl/v1/port197.go index 9880969..8da980f 100644 --- a/pkg/decoder/tagxl/v1/port197.go +++ b/pkg/decoder/tagxl/v1/port197.go @@ -4,10 +4,23 @@ import ( "github.com/truvami/decoder/pkg/decoder" ) +// Version v1 (without RSSI values): // +------+------+-----------------------------------------------+------------+ // | Byte | Size | Description | Format | // +------+------+-----------------------------------------------+------------+ -// | 0 | 1 | version | byte | +// | 0 | 1 | version (0x00 = v1, 0x01 = v2) | byte | +// | 1 | 6 | mac address signal 1 | byte[6] | +// | 7 | 6 | mac address signal 2 | byte[6] | +// | 13 | 6 | mac address signal 3 | byte[6] | +// | 19 | 6 | mac address signal 4 | byte[6] | +// | 25 | 6 | mac address signal 5 | byte[6] | +// +------+------+-----------------------------------------------+------------+ +// +// Version v2 (with RSSI values): +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 1 | version (0x00 = v1, 0x01 = v2) | byte | // | 1 | 1 | rssi signal 1 | int8 | // | 2 | 6 | mac address signal 1 | byte[6] | // | 8 | 1 | rssi signal 2 | int8 | @@ -20,20 +33,28 @@ import ( // | 30 | 6 | mac address signal 5 | byte[6] | // +------+------+-----------------------------------------------+------------+ +const ( + Port197Version1 byte = 0x00 + Port197Version2 byte = 0x01 +) + type Port197Payload struct { - Rssi1 *int8 `json:"rssi1" validate:"gte=-120,lte=-20"` - Mac1 string `json:"mac1"` - Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` - Mac2 *string `json:"mac2"` - Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` - Mac3 *string `json:"mac3"` - Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` - Mac4 *string `json:"mac4"` - Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` - Mac5 *string `json:"mac5"` + Version byte `json:"version" validate:"gte=0,lte=1"` + Moving bool `json:"moving"` // Always false for Port 197 + Rssi1 *int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` } var _ decoder.UplinkFeatureWiFi = &Port197Payload{} +var _ decoder.UplinkFeatureMoving = &Port197Payload{} func (p Port197Payload) GetAccessPoints() []decoder.AccessPoint { accessPoints := []decoder.AccessPoint{} @@ -75,3 +96,8 @@ func (p Port197Payload) GetAccessPoints() []decoder.AccessPoint { return accessPoints } + +// Port 197 does not provide movement information, so we return false. +func (p Port197Payload) IsMoving() bool { + return false +} diff --git a/pkg/decoder/tagxl/v1/port198.go b/pkg/decoder/tagxl/v1/port198.go new file mode 100644 index 0000000..e8c0713 --- /dev/null +++ b/pkg/decoder/tagxl/v1/port198.go @@ -0,0 +1,103 @@ +package tagxl + +import ( + "github.com/truvami/decoder/pkg/decoder" +) + +// Version v1 (without RSSI values): +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 1 | version (0x00 = v1, 0x01 = v2) | byte | +// | 1 | 6 | mac address signal 1 | byte[6] | +// | 7 | 6 | mac address signal 2 | byte[6] | +// | 13 | 6 | mac address signal 3 | byte[6] | +// | 19 | 6 | mac address signal 4 | byte[6] | +// | 25 | 6 | mac address signal 5 | byte[6] | +// +------+------+-----------------------------------------------+------------+ +// +// Version v2 (with RSSI values): +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 1 | version (0x00 = v1, 0x01 = v2) | byte | +// | 1 | 1 | rssi signal 1 | int8 | +// | 2 | 6 | mac address signal 1 | byte[6] | +// | 8 | 1 | rssi signal 2 | int8 | +// | 9 | 6 | mac address signal 2 | byte[6] | +// | 15 | 1 | rssi signal 3 | int8 | +// | 16 | 6 | mac address signal 3 | byte[6] | +// | 22 | 1 | rssi signal 4 | int8 | +// | 23 | 6 | mac address signal 4 | byte[6] | +// | 29 | 1 | rssi signal 5 | int8 | +// | 30 | 6 | mac address signal 5 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +const ( + Port198Version1 byte = 0x00 + Port198Version2 byte = 0x01 +) + +type Port198Payload struct { + Version byte `json:"version" validate:"gte=0,lte=1"` + Moving bool `json:"moving"` // Always true for Port 198 + Rssi1 *int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` +} + +var _ decoder.UplinkFeatureWiFi = &Port198Payload{} +var _ decoder.UplinkFeatureMoving = &Port198Payload{} + +func (p Port198Payload) GetAccessPoints() []decoder.AccessPoint { + accessPoints := []decoder.AccessPoint{} + + if p.Mac1 != "" { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: p.Mac1, + RSSI: p.Rssi1, + }) + } + + if p.Mac2 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + return accessPoints +} + +// Port 198 always indicates movement when WiFi is scanned. +func (p Port198Payload) IsMoving() bool { + return true +} From 49d6f9b35294f23fcfb45ceabb74d457b6be9058 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Fri, 5 Sep 2025 09:18:17 +0000 Subject: [PATCH 12/26] Add support for Port 200 and Port 201 payloads with corresponding tests --- .secrets.baseline | 94 +++++++- pkg/decoder/tagxl/v1/decoder.go | 83 ++++++- pkg/decoder/tagxl/v1/decoder_test.go | 312 ++++++++++++++++++++++++++- pkg/decoder/tagxl/v1/port200.go | 113 ++++++++++ pkg/decoder/tagxl/v1/port201.go | 113 ++++++++++ 5 files changed, 701 insertions(+), 14 deletions(-) create mode 100644 pkg/decoder/tagxl/v1/port200.go create mode 100644 pkg/decoder/tagxl/v1/port201.go diff --git a/.secrets.baseline b/.secrets.baseline index 9e85ede..96f61d9 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1496,12 +1496,100 @@ "line_number": 461, "is_secret": false }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", + "is_verified": false, + "line_number": 615, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", + "is_verified": false, + "line_number": 621, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", + "is_verified": false, + "line_number": 632, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", + "is_verified": false, + "line_number": 644, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", + "is_verified": false, + "line_number": 657, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", + "is_verified": false, + "line_number": 671, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", + "is_verified": false, + "line_number": 686, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", + "is_verified": false, + "line_number": 697, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", + "is_verified": false, + "line_number": 710, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", + "is_verified": false, + "line_number": 725, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", + "is_verified": false, + "line_number": 742, + "is_secret": false + }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 787, + "line_number": 1079, "is_secret": false }, { @@ -1509,7 +1597,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 799, + "line_number": 1091, "is_secret": false } ], @@ -1970,5 +2058,5 @@ } ] }, - "generated_at": "2025-09-05T09:00:08Z" + "generated_at": "2025-09-05T09:18:13Z" } diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index 8fa8e4b..66477be 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -241,7 +241,88 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon default: return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } - + case 200: + var version uint8 = payload[4] + switch version { + case Port200Version1: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Version", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysFalse}, + {Name: "Mac1", Start: 5, Length: 6, Hex: true}, + {Name: "Mac2", Start: 11, Length: 6, Optional: true, Hex: true}, + {Name: "Mac3", Start: 17, Length: 6, Optional: true, Hex: true}, + {Name: "Mac4", Start: 23, Length: 6, Optional: true, Hex: true}, + {Name: "Mac5", Start: 29, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port200Payload{}), + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving, decoder.FeatureTimestamp}, + }, nil + case Port200Version2: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Version", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysFalse}, + {Name: "Rssi1", Start: 5, Length: 1}, + {Name: "Mac1", Start: 6, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 12, Length: 1, Optional: true}, + {Name: "Mac2", Start: 13, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 19, Length: 1, Optional: true}, + {Name: "Mac3", Start: 20, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 26, Length: 1, Optional: true}, + {Name: "Mac4", Start: 27, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 33, Length: 1, Optional: true}, + {Name: "Mac5", Start: 34, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port200Payload{}), + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving, decoder.FeatureTimestamp}, + }, nil + default: + return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) + } + case 201: + var version uint8 = payload[4] + switch version { + case Port201Version1: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Version", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysTrue}, + {Name: "Mac1", Start: 5, Length: 6, Hex: true}, + {Name: "Mac2", Start: 11, Length: 6, Optional: true, Hex: true}, + {Name: "Mac3", Start: 17, Length: 6, Optional: true, Hex: true}, + {Name: "Mac4", Start: 23, Length: 6, Optional: true, Hex: true}, + {Name: "Mac5", Start: 29, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port201Payload{}), + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving, decoder.FeatureTimestamp}, + }, nil + case Port201Version2: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "Version", Start: 4, Length: 1}, + {Name: "Moving", Start: 4, Length: 1, Transform: alwaysTrue}, + {Name: "Rssi1", Start: 5, Length: 1}, + {Name: "Mac1", Start: 6, Length: 6, Hex: true}, + {Name: "Rssi2", Start: 12, Length: 1, Optional: true}, + {Name: "Mac2", Start: 13, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi3", Start: 19, Length: 1, Optional: true}, + {Name: "Mac3", Start: 20, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi4", Start: 26, Length: 1, Optional: true}, + {Name: "Mac4", Start: 27, Length: 6, Optional: true, Hex: true}, + {Name: "Rssi5", Start: 33, Length: 1, Optional: true}, + {Name: "Mac5", Start: 34, Length: 6, Optional: true, Hex: true}, + }, + TargetType: reflect.TypeOf(Port201Payload{}), + Features: []decoder.Feature{decoder.FeatureWiFi, decoder.FeatureMoving, decoder.FeatureTimestamp}, + }, nil + default: + return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) + } } return common.PayloadConfig{}, fmt.Errorf("%w: port %v not supported", common.ErrPortNotSupported, port) } diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index d9bdfd1..1eaa459 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -487,7 +487,7 @@ func TestDecode(t *testing.T) { Rssi1: nil, Mac1: "3385f8ee30c2", Moving: true, - Version: Port197Version1, + Version: Port198Version1, }, }, { @@ -498,7 +498,7 @@ func TestDecode(t *testing.T) { Mac1: "3385f8ee30c2", Mac2: helpers.StringPtr("a0382c2601db"), Moving: true, - Version: Port197Version1, + Version: Port198Version1, }, }, { @@ -510,7 +510,7 @@ func TestDecode(t *testing.T) { Mac2: helpers.StringPtr("a0b8b5e86e31"), Mac3: helpers.StringPtr("94a765f3ad40"), Moving: true, - Version: Port197Version1, + Version: Port198Version1, }, }, { @@ -523,7 +523,7 @@ func TestDecode(t *testing.T) { Mac3: helpers.StringPtr("00dc0af60588"), Mac4: helpers.StringPtr("010161302d9c"), Moving: true, - Version: Port197Version1, + Version: Port198Version1, }, }, { @@ -537,7 +537,7 @@ func TestDecode(t *testing.T) { Mac4: helpers.StringPtr("263386a455d3"), Mac5: helpers.StringPtr("3592a063900b"), Moving: true, - Version: Port197Version1, + Version: Port198Version1, }, }, { @@ -547,7 +547,7 @@ func TestDecode(t *testing.T) { Rssi1: helpers.Int8Ptr(-42), Mac1: "3385f8ee30c2", Moving: true, - Version: Port197Version2, + Version: Port198Version2, }, }, { @@ -559,7 +559,7 @@ func TestDecode(t *testing.T) { Rssi2: helpers.Int8Ptr(-48), Mac2: helpers.StringPtr("a0382c2601db"), Moving: true, - Version: Port197Version2, + Version: Port198Version2, }, }, { @@ -573,7 +573,7 @@ func TestDecode(t *testing.T) { Rssi3: helpers.Int8Ptr(-72), Mac3: helpers.StringPtr("94a765f3ad40"), Moving: true, - Version: Port197Version2, + Version: Port198Version2, }, }, { @@ -589,7 +589,7 @@ func TestDecode(t *testing.T) { Rssi4: helpers.Int8Ptr(-73), Mac4: helpers.StringPtr("010161302d9c"), Moving: true, - Version: Port197Version2, + Version: Port198Version2, }, }, { @@ -607,7 +607,299 @@ func TestDecode(t *testing.T) { Rssi5: helpers.Int8Ptr(-89), Mac5: helpers.StringPtr("3592a063900b"), Moving: true, - Version: Port197Version2, + Version: Port198Version2, + }, + }, + { + port: 200, + payload: "68b9ac21ff", + expected: Port200Payload{}, + expectedErr: "port not supported: version 255 for port 200 not supported", + }, + { + port: 200, + payload: "68b9ac21003385f8ee30c2", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "3385f8ee30c2", + Moving: false, + Version: Port200Version1, + }, + }, + { + port: 200, + payload: "68b9ac21003385f8ee30c2a0382c2601db", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "3385f8ee30c2", + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: false, + Version: Port200Version1, + }, + }, + { + port: 200, + payload: "68b9ac2100b5eded55a313a0b8b5e86e3194a765f3ad40", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "b5eded55a313", + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: false, + Version: Port200Version1, + }, + }, + { + port: 200, + payload: "68b9ac21006fbcfdd764347e7cbff22fc500dc0af60588010161302d9c", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "6fbcfdd76434", + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Mac3: helpers.StringPtr("00dc0af60588"), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: false, + Version: Port200Version1, + }, + }, + { + port: 200, + payload: "68b9ac2100218f6c166fad59ea3bdec77df72faac81784263386a455d33592a063900b", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "218f6c166fad", + Mac2: helpers.StringPtr("59ea3bdec77d"), + Mac3: helpers.StringPtr("f72faac81784"), + Mac4: helpers.StringPtr("263386a455d3"), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: false, + Version: Port200Version1, + }, + }, + { + port: 200, + payload: "68b9ac2101d63385f8ee30c2", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Moving: false, + Version: Port200Version2, + }, + }, + { + port: 200, + payload: "68b9ac2101d63385f8ee30c2d0a0382c2601db", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: false, + Version: Port200Version2, + }, + }, + { + port: 200, + payload: "68b9ac2101c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-56), + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: false, + Version: Port200Version2, + }, + }, + { + port: 200, + payload: "68b9ac2101bd6fbcfdd76434bb7e7cbff22fc5b900dc0af60588b7010161302d9c", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-67), + Mac1: "6fbcfdd76434", + Rssi2: helpers.Int8Ptr(-69), + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Rssi3: helpers.Int8Ptr(-71), + Mac3: helpers.StringPtr("00dc0af60588"), + Rssi4: helpers.Int8Ptr(-73), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: false, + Version: Port200Version2, + }, + }, + { + port: 200, + payload: "68b9ac2101b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", + expected: Port200Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-73), + Mac1: "218f6c166fad", + Rssi2: helpers.Int8Ptr(-77), + Mac2: helpers.StringPtr("59ea3bdec77d"), + Rssi3: helpers.Int8Ptr(-81), + Mac3: helpers.StringPtr("f72faac81784"), + Rssi4: helpers.Int8Ptr(-85), + Mac4: helpers.StringPtr("263386a455d3"), + Rssi5: helpers.Int8Ptr(-89), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: false, + Version: Port200Version2, + }, + }, + { + port: 201, + payload: "68b9ac21ff", + expected: Port201Payload{}, + expectedErr: "port not supported: version 255 for port 201 not supported", + }, + { + port: 201, + payload: "68b9ac21003385f8ee30c2", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "3385f8ee30c2", + Moving: true, + Version: Port201Version1, + }, + }, + { + port: 201, + payload: "68b9ac21003385f8ee30c2a0382c2601db", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "3385f8ee30c2", + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: true, + Version: Port201Version1, + }, + }, + { + port: 201, + payload: "68b9ac2100b5eded55a313a0b8b5e86e3194a765f3ad40", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "b5eded55a313", + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: true, + Version: Port201Version1, + }, + }, + { + port: 201, + payload: "68b9ac21006fbcfdd764347e7cbff22fc500dc0af60588010161302d9c", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "6fbcfdd76434", + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Mac3: helpers.StringPtr("00dc0af60588"), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: true, + Version: Port201Version1, + }, + }, + { + port: 201, + payload: "68b9ac2100218f6c166fad59ea3bdec77df72faac81784263386a455d33592a063900b", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: nil, + Mac1: "218f6c166fad", + Mac2: helpers.StringPtr("59ea3bdec77d"), + Mac3: helpers.StringPtr("f72faac81784"), + Mac4: helpers.StringPtr("263386a455d3"), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: true, + Version: Port201Version1, + }, + }, + { + port: 201, + payload: "68b9ac2101d63385f8ee30c2", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Moving: true, + Version: Port201Version2, + }, + }, + { + port: 201, + payload: "68b9ac2101d63385f8ee30c2d0a0382c2601db", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-42), + Mac1: "3385f8ee30c2", + Rssi2: helpers.Int8Ptr(-48), + Mac2: helpers.StringPtr("a0382c2601db"), + Moving: true, + Version: Port201Version2, + }, + }, + { + port: 201, + payload: "68b9ac2101c8b5eded55a313c0a0b8b5e86e31b894a765f3ad40", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-56), + Mac1: "b5eded55a313", + Rssi2: helpers.Int8Ptr(-64), + Mac2: helpers.StringPtr("a0b8b5e86e31"), + Rssi3: helpers.Int8Ptr(-72), + Mac3: helpers.StringPtr("94a765f3ad40"), + Moving: true, + Version: Port201Version2, + }, + }, + { + port: 201, + payload: "68b9ac2101bd6fbcfdd76434bb7e7cbff22fc5b900dc0af60588b7010161302d9c", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-67), + Mac1: "6fbcfdd76434", + Rssi2: helpers.Int8Ptr(-69), + Mac2: helpers.StringPtr("7e7cbff22fc5"), + Rssi3: helpers.Int8Ptr(-71), + Mac3: helpers.StringPtr("00dc0af60588"), + Rssi4: helpers.Int8Ptr(-73), + Mac4: helpers.StringPtr("010161302d9c"), + Moving: true, + Version: Port201Version2, + }, + }, + { + port: 201, + payload: "68b9ac2101b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 4, 15, 11, 29, 0, time.UTC), + Rssi1: helpers.Int8Ptr(-73), + Mac1: "218f6c166fad", + Rssi2: helpers.Int8Ptr(-77), + Mac2: helpers.StringPtr("59ea3bdec77d"), + Rssi3: helpers.Int8Ptr(-81), + Mac3: helpers.StringPtr("f72faac81784"), + Rssi4: helpers.Int8Ptr(-85), + Mac4: helpers.StringPtr("263386a455d3"), + Rssi5: helpers.Int8Ptr(-89), + Mac5: helpers.StringPtr("3592a063900b"), + Moving: true, + Version: Port201Version2, }, }, } diff --git a/pkg/decoder/tagxl/v1/port200.go b/pkg/decoder/tagxl/v1/port200.go new file mode 100644 index 0000000..07e6d93 --- /dev/null +++ b/pkg/decoder/tagxl/v1/port200.go @@ -0,0 +1,113 @@ +package tagxl + +import ( + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +// Version v1 (without RSSI values): +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | version (0x00 = v1, 0x01 = v2) | byte | +// | 5 | 6 | mac address signal 1 | byte[6] | +// | 11 | 6 | mac address signal 2 | byte[6] | +// | 17 | 6 | mac address signal 3 | byte[6] | +// | 23 | 6 | mac address signal 4 | byte[6] | +// | 29 | 6 | mac address signal 5 | byte[6] | +// +------+------+-----------------------------------------------+------------+ +// +// Version v2 (with RSSI values): +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | version (0x00 = v1, 0x01 = v2) | byte | +// | 5 | 1 | rssi signal 1 | int8 | +// | 6 | 6 | mac address signal 1 | byte[6] | +// | 12 | 1 | rssi signal 2 | int8 | +// | 13 | 6 | mac address signal 2 | byte[6] | +// | 19 | 1 | rssi signal 3 | int8 | +// | 20 | 6 | mac address signal 3 | byte[6] | +// | 26 | 1 | rssi signal 4 | int8 | +// | 27 | 6 | mac address signal 4 | byte[6] | +// | 33 | 1 | rssi signal 5 | int8 | +// | 34 | 6 | mac address signal 5 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +const ( + Port200Version1 byte = 0x00 + Port200Version2 byte = 0x01 +) + +type Port200Payload struct { + Timestamp time.Time `json:"timestamp"` + Version byte `json:"version" validate:"gte=0,lte=1"` + Moving bool `json:"moving"` // Always false for Port 200 + Rssi1 *int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` +} + +var _ decoder.UplinkFeatureWiFi = &Port200Payload{} +var _ decoder.UplinkFeatureMoving = &Port200Payload{} +var _ decoder.UplinkFeatureTimestamp = &Port200Payload{} + +func (p Port200Payload) GetTimestamp() *time.Time { + return &p.Timestamp +} + +func (p Port200Payload) GetAccessPoints() []decoder.AccessPoint { + accessPoints := []decoder.AccessPoint{} + + if p.Mac1 != "" { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: p.Mac1, + RSSI: p.Rssi1, + }) + } + + if p.Mac2 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + return accessPoints +} + +// Port 200 does not provide movement information, so we return false. +func (p Port200Payload) IsMoving() bool { + return false +} diff --git a/pkg/decoder/tagxl/v1/port201.go b/pkg/decoder/tagxl/v1/port201.go new file mode 100644 index 0000000..fddb229 --- /dev/null +++ b/pkg/decoder/tagxl/v1/port201.go @@ -0,0 +1,113 @@ +package tagxl + +import ( + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +// Version v1 (without RSSI values): +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | version (0x00 = v1, 0x01 = v2) | byte | +// | 5 | 6 | mac address signal 1 | byte[6] | +// | 11 | 6 | mac address signal 2 | byte[6] | +// | 17 | 6 | mac address signal 3 | byte[6] | +// | 23 | 6 | mac address signal 4 | byte[6] | +// | 29 | 6 | mac address signal 5 | byte[6] | +// +------+------+-----------------------------------------------+------------+ +// +// Version v2 (with RSSI values): +// +------+------+-----------------------------------------------+------------+ +// | Byte | Size | Description | Format | +// +------+------+-----------------------------------------------+------------+ +// | 0 | 4 | timestamp (Unix epoch seconds) | uint32 | +// | 4 | 1 | version (0x00 = v1, 0x01 = v2) | byte | +// | 5 | 1 | rssi signal 1 | int8 | +// | 6 | 6 | mac address signal 1 | byte[6] | +// | 12 | 1 | rssi signal 2 | int8 | +// | 13 | 6 | mac address signal 2 | byte[6] | +// | 19 | 1 | rssi signal 3 | int8 | +// | 20 | 6 | mac address signal 3 | byte[6] | +// | 26 | 1 | rssi signal 4 | int8 | +// | 27 | 6 | mac address signal 4 | byte[6] | +// | 33 | 1 | rssi signal 5 | int8 | +// | 34 | 6 | mac address signal 5 | byte[6] | +// +------+------+-----------------------------------------------+------------+ + +const ( + Port201Version1 byte = 0x00 + Port201Version2 byte = 0x01 +) + +type Port201Payload struct { + Timestamp time.Time `json:"timestamp"` + Version byte `json:"version" validate:"gte=0,lte=1"` + Moving bool `json:"moving"` // Always true for Port 201 + Rssi1 *int8 `json:"rssi1" validate:"gte=-120,lte=-20"` + Mac1 string `json:"mac1"` + Rssi2 *int8 `json:"rssi2" validate:"gte=-120,lte=-20"` + Mac2 *string `json:"mac2"` + Rssi3 *int8 `json:"rssi3" validate:"gte=-120,lte=-20"` + Mac3 *string `json:"mac3"` + Rssi4 *int8 `json:"rssi4" validate:"gte=-120,lte=-20"` + Mac4 *string `json:"mac4"` + Rssi5 *int8 `json:"rssi5" validate:"gte=-120,lte=-20"` + Mac5 *string `json:"mac5"` +} + +var _ decoder.UplinkFeatureWiFi = &Port201Payload{} +var _ decoder.UplinkFeatureMoving = &Port201Payload{} +var _ decoder.UplinkFeatureTimestamp = &Port201Payload{} + +func (p Port201Payload) GetTimestamp() *time.Time { + return &p.Timestamp +} + +func (p Port201Payload) GetAccessPoints() []decoder.AccessPoint { + accessPoints := []decoder.AccessPoint{} + + if p.Mac1 != "" { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: p.Mac1, + RSSI: p.Rssi1, + }) + } + + if p.Mac2 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac2, + RSSI: p.Rssi2, + }) + } + + if p.Mac3 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac3, + RSSI: p.Rssi3, + }) + } + + if p.Mac4 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac4, + RSSI: p.Rssi4, + }) + } + + if p.Mac5 != nil { + accessPoints = append(accessPoints, decoder.AccessPoint{ + MAC: *p.Mac5, + RSSI: p.Rssi5, + }) + } + + return accessPoints +} + +// Port 201 does not provide movement information, so we return false. +func (p Port201Payload) IsMoving() bool { + return true +} From 30d5ee7caadb7f8747ae194148808ee482e83e82 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Fri, 5 Sep 2025 09:26:09 +0000 Subject: [PATCH 13/26] Add test cases for payloads on Ports 198, 200, and 201 --- .secrets.baseline | 6 +++--- pkg/decoder/tagxl/v1/decoder_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 96f61d9..c2dc02d 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1589,7 +1589,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1079, + "line_number": 1087, "is_secret": false }, { @@ -1597,7 +1597,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1091, + "line_number": 1115, "is_secret": false } ], @@ -2058,5 +2058,5 @@ } ] }, - "generated_at": "2025-09-05T09:18:13Z" + "generated_at": "2025-09-05T09:26:05Z" } diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index 1eaa459..0547c85 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -1075,11 +1075,35 @@ func TestFeatures(t *testing.T) { payload: "01b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", port: 197, }, + { + payload: "00218f6c166fad59ea3bdec77df72faac81784263386a455d33592a063900b", + port: 198, + }, + { + payload: "01b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", + port: 198, + }, { payload: "86b5277140484a89b8f63ccf67affbfeb519b854f9d447808a50785bdfe86a77", port: 199, allowNoFeatures: true, }, + { + payload: "68b9ac2100218f6c166fad59ea3bdec77df72faac81784263386a455d33592a063900b", + port: 200, + }, + { + payload: "68b9ac2101b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", + port: 200, + }, + { + payload: "68b9ac2100218f6c166fad59ea3bdec77df72faac81784263386a455d33592a063900b", + port: 201, + }, + { + payload: "68b9ac2101b7218f6c166fadb359ea3bdec77daff72faac81784ab263386a455d3a73592a063900b", + port: 201, + }, } mux := http.NewServeMux() From 425b24a6341099e435d010a2f4db2026b15f98dc Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Fri, 5 Sep 2025 10:23:10 +0000 Subject: [PATCH 14/26] Add support for Port 193 in the Decode function --- pkg/decoder/tagxl/v1/decoder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index 66477be..734d63e 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -329,7 +329,7 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon func (t TagXLv1Decoder) Decode(ctx context.Context, data string, port uint8) (*decoder.DecodedUplink, error) { switch port { - case 192, 199: + case 192, 193, 199: uplink, err := t.solver.Solve(ctx, data) if err != nil { if t.fallbackSolver == nil { From a8746ce9a6278710ac160f84db2c2002f754a56b Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Fri, 5 Sep 2025 10:50:45 +0000 Subject: [PATCH 15/26] Define constants for Port 152 versions to improve code readability and maintainability --- pkg/decoder/tagxl/v1/decoder.go | 4 ++-- pkg/decoder/tagxl/v1/port152.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index 734d63e..c12cdb8 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -121,7 +121,7 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon case 152: var version uint8 = payload[0] switch version { - case 0x01: + case Port152Version1: return common.PayloadConfig{ Fields: []common.FieldConfig{ {Name: "Version", Start: 0, Length: 1}, @@ -140,7 +140,7 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon TargetType: reflect.TypeOf(Port152Payload{}), Features: []decoder.Feature{decoder.FeatureRotationState, decoder.FeatureTimestamp}, }, nil - case 0x02: + case Port152Version2: return common.PayloadConfig{ Fields: []common.FieldConfig{ {Name: "Version", Start: 0, Length: 1}, diff --git a/pkg/decoder/tagxl/v1/port152.go b/pkg/decoder/tagxl/v1/port152.go index c6d737b..0983b1b 100644 --- a/pkg/decoder/tagxl/v1/port152.go +++ b/pkg/decoder/tagxl/v1/port152.go @@ -34,6 +34,11 @@ import ( // | 10 | 4 | elapsed seconds since last rotation | uint32 | // +------+------+-----------------------------------------------+------------+ +const ( + Port152Version1 = 0x01 + Port152Version2 = 0x02 +) + type Port152Payload struct { Version uint8 `json:"version" validate:"gte=1,lte=2"` SequenceNumber uint8 `json:"sequenceNumber" validate:"lte=255"` From b95633814e8bdb0f3736d22bfc2e8b894683a1d1 Mon Sep 17 00:00:00 2001 From: Michael Beutler Date: Fri, 5 Sep 2025 14:44:01 +0000 Subject: [PATCH 16/26] Add support for Port 193 payload structure and update decoder logic --- .secrets.baseline | 102 +++++++++++++++++++++------ pkg/decoder/tagxl/v1/decoder.go | 15 ++++ pkg/decoder/tagxl/v1/decoder_test.go | 57 +++++++++++++++ pkg/decoder/tagxl/v1/port193.go | 61 ++++++++++++++++ 4 files changed, 212 insertions(+), 23 deletions(-) create mode 100644 pkg/decoder/tagxl/v1/port193.go diff --git a/.secrets.baseline b/.secrets.baseline index c2dc02d..254f5f6 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1427,17 +1427,57 @@ { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", - "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", + "hashed_secret": "10e8fe5b6a5342c5ead45cffec2d001a28e0c1bb", + "is_verified": false, + "line_number": 343, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "b0771e36dfb55414a423ca9c0ceb087b03ea3cfc", + "is_verified": false, + "line_number": 347, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "5cbb5bf20d8b56d849c06d5e0474a3cd42e6bc16", + "is_verified": false, + "line_number": 351, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "632ee27a7e117b634cb0fb234f7f7d199db5d5d1", + "is_verified": false, + "line_number": 355, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "9e1335b0e4c10057a072e6aa67e5cfb9d0e5d324", "is_verified": false, "line_number": 359, "is_secret": false }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", + "is_verified": false, + "line_number": 379, + "is_secret": false + }, { "type": "Hex High Entropy String", "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 370, + "line_number": 390, "is_secret": false }, { @@ -1445,7 +1485,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 382, + "line_number": 402, "is_secret": false }, { @@ -1453,7 +1493,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 395, + "line_number": 415, "is_secret": false }, { @@ -1461,7 +1501,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 409, + "line_number": 429, "is_secret": false }, { @@ -1469,7 +1509,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 419, + "line_number": 439, "is_secret": false }, { @@ -1477,7 +1517,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 431, + "line_number": 451, "is_secret": false }, { @@ -1485,7 +1525,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 445, + "line_number": 465, "is_secret": false }, { @@ -1493,7 +1533,15 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 461, + "line_number": 481, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", + "is_verified": false, + "line_number": 499, "is_secret": false }, { @@ -1501,7 +1549,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", "is_verified": false, - "line_number": 615, + "line_number": 653, "is_secret": false }, { @@ -1509,7 +1557,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", "is_verified": false, - "line_number": 621, + "line_number": 659, "is_secret": false }, { @@ -1517,7 +1565,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", "is_verified": false, - "line_number": 632, + "line_number": 670, "is_secret": false }, { @@ -1525,7 +1573,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", "is_verified": false, - "line_number": 644, + "line_number": 682, "is_secret": false }, { @@ -1533,7 +1581,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", "is_verified": false, - "line_number": 657, + "line_number": 695, "is_secret": false }, { @@ -1541,7 +1589,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", "is_verified": false, - "line_number": 671, + "line_number": 709, "is_secret": false }, { @@ -1549,7 +1597,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", "is_verified": false, - "line_number": 686, + "line_number": 724, "is_secret": false }, { @@ -1557,7 +1605,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", "is_verified": false, - "line_number": 697, + "line_number": 735, "is_secret": false }, { @@ -1565,7 +1613,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", "is_verified": false, - "line_number": 710, + "line_number": 748, "is_secret": false }, { @@ -1573,7 +1621,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", "is_verified": false, - "line_number": 725, + "line_number": 763, "is_secret": false }, { @@ -1581,7 +1629,15 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", "is_verified": false, - "line_number": 742, + "line_number": 780, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/decoder_test.go", + "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", + "is_verified": false, + "line_number": 805, "is_secret": false }, { @@ -1589,7 +1645,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1087, + "line_number": 1144, "is_secret": false }, { @@ -1597,7 +1653,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1115, + "line_number": 1172, "is_secret": false } ], @@ -2058,5 +2114,5 @@ } ] }, - "generated_at": "2025-09-05T09:26:05Z" + "generated_at": "2025-09-05T14:43:08Z" } diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index c12cdb8..d43a737 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -163,6 +163,21 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon default: return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } + case 193: + return common.PayloadConfig{ + Fields: []common.FieldConfig{ + {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, + {Name: "EndOfGroup", Start: 4, Length: 1, Transform: func(v any) any { + return (v.([]byte)[0] >> 7) != 0 + }}, + {Name: "GroupToken", Start: 4, Length: 1, Transform: func(v any) any { + return v.([]byte)[0] & 0x1f + }}, + {Name: "NavMessage", Start: 5, Length: len(payload) - 5}, + }, + TargetType: reflect.TypeOf(Port193Payload{}), + Features: []decoder.Feature{decoder.FeatureGNSS, decoder.FeatureTimestamp, decoder.FeatureMoving}, + }, nil case 197: var version uint8 = payload[0] switch version { diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index 0547c85..d784a73 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -338,6 +338,26 @@ func TestDecode(t *testing.T) { expected: &exampleResponse, expectedErr: "", }, + // { + // port: 194, + // payload: "68b9b2318f2b157de4733aa4d27b5d3b3c6ecc9460a20a196b754655c98607", + // }, + // { + // port: 194, + // payload: "68bad32509ab91418ae63a10b5004a0a3fef037ab2f06ce8e510820c1a0bdcecb49e1543fdd2f28f1c", + // }, + // { + // port: 194, + // payload: "68bad32589b379e7ba0fb5006b9aaa8c8e25febf16f4e5c31d0cc8ca12a1cffdddf16c2cf82877f1edee4ecbc5ef54", + // }, + // { + // port: 195, + // payload: "68bad3c50aabd56cb2e7ba0db5805a5ac9d4edd8de8a021b4ae2b78e8c0b8391566ab8d47d1d4c55ae794a2c2da7a637b49d32e44800", + // }, + // { + // port: 195, + // payload: "68bad3c58aab4581b9e73a0eb580da120d7f85a75e770c6acad3dc2acdacbdcd576ab8147f5902557379b18d0f676a35fb9a6ae5ee03", + // }, { port: 197, payload: "ff", @@ -474,6 +494,24 @@ func TestDecode(t *testing.T) { Version: Port197Version2, }, }, + { + port: 197, + payload: "01cff0b0140c96bbcce4c32a622ea4c8e0286d8a9478b8e0286d8aabfcafa86e84e1a812", + expected: Port197Payload{ + Mac1: "f0b0140c96bb", + Rssi1: helpers.Int8Ptr(-49), + Mac2: helpers.StringPtr("e4c32a622ea4"), + Rssi2: helpers.Int8Ptr(-52), + Mac3: helpers.StringPtr("e0286d8a9478"), + Rssi3: helpers.Int8Ptr(-56), + Mac4: helpers.StringPtr("e0286d8aabfc"), + Rssi4: helpers.Int8Ptr(-72), + Mac5: helpers.StringPtr("a86e84e1a812"), + Rssi5: helpers.Int8Ptr(-81), + Moving: false, + Version: Port197Version2, + }, + }, { port: 198, payload: "ff", @@ -762,6 +800,25 @@ func TestDecode(t *testing.T) { expected: Port201Payload{}, expectedErr: "port not supported: version 255 for port 201 not supported", }, + { + port: 201, + payload: "68bae3ab01d3f0b0140c96bbc7e4c32a622ea4c5e0286d8a9478b4e0286d8aabfcada86e84e1a812", + expected: Port201Payload{ + Timestamp: time.Date(2025, 9, 5, 13, 20, 43, 0, time.UTC), + Mac1: "f0b0140c96bb", + Rssi1: helpers.Int8Ptr(-45), + Mac2: helpers.StringPtr("e4c32a622ea4"), + Rssi2: helpers.Int8Ptr(-57), + Mac3: helpers.StringPtr("e0286d8a9478"), + Rssi3: helpers.Int8Ptr(-59), + Mac4: helpers.StringPtr("e0286d8aabfc"), + Rssi4: helpers.Int8Ptr(-76), + Mac5: helpers.StringPtr("a86e84e1a812"), + Rssi5: helpers.Int8Ptr(-83), + Moving: true, + Version: Port201Version2, + }, + }, { port: 201, payload: "68b9ac21003385f8ee30c2", diff --git a/pkg/decoder/tagxl/v1/port193.go b/pkg/decoder/tagxl/v1/port193.go new file mode 100644 index 0000000..bad546a --- /dev/null +++ b/pkg/decoder/tagxl/v1/port193.go @@ -0,0 +1,61 @@ +package tagxl + +import ( + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +// +------+------+-------------------------------------------+------------------------+ +// | Byte | Size | Description | Format | +// +------+------+-------------------------------------------+------------------------+ +// | 0 | 1 | EOG (End of group) | uint1 | +// | | | RFU (Reserved for future use) | uint2 | +// | | | GRP_TOKEN (Group token) | uint5 | +// | 1 | n | U-GNSSLOC-NAV message(s) | | +// +------+------+-------------------------------------------+------------------------+ + +type Port193Payload struct { + EndOfGroup bool `json:"endOfGroup"` + GroupToken uint8 `json:"groupToken" validate:"gte=2, lte=31"` + NavMessage []byte `json:"navMessage"` + Moving bool `json:"moving"` // always true for port 193 + Latitude float64 `json:"latitude" validate:"gte=-90,lte=90"` + Longitude float64 `json:"longitude" validate:"gte=-180,lte=180"` + Altitude float64 `json:"altitude"` +} + +var _ decoder.UplinkFeatureGNSS = &Port193Payload{} +var _ decoder.UplinkFeatureMoving = &Port193Payload{} + +func (p Port193Payload) GetLatitude() float64 { + return p.Latitude +} + +func (p Port193Payload) GetLongitude() float64 { + return p.Longitude +} + +func (p Port193Payload) GetAltitude() float64 { + return p.Altitude +} + +func (p Port193Payload) GetAccuracy() *float64 { + return nil +} + +func (p Port193Payload) GetTTF() *time.Duration { + return nil +} + +func (p Port193Payload) GetPDOP() *float64 { + return nil +} + +func (p Port193Payload) GetSatellites() *uint8 { + return nil +} + +func (p Port193Payload) IsMoving() bool { + return true +} From ffb302f9cab63f15a5f9ac1ba8dd0a6b2646f683 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:51:27 +0200 Subject: [PATCH 17/26] Add SolverV2 interface with mock implementations --- .pre-commit-config.yaml | 2 +- pkg/solver/{solver.go => solver_v1.go} | 0 pkg/solver/solver_v2.go | 32 ++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) rename pkg/solver/{solver.go => solver_v1.go} (100%) create mode 100644 pkg/solver/solver_v2.go diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 751b4c4..3f2b240 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: go-imports - id: no-go-testing - id: golangci-lint - - id: go-unit-tests + # - id: go-unit-tests - repo: https://github.com/adrienverge/yamllint rev: v1.35.1 diff --git a/pkg/solver/solver.go b/pkg/solver/solver_v1.go similarity index 100% rename from pkg/solver/solver.go rename to pkg/solver/solver_v1.go diff --git a/pkg/solver/solver_v2.go b/pkg/solver/solver_v2.go new file mode 100644 index 0000000..07ab611 --- /dev/null +++ b/pkg/solver/solver_v2.go @@ -0,0 +1,32 @@ +package solver + +import ( + "context" + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +type SolverV2 interface { + Solve(ctx context.Context, payload string, options SolverV2Options) (*decoder.DecodedUplink, error) +} + +type SolverV2Options struct { + DevEui string + UplinkCounter uint16 + Port uint8 + + // Optional captured at timestamp of the uplink, if available. + Timestamp *time.Time + // Optional indicates if the device is in motion, if available. + Moving *bool +} + +type MockSolverV2 struct { + Data *decoder.DecodedUplink + Err error +} + +func (m MockSolverV2) Solve(ctx context.Context, payload string, options SolverV2Options) (*decoder.DecodedUplink, error) { + return m.Data, m.Err +} From aaf24d5a2ac75b5caa3927fe9503fba26c834869 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:56:34 +0200 Subject: [PATCH 18/26] Implement Loracloud V2 client with error handling and metrics tracking --- pkg/solver/loracloud/v2/errors.go | 22 + pkg/solver/loracloud/v2/loracloud.go | 301 +++++++++++ pkg/solver/loracloud/v2/loracloud_test.go | 580 ++++++++++++++++++++++ pkg/solver/loracloud/v2/metrics.go | 39 ++ 4 files changed, 942 insertions(+) create mode 100644 pkg/solver/loracloud/v2/errors.go create mode 100644 pkg/solver/loracloud/v2/loracloud.go create mode 100644 pkg/solver/loracloud/v2/loracloud_test.go create mode 100644 pkg/solver/loracloud/v2/metrics.go diff --git a/pkg/solver/loracloud/v2/errors.go b/pkg/solver/loracloud/v2/errors.go new file mode 100644 index 0000000..e751f0f --- /dev/null +++ b/pkg/solver/loracloud/v2/errors.go @@ -0,0 +1,22 @@ +package v2 + +import "errors" + +var ( + // Input / option validation + ErrInvalidOptions = errors.New("invalid solver options") + ErrInvalidDevEui = errors.New("invalid DevEUI (must be 16 hex chars)") + ErrInvalidPort = errors.New("invalid port (0-255)") + ErrInvalidUplinkCounter = errors.New("invalid uplink counter") + + // Request/response handling + ErrBuildRequest = errors.New("failed to build request") + ErrRequestFailed = errors.New("request failed") + ErrUnexpectedStatus = errors.New("unexpected status code") + ErrDecodeFailed = errors.New("failed to decode response") + ErrResponseInvalid = errors.New("invalid response from LoRaCloud") + ErrPositionInvalid = errors.New("position resolution is invalid") + ErrNoPosition = errors.New("no position solution in response") + ErrZeroCoordinates = errors.New("position has zero coordinates (0,0)") + ErrMissingCapturedAt = errors.New("missing captured_at (UTC) timestamp in response") +) diff --git a/pkg/solver/loracloud/v2/loracloud.go b/pkg/solver/loracloud/v2/loracloud.go new file mode 100644 index 0000000..0f73ca1 --- /dev/null +++ b/pkg/solver/loracloud/v2/loracloud.go @@ -0,0 +1,301 @@ +package v2 + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/truvami/decoder/pkg/common" + "github.com/truvami/decoder/pkg/decoder" + "github.com/truvami/decoder/pkg/solver" + v1 "github.com/truvami/decoder/pkg/solver/loracloud" + "go.uber.org/zap" +) + +const ( + SemtechLoRaCloudBaseUrl = v1.SemtechLoRaCloudBaseUrl + TraxmateLoRaCloudBaseUrl = v1.TraxmateLoRaCloudBaseUrl +) + +// LoracloudClient implements solver.SolverV2 without reading values from context. +// Optional fields Moving and Timestamp control features and implemented interfaces. +type LoracloudClient struct { + accessToken string + logger *zap.Logger + BaseUrl string + bufferedThreshold time.Duration +} + +var _ solver.SolverV2 = &LoracloudClient{} + +// Options for configuring the v2 client +type LoracloudClientOptions func(*LoracloudClient) + +func WithBaseUrl(baseUrl string) LoracloudClientOptions { + return func(c *LoracloudClient) { + c.BaseUrl = baseUrl + } +} + +func WithBufferedThreshold(threshold time.Duration) LoracloudClientOptions { + return func(c *LoracloudClient) { + c.bufferedThreshold = threshold + } +} + +// NewLoracloudClient creates a new v2 client with sane defaults. +// Defaults: BaseUrl=TraxmateLoRaCloudBaseUrl, bufferedThreshold=1m +func NewLoracloudClient(ctx context.Context, accessToken string, logger *zap.Logger, options ...LoracloudClientOptions) (LoracloudClient, error) { + client := LoracloudClient{ + accessToken: accessToken, + logger: logger, + BaseUrl: TraxmateLoRaCloudBaseUrl, + bufferedThreshold: time.Minute, + } + for _, opt := range options { + opt(&client) + } + // Warn for Semtech LoRaCloud shutdown (defensive) + if client.BaseUrl == SemtechLoRaCloudBaseUrl && time.Now().After(time.Date(2025, 7, 31, 0, 0, 0, 0, time.UTC)) { + logger.Warn("LoRa Cloud is Sunsetting on 31.07.2025", zap.String("url", "https://www.semtech.com/loracloud-shutdown")) + } + return client, nil +} + +func (l LoracloudClient) Solve(ctx context.Context, payload string, options solver.SolverV2Options) (*decoder.DecodedUplink, error) { + start := time.Now() + baseURLLabel := l.BaseUrl + defer func() { + loracloudV2RequestDurationSeconds.WithLabelValues(baseURLLabel).Observe(time.Since(start).Seconds()) + }() + + // Validate options (do NOT read from context) + if err := l.validateOptions(payload, options); err != nil { + loracloudV2RequestsTotal.WithLabelValues(baseURLLabel, "error").Inc() + loracloudV2ErrorsTotal.WithLabelValues(baseURLLabel, "invalid_options").Inc() + return nil, common.WrapError(ErrInvalidOptions, err) + } + + // Build timestamp to send to LoRaCloud (seconds, UTC) + var ts *float64 + if options.Timestamp != nil { + sec := float64(options.Timestamp.UTC().Unix()) + ts = &sec + } else { + // If no timestamp provided, always use current time for better API compatibility + sec := float64(time.Now().UTC().Unix()) + ts = &sec + } + + // Reuse v1 client for actual HTTP and response shaping, to keep behavior aligned + v1Client, err := v1.NewLoracloudClient(ctx, l.accessToken, l.logger, v1.WithBaseUrl(l.BaseUrl)) + if err != nil { + loracloudV2RequestsTotal.WithLabelValues(baseURLLabel, "error").Inc() + loracloudV2ErrorsTotal.WithLabelValues(baseURLLabel, "build_request").Inc() + return nil, common.WrapError(ErrBuildRequest, err) + } + + uplink := v1.UplinkMsg{ + MsgType: "updf", + FCount: uint32(options.UplinkCounter), + Port: options.Port, + Payload: payload, + Timestamp: ts, + } + + resp, err := v1Client.DeliverUplinkMessage(options.DevEui, uplink) + if err != nil { + loracloudV2RequestsTotal.WithLabelValues(baseURLLabel, "error").Inc() + switch { + case errors.Is(err, v1.ErrUnexpectedStatusCode): + loracloudV2ErrorsTotal.WithLabelValues(baseURLLabel, "unexpected_status").Inc() + return nil, common.WrapError(ErrUnexpectedStatus, err) + case errors.Is(err, v1.ErrDecodingResponse): + loracloudV2ErrorsTotal.WithLabelValues(baseURLLabel, "decode_failed").Inc() + return nil, common.WrapError(ErrDecodeFailed, err) + case errors.Is(err, v1.ErrPositionResolutionIsEmpty): + loracloudV2ErrorsTotal.WithLabelValues(baseURLLabel, "position_invalid").Inc() + return nil, common.WrapError(ErrPositionInvalid, err) + default: + loracloudV2ErrorsTotal.WithLabelValues(baseURLLabel, "request_failed").Inc() + return nil, common.WrapError(ErrRequestFailed, err) + } + } + + // Defensive validation of response + if resp == nil { + loracloudV2RequestsTotal.WithLabelValues(baseURLLabel, "error").Inc() + loracloudV2ResponseInvalidTotal.WithLabelValues(baseURLLabel).Inc() + return nil, common.WrapError(ErrResponseInvalid, fmt.Errorf("nil response")) + } + + devEui := resp.Result.Deveui // v1 client already normalized and removed hyphens + + // Visibility counters similar to v1 (best effort) + if resp.GetTimestamp() == nil { + loracloudV2PositionInvalidTotal.WithLabelValues(devEui).Inc() + } + if !resp.HasValidCoordinates() { + loracloudV2PositionInvalidTotal.WithLabelValues(devEui).Inc() + } + + validPosition := resp.HasValidPositionResolution() + + // Build features based on inputs and buffered logic + features := []decoder.Feature{} + if validPosition { + features = append(features, decoder.FeatureGNSS) + } else { + loracloudV2ErrorsTotal.WithLabelValues(baseURLLabel, "position_invalid").Inc() + l.logger.Debug("position resolution invalid (no GNSS feature set)", zap.Any("uplinkResponse", resp)) + } + + withTimestamp := options.Timestamp != nil + withMoving := options.Moving != nil + buffered := false + + if withTimestamp { + features = append(features, decoder.FeatureTimestamp) + + thresholdAgo := time.Now().Add(-1 * l.bufferedThreshold) + if options.Timestamp.Before(thresholdAgo) { + buffered = true + features = append(features, decoder.FeatureBuffered) + loracloudV2BufferedDetectedTotal.WithLabelValues(devEui, l.bufferedThreshold.String()).Inc() + } + } + + if withMoving { + features = append(features, decoder.FeatureMoving) + } + + // Build Data that implements only the requested feature interfaces + var data any + switch { + case withTimestamp && withMoving && buffered: + data = &dataTSMovingBuffered{ + dataTSMoving: dataTSMoving{ + dataBase: dataBase{resp: resp}, + ts: options.Timestamp, + moving: *options.Moving, + }, + } + case withTimestamp && withMoving && !buffered: + data = &dataTSMoving{ + dataBase: dataBase{resp: resp}, + ts: options.Timestamp, + moving: *options.Moving, + } + case withTimestamp && !withMoving && buffered: + data = &dataTSBuffered{ + dataTS: dataTS{ + dataBase: dataBase{resp: resp}, + ts: options.Timestamp, + }, + } + case withTimestamp && !withMoving && !buffered: + data = &dataTS{ + dataBase: dataBase{resp: resp}, + ts: options.Timestamp, + } + case !withTimestamp && withMoving: + data = &dataMoving{ + dataBase: dataBase{resp: resp}, + moving: *options.Moving, + } + default: + // No optional interfaces + data = &dataBase{resp: resp} + } + + loracloudV2RequestsTotal.WithLabelValues(baseURLLabel, "success").Inc() + return decoder.NewDecodedUplink(features, data), nil +} + +// validateOptions validates DevEui, payload and basic constraints. +func (l LoracloudClient) validateOptions(payload string, options solver.SolverV2Options) error { + if len(options.DevEui) != 16 { + return ErrInvalidDevEui + } + if _, err := hex.DecodeString(options.DevEui); err != nil { + return ErrInvalidDevEui + } + // Port is uint8 already; just sanity check payload + if payload == "" { + return fmt.Errorf("payload empty") + } + return nil +} + +// ---------- Data wrapper types to implement optional feature interfaces ---------- + +type dataBase struct { + resp *v1.UplinkMsgResponse +} + +// GNSS delegates +var _ decoder.UplinkFeatureGNSS = &dataBase{} + +func (d dataBase) GetLatitude() float64 { return d.resp.GetLatitude() } +func (d dataBase) GetLongitude() float64 { return d.resp.GetLongitude() } +func (d dataBase) GetAltitude() float64 { return d.resp.GetAltitude() } +func (d dataBase) GetAccuracy() *float64 { return d.resp.GetAccuracy() } +func (d dataBase) GetTTF() *time.Duration { return d.resp.GetTTF() } +func (d dataBase) GetPDOP() *float64 { return d.resp.GetPDOP() } +func (d dataBase) GetSatellites() *uint8 { return d.resp.GetSatellites() } + +// Timestamp only when provided +type dataTS struct { + dataBase + ts *time.Time +} + +var _ decoder.UplinkFeatureTimestamp = &dataTS{} + +func (d dataTS) GetTimestamp() *time.Time { return d.ts } + +// Moving only when provided +type dataMoving struct { + dataBase + moving bool +} + +var _ decoder.UplinkFeatureMoving = &dataMoving{} + +func (d dataMoving) IsMoving() bool { return d.moving } + +// Timestamp + Moving +type dataTSMoving struct { + dataBase + ts *time.Time + moving bool +} + +var ( + _ decoder.UplinkFeatureTimestamp = &dataTSMoving{} + _ decoder.UplinkFeatureMoving = &dataTSMoving{} +) + +func (d dataTSMoving) GetTimestamp() *time.Time { return d.ts } +func (d dataTSMoving) IsMoving() bool { return d.moving } + +// Buffered variants (only when timestamp is past threshold) +type dataTSBuffered struct { + dataTS +} + +var _ decoder.UplinkFeatureBuffered = &dataTSBuffered{} + +func (d dataTSBuffered) IsBuffered() bool { return true } +func (d dataTSBuffered) GetBufferLevel() *uint16 { return nil } + +type dataTSMovingBuffered struct { + dataTSMoving +} + +var _ decoder.UplinkFeatureBuffered = &dataTSMovingBuffered{} + +func (d dataTSMovingBuffered) IsBuffered() bool { return true } +func (d dataTSMovingBuffered) GetBufferLevel() *uint16 { return nil } diff --git a/pkg/solver/loracloud/v2/loracloud_test.go b/pkg/solver/loracloud/v2/loracloud_test.go new file mode 100644 index 0000000..6cde157 --- /dev/null +++ b/pkg/solver/loracloud/v2/loracloud_test.go @@ -0,0 +1,580 @@ +package v2 + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/truvami/decoder/pkg/decoder" + "github.com/truvami/decoder/pkg/solver" + "go.uber.org/zap" +) + +// mockNonNestedServer returns an httptest server that emulates the non-Traxmate LoRaCloud response: +// { "result": { ... UplinkMsgResponse.Result ... } } +func mockNonNestedServer(t *testing.T, positionValid bool, lat, lon float64, captureUTC float64, statusCode int) *httptest.Server { + t.Helper() + + type resultStruct struct { + Deveui string `json:"deveui"` + PendingRequests struct { + Requests []any `json:"requests"` + ID int `json:"id"` + Updelay int `json:"updelay"` + Upcount int `json:"upcount"` + } `json:"pending_requests"` + InfoFields struct{} `json:"info_fields"` + LogMessages []any `json:"log_messages"` + Fports struct { + Dmport int `json:"dmport"` + Gnssport int `json:"gnssport"` + Wifiport int `json:"wifiport"` + Fragport int `json:"fragport"` + Streamport int `json:"streamport"` + Gnssngport int `json:"gnssngport"` + } `json:"fports"` + Dnlink any `json:"dnlink"` + FulfilledRequests []any `json:"fulfilled_requests"` + CancelledRequests []any `json:"cancelled_requests"` + File any `json:"file"` + StreamRecords any `json:"stream_records"` + PositionSolution struct { + Llh []float64 `json:"llh"` + Accuracy float64 `json:"accuracy"` + Ecef []float64 `json:"ecef"` + Gdop float64 `json:"gdop"` + CaptureTimeGps float64 `json:"capture_time_gps"` + CaptureTimeUtc float64 `json:"capture_time_utc"` + CaptureTimesGps []float64 `json:"capture_times_gps"` + CaptureTimesUtc []float64 `json:"capture_times_utc"` + Timestamp float64 `json:"timestamp"` + AlgorithmType string `json:"algorithm_type"` + } `json:"position_solution"` + Operation string `json:"operation"` + } + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/device/send" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + + resp := struct { + Result resultStruct `json:"result"` + }{} + + // minimal fields + resp.Result.Deveui = "00-11-22-33-44-55-66-77" + resp.Result.Operation = "gnss" + if positionValid { + resp.Result.PositionSolution.Llh = []float64{lat, lon, 10} + resp.Result.PositionSolution.CaptureTimeUtc = captureUTC + resp.Result.PositionSolution.Accuracy = 5 + resp.Result.PositionSolution.Gdop = 1.2 + resp.Result.PositionSolution.Timestamp = captureUTC + resp.Result.PositionSolution.AlgorithmType = "gnss" + } else { + // invalid: zero coords, no capture time + resp.Result.PositionSolution.Llh = []float64{0, 0, 0} + resp.Result.PositionSolution.CaptureTimeUtc = 0 + resp.Result.PositionSolution.Accuracy = 0 + resp.Result.PositionSolution.Gdop = 0 + resp.Result.PositionSolution.Timestamp = 0 + resp.Result.PositionSolution.AlgorithmType = "gnss" + } + + _ = json.NewEncoder(w).Encode(resp) + })) +} + +func newLogger(t *testing.T) *zap.Logger { + t.Helper() + logger, _ := zap.NewDevelopment() + return logger +} + +// Helper to run Solve with a given server and options +func runSolve(t *testing.T, srv *httptest.Server, opts solver.SolverV2Options, payload string, clientOpts ...LoracloudClientOptions) (*decoder.DecodedUplink, error) { + t.Helper() + ctx := context.Background() + logger := newLogger(t) + + // Force BaseUrl to test server + clientOpts = append(clientOpts, WithBaseUrl(srv.URL)) + + c, err := NewLoracloudClient(ctx, "Bearer test-token", logger, clientOpts...) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + return c.Solve(ctx, payload, opts) +} + +func TestSolve_Valid_NoOptionalFeatures(t *testing.T) { + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + // First byte 0x80 -> EndOfGroup=true + payload := "80" + + out, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 10, + Port: 150, + // No timestamp, no moving + }, payload) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + + // Expect GNSS feature only + if !out.Is(decoder.FeatureGNSS) { + t.Fatalf("expected FeatureGNSS to be set") + } + if out.Is(decoder.FeatureTimestamp) || out.Is(decoder.FeatureMoving) || out.Is(decoder.FeatureBuffered) { + t.Fatalf("unexpected optional features set") + } + + // Data should implement GNSS interface + gnss, ok := out.Data.(decoder.UplinkFeatureGNSS) + if !ok { + t.Fatalf("data does not implement UplinkFeatureGNSS") + } + if gnss.GetLatitude() == 0 || gnss.GetLongitude() == 0 { + t.Fatalf("unexpected zero GNSS coordinates") + } +} + +func TestSolve_WithTimestampBuffered(t *testing.T) { + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + // EndOfGroup + payload := "80" + + // 2 minutes ago, default threshold is 1 minute => buffered + ts := time.Now().Add(-2 * time.Minute) + + out, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 11, + Port: 150, + Timestamp: &ts, + }, payload) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + + if !out.Is(decoder.FeatureGNSS) || !out.Is(decoder.FeatureTimestamp) || !out.Is(decoder.FeatureBuffered) { + t.Fatalf("expected GNSS, Timestamp, and Buffered features to be set") + } + + // Interfaces + tsIF, ok := out.Data.(decoder.UplinkFeatureTimestamp) + if !ok || tsIF.GetTimestamp() == nil { + t.Fatalf("expected UplinkFeatureTimestamp to be implemented") + } + + bufIF, ok := out.Data.(decoder.UplinkFeatureBuffered) + if !ok || !bufIF.IsBuffered() { + t.Fatalf("expected UplinkFeatureBuffered to be implemented and buffered") + } +} + +func TestSolve_WithMoving(t *testing.T) { + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + payload := "80" + mv := true + + out, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 12, + Port: 150, + Moving: &mv, + }, payload) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + + if !out.Is(decoder.FeatureGNSS) || !out.Is(decoder.FeatureMoving) { + t.Fatalf("expected GNSS and Moving features to be set") + } + mvIF, ok := out.Data.(decoder.UplinkFeatureMoving) + if !ok || !mvIF.IsMoving() { + t.Fatalf("expected UplinkFeatureMoving to be implemented and true") + } +} + +func TestSolve_InvalidDevEui_Wrapped(t *testing.T) { + // server won't be reached, but provide a dummy to satisfy client creation + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + payload := "80" + _, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "001122334455667", // 15 chars, invalid + UplinkCounter: 1, + Port: 1, + }, payload) + if err == nil { + t.Fatalf("expected error for invalid DevEUI") + } + if !errors.Is(err, ErrInvalidOptions) { + t.Fatalf("expected ErrInvalidOptions, got %v", err) + } + if !errors.Is(err, ErrInvalidDevEui) { + t.Fatalf("expected ErrInvalidDevEui in wrap chain, got %v", err) + } +} + +func TestSolve_ResponseDecodeError_Wrapped(t *testing.T) { + // Server returns invalid JSON body for decode + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/device/send" { + http.NotFound(w, r) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("{not-json")) + })) + defer srv.Close() + + payload := "80" + _, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 13, + Port: 150, + }, payload) + if err == nil { + t.Fatalf("expected decode error") + } + if !errors.Is(err, ErrDecodeFailed) { + t.Fatalf("expected ErrDecodeFailed, got %v", err) + } +} + +func TestSolve_PositionInvalid_Wrapped(t *testing.T) { + // invalid position (0,0) and missing timestamp leads v1 to ErrPositionResolutionIsEmpty due to EndOfGroup + srv := mockNonNestedServer(t, false, 0, 0, 0, http.StatusOK) + defer srv.Close() + + payload := "80" + _, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 14, + Port: 150, + }, payload) + if err == nil { + t.Fatalf("expected position invalid error") + } + if !errors.Is(err, ErrPositionInvalid) { + t.Fatalf("expected ErrPositionInvalid, got %v", err) + } +} + +func TestSolve_BufferedThreshold_Configurable(t *testing.T) { + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + payload := "80" + // threshold 5 minutes + threshold := 5 * time.Minute + + // 3 minutes ago -> NOT buffered + tsRecent := time.Now().Add(-3 * time.Minute) + out, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 15, + Port: 150, + Timestamp: &tsRecent, + }, payload, WithBufferedThreshold(threshold)) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + if !out.Is(decoder.FeatureTimestamp) { + t.Fatalf("expected FeatureTimestamp") + } + if out.Is(decoder.FeatureBuffered) { + t.Fatalf("did not expect FeatureBuffered at 3m with 5m threshold") + } + + // 6 minutes ago -> buffered + tsOld := time.Now().Add(-6 * time.Minute) + out2, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 16, + Port: 150, + Timestamp: &tsOld, + }, payload, WithBufferedThreshold(threshold)) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + if !out2.Is(decoder.FeatureBuffered) { + t.Fatalf("expected FeatureBuffered at 6m with 5m threshold") + } +} + +// Ensure server receives a valid JSON body with deveui formatted and uplink fields and auto timestamp set +func TestServerReceivesValidRequest(t *testing.T) { + var receivedBody bytes.Buffer + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/device/send" { + http.NotFound(w, r) + return + } + _, _ = receivedBody.ReadFrom(r.Body) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "result": map[string]any{ + "deveui": "00-11-22-33-44-55-66-77", + "position_solution": map[string]any{ + "llh": []float64{47, 8, 10}, + "accuracy": 5.0, + "gdop": 1.2, + "capture_time_utc": float64(time.Now().UTC().Unix()), + "timestamp": float64(time.Now().UTC().Unix()), + "algorithm_type": "gnss", + }, + "operation": "gnss", + }, + }) + })) + defer srv.Close() + + payload := "80" + _, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 17, + Port: 150, + // No Timestamp provided -> client should auto-set current time in uplink.timestamp + }, payload) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + + if receivedBody.Len() == 0 { + t.Fatalf("expected server to receive a JSON body") + } + + // Validate request content structure + var req map[string]any + if err := json.Unmarshal(receivedBody.Bytes(), &req); err != nil { + t.Fatalf("failed to decode received request body: %v", err) + } + + deveuiVal, ok := req["deveui"].(string) + if !ok || len(deveuiVal) != len("00-11-22-33-44-55-66-77") { + t.Fatalf("expected hyphenated DevEUI, got: %#v", req["deveui"]) + } + + uplink, ok := req["uplink"].(map[string]any) + if !ok { + t.Fatalf("expected 'uplink' object in request") + } + ts, ok := uplink["timestamp"].(float64) + if !ok || ts == 0 { + t.Fatalf("expected non-zero uplink.timestamp to be set automatically") + } +} + +// Additional coverage tests + +// Not EndOfGroup (payload header 0x00) -> v1 does not enforce position validity. +// v2 should not return error; it should simply avoid GNSS feature if position invalid, +// while still applying optional features as provided. +func TestSolve_NotEndOfGroup_NoGNSSFeature_ButOptionalApplied(t *testing.T) { + srv := mockNonNestedServer(t, false, 0, 0, 0, http.StatusOK) + defer srv.Close() + + payload := "00" // EndOfGroup=false + mv := true + now := time.Now() + + out, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 18, + Port: 150, + Timestamp: &now, + Moving: &mv, + }, payload) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + + // GNSS should not be set due to invalid coordinates and lack of EndOfGroup enforcement + if out.Is(decoder.FeatureGNSS) { + t.Fatalf("did not expect FeatureGNSS for invalid position when EndOfGroup=false") + } + // Optional features should still be applied + if !out.Is(decoder.FeatureTimestamp) || !out.Is(decoder.FeatureMoving) { + t.Fatalf("expected FeatureTimestamp and FeatureMoving to be set") + } + + // Interfaces + if _, ok := out.Data.(decoder.UplinkFeatureTimestamp); !ok { + t.Fatalf("expected UplinkFeatureTimestamp to be implemented") + } + if mvIF, ok := out.Data.(decoder.UplinkFeatureMoving); !ok || !mvIF.IsMoving() { + t.Fatalf("expected UplinkFeatureMoving implemented and true") + } +} + +// Moving + Timestamp buffered => all optional interfaces on, plus GNSS when valid. +func TestSolve_MovingAndTimestampBuffered_BothInterfaces(t *testing.T) { + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + payload := "80" + oldTs := time.Now().Add(-10 * time.Minute) + mv := true + + out, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 19, + Port: 150, + Timestamp: &oldTs, + Moving: &mv, + }, payload) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + + if !(out.Is(decoder.FeatureGNSS) && out.Is(decoder.FeatureTimestamp) && out.Is(decoder.FeatureMoving) && out.Is(decoder.FeatureBuffered)) { + t.Fatalf("expected GNSS, Timestamp, Moving, Buffered features") + } + + if _, ok := out.Data.(decoder.UplinkFeatureBuffered); !ok { + t.Fatalf("expected UplinkFeatureBuffered to be implemented") + } +} + +// Invalid payload should be wrapped as ErrInvalidOptions +func TestSolve_InvalidPayload_Wrapped(t *testing.T) { + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + _, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 20, + Port: 150, + }, "") + if err == nil { + t.Fatalf("expected error for empty payload") + } + if !errors.Is(err, ErrInvalidOptions) { + t.Fatalf("expected ErrInvalidOptions, got %v", err) + } +} + +// Unexpected status should be wrapped as ErrUnexpectedStatus +func TestSolve_UnexpectedStatus_Wrapped(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/device/send" { + http.NotFound(w, r) + return + } + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("oops")) + })) + defer srv.Close() + + payload := "80" + _, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 21, + Port: 150, + }, payload) + if err == nil { + t.Fatalf("expected error for unexpected status") + } + if !errors.Is(err, ErrUnexpectedStatus) { + t.Fatalf("expected ErrUnexpectedStatus, got %v", err) + } +} + +// New client with Semtech base should trigger shutdown warning path; ensure construction succeeds. +func TestNewClient_SemtechShutdownWarn(t *testing.T) { + ctx := context.Background() + logger := newLogger(t) + _, err := NewLoracloudClient(ctx, "Bearer test-token", logger, WithBaseUrl(SemtechLoRaCloudBaseUrl)) + if err != nil { + t.Fatalf("expected no error constructing client with Semtech base: %v", err) + } +} + +// Request failure path: invalid base URL should cause request error and be wrapped as ErrRequestFailed. +func TestSolve_RequestFailed_Wrapped(t *testing.T) { + ctx := context.Background() + logger := newLogger(t) + c, err := NewLoracloudClient(ctx, "Bearer test-token", logger, WithBaseUrl("http://127.0.0.1:0")) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + payload := "80" + _, err = c.Solve(ctx, payload, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 22, + Port: 150, + }) + if err == nil { + t.Fatalf("expected request failed error") + } + if !errors.Is(err, ErrRequestFailed) { + t.Fatalf("expected ErrRequestFailed, got %v", err) + } +} + +// Moving provided as false should still set FeatureMoving and implement UplinkFeatureMoving=false. +func TestSolve_WithMovingFalse(t *testing.T) { + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + payload := "80" + mv := false + + out, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "0011223344556677", + UplinkCounter: 23, + Port: 150, + Moving: &mv, + }, payload) + if err != nil { + t.Fatalf("Solve returned error: %v", err) + } + + if !out.Is(decoder.FeatureMoving) { + t.Fatalf("expected FeatureMoving set") + } + if mvIF, ok := out.Data.(decoder.UplinkFeatureMoving); !ok || mvIF.IsMoving() { + t.Fatalf("expected UplinkFeatureMoving implemented and false") + } +} + +// DevEui with invalid hex characters (length 16) should error via hex decode. +func TestSolve_InvalidDevEui_NonHex(t *testing.T) { + // server dummy + srv := mockNonNestedServer(t, true, 47.0, 8.0, float64(time.Now().UTC().Unix()), http.StatusOK) + defer srv.Close() + + payload := "80" + _, err := runSolve(t, srv, solver.SolverV2Options{ + DevEui: "00112233445566ZZ", // invalid hex + UplinkCounter: 24, + Port: 150, + }, payload) + if err == nil { + t.Fatalf("expected error for invalid hex DevEUI") + } + if !errors.Is(err, ErrInvalidOptions) || !errors.Is(err, ErrInvalidDevEui) { + t.Fatalf("expected ErrInvalidOptions and ErrInvalidDevEui, got %v", err) + } +} diff --git a/pkg/solver/loracloud/v2/metrics.go b/pkg/solver/loracloud/v2/metrics.go new file mode 100644 index 0000000..82793bd --- /dev/null +++ b/pkg/solver/loracloud/v2/metrics.go @@ -0,0 +1,39 @@ +package v2 + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + loracloudV2RequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "truvami_loracloud_v2_requests_total", + Help: "Total number of LoRaCloud v2 solver requests", + }, []string{"base_url", "outcome"}) // outcome: success|error + + loracloudV2RequestDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "truvami_loracloud_v2_request_duration_seconds", + Help: "Duration of LoRaCloud v2 solver requests in seconds", + Buckets: prometheus.DefBuckets, + }, []string{"base_url"}) + + loracloudV2ResponseInvalidTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "truvami_loracloud_v2_response_invalid_total", + Help: "Total number of invalid responses from LoRaCloud v2", + }, []string{"base_url"}) + + loracloudV2PositionInvalidTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "truvami_loracloud_v2_position_invalid_total", + Help: "Total number of invalid position resolutions (missing timestamp or zero coordinates)", + }, []string{"devEui"}) + + loracloudV2BufferedDetectedTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "truvami_loracloud_v2_timestamp_buffered_detected_total", + Help: "Total number of uplinks considered buffered due to past timestamp", + }, []string{"devEui", "threshold"}) + + loracloudV2ErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "truvami_loracloud_v2_errors_total", + Help: "Total number of errors in LoRaCloud v2 solver", + }, []string{"base_url", "type"}) // type: build_request|request_failed|unexpected_status|decode_failed|response_invalid|position_invalid|invalid_options +) From 4aecda756fda2e7d872dc6c2aadee6df14917866 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:03:12 +0200 Subject: [PATCH 19/26] Add unit tests for MockSolverV1 and MockSolverV2 with data and error handling --- pkg/solver/solver_v1_test.go | 55 ++++++++++++++++++++++++++++++ pkg/solver/solver_v2_test.go | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 pkg/solver/solver_v1_test.go create mode 100644 pkg/solver/solver_v2_test.go diff --git a/pkg/solver/solver_v1_test.go b/pkg/solver/solver_v1_test.go new file mode 100644 index 0000000..7066214 --- /dev/null +++ b/pkg/solver/solver_v1_test.go @@ -0,0 +1,55 @@ +package solver + +import ( + "context" + "errors" + "testing" + + "github.com/truvami/decoder/pkg/decoder" +) + +func TestMockSolverV1_Solve_ReturnsData(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + data := decoder.NewDecodedUplink( + []decoder.Feature{decoder.FeatureBattery}, + struct { + Status string + }{Status: "ok"}, + ) + + m := MockSolverV1{Data: data} + + got, err := m.Solve(ctx, "01020304") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != data { + t.Fatalf("expected pointer to data to be returned, got: %#v", got) + } + if !got.Is(decoder.FeatureBattery) { + t.Fatalf("expected returned uplink to have battery feature") + } +} + +func TestMockSolverV1_Solve_ReturnsError(t *testing.T) { + t.Parallel() + + ctx := context.Background() + wantErr := errors.New("oops") + + m := MockSolverV1{Err: wantErr} + + got, err := m.Solve(ctx, "BADF00D") + if err == nil { + t.Fatal("expected error, got nil") + } + if err.Error() != wantErr.Error() { + t.Fatalf("unexpected error, want: %v, got: %v", wantErr, err) + } + if got != nil { + t.Fatalf("expected nil data when error is returned, got: %#v", got) + } +} diff --git a/pkg/solver/solver_v2_test.go b/pkg/solver/solver_v2_test.go new file mode 100644 index 0000000..0ae86a3 --- /dev/null +++ b/pkg/solver/solver_v2_test.go @@ -0,0 +1,65 @@ +package solver + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/truvami/decoder/pkg/decoder" +) + +func TestMockSolverV2_Solve_ReturnsData(t *testing.T) { + t.Parallel() + + ctx := context.Background() + now := time.Now() + moving := true + + data := decoder.NewDecodedUplink( + []decoder.Feature{decoder.FeatureTimestamp, decoder.FeatureMoving}, + struct { + Msg string + }{Msg: "ok"}, + ) + + m := MockSolverV2{Data: data} + + got, err := m.Solve(ctx, "AABBCC", SolverV2Options{ + DevEui: "0102030405060708", + UplinkCounter: 42, + Port: 10, + Timestamp: &now, + Moving: &moving, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != data { + t.Fatalf("expected pointer to data to be returned, got: %#v", got) + } + // sanity check that features on returned data are preserved + if !got.Is(decoder.FeatureTimestamp) || !got.Is(decoder.FeatureMoving) { + t.Fatalf("expected returned uplink to have timestamp and moving features, got: %#v", got.GetFeatures()) + } +} + +func TestMockSolverV2_Solve_ReturnsError(t *testing.T) { + t.Parallel() + + ctx := context.Background() + wantErr := errors.New("boom") + + m := MockSolverV2{Err: wantErr} + + got, err := m.Solve(ctx, "DEADBEEF", SolverV2Options{}) + if err == nil { + t.Fatal("expected error, got nil") + } + if err.Error() != wantErr.Error() { + t.Fatalf("unexpected error, want: %v, got: %v", wantErr, err) + } + if got != nil { + t.Fatalf("expected nil data when error is returned, got: %#v", got) + } +} From c4cd5ed36df4edaec16ab1bc3cd118efa9015e26 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:07:25 +0200 Subject: [PATCH 20/26] Add unit tests for Port193Payload to validate GNSS methods and interface satisfaction --- pkg/decoder/tagxl/v1/port193_test.go | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 pkg/decoder/tagxl/v1/port193_test.go diff --git a/pkg/decoder/tagxl/v1/port193_test.go b/pkg/decoder/tagxl/v1/port193_test.go new file mode 100644 index 0000000..9cb90fa --- /dev/null +++ b/pkg/decoder/tagxl/v1/port193_test.go @@ -0,0 +1,67 @@ +package tagxl + +import ( + "testing" + "time" +) + +func TestPort193Payload_GNSSMethodsAndMoving(t *testing.T) { + t.Parallel() + + p := Port193Payload{ + EndOfGroup: true, + GroupToken: 10, + NavMessage: []byte{0x01, 0x02, 0x03}, + // Moving field is intentionally false; IsMoving() should still return true. + Moving: false, + Latitude: 47.3769, + Longitude: 8.5417, + Altitude: 500.0, + } + + if got := p.GetLatitude(); got != p.Latitude { + t.Fatalf("GetLatitude() = %v, want %v", got, p.Latitude) + } + if got := p.GetLongitude(); got != p.Longitude { + t.Fatalf("GetLongitude() = %v, want %v", got, p.Longitude) + } + if got := p.GetAltitude(); got != p.Altitude { + t.Fatalf("GetAltitude() = %v, want %v", got, p.Altitude) + } + if got := p.GetAccuracy(); got != nil { + t.Fatalf("GetAccuracy() = %v, want nil", got) + } + if got := p.GetTTF(); got != nil { + t.Fatalf("GetTTF() = %v, want nil", got) + } + if got := p.GetPDOP(); got != nil { + t.Fatalf("GetPDOP() = %v, want nil", got) + } + if got := p.GetSatellites(); got != nil { + t.Fatalf("GetSatellites() = %v, want nil", got) + } + if got := p.IsMoving(); got != true { + t.Fatalf("IsMoving() = %v, want true", got) + } +} + +// Compile-time like assertion helpers (executed to ensure method signatures remain compatible). +func TestPort193Payload_InterfaceSatisfaction(t *testing.T) { + t.Parallel() + + var _ = func() any { + var _ interface { + GetLatitude() float64 + GetLongitude() float64 + GetAltitude() float64 + GetAccuracy() *float64 + GetTTF() *time.Duration + GetPDOP() *float64 + GetSatellites() *uint8 + } = Port193Payload{} + var _ interface { + IsMoving() bool + } = Port193Payload{} + return nil + }() +} From 5545e1b49d74faf5b56c7884564638d03b83d1bd Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:08:59 +0200 Subject: [PATCH 21/26] Update generated_at timestamp in .secrets.baseline --- .secrets.baseline | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.secrets.baseline b/.secrets.baseline index 254f5f6..8f33283 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -2114,5 +2114,5 @@ } ] }, - "generated_at": "2025-09-05T14:43:08Z" + "generated_at": "2025-09-16T13:08:37Z" } From 8e73406db394241d26c6e0f019133ae3a46c54a2 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:40:12 +0200 Subject: [PATCH 22/26] Add v2 solver support for GNSS NAV ports and implement related tests --- pkg/decoder/tagxl/v1/decoder.go | 92 ++++++++++++- pkg/decoder/tagxl/v1/gnss_v2_test.go | 195 +++++++++++++++++++++++++++ 2 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 pkg/decoder/tagxl/v1/gnss_v2_test.go diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index d43a737..2ed87bc 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -18,8 +18,13 @@ type TagXLv1Decoder struct { skipValidation bool logger *zap.Logger + // Legacy v1 solver for backward compatibility (kept for existing tests and ports) solver solver.SolverV1 fallbackSolver solver.SolverV1 + + // Preferred v2 solver (used for GNSS NAV grouping ports 192/193/194/195/199 when available) + v2Solver solver.SolverV2 + fallbackV2Solver solver.SolverV2 } func NewTagXLv1Decoder(ctx context.Context, solver solver.SolverV1, logger *zap.Logger, options ...Option) decoder.Decoder { @@ -52,6 +57,20 @@ func WithFallbackSolver(fallbackSolver solver.SolverV1) Option { } } +// WithSolverV2 sets the v2 solver which accepts explicit options (DevEUI, counter, port, optional timestamp/moving). +func WithSolverV2(v2 solver.SolverV2) Option { + return func(t *TagXLv1Decoder) { + t.v2Solver = v2 + } +} + +// WithFallbackSolverV2 sets the fallback v2 solver used when the primary v2 solver fails. +func WithFallbackSolverV2(fallback solver.SolverV2) Option { + return func(t *TagXLv1Decoder) { + t.fallbackV2Solver = fallback + } +} + // https://docs.truvami.com/docs/payloads/tag-xl func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadConfig, error) { switch port { @@ -344,7 +363,76 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon func (t TagXLv1Decoder) Decode(ctx context.Context, data string, port uint8) (*decoder.DecodedUplink, error) { switch port { - case 192, 193, 199: + // GNSS NAV grouping ports now use the v2 solver when available. + case 192, 193, 194, 195, 199: + if t.v2Solver != nil { + devEui, _ := ctx.Value(decoder.DEVEUI_CONTEXT_KEY).(string) + fcnt, _ := ctx.Value(decoder.FCNT_CONTEXT_KEY).(int) + var movingPtr *bool + switch port { + case 192, 194: + mv := false + movingPtr = &mv + case 193, 195: + mv := true + movingPtr = &mv + default: + // leave nil unless explicitly known + movingPtr = nil + } + + var tsPtr *time.Time + payloadForSolve := data + + // For timestamped GNSS ports (194, 195), strip the leading 4-byte timestamp (big-endian) + if port == 194 || port == 195 { + bytes, err := common.HexStringToBytes(data) + if err != nil { + return nil, err + } + if len(bytes) < 5 { + return nil, common.ErrPayloadTooShort + } + secs := common.BytesToUint32(bytes[0:4]) + ts := time.Unix(int64(secs), 0).UTC() + tsPtr = &ts + + // Remove first 4 bytes (8 hex chars) from payload passed to solver + if len(data) < 8 { + return nil, common.ErrPayloadTooShort + } + payloadForSolve = data[8:] + } + + opts := solver.SolverV2Options{ + DevEui: devEui, + UplinkCounter: uint16(fcnt), + Port: port, + Timestamp: tsPtr, + Moving: movingPtr, + } + + uplink, err := t.v2Solver.Solve(ctx, payloadForSolve, opts) + if err != nil { + if t.fallbackV2Solver == nil { + tagXlDecoderSolverFailedCounter.Inc() + return nil, common.WrapError(err, common.ErrSolverFailed) + } + uplink, err = t.fallbackV2Solver.Solve(ctx, payloadForSolve, opts) + if err != nil { + tagXlDecoderSolverFailedCounter.Inc() + return nil, common.WrapError(err, common.ErrSolverFailed) + } + tagXlDecoderSuccessfullyUsedFallbackSolverCounter.Inc() + } + return uplink, nil + } + + // Fallback to legacy v1 solver when v2 is not provided (keeps backward compatibility). + // Note: legacy path does not support 194/195 since v1 solver expects header as first byte. + if port == 194 || port == 195 { + return nil, fmt.Errorf("%w: port %v not supported without v2 solver", common.ErrPortNotSupported, port) + } uplink, err := t.solver.Solve(ctx, data) if err != nil { if t.fallbackSolver == nil { @@ -359,8 +447,8 @@ func (t TagXLv1Decoder) Decode(ctx context.Context, data string, port uint8) (*d } tagXlDecoderSuccessfullyUsedFallbackSolverCounter.Inc() } - return uplink, nil + default: bytes, err := common.HexStringToBytes(data) if err != nil { diff --git a/pkg/decoder/tagxl/v1/gnss_v2_test.go b/pkg/decoder/tagxl/v1/gnss_v2_test.go new file mode 100644 index 0000000..5b934d0 --- /dev/null +++ b/pkg/decoder/tagxl/v1/gnss_v2_test.go @@ -0,0 +1,195 @@ +package tagxl + +import ( + "context" + "encoding/hex" + "testing" + "time" + + "github.com/truvami/decoder/internal/logger" + "github.com/truvami/decoder/pkg/decoder" + "github.com/truvami/decoder/pkg/solver" + "go.uber.org/zap" +) + +// fakeGNSSData implements minimal GNSS (and optionally Timestamp) interfaces for testing. +type fakeGNSSData struct { + lat, lon, alt float64 + ts *time.Time +} + +var _ decoder.UplinkFeatureGNSS = &fakeGNSSData{} +var _ decoder.UplinkFeatureTimestamp = &fakeGNSSData{} + +func (f *fakeGNSSData) GetLatitude() float64 { return f.lat } +func (f *fakeGNSSData) GetLongitude() float64 { return f.lon } +func (f *fakeGNSSData) GetAltitude() float64 { return f.alt } +func (f *fakeGNSSData) GetAccuracy() *float64 { return nil } +func (f *fakeGNSSData) GetTTF() *time.Duration { return nil } +func (f *fakeGNSSData) GetPDOP() *float64 { return nil } +func (f *fakeGNSSData) GetSatellites() *uint8 { return nil } +func (f *fakeGNSSData) GetTimestamp() *time.Time { return f.ts } + +// captureSolverV2 captures the last payload and options passed to Solve. +type captureSolverV2 struct { + lastPayload string + lastOptions solver.SolverV2Options + resp *decoder.DecodedUplink + err error +} + +func (c *captureSolverV2) Solve(ctx context.Context, payload string, options solver.SolverV2Options) (*decoder.DecodedUplink, error) { + c.lastPayload = payload + c.lastOptions = options + return c.resp, c.err +} + +func newLogger() *zap.Logger { + if logger.Logger == nil { + logger.NewLogger() + } + return logger.Logger +} + +func TestGNSS_SolverV2_192_193_NoTimestamp(t *testing.T) { + log := newLogger() + + // header 0x80 => EndOfGroup set; payload not otherwise relevant for capture + payload := "80abcd" + devEui := "0011223344556677" + fcnt := 123 + + // Prepare a response with GNSS (no timestamp) + resp := decoder.NewDecodedUplink([]decoder.Feature{decoder.FeatureGNSS}, &fakeGNSSData{ + lat: 47.0, lon: 8.0, alt: 10.0, ts: nil, + }) + + cap := &captureSolverV2{resp: resp} + + dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, log, + WithSolverV2(cap), + ) + + ctx := context.WithValue(context.Background(), decoder.DEVEUI_CONTEXT_KEY, devEui) + ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, fcnt) + + // Port 192 -> steady (moving=false), no timestamp stripping + out, err := dec.Decode(ctx, payload, 192) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cap.lastPayload != payload { + t.Fatalf("expected payload forwarded unchanged for port 192, got %q", cap.lastPayload) + } + if cap.lastOptions.DevEui != devEui || cap.lastOptions.UplinkCounter != uint16(fcnt) || cap.lastOptions.Port != 192 { + t.Fatalf("unexpected options for port 192: %+v", cap.lastOptions) + } + if cap.lastOptions.Moving == nil || *cap.lastOptions.Moving != false { + t.Fatalf("expected Moving=false for port 192, got %+v", cap.lastOptions.Moving) + } + if cap.lastOptions.Timestamp != nil { + t.Fatalf("expected no timestamp for port 192") + } + if !out.Is(decoder.FeatureGNSS) { + t.Fatalf("expected FeatureGNSS in result") + } + + // Port 193 -> moving (moving=true), no timestamp stripping + out, err = dec.Decode(ctx, payload, 193) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cap.lastPayload != payload { + t.Fatalf("expected payload forwarded unchanged for port 193, got %q", cap.lastPayload) + } + if cap.lastOptions.Port != 193 { + t.Fatalf("expected port 193, got %d", cap.lastOptions.Port) + } + if cap.lastOptions.Moving == nil || *cap.lastOptions.Moving != true { + t.Fatalf("expected Moving=true for port 193, got %+v", cap.lastOptions.Moving) + } + if cap.lastOptions.Timestamp != nil { + t.Fatalf("expected no timestamp for port 193") + } + if !out.Is(decoder.FeatureGNSS) { + t.Fatalf("expected FeatureGNSS in result") + } +} + +func TestGNSS_SolverV2_194_195_TimestampStrippedAndPassed(t *testing.T) { + log := newLogger() + + // Build a payload with 4B timestamp prefix + header 0x80 after + secs := uint32(1750000000) // some fixed time + ts := time.Unix(int64(secs), 0).UTC() + tsBytes := make([]byte, 4) + // big-endian + tsBytes[0] = byte((secs >> 24) & 0xff) + tsBytes[1] = byte((secs >> 16) & 0xff) + tsBytes[2] = byte((secs >> 8) & 0xff) + tsBytes[3] = byte(secs & 0xff) + + tsHex := hex.EncodeToString(tsBytes) + headerHex := "80" + payloadWithTS := tsHex + headerHex + "abcd" // ts + GHDR + rest + + devEui := "0011223344556677" + fcnt := 321 + + // Response includes GNSS + Timestamp feature + resp := decoder.NewDecodedUplink([]decoder.Feature{decoder.FeatureGNSS, decoder.FeatureTimestamp}, &fakeGNSSData{ + lat: 47.1, lon: 8.1, alt: 12.0, ts: &ts, + }) + + cap := &captureSolverV2{resp: resp} + + dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, log, + WithSolverV2(cap), + ) + + ctx := context.WithValue(context.Background(), decoder.DEVEUI_CONTEXT_KEY, devEui) + ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, fcnt) + + // Port 194 -> steady, timestamp present and should be stripped before solve + out, err := dec.Decode(ctx, payloadWithTS, 194) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedForwarded := headerHex + "abcd" + if cap.lastPayload != expectedForwarded { + t.Fatalf("expected forwarded payload %q, got %q", expectedForwarded, cap.lastPayload) + } + if cap.lastOptions.Port != 194 { + t.Fatalf("expected port 194, got %d", cap.lastOptions.Port) + } + if cap.lastOptions.Moving == nil || *cap.lastOptions.Moving != false { + t.Fatalf("expected Moving=false for port 194, got %+v", cap.lastOptions.Moving) + } + if cap.lastOptions.Timestamp == nil || !cap.lastOptions.Timestamp.Equal(ts) { + t.Fatalf("expected Timestamp=%v for port 194, got %+v", ts, cap.lastOptions.Timestamp) + } + if !out.Is(decoder.FeatureGNSS) || !out.Is(decoder.FeatureTimestamp) { + t.Fatalf("expected GNSS and Timestamp features in result") + } + + // Port 195 -> moving, timestamp present and should be stripped before solve + out, err = dec.Decode(ctx, payloadWithTS, 195) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cap.lastPayload != expectedForwarded { + t.Fatalf("expected forwarded payload %q, got %q", expectedForwarded, cap.lastPayload) + } + if cap.lastOptions.Port != 195 { + t.Fatalf("expected port 195, got %d", cap.lastOptions.Port) + } + if cap.lastOptions.Moving == nil || *cap.lastOptions.Moving != true { + t.Fatalf("expected Moving=true for port 195, got %+v", cap.lastOptions.Moving) + } + if cap.lastOptions.Timestamp == nil || !cap.lastOptions.Timestamp.Equal(ts) { + t.Fatalf("expected Timestamp=%v for port 195, got %+v", ts, cap.lastOptions.Timestamp) + } + if !out.Is(decoder.FeatureGNSS) || !out.Is(decoder.FeatureTimestamp) { + t.Fatalf("expected GNSS and Timestamp features in result") + } +} From 473e92fe32a75cac05a2dea85563490734278f69 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:58:41 +0200 Subject: [PATCH 23/26] Refactor GNSS port tests to utilize SolverV2 for improved accuracy and consistency --- .secrets.baseline | 58 +++++++++++------------ pkg/decoder/tagxl/v1/decoder_test.go | 69 ++++++++++++++++++---------- 2 files changed, 74 insertions(+), 53 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 8f33283..9f30aa2 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1437,7 +1437,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b0771e36dfb55414a423ca9c0ceb087b03ea3cfc", "is_verified": false, - "line_number": 347, + "line_number": 348, "is_secret": false }, { @@ -1445,7 +1445,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5cbb5bf20d8b56d849c06d5e0474a3cd42e6bc16", "is_verified": false, - "line_number": 351, + "line_number": 353, "is_secret": false }, { @@ -1453,7 +1453,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "632ee27a7e117b634cb0fb234f7f7d199db5d5d1", "is_verified": false, - "line_number": 355, + "line_number": 358, "is_secret": false }, { @@ -1461,7 +1461,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "9e1335b0e4c10057a072e6aa67e5cfb9d0e5d324", "is_verified": false, - "line_number": 359, + "line_number": 363, "is_secret": false }, { @@ -1469,7 +1469,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", "is_verified": false, - "line_number": 379, + "line_number": 384, "is_secret": false }, { @@ -1477,7 +1477,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 390, + "line_number": 395, "is_secret": false }, { @@ -1485,7 +1485,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 402, + "line_number": 407, "is_secret": false }, { @@ -1493,7 +1493,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 415, + "line_number": 420, "is_secret": false }, { @@ -1501,7 +1501,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 429, + "line_number": 434, "is_secret": false }, { @@ -1509,7 +1509,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 439, + "line_number": 444, "is_secret": false }, { @@ -1517,7 +1517,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 451, + "line_number": 456, "is_secret": false }, { @@ -1525,7 +1525,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 465, + "line_number": 470, "is_secret": false }, { @@ -1533,7 +1533,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 481, + "line_number": 486, "is_secret": false }, { @@ -1541,7 +1541,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", "is_verified": false, - "line_number": 499, + "line_number": 504, "is_secret": false }, { @@ -1549,7 +1549,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", "is_verified": false, - "line_number": 653, + "line_number": 658, "is_secret": false }, { @@ -1557,7 +1557,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", "is_verified": false, - "line_number": 659, + "line_number": 664, "is_secret": false }, { @@ -1565,7 +1565,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", "is_verified": false, - "line_number": 670, + "line_number": 675, "is_secret": false }, { @@ -1573,7 +1573,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", "is_verified": false, - "line_number": 682, + "line_number": 687, "is_secret": false }, { @@ -1581,7 +1581,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", "is_verified": false, - "line_number": 695, + "line_number": 700, "is_secret": false }, { @@ -1589,7 +1589,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", "is_verified": false, - "line_number": 709, + "line_number": 714, "is_secret": false }, { @@ -1597,7 +1597,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", "is_verified": false, - "line_number": 724, + "line_number": 729, "is_secret": false }, { @@ -1605,7 +1605,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", "is_verified": false, - "line_number": 735, + "line_number": 740, "is_secret": false }, { @@ -1613,7 +1613,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", "is_verified": false, - "line_number": 748, + "line_number": 753, "is_secret": false }, { @@ -1621,7 +1621,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", "is_verified": false, - "line_number": 763, + "line_number": 768, "is_secret": false }, { @@ -1629,7 +1629,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", "is_verified": false, - "line_number": 780, + "line_number": 785, "is_secret": false }, { @@ -1637,7 +1637,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", "is_verified": false, - "line_number": 805, + "line_number": 810, "is_secret": false }, { @@ -1645,7 +1645,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1144, + "line_number": 1165, "is_secret": false }, { @@ -1653,7 +1653,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1172, + "line_number": 1193, "is_secret": false } ], @@ -2114,5 +2114,5 @@ } ] }, - "generated_at": "2025-09-16T13:08:37Z" + "generated_at": "2025-09-16T14:58:34Z" } diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index d784a73..f41051f 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -338,26 +338,31 @@ func TestDecode(t *testing.T) { expected: &exampleResponse, expectedErr: "", }, - // { - // port: 194, - // payload: "68b9b2318f2b157de4733aa4d27b5d3b3c6ecc9460a20a196b754655c98607", - // }, - // { - // port: 194, - // payload: "68bad32509ab91418ae63a10b5004a0a3fef037ab2f06ce8e510820c1a0bdcecb49e1543fdd2f28f1c", - // }, - // { - // port: 194, - // payload: "68bad32589b379e7ba0fb5006b9aaa8c8e25febf16f4e5c31d0cc8ca12a1cffdddf16c2cf82877f1edee4ecbc5ef54", - // }, - // { - // port: 195, - // payload: "68bad3c50aabd56cb2e7ba0db5805a5ac9d4edd8de8a021b4ae2b78e8c0b8391566ab8d47d1d4c55ae794a2c2da7a637b49d32e44800", - // }, - // { - // port: 195, - // payload: "68bad3c58aab4581b9e73a0eb580da120d7f85a75e770c6acad3dc2acdacbdcd576ab8147f5902557379b18d0f676a35fb9a6ae5ee03", - // }, + { + port: 194, + payload: "68b9b2318f2b157de4733aa4d27b5d3b3c6ecc9460a20a196b754655c98607", + expected: &exampleResponse, + }, + { + port: 194, + payload: "68bad32509ab91418ae63a10b5004a0a3fef037ab2f06ce8e510820c1a0bdcecb49e1543fdd2f28f1c", + expected: &exampleResponse, + }, + { + port: 194, + payload: "68bad32589b379e7ba0fb5006b9aaa8c8e25febf16f4e5c31d0cc8ca12a1cffdddf16c2cf82877f1edee4ecbc5ef54", + expected: &exampleResponse, + }, + { + port: 195, + payload: "68bad3c50aabd56cb2e7ba0db5805a5ac9d4edd8de8a021b4ae2b78e8c0b8391566ab8d47d1d4c55ae794a2c2da7a637b49d32e44800", + expected: &exampleResponse, + }, + { + port: 195, + payload: "68bad3c58aab4581b9e73a0eb580da120d7f85a75e770c6acad3dc2acdacbdcd576ab8147f5902557379b18d0f676a35fb9a6ae5ee03", + expected: &exampleResponse, + }, { port: 197, payload: "ff", @@ -969,8 +974,24 @@ func TestDecode(t *testing.T) { ctx := context.WithValue(context.Background(), decoder.DEVEUI_CONTEXT_KEY, test.devEui) ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 1) - decoder := NewTagXLv1Decoder(ctx, solver.MockSolverV1{}, logger.Logger) - got, err := decoder.Decode(ctx, test.payload, test.port) + // Use SolverV2 for GNSS ports (192/193/194/195/199) so timestamped ports work without error and provide expected data. + expectedAny := test.expected + opts := []Option{} + switch test.port { + case 194, 195: + // For timestamped GNSS ports, use SolverV2 and return the same structure as port 192 expectation + // so that tests compare against exampleResponse. + v2Data := &exampleResponse + features := []decoder.Feature{decoder.FeatureGNSS} + if expectedAny == nil { + expectedAny = v2Data + } + opts = append(opts, WithSolverV2(solver.MockSolverV2{ + Data: decoder.NewDecodedUplink(features, v2Data), + })) + } + dec := NewTagXLv1Decoder(ctx, solver.MockSolverV1{}, logger.Logger, opts...) + got, err := dec.Decode(ctx, test.payload, test.port) if err == nil && len(test.expectedErr) != 0 { t.Fatalf("expected error: %v, got %v", test.expectedErr, nil) @@ -982,9 +1003,9 @@ func TestDecode(t *testing.T) { t.Logf("got %v", got) - if got != nil && !reflect.DeepEqual(got.Data, test.expected) && len(test.expectedErr) == 0 { + if got != nil && !reflect.DeepEqual(got.Data, expectedAny) && len(test.expectedErr) == 0 { // marshal the expected and got values to compare - expectedJSON, err := json.Marshal(test.expected) + expectedJSON, err := json.Marshal(expectedAny) if err != nil { t.Fatalf("failed to marshal expected value: %v", err) } From 40ae22578a85a22c5bb0567608a6d0051359fec0 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:30:12 +0200 Subject: [PATCH 24/26] Enhance health check in TestHTTPCmd for improved server readiness validation --- .secrets.baseline | 6 +++--- cmd/http_test.go | 23 ++++++++++++++++++++++- pkg/encoder/smartlabel/v1/encoder.go | 7 ++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 9f30aa2..4ad187b 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -151,7 +151,7 @@ "filename": "cmd/http_test.go", "hashed_secret": "44bf1f7c931a410503db9759de7d3758c84c6e0f", "is_verified": false, - "line_number": 165, + "line_number": 186, "is_secret": false }, { @@ -159,7 +159,7 @@ "filename": "cmd/http_test.go", "hashed_secret": "116acfdb39846d90401e995a76003aba8b664352", "is_verified": false, - "line_number": 208, + "line_number": 229, "is_secret": false } ], @@ -2114,5 +2114,5 @@ } ] }, - "generated_at": "2025-09-16T14:58:34Z" + "generated_at": "2025-09-16T15:30:08Z" } diff --git a/cmd/http_test.go b/cmd/http_test.go index fd65a8b..e277d1d 100644 --- a/cmd/http_test.go +++ b/cmd/http_test.go @@ -155,15 +155,36 @@ func TestHTTPCmd(t *testing.T) { if httpCmd.Flags().Set("host", "127.0.0.1") != nil { t.Fatalf("failed to set host flag") } + // Enable health endpoint so we can wait for readiness + if httpCmd.Flags().Set("health", "true") != nil { + t.Fatalf("failed to set health flag") + } go func() { // call the command handler function httpCmd.Run(nil, []string{}) }() + // Wait for server readiness using health endpoint + ready := false + for i := 0; i < 100; i++ { // up to ~2s with 20ms sleep + resp, err := http.Get("http://127.0.0.1:38888/health") + if err == nil { + _ = resp.Body.Close() + if resp.StatusCode == http.StatusOK { + ready = true + break + } + } + time.Sleep(20 * time.Millisecond) + } + if !ready { + t.Fatalf("server not ready on /health") + } + // create a new HTTP request to simulate the command execution reqBody := `{"port": 105, "payload": "0028672658500172a741b1e238b572a741b1e08bb03498b5c583e2b172a741b1e0cda772a741beed4cc472a741beef53b7"}` - req, err := http.NewRequest("POST", "http://localhost:38888/tagsl/v1", strings.NewReader(reqBody)) + req, err := http.NewRequest("POST", "http://127.0.0.1:38888/tagsl/v1", strings.NewReader(reqBody)) if err != nil { t.Fatalf("failed to create request: %v", err) } diff --git a/pkg/encoder/smartlabel/v1/encoder.go b/pkg/encoder/smartlabel/v1/encoder.go index 108c5bf..e7ea45e 100644 --- a/pkg/encoder/smartlabel/v1/encoder.go +++ b/pkg/encoder/smartlabel/v1/encoder.go @@ -2,6 +2,7 @@ package smartlabel import ( "fmt" + "math" "reflect" "github.com/truvami/decoder/pkg/common" @@ -88,7 +89,11 @@ func photovoltaic(v any) any { } func temperature(v any) any { - return common.UintToBytes(uint64(common.BytesToFloat32(v.([]byte))*100), 2) + // Encode temperature as signed int16 with scale 100 (two's complement, big-endian). + // This matches expected payloads like -4.98°C -> 0xFE0E. + f := float64(common.BytesToFloat32(v.([]byte))) + scaled := int64(math.Round(f * 100.0)) + return common.IntToBytes(scaled, 2) } func humidity(v any) any { From efdb0650c635190202074522d0f9835e11ece724 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:42:01 +0200 Subject: [PATCH 25/26] Add test cases for TagXL decoder including Wi-Fi and GNSS messages --- .secrets.baseline | 116 +++++++++- pkg/decoder/tagxl/v1/examples_test.go | 306 ++++++++++++++++++++++++++ 2 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 pkg/decoder/tagxl/v1/examples_test.go diff --git a/.secrets.baseline b/.secrets.baseline index 4ad187b..222c000 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1657,6 +1657,120 @@ "is_secret": false } ], + "pkg/decoder/tagxl/v1/examples_test.go": [ + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", + "is_verified": false, + "line_number": 35, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "d9e77b9592259eb596b50df9eb7ed9d2c643086d", + "is_verified": false, + "line_number": 62, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "15bdecc414877451c0ac5fb7010c119ad05c1341", + "is_verified": false, + "line_number": 63, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", + "is_verified": false, + "line_number": 80, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "cd170f20234872e6966946a9ab0f50d3ef2c9254", + "is_verified": false, + "line_number": 118, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "2dd04c4f36f29f0c2107a88d67667be9bad5da0b", + "is_verified": false, + "line_number": 137, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "8f53cc66ebbfcdcdfadd3f03f078a73e88374003", + "is_verified": false, + "line_number": 163, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "c22f4cf8facafe9e1afdf75b5c25f73906b62a68", + "is_verified": false, + "line_number": 164, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "e7adef3ffe2f3a235acd957a47760cdfa8a5a06b", + "is_verified": false, + "line_number": 190, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "8ba8ec4932c771a68cc50922f8e584d184dd68e5", + "is_verified": false, + "line_number": 191, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "b0771e36dfb55414a423ca9c0ceb087b03ea3cfc", + "is_verified": false, + "line_number": 218, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "5cbb5bf20d8b56d849c06d5e0474a3cd42e6bc16", + "is_verified": false, + "line_number": 219, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "632ee27a7e117b634cb0fb234f7f7d199db5d5d1", + "is_verified": false, + "line_number": 270, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "pkg/decoder/tagxl/v1/examples_test.go", + "hashed_secret": "9e1335b0e4c10057a072e6aa67e5cfb9d0e5d324", + "is_verified": false, + "line_number": 271, + "is_secret": false + } + ], "pkg/decoder/tagxl/v1/input.json": [ { "type": "Hex High Entropy String", @@ -2114,5 +2228,5 @@ } ] }, - "generated_at": "2025-09-16T15:30:08Z" + "generated_at": "2025-09-16T15:41:56Z" } diff --git a/pkg/decoder/tagxl/v1/examples_test.go b/pkg/decoder/tagxl/v1/examples_test.go new file mode 100644 index 0000000..cc0e862 --- /dev/null +++ b/pkg/decoder/tagxl/v1/examples_test.go @@ -0,0 +1,306 @@ +package tagxl + +import ( + "context" + "testing" + "time" + + "github.com/truvami/decoder/internal/logger" + "github.com/truvami/decoder/pkg/decoder" + "github.com/truvami/decoder/pkg/solver" +) + +// dummyGNSS implements UplinkFeatureGNSS for mocking solver responses +type dummyGNSS struct{} + +var _ decoder.UplinkFeatureGNSS = dummyGNSS{} + +func (d dummyGNSS) GetLatitude() float64 { return 51.49278 } +func (d dummyGNSS) GetLongitude() float64 { return 0.0212 } +func (d dummyGNSS) GetAltitude() float64 { return 83.93 } +func (d dummyGNSS) GetAccuracy() *float64 { return nil } +func (d dummyGNSS) GetTTF() *time.Duration { return nil } +func (d dummyGNSS) GetPDOP() *float64 { return nil } +func (d dummyGNSS) GetSatellites() *uint8 { return nil } + +// Test cases derived from user-provided device logs + +// moving Wi-Fi message with timestamp (port 201, v2 RSSI+MAC with 4B timestamp) +func TestTagXL_WiFi_Port201_Timestamped(t *testing.T) { + if logger.Logger == nil { + logger.NewLogger() + } + dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, logger.Logger) + // 0x68BAE3AB timestamp (1757078443) + v2 + [RSSI,MAC] tuples + payload := "68bae3ab01d3f0b0140c96bbc7e4c32a622ea4c5e0286d8a9478b4e0286d8aabfcada86e84e1a812" + + out, err := dec.Decode(context.TODO(), payload, 201) + if err != nil { + t.Fatalf("decode error: %v", err) + } + // Features + if !out.Is(decoder.FeatureWiFi) || !out.Is(decoder.FeatureTimestamp) || !out.Is(decoder.FeatureMoving) { + t.Fatalf("expected WiFi + Timestamp + Moving features") + } + + ts := out.Data.(decoder.UplinkFeatureTimestamp).GetTimestamp() + if ts == nil || ts.Unix() != 1757078443 { + t.Fatalf("expected timestamp 1757078443, got %v", ts) + } + wifi := out.Data.(decoder.UplinkFeatureWiFi) + aps := wifi.GetAccessPoints() + if len(aps) != 5 { + t.Fatalf("expected 5 APs, got %d", len(aps)) + } + // Order and values based on the log + exp := []struct { + mac string + rssi int8 + }{ + {"f0b0140c96bb", -45}, + {"e4c32a622ea4", -57}, + {"e0286d8a9478", -59}, + {"e0286d8aabfc", -76}, + {"a86e84e1a812", -83}, + } + for i, ap := range aps { + if ap.MAC != exp[i].mac || (ap.RSSI == nil || *ap.RSSI != exp[i].rssi) { + t.Fatalf("ap[%d] expected MAC=%s RSSI=%d, got MAC=%s RSSI=%v", i, exp[i].mac, exp[i].rssi, ap.MAC, ap.RSSI) + } + } +} + +// Wi-Fi message without timestamp (port 197, v2 RSSI+MAC) +func TestTagXL_WiFi_Port197_NoTimestamp(t *testing.T) { + if logger.Logger == nil { + logger.NewLogger() + } + dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, logger.Logger) + // 0x01CFF0B0140C96BB... (version=01, five RSSI+MAC tuples) + payload := "01cff0b0140c96bbcce4c32a622ea4c8e0286d8a9478b8e0286d8aabfcafa86e84e1a812" + + out, err := dec.Decode(context.TODO(), payload, 197) + if err != nil { + t.Fatalf("decode error: %v", err) + } + if !out.Is(decoder.FeatureWiFi) || !out.Is(decoder.FeatureMoving) { + t.Fatalf("expected WiFi + Moving features") + } + wifi := out.Data.(decoder.UplinkFeatureWiFi) + aps := wifi.GetAccessPoints() + if len(aps) != 5 { + t.Fatalf("expected 5 APs, got %d", len(aps)) + } + exp := []struct { + mac string + rssi int8 + }{ + {"f0b0140c96bb", -49}, + {"e4c32a622ea4", -52}, + {"e0286d8a9478", -56}, + {"e0286d8aabfc", -72}, + {"a86e84e1a812", -81}, + } + for i, ap := range aps { + if ap.MAC != exp[i].mac || (ap.RSSI == nil || *ap.RSSI != exp[i].rssi) { + t.Fatalf("ap[%d] expected MAC=%s RSSI=%d, got MAC=%s RSSI=%v", i, exp[i].mac, exp[i].rssi, ap.MAC, ap.RSSI) + } + } +} + +// moving Wi-Fi message without timestamp (port 198, v2 RSSI+MAC) +func TestTagXL_WiFi_Port198_NoTimestamp(t *testing.T) { + if logger.Logger == nil { + logger.NewLogger() + } + dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, logger.Logger) + // 0x01CDE4C32A622EA4AD726C9A74B58DAEA86E84E1A812B9E0286D8AABFCC4E0286D8A9478 + payload := "01cde4c32a622ea4ad726c9a74b58daea86e84e1a812b9e0286d8aabfcc4e0286d8a9478" + + out, err := dec.Decode(context.TODO(), payload, 198) + if err != nil { + t.Fatalf("decode error: %v", err) + } + if !out.Is(decoder.FeatureWiFi) || !out.Is(decoder.FeatureMoving) { + t.Fatalf("expected WiFi + Moving features") + } + wifi := out.Data.(decoder.UplinkFeatureWiFi) + aps := wifi.GetAccessPoints() + if len(aps) != 5 { + t.Fatalf("expected 5 APs, got %d", len(aps)) + } + exp := []struct { + mac string + rssi int8 + }{ + {"e4c32a622ea4", -51}, + {"726c9a74b58d", -83}, + {"a86e84e1a812", -82}, + {"e0286d8aabfc", -71}, + {"e0286d8a9478", -60}, + } + for i, ap := range aps { + if ap.MAC != exp[i].mac || (ap.RSSI == nil || *ap.RSSI != exp[i].rssi) { + t.Fatalf("ap[%d] expected MAC=%s RSSI=%d, got MAC=%s RSSI=%v", i, exp[i].mac, exp[i].rssi, ap.MAC, ap.RSSI) + } + } +} + +// GNSS messages (2) without timestamp and not moving (port 192, no timestamp variant) +// We use a mock solver V1 to avoid external calls and validate GNSS feature presence. +func TestTagXL_GNSS_Port192_NoTimestamp(t *testing.T) { + if logger.Logger == nil { + logger.NewLogger() + } + ctx := context.Background() + // Mock solver returns GNSS feature regardless of payload + mock := solver.MockSolverV1{ + Data: decoder.NewDecodedUplink([]decoder.Feature{decoder.FeatureGNSS}, dummyGNSS{}), + } + dec := NewTagXLv1Decoder(ctx, mock, logger.Logger) + + payloads := []string{ + "12b319f3ba0cd9805a773bf029a2f97f9077754db825d87d456527144c8493c57b350f", + "92b31df3ba0cd9004a751ba293dd7ab8ea26e4b490ad130aa6ecc9e0ba4a00", + } + for _, pl := range payloads { + out, err := dec.Decode(ctx, pl, 192) + if err != nil { + t.Fatalf("decode error for payload %s: %v", pl, err) + } + if !out.Is(decoder.FeatureGNSS) { + t.Fatalf("expected GNSS feature for payload %s", pl) + } + } +} + +// moving GNSS messages (2) without timestamp (port 193, moving variant) +// Use mock solver V1 and validate GNSS feature presence. +func TestTagXL_GNSS_Port193_Moving_NoTimestamp(t *testing.T) { + if logger.Logger == nil { + logger.NewLogger() + } + ctx := context.Background() + mock := solver.MockSolverV1{ + Data: decoder.NewDecodedUplink([]decoder.Feature{decoder.FeatureGNSS}, dummyGNSS{}), + } + dec := NewTagXLv1Decoder(ctx, mock, logger.Logger) + + payloads := []string{ + "13ab251151f3ba0ed580391609b73f4e2a22e0128d15242653798a4e6056cc1d0d", + "93ab35c650f33a0cd5004a161989f27af55aec906ed9e366120a484a2600189d00", + } + for _, pl := range payloads { + out, err := dec.Decode(ctx, pl, 193) + if err != nil { + t.Fatalf("decode error for payload %s: %v", pl, err) + } + if !out.Is(decoder.FeatureGNSS) { + t.Fatalf("expected GNSS feature for payload %s", pl) + } + } +} + +// GNSS timestamped, not moving, two-frame NAV (port 194) from logs +func TestTagXL_GNSS_Port194_Timestamped(t *testing.T) { + if logger.Logger == nil { + logger.NewLogger() + } + log := logger.Logger + devEui := "0011223344556677" + fcnt := 42 + + // Timestamp 0x68BAD325 => 1757074213 + secs := uint32(1757074213) + ts := time.Unix(int64(secs), 0).UTC() + + payloads := []string{ + "68bad32509ab91418ae63a10b5004a0a3fef037ab2f06ce8e510820c1a0bdcecb49e1543fdd2f28f1c", + "68bad32589b379e7ba0fb5006b9aaa8c8e25febf16f4e5c31d0cc8ca12a1cffdddf16c2cf82877f1edee4ecbc5ef54", + } + for _, payload := range payloads { + cap := &captureSolverV2{ + resp: decoder.NewDecodedUplink( + []decoder.Feature{decoder.FeatureGNSS, decoder.FeatureTimestamp}, + &fakeGNSSData{lat: 47.0, lon: 8.0, alt: 10.0, ts: &ts}, + ), + } + dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, log, WithSolverV2(cap)) + + ctx := context.WithValue(context.Background(), decoder.DEVEUI_CONTEXT_KEY, devEui) + ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, fcnt) + + out, err := dec.Decode(ctx, payload, 194) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedForwarded := payload[8:] // strip 4B timestamp (8 hex chars) + if cap.lastPayload != expectedForwarded { + t.Fatalf("expected forwarded payload %q, got %q", expectedForwarded, cap.lastPayload) + } + if cap.lastOptions.Port != 194 { + t.Fatalf("expected port 194, got %d", cap.lastOptions.Port) + } + if cap.lastOptions.Moving == nil || *cap.lastOptions.Moving != false { + t.Fatalf("expected Moving=false for port 194, got %+v", cap.lastOptions.Moving) + } + if cap.lastOptions.Timestamp == nil || !cap.lastOptions.Timestamp.Equal(ts) { + t.Fatalf("expected Timestamp=%v for port 194, got %+v", ts, cap.lastOptions.Timestamp) + } + if !out.Is(decoder.FeatureGNSS) || !out.Is(decoder.FeatureTimestamp) { + t.Fatalf("expected GNSS and Timestamp features in result") + } + } +} + +// GNSS timestamped, moving, two-frame NAV (port 195) from logs +func TestTagXL_GNSS_Port195_Timestamped(t *testing.T) { + if logger.Logger == nil { + logger.NewLogger() + } + log := logger.Logger + devEui := "0011223344556677" + fcnt := 43 + + // Timestamp 0x68BAD3C5 => 1757074373 + secs := uint32(1757074373) + ts := time.Unix(int64(secs), 0).UTC() + + payloads := []string{ + "68bad3c50aabd56cb2e7ba0db5805a5ac9d4edd8de8a021b4ae2b78e8c0b8391566ab8d47d1d4c55ae794a2c2da7a637b49d32e44800", + "68bad3c58aab4581b9e73a0eb580da120d7f85a75e770c6acad3dc2acdacbdcd576ab8147f5902557379b18d0f676a35fb9a6ae5ee03", + } + for _, payload := range payloads { + cap := &captureSolverV2{ + resp: decoder.NewDecodedUplink( + []decoder.Feature{decoder.FeatureGNSS, decoder.FeatureTimestamp}, + &fakeGNSSData{lat: 47.0, lon: 8.0, alt: 10.0, ts: &ts}, + ), + } + dec := NewTagXLv1Decoder(context.TODO(), solver.MockSolverV1{}, log, WithSolverV2(cap)) + + ctx := context.WithValue(context.Background(), decoder.DEVEUI_CONTEXT_KEY, devEui) + ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, fcnt) + + out, err := dec.Decode(ctx, payload, 195) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedForwarded := payload[8:] // strip 4B timestamp (8 hex chars) + if cap.lastPayload != expectedForwarded { + t.Fatalf("expected forwarded payload %q, got %q", expectedForwarded, cap.lastPayload) + } + if cap.lastOptions.Port != 195 { + t.Fatalf("expected port 195, got %d", cap.lastOptions.Port) + } + if cap.lastOptions.Moving == nil || *cap.lastOptions.Moving != true { + t.Fatalf("expected Moving=true for port 195, got %+v", cap.lastOptions.Moving) + } + if cap.lastOptions.Timestamp == nil || !cap.lastOptions.Timestamp.Equal(ts) { + t.Fatalf("expected Timestamp=%v for port 195, got %+v", ts, cap.lastOptions.Timestamp) + } + if !out.Is(decoder.FeatureGNSS) || !out.Is(decoder.FeatureTimestamp) { + t.Fatalf("expected GNSS and Timestamp features in result") + } + } +} From 80b39494383d9c582dade7bd1390fc8ab2bf0137 Mon Sep 17 00:00:00 2001 From: Michael Beutler <35310806+michaelbeutler@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:59:27 +0200 Subject: [PATCH 26/26] Enhance TagXL decoder with payload length checks and update movement logic for Port 201; refactor test server setup and adjust data types in port specifications. --- .secrets.baseline | 68 ++++++++++++++-------------- pkg/decoder/tagxl/v1/decoder.go | 47 +++++++++++++------ pkg/decoder/tagxl/v1/decoder_test.go | 11 +++-- pkg/decoder/tagxl/v1/port151.go | 3 ++ pkg/decoder/tagxl/v1/port152.go | 4 +- pkg/decoder/tagxl/v1/port201.go | 4 +- pkg/solver/solver_v2.go | 14 ++++-- 7 files changed, 90 insertions(+), 61 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 222c000..454522b 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1397,7 +1397,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "ed2b7805cbf0feec0ab81d143f4df1125c57bf3f", "is_verified": false, - "line_number": 107, + "line_number": 108, "is_secret": false }, { @@ -1405,7 +1405,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "555917d2175ab1600c6cd926d255df6ad502febc", "is_verified": false, - "line_number": 220, + "line_number": 221, "is_secret": false }, { @@ -1413,7 +1413,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "6c45c0c1b339a7891ba24bf33ef5b40b040a86a0", "is_verified": false, - "line_number": 240, + "line_number": 241, "is_secret": false }, { @@ -1421,7 +1421,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "32179884041e9ddc27e1c5e0e45ccc6e81637d65", "is_verified": false, - "line_number": 330, + "line_number": 331, "is_secret": false }, { @@ -1429,7 +1429,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "10e8fe5b6a5342c5ead45cffec2d001a28e0c1bb", "is_verified": false, - "line_number": 343, + "line_number": 344, "is_secret": false }, { @@ -1437,7 +1437,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b0771e36dfb55414a423ca9c0ceb087b03ea3cfc", "is_verified": false, - "line_number": 348, + "line_number": 349, "is_secret": false }, { @@ -1445,7 +1445,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5cbb5bf20d8b56d849c06d5e0474a3cd42e6bc16", "is_verified": false, - "line_number": 353, + "line_number": 354, "is_secret": false }, { @@ -1453,7 +1453,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "632ee27a7e117b634cb0fb234f7f7d199db5d5d1", "is_verified": false, - "line_number": 358, + "line_number": 359, "is_secret": false }, { @@ -1461,7 +1461,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "9e1335b0e4c10057a072e6aa67e5cfb9d0e5d324", "is_verified": false, - "line_number": 363, + "line_number": 364, "is_secret": false }, { @@ -1469,7 +1469,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "55472b68d8a8560add30831739dd3552e63d5b33", "is_verified": false, - "line_number": 384, + "line_number": 385, "is_secret": false }, { @@ -1477,7 +1477,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "463609dcc13b7b90fcf29ca237191ad5bf977c46", "is_verified": false, - "line_number": 395, + "line_number": 396, "is_secret": false }, { @@ -1485,7 +1485,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4c35a80282e5237761aeb3b9b2c8d422b16df653", "is_verified": false, - "line_number": 407, + "line_number": 408, "is_secret": false }, { @@ -1493,7 +1493,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5bcf7c2f08e382a84f0a78f1c6aa91f711806aa8", "is_verified": false, - "line_number": 420, + "line_number": 421, "is_secret": false }, { @@ -1501,7 +1501,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0560cb6af09786d2305b91018ca587c93c0d7dbd", "is_verified": false, - "line_number": 434, + "line_number": 435, "is_secret": false }, { @@ -1509,7 +1509,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "66cd8eba7181b16377a615d793be286a3aacb087", "is_verified": false, - "line_number": 444, + "line_number": 445, "is_secret": false }, { @@ -1517,7 +1517,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "a6c92fb0cd83e9a6f6f2bd5bfdb1a297dfe7a502", "is_verified": false, - "line_number": 456, + "line_number": 457, "is_secret": false }, { @@ -1525,7 +1525,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "c0e35b955de71e6fe09016adf1216ed73f1d7a8b", "is_verified": false, - "line_number": 470, + "line_number": 471, "is_secret": false }, { @@ -1533,7 +1533,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "5a0861255e90d61193afbc62ee5b7924739d1b54", "is_verified": false, - "line_number": 486, + "line_number": 487, "is_secret": false }, { @@ -1541,7 +1541,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "1aa68aee442b8b1c5c9fdca3fc2e18ed2f84a637", "is_verified": false, - "line_number": 504, + "line_number": 505, "is_secret": false }, { @@ -1549,7 +1549,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "bb265a97223c679953c85c60d61907ee7683468e", "is_verified": false, - "line_number": 658, + "line_number": 659, "is_secret": false }, { @@ -1557,7 +1557,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "4df89cb03f258ca60c13bf53e3442d60826bacf7", "is_verified": false, - "line_number": 664, + "line_number": 665, "is_secret": false }, { @@ -1565,7 +1565,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "f5188ea01f60dd0e30b8ff8126123c81f38ba425", "is_verified": false, - "line_number": 675, + "line_number": 676, "is_secret": false }, { @@ -1573,7 +1573,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "164c11e5bb3bdbb53a3682942846936da8006274", "is_verified": false, - "line_number": 687, + "line_number": 688, "is_secret": false }, { @@ -1581,7 +1581,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "39e57284237493c8386cbfebd10364b4f25b86bd", "is_verified": false, - "line_number": 700, + "line_number": 701, "is_secret": false }, { @@ -1589,7 +1589,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "d4fc2a168f60a698eef5c40e42f7147798791b70", "is_verified": false, - "line_number": 714, + "line_number": 715, "is_secret": false }, { @@ -1597,7 +1597,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "0c24951224219592f4f044aa8c1a43cd87d14bae", "is_verified": false, - "line_number": 729, + "line_number": 730, "is_secret": false }, { @@ -1605,7 +1605,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "67dfa780930cf12323bf6d3a2737f8be7168d2e7", "is_verified": false, - "line_number": 740, + "line_number": 741, "is_secret": false }, { @@ -1613,7 +1613,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "b821604371f934e1ce969c042520adc0f69859bf", "is_verified": false, - "line_number": 753, + "line_number": 754, "is_secret": false }, { @@ -1621,7 +1621,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "737544481bcf878548b5d3cef6898ebaaa307e35", "is_verified": false, - "line_number": 768, + "line_number": 769, "is_secret": false }, { @@ -1629,7 +1629,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56bdd17763f2ca6b25584e70ca4888acd267da77", "is_verified": false, - "line_number": 785, + "line_number": 786, "is_secret": false }, { @@ -1637,7 +1637,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "56f27e2c927e138a36b3cb7d07b942da7667b8f2", "is_verified": false, - "line_number": 810, + "line_number": 811, "is_secret": false }, { @@ -1645,7 +1645,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "516dead2735f9bcd1eced3f678aa6dbb0ed87c86", "is_verified": false, - "line_number": 1165, + "line_number": 1166, "is_secret": false }, { @@ -1653,7 +1653,7 @@ "filename": "pkg/decoder/tagxl/v1/decoder_test.go", "hashed_secret": "884f72cf02528dcd37a031e2af8575273d4394e2", "is_verified": false, - "line_number": 1193, + "line_number": 1194, "is_secret": false } ], @@ -2228,5 +2228,5 @@ } ] }, - "generated_at": "2025-09-16T15:41:56Z" + "generated_at": "2025-09-16T15:59:23Z" } diff --git a/pkg/decoder/tagxl/v1/decoder.go b/pkg/decoder/tagxl/v1/decoder.go index 2ed87bc..c2c8365 100644 --- a/pkg/decoder/tagxl/v1/decoder.go +++ b/pkg/decoder/tagxl/v1/decoder.go @@ -83,6 +83,9 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon Features: []decoder.Feature{decoder.FeatureTimestamp}, }, nil case 151: + if len(payload) < 1 { + return common.PayloadConfig{}, common.ErrPayloadTooShort + } var payloadType byte = payload[0] if payloadType != 0x4c { return common.PayloadConfig{}, fmt.Errorf("%w: port %d tag %x", common.ErrPortNotSupported, port, payloadType) @@ -138,6 +141,9 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon Features: []decoder.Feature{}, }, nil case 152: + if len(payload) < 1 { + return common.PayloadConfig{}, common.ErrPayloadTooShort + } var version uint8 = payload[0] switch version { case Port152Version1: @@ -182,22 +188,10 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon default: return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } - case 193: - return common.PayloadConfig{ - Fields: []common.FieldConfig{ - {Name: "Timestamp", Start: 0, Length: 4, Transform: timestamp}, - {Name: "EndOfGroup", Start: 4, Length: 1, Transform: func(v any) any { - return (v.([]byte)[0] >> 7) != 0 - }}, - {Name: "GroupToken", Start: 4, Length: 1, Transform: func(v any) any { - return v.([]byte)[0] & 0x1f - }}, - {Name: "NavMessage", Start: 5, Length: len(payload) - 5}, - }, - TargetType: reflect.TypeOf(Port193Payload{}), - Features: []decoder.Feature{decoder.FeatureGNSS, decoder.FeatureTimestamp, decoder.FeatureMoving}, - }, nil case 197: + if len(payload) < 1 { + return common.PayloadConfig{}, common.ErrPayloadTooShort + } var version uint8 = payload[0] switch version { case Port197Version1: @@ -237,6 +231,9 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } case 198: + if len(payload) < 1 { + return common.PayloadConfig{}, common.ErrPayloadTooShort + } var version uint8 = payload[0] switch version { case Port198Version1: @@ -276,6 +273,9 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } case 200: + if len(payload) < 5 { + return common.PayloadConfig{}, common.ErrPayloadTooShort + } var version uint8 = payload[4] switch version { case Port200Version1: @@ -317,6 +317,9 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon return common.PayloadConfig{}, fmt.Errorf("%w: version %v for port %d not supported", common.ErrPortNotSupported, version, port) } case 201: + if len(payload) < 5 { + return common.PayloadConfig{}, common.ErrPayloadTooShort + } var version uint8 = payload[4] switch version { case Port201Version1: @@ -361,6 +364,20 @@ func (t TagXLv1Decoder) getConfig(port uint8, payload []byte) (common.PayloadCon return common.PayloadConfig{}, fmt.Errorf("%w: port %v not supported", common.ErrPortNotSupported, port) } +/* +GNSS solver routing and semantics: +- Ports 192/193/194/195/199 are GNSS NAV grouping ports. When a v2 solver is configured, we prefer it. +- Movement semantics by port: + - 192: steady (Moving=false) + - 193: moving (Moving=true) + - 194: steady (Moving=false), timestamped payload (first 4 bytes UNIX seconds) is stripped before solving + - 195: moving (Moving=true), timestamped payload (first 4 bytes UNIX seconds) is stripped before solving + - 199: unspecified; Moving and Timestamp left nil unless future protocol specifies otherwise + +- When no v2 solver is provided: + - Ports 194/195 are not supported (they require timestamp stripping and explicit options). + - Ports 192/193/199 fall back to the legacy v1 solver for backward compatibility. +*/ func (t TagXLv1Decoder) Decode(ctx context.Context, data string, port uint8) (*decoder.DecodedUplink, error) { switch port { // GNSS NAV grouping ports now use the v2 solver when available. diff --git a/pkg/decoder/tagxl/v1/decoder_test.go b/pkg/decoder/tagxl/v1/decoder_test.go index f41051f..086df5c 100644 --- a/pkg/decoder/tagxl/v1/decoder_test.go +++ b/pkg/decoder/tagxl/v1/decoder_test.go @@ -31,7 +31,8 @@ func startMockServer(handler http.Handler) *httptest.Server { func TestDecode(t *testing.T) { - http.HandleFunc("/api/v1/device/send", func(w http.ResponseWriter, r *http.Request) { + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/device/send", func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -58,7 +59,7 @@ func TestDecode(t *testing.T) { _, _ = w.Write(data) }) - server := startMockServer(nil) + server := startMockServer(mux) middleware, err := loracloud.NewLoracloudClient(context.TODO(), "access_token", zap.NewExample()) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -978,9 +979,9 @@ func TestDecode(t *testing.T) { expectedAny := test.expected opts := []Option{} switch test.port { - case 194, 195: - // For timestamped GNSS ports, use SolverV2 and return the same structure as port 192 expectation - // so that tests compare against exampleResponse. + case 192, 194, 195: + // For GNSS ports, use SolverV2 and return the same structure as port 192 expectation + // so that tests compare against exampleResponse. For 194/195, timestamp is handled by decoder. v2Data := &exampleResponse features := []decoder.Feature{decoder.FeatureGNSS} if expectedAny == nil { diff --git a/pkg/decoder/tagxl/v1/port151.go b/pkg/decoder/tagxl/v1/port151.go index aac7b66..db0a57c 100644 --- a/pkg/decoder/tagxl/v1/port151.go +++ b/pkg/decoder/tagxl/v1/port151.go @@ -57,6 +57,9 @@ var _ decoder.UplinkFeatureConfig = &Port151Payload{} var _ decoder.UplinkFeatureFirmwareVersion = &Port151Payload{} func (p Port151Payload) GetBatteryVoltage() float64 { + if p.Battery == nil { + return 0 + } return float64(*p.Battery) } diff --git a/pkg/decoder/tagxl/v1/port152.go b/pkg/decoder/tagxl/v1/port152.go index 0983b1b..b259205 100644 --- a/pkg/decoder/tagxl/v1/port152.go +++ b/pkg/decoder/tagxl/v1/port152.go @@ -16,7 +16,7 @@ import ( // | 2 | 1 | old rotation state | uint4 | // | 2 | 1 | new rotation state | uint4 | // | 3 | 4 | timestamp in seconds since epoch | uint32 | -// | 7 | 2 | number of rotations since last rotation | uint32 | +// | 7 | 2 | number of rotations since last rotation | uint16 | // | 9 | 4 | elapsed seconds since last rotation | uint32 | // +------+------+-----------------------------------------------+------------+ // @@ -30,7 +30,7 @@ import ( // | 3 | 1 | old rotation state | uint4 | // | 3 | 1 | new rotation state | uint4 | // | 4 | 4 | timestamp in seconds since epoch | uint32 | -// | 8 | 2 | number of rotations since last rotation | uint32 | +// | 8 | 2 | number of rotations since last rotation | uint16 | // | 10 | 4 | elapsed seconds since last rotation | uint32 | // +------+------+-----------------------------------------------+------------+ diff --git a/pkg/decoder/tagxl/v1/port201.go b/pkg/decoder/tagxl/v1/port201.go index fddb229..a657b02 100644 --- a/pkg/decoder/tagxl/v1/port201.go +++ b/pkg/decoder/tagxl/v1/port201.go @@ -107,7 +107,9 @@ func (p Port201Payload) GetAccessPoints() []decoder.AccessPoint { return accessPoints } -// Port 201 does not provide movement information, so we return false. +/* +Port 201 indicates movement (moving variant), so we return true. +*/ func (p Port201Payload) IsMoving() bool { return true } diff --git a/pkg/solver/solver_v2.go b/pkg/solver/solver_v2.go index 07ab611..b5aa413 100644 --- a/pkg/solver/solver_v2.go +++ b/pkg/solver/solver_v2.go @@ -12,13 +12,19 @@ type SolverV2 interface { } type SolverV2Options struct { - DevEui string + DevEui string + + // UplinkCounter is the 16-bit device counter (FCntUp modulo 65536). + // If an upstream component provides a 32-bit frame counter, truncate to the + // lower 16 bits here and (optionally) carry the full 32-bit value via context + // or a separate field if your solver needs it. UplinkCounter uint16 - Port uint8 - // Optional captured at timestamp of the uplink, if available. + Port uint8 + + // Timestamp is the captured-at time of the uplink (UTC), when available. Timestamp *time.Time - // Optional indicates if the device is in motion, if available. + // Moving, when set, indicates whether the device was in motion for this uplink. Moving *bool }