Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
bofh69 committed Mar 5, 2024
1 parent 713dcb0 commit 4c9cb07
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 1 deletion.
52 changes: 52 additions & 0 deletions .github/workflows/bandit.yml
Original file line number Diff line number Diff line change
@@ -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

39 changes: 39 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
@@ -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')
83 changes: 82 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,83 @@
# 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 (Elechouse PN532 NFC RFID Module V3, if you
want to use the same) connected via UART to the raspberry pi where this
program is running.


Many pages suggest connecting its VCC pin to 5V on the RPi. Don't!
It can run from 3.3V and then it won't risk destroying the RPi's GPIO pins.


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?
143 changes: 143 additions & 0 deletions nfc2klipper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/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",
)


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 get_data_from_ndef_records(records):
"""Find wanted data from the NDEF records.
>>> import ndef
>>> record0 = ndef.TextRecord("")
>>> record1 = ndef.TextRecord("SPOOL:23\\n")
>>> record2 = ndef.TextRecord("FILAMENT:14\\n")
>>> record3 = ndef.TextRecord("SPOOL:23\\nFILAMENT:14\\n")
>>> get_data_from_ndef_records([record0])
(None, None)
>>> get_data_from_ndef_records([record3])
('23', '14')
>>> get_data_from_ndef_records([record1])
('23', None)
>>> get_data_from_ndef_records([record2])
(None, '14')
>>> get_data_from_ndef_records([record0, record3])
('23', '14')
>>> get_data_from_ndef_records([record3, record0])
('23', '14')
>>> get_data_from_ndef_records([record1, record2])
('23', '14')
>>> get_data_from_ndef_records([record2, record1])
('23', '14')
"""

spool = None
filament = None

for record in records:
if record.type == NDEF_TEXT_TYPE:
for line in record.text.splitlines():
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}")

return spool, filament


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, filament = get_data_from_ndef_records(tag.ndef.records)

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


if __name__ == "__main__":
args = parser.parse_args()

# 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)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nfcpy==1.0.4
requests==2.31.0

0 comments on commit 4c9cb07

Please sign in to comment.