{"id":38767824,"url":"https://github.com/blues/example-firmware-manager","last_synced_at":"2026-01-17T12:01:49.044Z","repository":{"id":281118833,"uuid":"942258078","full_name":"blues/example-firmware-manager","owner":"blues","description":"Use the Notehub API to assist with Device Firmware Management.  Python example designed to be used with serverless cloud functions.","archived":false,"fork":false,"pushed_at":"2025-09-02T20:52:01.000Z","size":73,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-12-26T17:50:40.382Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/blues.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-03T20:34:22.000Z","updated_at":"2025-09-02T20:46:29.000Z","dependencies_parsed_at":"2025-03-07T04:42:49.853Z","dependency_job_id":null,"html_url":"https://github.com/blues/example-firmware-manager","commit_stats":null,"previous_names":["blues/example-firmware-manager"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/blues/example-firmware-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blues%2Fexample-firmware-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blues%2Fexample-firmware-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blues%2Fexample-firmware-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blues%2Fexample-firmware-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/blues","download_url":"https://codeload.github.com/blues/example-firmware-manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/blues%2Fexample-firmware-manager/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28508464,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T11:50:55.898Z","status":"ssl_error","status_checked_at":"2026-01-17T11:50:55.569Z","response_time":85,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-17T12:01:48.767Z","updated_at":"2026-01-17T12:01:48.957Z","avatar_url":"https://github.com/blues.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Firmware Update by Rules\n\n_Python Edition_\n\nEnforce Notecard and Host firmware combinations, and provide rules for when to update Notecard and Host MCU firmware.\n\n## Key Features\n\n- **🛡️ Secure Authentication**: Multiple authentication methods with robust security features\n- **🧪 Dry-Run Testing**: Test firmware update logic safely without making actual requests  \n- **⚡ Flexible Rules Engine**: Support for arbitrary device fields and complex conditions\n- **📊 100% Test Coverage**: Comprehensive unit tests ensure reliability\n- **🔄 Intelligent Caching**: Optimized firmware version caching to reduce API calls\n- **🎯 Dot Notation Support**: Access nested object properties in rule conditions\n\n## Quick Start with Dry-Run Testing\n\n**🚨 IMPORTANT**: Always test your firmware update routes with dry-run mode first!\n\n```bash\n# Test your route safely - no actual firmware updates will be made\ncurl -X POST \"https://your-endpoint.com/firmware-check?is_dry_run=true\" \\\n  -H \"Authorization: Bearer your-secure-token-here\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"]}'\n```\n\nThis will:\n- ✅ Execute your complete rules engine logic\n- ✅ Validate firmware versions and availability  \n- ✅ Show what updates *would* be requested\n- ❌ **Not make any actual firmware update requests**\n- 📝 Return messages prefixed with \"Dry-Run: \"\n\nOnce you've verified the dry-run behavior is correct, you can enable the route for automated firmware updates.\n\nRules in this example can be based on **any device characteristics**, including:\n\n- Notecard firmware version (`firmware_notecard`)\n- Host MCU firmware version (`firmware_host`)  \n- Fleet membership (`fleets`)\n- Device type, location, environment conditions\n- Custom device properties and metadata\n- Any other device field available in your system\n\nThis example assumes the firmware version rule checks and subsequent firmware update requests are invoked by execution of a Notehub route.  That is, this is driven by Notecard event behavior rather than assigning a specific period.\n\nIt is worth considering having a periodic version of firmware update checks execute once a day to catch any devices that don't connect frequently, or may have been missed for some reason in addition to the event driven updates.\n\n## Notehub Route Configuration\n\nThis example is designed to be invoked by the execution of a Notehub route.  A route can be configured to call any number of endpoints including a RESTful API, AWS Lambda, Google Cloud Function, Azure Cloud, MQTT, etc.\n\nThe route can be filtered to only execute if certain conditions are met.  In this example, it will be filtered to the creation of `_session.qo` events, but only on session opening, not session closing.\n\nAll of the following configurations can be applied to any of the appropriate Notehub route types.  For specific Route configurations, please consult the Blues documentation.\n\n|Name|Value|Comments|\n|---|---|---|\n|Fleets|All Fleets|You can filter by fleets if the firmware check and update process only applies to devices in specific fleets|\n|Notefiles|Selected Notefiles| |\n|Include Other Notefiles|_session.qo| |\n|Transform Data|JSONata Expression| |\n|JSONata Expression| `((body.closed) ? $doNotRoute(): $)` | Exclude session closing events. Send the entire Note to the firmware check otherwise|\n|**For Authorization**| |When using with AWS Lambda URL or custom RESTful API, add an authorization token to the request headers|\n|HTTP Headers|Additional Headers|Enable inclusion of additional request headers|\n|Header name|x-api-key||\n|Header value|your-custom-token||\n|**For Testing**||When testing the route setup and configuration, also apply the following|\n|HTTP Headers|Additional Headers|Enable inclusion of additional request headers|\n|Header name|x-dry-run||\n|Header value|true||\n\n---\n\u003e **❗IMPORTANT**\n\u003e \n\u003e Click the \"Apply changes\" or \"Create route\" button in order to save the configuration changes\n\n### Testing Routes\n\n**Recommended Testing Approach:**\n\n1. **Start with Dry-Run Mode**: Always test your route with `is_dry_run=true` first\n   ```bash\n   curl -X POST \"https://your-endpoint.com/firmware-check?is_dry_run=true\" \\\n     -H \"Authorization: Bearer your-token\" \\\n     -H \"Content-Type: application/json\" \\\n     -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"]}'\n   ```\n\n2. **Manual Event Routing**: Test with existing Notehub events without affecting live devices\n\nIt is possible in Notehub to manually route an existing event in Notehub to any route, including a disabled route.\n\n\u003chttps://dev.blues.io/notehub/notehub-walkthrough/#manually-routing-events\u003e\n\n\n**⚠️ Safety Tip**: The dry-run mode provides an additional safety layer - even if you accidentally manually trigger it, no actual firmware updates will occur when `is_dry_run=true`.\n\n## Authentication and Script Environment Configuration\n\nThis example script uses the Notehub Project API to gather additional data from a Notehub project and device, as well as sending firmware update requests.\n\nThis requires authentication with the Notehub.\n\nThis example uses the OAuth method of interacting with Notehub.\n\n### Generating Client ID and Secret\n\nYou can generate Client ID and Client Secrets for a given Notehub project in the Notehub Project settings:\n\n\u003chttps://dev.blues.io/api-reference/notehub-api/api-introduction/#authentication-with-oauth-bearer-tokens\u003e\n\n### Obtaining Notehub Project UID\n\nNavigate to the `Settings` page for your Notehub project.\n\nUnder the `Project information` section, select the copy button next to the value in the `Project UID` field.\n\n### Environment Variables\n\nThe best practice is to store the Client ID, Client Secret, and Project UID in system environment variables.\n\nThe exact mechanism required for creating the environment variable will vary depending on the deployment system used to execute this example.\n\nYou will want to create the following environment variables with the values obtained from the steps above\n\n|Environment Variable|Value|\n|---|---|\n|NOTEHUB_CLIENT_ID|`\u003cclient id obtained above\u003e`|\n|NOTEHUB_CLIENT_SECRET|`\u003cclient secret obtained above\u003e`|\n|NOTEHUB_PROJECT_UID|`\u003capp:xxxxx-xxxx-xxxx-xxxx-xxxxx\u003e`|\n|FIRMWARE_CHECK_AUTH_TOKEN|`\u003cendpoint-authorization-token\u003e`|\n\n\u003e **❗IMPORTANT**\n\u003e \n\u003e Do not store these values in source control.\n\n## Request Authorization\n\nThis system includes authentication capabilities to secure access to the firmware management endpoints. The `auth.py` module provides comprehensive request authentication using various header formats.\n\n### Supported Authentication Methods\n\nThe system supports multiple authentication schemes for maximum flexibility:\n\n1. **Bearer Token** (Recommended)\n   ```\n   Authorization: Bearer \u003cyour-token\u003e\n   ```\n\n2. **Direct Token** in Authorization Header\n   ```\n   Authorization: \u003cyour-token\u003e\n   ```\n\n3. **API Key Header**\n   ```\n   x-api-key: \u003cyour-token\u003e\n   ```\n\n### Authentication Behavior\n\n- **Header Precedence**: The `x-api-key` header takes precedence over the `Authorization` header if both are present\n- **Case Insensitive**: All header names are processed case-insensitively (e.g., `AUTHORIZATION`, `X-API-KEY`)\n- **Whitespace Handling**: Tokens are automatically trimmed of leading/trailing whitespace\n- **Security**: Uses constant-time comparison (`hmac.compare_digest()`) to prevent timing attacks\n\n### Configuration\n\nTo enable request authentication, configure your authentication token as an environment variable:\n\n```bash\nexport FIRMWARE_CHECK_AUTH_TOKEN=\"your-secure-token-here\"\n```\n\n### Example Usage\n\n#### Using Bearer Token (Recommended)\n```bash\ncurl -X POST https://your-endpoint.com/firmware-check \\\n  -H \"Authorization: Bearer your-secure-token-here\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"]}'\n```\n\n#### Using API Key Header\n```bash\ncurl -X POST https://your-endpoint.com/firmware-check \\\n  -H \"x-api-key: your-secure-token-here\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"]}'\n```\n\n### Authentication Errors\n\nThe system returns specific error messages for authentication failures:\n\n- `Missing authorization header` - No authentication header provided\n- `Empty authorization token` - Authentication header is empty or contains only whitespace\n- `Invalid authorization token` - Token doesn't match the expected value\n- `Authentication not configured` - Server-side authentication token is not configured\n- `Authentication system error` - Internal error during authentication processing\n\n\n## Dry-Run Mode\n\nTo test firmware update logic without making actual update requests, add the `is_dry_run` flag to your request:\n\n**In Request Body:**\n```bash\ncurl -X POST https://your-endpoint.com/firmware-check \\\n  -H \"Authorization: Bearer your-secure-token-here\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"], \"is_dry_run\": true}'\n```\n\n**As Query Parameter:**\n```bash\ncurl -X POST \"https://your-endpoint.com/firmware-check?is_dry_run=true\" \\\n  -H \"Authorization: Bearer your-secure-token-here\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"]}'\n```\n\n**As Header:**\n```bash\ncurl -X POST https://your-endpoint.com/firmware-check \\\n  -H \"Authorization: Bearer your-secure-token-here\" \\\n  -H \"x-dry-run: true\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"]}'\n```\n\nWhen `is_dry_run` is enabled:\n- Rules engine executes normally to determine required updates\n- Firmware update validation occurs (checking available versions)\n- **No actual firmware update requests are sent to Notehub**\n- Response messages are prefixed with \"Dry-Run: \"\n- Returns \"Would request\" instead of \"Requested\" in update messages\n\n\n## Rules Configuration\n\nThe rules engine provides a powerful and flexible system for defining firmware update conditions based on **any device characteristics**. Rules are evaluated against device data to determine when firmware updates should be applied.\n\n### Generic Rules Engine\n\nThe rules engine supports arbitrary field names in conditions, making it extensible beyond the traditional notecard/host/fleet model. You can create conditions based on:\n\n- **Firmware versions**: `firmware_notecard`, `firmware_host`\n- **Device characteristics**: `deviceType`, `location`, `environment`\n- **Operational data**: `batteryLevel`, `signalStrength`, `temperature`\n- **Custom fields**: Any field name your system provides\n\n### Device Data Structure\n\nDevice data is passed as a dictionary to the rules engine. Firmware information is typically provided as JSON objects containing version details and other metadata:\n\n```python\ndevice_data = {\n    \"firmware_notecard\": {\n        \"version\": \"notecard-8.1.3.17074\",   # Full version string (may have prefix)\n        \"ver_major\": 8,                      # Parsed major version (recommended for rules)\n        \"ver_minor\": 1,                      # Parsed minor version (recommended for rules)\n        \"ver_patch\": 3,                      # Parsed patch version (recommended for rules)\n        \"ver_build\": 17074,                  # Parsed build number (recommended for rules)\n        \"built\": \"2024-01-15T10:30:00Z\",\n        \"type\": \"release\",\n        \"size\": 2048576\n    },\n    \"firmware_host\": {\n        \"version\": \"host-3.1.2\",             # Full version string (may have prefix)\n        \"ver_major\": 3,                      # Parsed major version (recommended for rules)\n        \"ver_minor\": 1,                      # Parsed minor version (recommended for rules)\n        \"ver_patch\": 2,                      # Parsed patch version (recommended for rules)\n        \"built\": \"2024-01-10T14:22:00Z\",\n        \"type\": \"production\",\n        \"size\": 1024000,\n        \"checksum\": \"abc123def456\"\n    },\n    \"fleets\": [\"fleet:production\", \"fleet:sensors\"],  # List of fleet UIDs\n    \"deviceType\": \"environmental-sensor\",\n    \"location\": \"outdoor\",\n    \"batteryLevel\": 85,\n    \"signalStrength\": -65,\n    \"environment\": \"harsh\"\n}\n```\n\n### Version Field Handling\n\nThe system automatically parses JSON string firmware data and extracts individual version components for reliable rule matching:\n\n```python\n# Firmware data can arrive as JSON strings (common from external systems)\n\"firmware_notecard\": '{\"version\":\"notecard-6.2.5\",\"ver_major\":6,\"ver_minor\":2,\"ver_patch\":5,\"ver_build\":16868}'\n\n# System automatically parses this to:\n\"firmware_notecard\": {\n    \"version\": \"notecard-6.2.5\",\n    \"ver_major\": 6,        # Integer - reliable for comparisons\n    \"ver_minor\": 2,        # Integer - reliable for comparisons  \n    \"ver_patch\": 5,        # Integer - reliable for comparisons\n    \"ver_build\": 16868     # Integer - reliable for comparisons\n}\n```\n\n#### JSON String Parsing\n\nThe system includes automatic JSON parsing for firmware fields that may arrive as JSON strings from external systems. This is handled in `main.py` through the `parse_firmware_fields()` function:\n\n```python\n# Input payload with JSON strings\npayload = {\n    \"device\": \"dev:123456\",\n    \"firmware_notecard\": '{\"version\":\"notecard-8.1.3\",\"ver_major\":8,\"ver_minor\":1,\"ver_patch\":3}',\n    \"firmware_host\": '{\"version\":\"host-3.1.2\",\"ver_major\":3,\"ver_minor\":1,\"ver_patch\":2}'\n}\n\n# Automatically parsed to structured data for rules engine\nparsed_payload = {\n    \"device\": \"dev:123456\",\n    \"firmware_notecard\": {\n        \"version\": \"notecard-8.1.3\",\n        \"ver_major\": 8,\n        \"ver_minor\": 1,\n        \"ver_patch\": 3\n    },\n    \"firmware_host\": {\n        \"version\": \"host-3.1.2\",\n        \"ver_major\": 3,\n        \"ver_minor\": 1,\n        \"ver_patch\": 2\n    }\n}\n```\n\n**Recommended Approach**: Use the parsed integer fields (`ver_major`, `ver_minor`, `ver_patch`, `ver_build`) in your rules instead of parsing version strings. This approach:\n\n- ✅ **Handles prefixes**: Works with `\"notecard-6.2.5\"`, `\"host-3.1.2\"`, etc.\n- ✅ **Reliable comparisons**: Integer comparisons are more reliable than string parsing\n- ✅ **Error-free**: Avoids parsing errors from version string variations\n- ✅ **Performance**: Integer comparisons are faster than string parsing\n\n### Dot Notation for Nested Objects\n\nThe rules engine supports **dot notation** to access nested object properties. This is particularly useful for firmware objects that contain multiple fields:\n\n```python\n# Access firmware version fields from nested objects\nrules = [\n    {\n        \"id\": \"firmware-version-check\",\n        \"conditions\": {\n            # Recommended: Use parsed version fields for reliable comparisons\n            \"firmware_notecard.ver_major\": 8,                    # Exact major version match\n            \"firmware_notecard.ver_minor\": lambda minor: minor \u003e= 1,  # Minor version \u003e= 1\n            \"firmware_host.ver_major\": 3,                        # Host major version\n            \"firmware_host.ver_minor\": lambda minor: minor \u003c 2,  # Host minor version \u003c 2\n            \n            # Alternative: String-based version checking (works but not recommended for new rules)\n            \"firmware_notecard.version\": lambda v: v and v.endswith(\"17074\"),\n            \n            # Other firmware metadata\n            \"firmware_notecard.type\": \"release\",                 # Check firmware type\n            \"firmware_host.built\": lambda date: \"2024-01\" in date  # Check build date\n        },\n        \"target_versions\": {\n            \"notecard\": \"8.1.4\",\n            \"host\": \"3.1.3\"\n        }\n    }\n]\n```\n\n### Initial Testing\n\nFor initial testing, use the dry-run functionality instead of special testing rules:\n\n```bash\n# Test your actual rules without making firmware update requests\ncurl -X POST https://your-endpoint.com/firmware-check \\\n  -H \"Authorization: Bearer your-secure-token-here\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-dry-run: true\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def\"]}'\n```\n\nThis approach allows you to:\n- Test your actual production rules safely\n- Verify rule conditions are met as expected  \n- See what firmware updates would be requested\n- Validate the complete firmware update logic\n- Get \"Dry-Run: \" prefixed messages showing intended actions\n\nThe dry-run mode executes the complete rules engine and firmware validation without making actual update requests to Notehub, making it ideal for testing and development.\n\n### Code Organization\n\nThe firmware management system is organized into several key modules:\n\n- **`rules_engine.py`**: Core rules evaluation engine with dot notation support\n- **`rules.py`**: Example rule definitions and helper functions  \n- **`main.py`**: Lambda handler with JSON parsing and authentication\n- **`manage_firmware.py`**: Main firmware management orchestration\n- **`auth.py`**: Request authentication handling\n\nWhen developing rules, you have two options:\n\n1. **Edit `rules.py`**: Modify the existing `DevicesInUpdateFleet` rules or add new rule sets\n2. **Create new rule files**: Import your custom rules into `main.py`\n\nThe `rules_engine.py` module provides the core `getFirmwareUpdateTargets()` function that processes any rule set, while `rules.py` contains example implementations.\n\n### Rules Development and Testing\n\nWhen developing a set of rules, edit the `rules.py` file.\n\nA rule set is a list of dictionaries, each item in the list defines a set of conditions and target firmware versions.\n\nEach rule has an `id`, a set of `conditions` and `target_versions`. _IF_ all of the `conditions` are met for a specific rule, then updates to the firmware versions in the `target_versions` are requested.\n\n**For testing** rule conditions, use the dry-run functionality rather than setting `target_versions` to `None`.\n\n```python\nMy_Rule_Set = [\n    {\n        \"id\":\"desired-state-rule\",\n        \"conditions\":{\n            # Use parsed version fields for reliable version checking\n            \"firmware_notecard.ver_major\": 8,\n            \"firmware_notecard.ver_minor\": 1,\n            \"firmware_notecard.ver_patch\": 3,\n            \"firmware_notecard.ver_build\": 1754,\n            \"firmware_host.ver_major\": 3,\n            \"firmware_host.ver_minor\": 1,\n            \"firmware_host.ver_patch\": 2,\n            \"fleets\": lambda fleet_list: \"fleet:abc-def-ghi-jklmno\" in fleet_list\n        },\n        \"target_versions\": None  # Device already at desired versions\n    },\n    {\n        \"id\":\"update-from-old-version\",\n        \"conditions\":{\n            \"firmware_notecard.ver_major\": 7,\n            \"firmware_notecard.ver_minor\": 5,\n            \"firmware_notecard.ver_patch\": 4,\n            \"firmware_host.ver_major\": 3,\n            \"firmware_host.ver_minor\": 1,\n            \"firmware_host.ver_patch\": 1,\n            \"fleets\": lambda fleet_list: \"fleet:abc-def-ghi-jklmno\" in fleet_list\n        },\n        \"target_versions\":{\n            \"notecard\":\"8.1.3.1754\",\n            \"host\":\"3.1.2\"\n        }\n    },\n    {\n        \"id\":\"update-legacy-firmware\",\n        \"conditions\":{\n            # Use lambda for range checks\n            \"firmware_notecard.ver_major\": lambda major: major \u003c 7,  # Any major version \u003c 7\n            \"firmware_host.ver_major\": lambda major: major \u003c= 2,     # Host major version \u003c= 2\n            \"fleets\": lambda fleet_list: \"fleet:abc-def-ghi-jklmno\" in fleet_list\n        },\n        \"target_versions\":{\n            \"notecard\":\"8.1.3.1754\",\n            \"host\":\"3.1.2\"\n        }\n    }\n]\n```\n\nOnce you have created your rule set, be sure to import it into `main.py` and include it in the `manage_firmware` function call.\n\n```python\nfrom rules import My_Rule_Set\n# Note: Import the rules engine from rules_engine module\nfrom rules_engine import getFirmwareUpdateTargets, DEFAULT_RULES\n```\n\nTest your rules safely using dry-run mode:\n\n```bash\n# Test rule conditions and see what updates would be made\ncurl -X POST https://your-endpoint.com/firmware-check \\\n  -H \"Authorization: Bearer your-secure-token-here\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"device\": \"dev:123456\", \"fleets\": [\"fleet:abc-def-ghi-jklmno\"], \"is_dry_run\": true}'\n```\n\n### Apply Target Firmware to Rules\n\nOnce you have verified the rule conditions are executing as expected, you can then configure the `target_versions` to the appropriate values for each rule.\n\nNotice in this example the highest precedent rule does not have a set of `target_versions`. This the desired firmware configuration for this fleet. It acts as a guard against the rest of the rules executing. And will not invoke a firmware update request if the device is already using the desired firmware configuration.\n\n```python\nMy_Rule_Set = [\n    {\n        \"id\":\"highest-precedent-rule\",\n        \"conditions\":{\n            \"firmware_notecard\": \"8.1.3.1754\",\n            \"firmware_host\": \"3.1.2\",\n            \"fleets\": lambda fleet_list: \"fleet:abc-def-ghi-jklmno\" in fleet_list\n        },\n        \"target_versions\": None  # Do not request firmware update\n    },\n    {\n        \"id\":\"next-highest-precedent-rule\",\n        \"conditions\":{\n            \"firmware_notecard\": \"7.5.4.345\",\n            \"firmware_host\": \"3.1.1\",\n            \"fleets\": lambda fleet_list: \"fleet:abc-def-ghi-jklmno\" in fleet_list\n        },\n        \"target_versions\":{\n            \"notecard\":\"8.1.3.1754\",\n            \"host\":\"3.1.2\"\n        }\n    },\n    {\n        \"id\":\"lowest-precedent-rule\",\n        \"conditions\":{\n            \"firmware_notecard\": \"6.2.3.123\",\n            \"firmware_host\": \"2.1\",\n            \"fleets\": lambda fleet_list: \"fleet:abc-def-ghi-jklmno\" in fleet_list\n        },\n        \"target_versions\":{\n            \"notecard\":\"7.5.4.345\",\n            \"host\":\"3.1.1\"\n        }\n    }\n]\n```\n\n## System Implementation Overview\n\nAn overview of the behavior of this example is illustrated in the following sequence diagram.\n\n```mermaid\nsequenceDiagram\n    Notecard-\u003e\u003eNotehub: Establish Session\n    Notehub-\u003e\u003eNotehub: Generate _session.qo\n    activate Notehub\n    note right of Notehub: Notehub Route\n    Notehub-\u003e\u003e+Firmware Check: [is session opening] (device id, fleet ids, )\n    deactivate Notehub\n    opt Notecard needs Update\n        Firmware Check -\u003e\u003e Notehub : Notecard Update Request\n    end\n    opt Host needs Update\n        Firmware Check -\u003e\u003e Notehub : Host Update Request\n    end\n    Firmware Check -\u003e\u003e -Notehub : Success and Rule Executed\n    Notecard-\u003e\u003e+Notehub: Sync\n    Notehub-\u003e\u003e-Notecard: Request any Firmware Updates\n```\n\n### Notehub Route\n\nThe Notehub Route responds to Notehub Events.  In this case, the route is invoked if an event occurs in a specific Notefile, and has the specific value of `\"opening\":true` in the event body.\n\nIf all of the routing conditions are met, it will invoke the route to call the Firmware Check function.  For a general HTTP Route this will be an HTTP request with the POST method.\n\nThe purpose of the route is to invoke a check on a Notecard's firmware condition as needed.  The route is designed to filter out calls to the Firmware Check in cases where it's not needed to reduce the number of superfluous calls to the remote procedure.\n\n### Firmware Check\n\nThis is the remote procedure that executes the rules check based on the device UID, the device fleet information, and anything else that may be provided by the execution of the Notehub Route.\n\nWhen checking the device firmware configuration, if the procedure finds there is a rule that requests a firmware update, the procedure will call back to the Notehub to request an update to a specific firmware image.  This is the case for both the Notecard firmware, and the host firmware.\n\nWhen the Firmware Check procedure is complete, it returns some information back to Notehub about the status of the firmware check.  Was a rule matched?  If so which one? Were any updates requested, and for which firmware type?\n\n## Rules and Matching\n\nThe rules engine evaluates device data against a set of rules to determine firmware update actions. Rules support **arbitrary field names**, making the system infinitely extensible.\n\n### Rule Structure\n\nEach rule is a Python dictionary with the following structure:\n\n```python\n{\n    \"id\": \"rule-n\",           # Unique identifier (auto-generated if not provided)\n    \"conditions\": {},         # Dictionary of field conditions\n    \"target_versions\": {}      # Target firmware versions to apply\n}\n```\n\n### Advanced Rule Examples\n\n```python\n# Example with arbitrary device characteristics\nEnvironmental_Sensor_Rules = [\n    {\n        \"id\": \"desired-state-outdoor-sensors\",\n        \"conditions\": {\n            # Use parsed version fields for reliable version checking\n            \"firmware_notecard.ver_major\": 8,\n            \"firmware_notecard.ver_minor\": 1,\n            \"firmware_notecard.ver_patch\": 3,\n            \"firmware_host.ver_major\": 3,\n            \"firmware_host.ver_minor\": 1,\n            \"firmware_host.ver_patch\": 2,\n            \"deviceType\": \"environmental-sensor\",\n            \"location\": \"outdoor\",\n            \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list,\n            \"batteryLevel\": lambda level: level \u003e 20  # Battery above 20%\n        },\n        \"target_versions\": None  # Already at desired state\n    },\n    {\n        \"id\": \"update-outdoor-sensors\",\n        \"conditions\": {\n            \"deviceType\": \"environmental-sensor\",\n            \"location\": \"outdoor\",\n            \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list,\n            # Use version fields for reliable version range checking\n            \"firmware_notecard.ver_major\": 8,\n            \"firmware_notecard.ver_minor\": 1,\n            \"firmware_notecard.ver_patch\": 2,  # Exactly version 8.1.2.x\n            \"batteryLevel\": lambda level: level \u003e 50  # Only update if battery sufficient\n        },\n        \"target_versions\": {\n            \"notecard\": \"8.1.3\",\n            \"host\": \"3.1.2\"\n        }\n    },\n    {\n        \"id\": \"harsh-environment-special-firmware\",\n        \"conditions\": {\n            \"environment\": \"harsh\",\n            \"signalStrength\": lambda strength: strength \u003e -75,  # Good signal\n            \"fleets\": lambda fleet_list: any(fleet in fleet_list for fleet in [\"fleet:industrial\", \"fleet:outdoor\"]),\n            # Only apply to specific version ranges\n            \"firmware_notecard.ver_major\": lambda major: major \u003c 8  # Upgrade older versions\n        },\n        \"target_versions\": {\n            \"notecard\": \"8.1.4-harsh\"  # Special firmware for harsh environments\n        }\n    }\n]\n```\n\n### ID\n\nThe value of the `id` field is a string that provides an identifier for the specific rule.  This should be unique for each rule within a rule set.\n\nIf an `id` field is not provided, one is automatically generated based on the index of the rule in the rule set.\n\nThe `id` is used to indicate which rule has been satisfied by the set of conditions.\n\n### Conditions\n\nConditions define what device characteristics must be true for a rule to be satisfied. The rules engine supports **any field name**, making it infinitely extensible.\n\nIf there are no conditions (i.e. the value of `conditions` is `None`), then the rule is always satisfied.\n\n#### Condition Types\n\nEach condition field can have different value types:\n\n- **String**: Exact match required\n- **Function/Lambda**: Custom logic that returns boolean\n- **None**: Always matches (condition ignored)\n\n#### Field Names\n\nYou can use any field names in conditions:\n\n```python\n{\n    \"firmware_notecard\": \"8.1.3\",           # Exact version match\n    \"firmware_host\": lambda v: v.startswith(\"3.1\"),  # Version prefix match\n    \"deviceType\": \"sensor\",                  # Device category\n    \"location\": \"outdoor\",                   # Physical location\n    \"environment\": lambda e: e in [\"harsh\", \"moderate\"],  # Environment check\n    \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list,  # Fleet membership\n    \"batteryLevel\": lambda b: b \u003e 50,        # Battery threshold\n    \"signalStrength\": lambda s: s \u003e -70,     # Signal quality\n    \"customField\": \"custom-value\"            # Any custom field\n}\n```\n\n#### Fleet Membership Example\n\nWhen device data contains a list of fleets, use a lambda to check membership:\n\n```python\n# Device data\ndevice_data = {\n    \"fleets\": [\"fleet:production\", \"fleet:sensors\", \"fleet:outdoor\"]\n}\n\n# Rule condition - check if device belongs to specific fleet\n\"conditions\": {\n    \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list\n}\n```\n\n#### Match Condition with String\n\nIf the value of a condition is a string, then an exact match is required. For example, if the value for `\"firmware_notecard.version\"` is `\"8.1.3\"`, then the condition will only be satisfied if the device's Notecard firmware version is exactly `\"8.1.3\"` and will not match `\"8.1.3.17074\"`.\n\n#### Match Condition with Function\n\nIf the value of a condition is a function, it must accept the device field value as input and return a boolean. For example:\n\n```python\n{\n    \"firmware_notecard.version\": lambda v: v and v.startswith(\"8.1.3\"),\n    \"batteryLevel\": lambda level: level \u003e 50,\n    \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list,\n    \"environment\": lambda env: env in [\"production\", \"staging\"]\n}\n```\n\nFunctions can be:\n- **Lambda functions**: Inline condition logic\n- **Named functions**: Defined in `rules.py` or imported modules  \n- **Semantic version checks**: Using imported version comparison libraries\n\n#### Complex Condition Examples\n\n```python\n# Multi-fleet membership check\n\"fleets\": lambda fleet_list: any(fleet in fleet_list for fleet in [\"fleet:prod\", \"fleet:staging\"])\n\n# Version range check\n\"firmware_notecard.version\": lambda v: v and \"8.1.2\" \u003c= v \u003c \"8.2.0\"\n\n# Combined environment and signal strength\n\"location\": lambda loc: loc == \"outdoor\",\n\"signalStrength\": lambda strength: strength \u003e -70\n```\n\n### Target Versions\n\nTarget versions define which firmware updates should be applied when rule conditions are met.\n\n#### Target Version Formats\n\n**No Updates**: Set to `None`\n```python\n\"target_versions\": None  # No firmware updates requested\n```\n\n**Specific Firmware Types**: Use a dictionary with firmware type keys\n```python\n\"target_versions\": {\n    \"notecard\": \"8.1.3\",     # Update Notecard to specific version\n    \"host\": \"3.1.2\"          # Update Host MCU to specific version\n}\n```\n\n**Single Firmware Type**: Update only one type\n```python\n\"target_versions\": {\n    \"notecard\": \"8.1.4-emergency\"  # Emergency Notecard update only\n}\n```\n\nIf a firmware type is omitted or set to `None`, no update request is made for that firmware type.\n\n### Precedence\n\nThe ordering of the rules in a rule set matters.\n\nThe rules engine will execute a target version update request for the first rule that is satisfied in the list of rules.\n\nAnother way to say this is the rule with the lowest index in the list of rules that is satisfied will make the associated firmware update requests.\n\nIf no condition is satisfied, then no firmware update requests will be made.\n\n#### Sequencing Firmware Updates\n\nIf a Notecard has both Notecard and Host firmware updates pending, it will _always_ choose to update the Notecard first.\n\nThere may be specific cases where this is undesirable. That is, there may be rare cases where the host firmware _must_ be updated prior to updating the Notecard firmware.\n\nSince the rules in a rule set have precedence, it is possible to sequence which firmware update will be made first.\n\nIn the following rule configuration, if the Notecard is on LTS version 8, and the host is on version 2, then do nothing.\n\nOtherwise, update the host to version 2, then update the Notecard to version 8.\n\n```python\nUpdate_Host_Before_Notecard = [\n    {\n        \"id\":\"has-correct-versions\",\n        \"conditions\":{\n            \"firmware_notecard\": lambda v: majorVersion(v) \u003e= 8,\n            \"firmware_host\": lambda v: majorVersion(v) \u003e= 2,\n            \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list\n        },\n        \"target_versions\": None  # Device already on appropriate versions\n    },\n    {\n        \"id\":\"update-host-first\",\n        \"conditions\":{\n            \"firmware_host\": lambda v: majorVersion(v) \u003c 2,\n            \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list\n        },\n        \"target_versions\":{\n            \"host\": \"2.1.1\"  # Update host first\n        }\n    },\n    {\n        \"id\":\"update-notecard-after-host\",\n        \"conditions\": {\n            \"fleets\": lambda fleet_list: \"fleet:production\" in fleet_list\n        },\n        \"target_versions\":{\n            \"notecard\": \"8.1.3.17054\"  # Now update Notecard\n        }\n    }\n]\n```\n\n## Additional Features\n\nThe `manage_firmware` function implement a number of features to try and optimize the number of RESTful API requests made to Notehub. As well as protect against updating when the device already has pending updates.\n\n### Fetch Notecard and Host Firmware Versions\n\nIf the Notecard and Host firmware versions are not provided for the device via the Notehub route, then the `manage_firmware` function will fetch the values for the device from Notehub.\n\n### Cache Firmware Version Information\n\nSince firmware images and versions available on a Notehub project don't change very frequently, then there's no need to retrieve the same information each time a Notecard connects to Notehub.\n\nInstead the available firmware for a Notehub project is cached (by default of 30 minutes) so that the system only asks for available firmware occasionally.\n\nThe advantage of this approach is if the firmware version information changes, and a device fails to request an update to an appropriate version because the cache hasn't updated yet, it will check again the next time the device establishes a connection to Notehub.  Eventually the cache will update, when the device connects to Notehub it will be able to proceed using the updated cache information.\n\n### Return if Firmware Update is Pending\n\nIf a device already has a firmware update pending for either the Notecard or the Host MCU, the function will return.  It won't execute any of the rule checks to see if a firmware update is needed.\n\nThere is some risk to this approach, as a change to the update rules will not propagate to devices that have pending updates until the pending update has completed or the pending update is cancelled.\n\nIt's worth considering updating this procedure to check to see if the pending update matches the results of the rules prior to making any firmware update requests to Notehub.\n\n## Troubleshooting\n\n### Common Issues and Solutions\n\n#### Version Parsing Errors\n\n**Problem**: `invalid literal for int() with base 10: 'notecard-6'`\n**Cause**: Attempting to parse version strings with prefixes using deprecated string parsing functions\n**Solution**: Use the integer version fields (`ver_major`, `ver_minor`, `ver_patch`, `ver_build`) instead of parsing version strings\n\n```python\n# ❌ Don't do this (may cause parsing errors)\n\"firmware_notecard.version\": lambda v: int(v.split('.')[0]) \u003e= 8\n\n# ✅ Do this instead (reliable)\n\"firmware_notecard.ver_major\": lambda major: major \u003e= 8\n```\n\n#### Import Path Issues\n\n**Problem**: `ModuleNotFoundError: No module named 'rules'`\n**Cause**: Incorrect import paths in examples or code\n**Solution**: Use the correct module imports:\n\n```python\n# Core rules engine\nfrom rules_engine import getFirmwareUpdateTargets, DEFAULT_RULES\n\n# Example rules (edit this file for your rules)\nfrom rules import DevicesInUpdateFleet, majorVersion, minorVersion\n```\n\n#### Rule Evaluation Debugging\n\n**Problem**: Rules not matching expected devices\n**Solution**: Use dry-run mode to debug rule evaluation:\n\n```bash\n# Test with dry-run to see rule evaluation without actual updates\ncurl -X POST \"https://your-endpoint.com/firmware-check?is_dry_run=true\" \\\n  -H \"Authorization: Bearer your-token\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"device\": \"dev:123456\", \n    \"fleets\": [\"fleet:abc-def\"],\n    \"firmware_notecard\": {\"ver_major\": 8, \"ver_minor\": 1, \"ver_patch\": 3},\n    \"firmware_host\": {\"ver_major\": 3, \"ver_minor\": 1, \"ver_patch\": 2}\n  }'\n```\n\n#### Firmware Cache Issues\n\n**Problem**: `Firmware version X.X.X not available in local firmware cache`\n**Cause**: Requested firmware version not uploaded to Notehub project\n**Solution**: \n1. Check available firmware versions in your Notehub project\n2. Upload missing firmware or update rules to use available versions\n3. The error message will list available versions to help you choose\n\n#### JSON Parsing Issues\n\n**Problem**: Firmware fields not being parsed correctly\n**Cause**: Invalid JSON strings in firmware fields\n**Solution**: Ensure firmware JSON strings are properly formatted:\n\n```python\n# ✅ Valid JSON string\n\"firmware_notecard\": '{\"version\":\"notecard-8.1.3\",\"ver_major\":8,\"ver_minor\":1,\"ver_patch\":3}'\n\n# ❌ Invalid JSON (missing quotes)\n\"firmware_notecard\": '{version:notecard-8.1.3,ver_major:8}'\n```\n\n## System Details\n\n### Full Behavior Model\n\nIn addition to the general behavior model described above, there are some additional behaviors that this example provides to help make this system robust.\n\nThe sequence of behaviors is captured in the following sequence diagram\n\n```mermaid\nsequenceDiagram\n    Notecard-\u003e\u003eNotehub: Establish Session\n    Notehub-\u003e\u003eNotehub: Generate _session.qo\n    Notehub-\u003e\u003e+Firmware Check: [is session opening] (device id, fleet ids, )\n    opt Session Token Expired\n        Firmware Check -\u003e\u003e Notehub: Request token (client ID, secret)\n        Notehub -\u003e\u003e Firmware Check: Session token\n    end\n    opt Missing Notecard Firmware Version\n        Firmware Check -\u003e\u003e Notehub: Request Firmware Version (device ID, projectUID)\n        Notehub -\u003e\u003e Firmware Check: Firmware Version\n    end\n    opt Missing Host Firmware Version\n        Firmware Check -\u003e\u003e Notehub: Request Firmware Version (device ID, projectUID)\n        Notehub -\u003e\u003e Firmware Check: Firmware Version\n    end\n    opt Notecard needs Update\n        Firmware Check -\u003e\u003e Notehub : Notecard Update Request\n    end\n    opt Host needs Update\n        Firmware Check -\u003e\u003e Notehub : Host Update Request\n    end\n    Firmware Check -\u003e\u003e -Notehub : Status and Rule Executed\n    Notecard-\u003e\u003e+Notehub: Sync\n    Notehub-\u003e\u003e-Notecard: Request any Firmware Updates\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblues%2Fexample-firmware-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fblues%2Fexample-firmware-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fblues%2Fexample-firmware-manager/lists"}