diff --git a/.github/workflows/bandit.yml b/.github/workflows/bandit.yml new file mode 100644 index 0000000..7f8d8e6 --- /dev/null +++ b/.github/workflows/bandit.yml @@ -0,0 +1,52 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# Bandit is a security linter designed to find common security issues in Python code. +# This action will run Bandit on your codebase. +# The results of the scan will be found under the Security tab of your repository. + +# https://github.com/marketplace/actions/bandit-scan is ISC licensed, by abirismyname +# https://pypi.org/project/bandit/ is Apache v2.0 licensed, by PyCQA + +name: Bandit +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '33 13 * * 6' + +jobs: + bandit: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Bandit Scan + uses: shundor/python-bandit-scan@9cc5aa4a006482b8a7f91134412df6772dbda22c + with: # optional arguments + # exit with 0, even with results found + exit_zero: true # optional, default is DEFAULT + # Github token of the repository (automatically created by Github) + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information. + # File or directory to run bandit on + # path: # optional, default is . + # Report only issues of a given severity level or higher. Can be LOW, MEDIUM or HIGH. Default is UNDEFINED (everything) + # level: # optional, default is UNDEFINED + # Report only issues of a given confidence level or higher. Can be LOW, MEDIUM or HIGH. Default is UNDEFINED (everything) + # confidence: # optional, default is UNDEFINED + # comma-separated list of paths (glob patterns supported) to exclude from scan (note that these are in addition to the excluded paths provided in the config file) (default: .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg) + # excluded_paths: # optional, default is DEFAULT + # comma-separated list of test IDs to skip + # skips: # optional, default is DEFAULT + # path to a .bandit file that supplies command line arguments + # ini_path: # optional, default is DEFAULT + diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..d19e21b --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,39 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable +# packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency review' +on: + pull_request: + branches: [ "main" ] + +# If using a dependency submission action in this workflow this permission will need to be set to: +# +# permissions: +# contents: write +# +# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api +permissions: + contents: read + # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option + pull-requests: write + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 + # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. + with: + comment-summary-in-pr: always + # fail-on-severity: moderate + # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later + # retry-on-snapshot-warnings: true diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..e44f0a7 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,24 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install -r requirements.txt + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') diff --git a/README.md b/README.md index 8c13d8b..c5b0b51 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,77 @@ # nfc2klipper -Set loaded spool & filament in klipper from NFC tags + +Set loaded spool & filament in klipper from NFC tags. + +WARNING: This is Work In Progress and not yet tested + + +## Preparing an NFC reader + +I use a PN532 based reader connected via UART to the raspberry pi +where this program is running. + +See [here](https://learn.adafruit.com/adafruit-nfc-rfid-on-raspberry-pi/pi-serial-port) +for how to configure a raspberry pi for it. + + +## Preparing klipper + +Klipper should have two gcode macros: + +* SET_ACTIVE_FILAMENT ID=n +* SET_ACTIVE_SPOOL ID=n + + +I use this configuration: +```ini +[gcode_macro SET_ACTIVE_SPOOL] +gcode: + {% if params.ID %} + {% set id = params.ID|int %} + {action_call_remote_method( + "spoolman_set_active_spool", + spool_id=id + )} + {% else %} + {action_respond_info("Parameter 'ID' is required")} + {% endif %} + +[gcode_macro SET_ACTIVE_FILAMENT] +variable_active_filament: 0 +gcode: + {% if params.ID %} + {% set id = params.ID|int %} + SET_GCODE_VARIABLE MACRO=SET_ACTIVE_FILAMENT VARIABLE=active_filament VALUE={id} + {% else %} + {action_respond_info("Parameter 'ID' is required")} + {% endif %} + +[gcode_macro ASSERT_ACTIVE_FILAMENT] +gcode: + {% if params.ID %} + {% set id = params.ID|int %} + {% current_id = printer["gcode_macro set_active_filament"].active_filament %} + {% if id != current_id %} + {# TODO: Change to PAUSE & M117 message #} + {action_raise_error("Wrong filament is loaded, should be " + id)} + {% endif %} + {% else %} + {action_respond_info("Parameter 'ID' is required")} + {% endif %} +``` + +## Preparing tags + +The tags should contain an NDEF record with a text block like this: +``` +SPOOL: 3 +FILAMENT: 2 +``` + +The numbers are the id numbers that will be sent to the macros in +klipper via the [Moonraker](https://github.com/Arksine/moonraker) API. + + +I've written to my tags with an Android phone and NXP's TagWriter. + +TODO: Write it with the NFC interface. A TUI selecting the spool to write? diff --git a/nfc2klipper.py b/nfc2klipper.py new file mode 100755 index 0000000..0719b01 --- /dev/null +++ b/nfc2klipper.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +""" +Program to set current filament & spool in klipper. +""" + +import argparse + +import nfc +import requests + +SPOOL = "spool" +FILAMENT = "FILAMENT" +NDEF_TEXT_TYPE = "urn:nfc:wkt:T" + +parser = argparse.ArgumentParser() +# description="Fetches filaments from Spoolman and creates SuperSlicer filament configs.", + +parser.add_argument("--version", action="version", version="%(prog)s 0.0.1") + +parser.add_argument( + "-c", + "--clear", + action="store_true", + help="Clears the spool & filamnet when no tag is present", +) + +parser.add_argument( + "-d", + "--nfc-device", + metavar="device", + default="ttyS0", + help="Which NFC reader to use, see https://nfcpy.readthedocs.io/en/latest/topics/get-started.html#open-a-local-device for format", +) + +parser.add_argument( + "-u", + "--url", + metavar="URL", + default="http://mainsailos.local", + help="URL for the moonraker installation", +) + + +args = parser.parse_args() + + +def set_spool_and_filament(url: str, spool: int, filament: int): + """Calls moonraker with the current spool & filament""" + + commands = { + "commands": [ + f"SET_ACTIVE_SPOOL ID={spool}", + f"SET_ACTIVE_FILAMENT ID={filament}", + ] + } + + response = requests.post( + url + "/api/printer/command", timeout=10, json=commands + ) + if response.status_code != 200: + raise ValueError(f"Request to moonraker failed: {response}") + + +def on_nfc_connect(tag): + """Handles a read tag""" + + if tag.ndef is None: + print("The tag doesn't have NDEF records") + return True + + spool = None + filament = None + + for record in tag.ndef.records: + if record.type == NDEF_TEXT_TYPE: + for line in record.text.split("\n"): + line = line.split(",") + if len(line) == 2: + if line[0] == SPOOL: + spool = line[1] + if line[0] == FILAMENT: + filament = line[1] + else: + print(f"Read other record: {record}") + + if not args.clear: + if not (spool and filament): + print("Did not find spool and filament records in tag") + if args.clear or (spool and filament): + if not spool: + spool = 0 + if not filament: + filament = 0 + set_spool_and_filament(args.url, spool, filament) + + # Don't let connect return until the tag is removed: + return True + + +# Open NFC reader. Will throw an exception if it fails. +clf = nfc.ContactlessFrontend(args.nfc_device) + +if args.clear: + # Start by unsetting current spool & filament: + set_spool_and_filament(args.url, 0, 0) + +while True: + clf.connect(rdwr={"on-connect": on_nfc_connect}) + # No tag connected anymore. + if args.clear: + set_spool_and_filament(args.url, 0, 0) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7e27f90 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +nfcpy==1.0.4 +requests==2.31.0