Skip to content

Commit 1ca77d4

Browse files
authored
Merge pull request #6 from bofh69/add_web_interface
Add web interface
2 parents cb3f3d0 + 998c746 commit 1ca77d4

File tree

14 files changed

+410
-124
lines changed

14 files changed

+410
-124
lines changed

.github/workflows/pylint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
steps:
1212
- uses: actions/checkout@v4
1313
- name: Set up Python ${{ matrix.python-version }}
14-
uses: actions/setup-python@v3
14+
uses: actions/setup-python@v5
1515
with:
1616
python-version: ${{ matrix.python-version }}
1717
- name: Install dependencies

.github/workflows/reuse.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
steps:
1717
- uses: actions/checkout@v4
1818
- name: Set up Python
19-
uses: actions/setup-python@v4
19+
uses: actions/setup-python@v5
2020
with:
2121
python-version: '3.10'
2222
- name: Install dependencies

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ dist/
1414
downloads/
1515
eggs/
1616
.eggs/
17-
lib/
1817
lib64/
1918
parts/
2019
sdist/

.reuse/dep5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Source: https://github.com/bofh69/nfc2klipper
55

66
# Sample paragraph, commented out:
77
#
8-
Files: .github/* requirements.txt .gitignore nfc2klipper.service
8+
Files: .github/* requirements.txt .gitignore nfc2klipper.service nfc2klipper-config.json5
99
Copyright: $YEAR $NAME <$CONTACT>
1010
License: CC0-1.0

README.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ virtualenv venv
2020
venv/bin/pip3 install -r requirements.txt
2121
```
2222

23+
Update the config in `nfc2klipper-config.json5`.
24+
2325
## Preparing an NFC reader
2426

2527
I use a PN532 based reader (Elechouse PN532 NFC RFID Module V3, if you
@@ -103,16 +105,35 @@ FILAMENT:2
103105
The numbers are the id numbers that will be sent to the macros in
104106
klipper via the [Moonraker](https://github.com/Arksine/moonraker) API.
105107

108+
### Write tags with the **extperimental** web server
109+
110+
It is possible to enable an **experimental** web server in `nfc2klipper.py`.
111+
It will then serve a web page for writing to the tags.
112+
The default address will be `http://mainsailos.local:5001/`,
113+
where `mainsailos.local` should be replaced with the computer's name (or IP address).
114+
115+
The program uses a development web server with **no security** at all so it
116+
shouldn't be run if the computer is running on an untrusted network.
117+
118+
The program has a configuration file (nfc2klipper-config.json5) for
119+
enabling the web server, setting the port number, addresses to moonraker
120+
and mainsail, the webserver's address and NFC device to use.
121+
122+
123+
### Write with an app
124+
125+
There is an Android app, [Spoolman Companion](https://github.com/V-aruu/SpoolCompanion), for writing
126+
to the tags.
106127

107-
One way to do this is to use the Android app [Spoolman Companion](https://github.com/V-aruu/SpoolCompanion).
128+
### Write with console application
108129

130+
The `write_tags.py` program fetches Spoolman's spools, shows a simple
131+
text interface where the spool can be chosen, and when pressing return,
132+
writes to the tag.
109133

110-
One can also use the `write_tags.py` program included here.
111-
It fetches Spoolman's filaments, shows a simple text interface where
112-
the spool can be chosen, and when pressing return, writes to the tag.
134+
Use the `write_tag` script to stop the nfc2klipper service, run the
135+
`write_tags.py` program and then start the service again after.
113136

114-
Use the `write_tag` script to stop the nfc2klipper service, run the write_tags.py program and
115-
then start the service again after.
116137

117138

118139
## Run automaticly with systemd

lib/__init__.py

Whitespace-only changes.

lib/moonraker_web_client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SPDX-FileCopyrightText: 2024 Sebastian Andersson <sebastian@bittr.nu>
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
"""Moonraker Web Client"""
5+
6+
import requests
7+
8+
9+
# pylint: disable=R0903
10+
class MoonrakerWebClient:
11+
"""Moonraker Web Client"""
12+
13+
def __init__(self, url: str):
14+
self.url = url
15+
16+
def set_spool_and_filament(self, spool: int, filament: int):
17+
"""Calls moonraker with the current spool & filament"""
18+
19+
commands = {
20+
"commands": [
21+
f"SET_ACTIVE_SPOOL ID={spool}",
22+
f"SET_ACTIVE_FILAMENT ID={filament}",
23+
]
24+
}
25+
26+
response = requests.post(
27+
self.url + "/api/printer/command", timeout=10, json=commands
28+
)
29+
if response.status_code != 200:
30+
raise ValueError(f"Request to moonraker failed: {response}")

lib/nfc_handler.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# SPDX-FileCopyrightText: 2024 Sebastian Andersson <sebastian@bittr.nu>
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
""" NFC tag handling """
5+
6+
import time
7+
from threading import Lock, Event
8+
9+
import ndef
10+
import nfc
11+
12+
13+
SPOOL = "SPOOL"
14+
FILAMENT = "FILAMENT"
15+
NDEF_TEXT_TYPE = "urn:nfc:wkt:T"
16+
17+
18+
# pylint: disable=R0902
19+
class NfcHandler:
20+
"""NFC Tag handling"""
21+
22+
def __init__(self, nfc_device: str):
23+
self.status = ""
24+
self.nfc_device = nfc_device
25+
self.on_nfc_no_tag_present = None
26+
self.on_nfc_tag_present = None
27+
self.should_stop_event = Event()
28+
self.write_lock = Lock()
29+
self.write_event = Event()
30+
self.write_spool = None
31+
self.write_filament = None
32+
33+
def set_no_tag_present_callback(self, on_nfc_no_tag_present):
34+
"""Sets a callback that will be called when no tag is present"""
35+
self.on_nfc_no_tag_present = on_nfc_no_tag_present
36+
37+
def set_tag_present_callback(self, on_nfc_tag_present):
38+
"""Sets a callback that will be called when a tag has been read"""
39+
self.on_nfc_tag_present = on_nfc_tag_present
40+
41+
@classmethod
42+
def get_data_from_ndef_records(cls, records: ndef.TextRecord):
43+
"""Find wanted data from the NDEF records.
44+
45+
>>> import ndef
46+
>>> record0 = ndef.TextRecord("")
47+
>>> record1 = ndef.TextRecord("SPOOL:23\\n")
48+
>>> record2 = ndef.TextRecord("FILAMENT:14\\n")
49+
>>> record3 = ndef.TextRecord("SPOOL:23\\nFILAMENT:14\\n")
50+
>>> NfcHandler.get_data_from_ndef_records([record0])
51+
(None, None)
52+
>>> NfcHandler.get_data_from_ndef_records([record3])
53+
('23', '14')
54+
>>> NfcHandler.get_data_from_ndef_records([record1])
55+
('23', None)
56+
>>> NfcHandler.get_data_from_ndef_records([record2])
57+
(None, '14')
58+
>>> NfcHandler.get_data_from_ndef_records([record0, record3])
59+
('23', '14')
60+
>>> NfcHandler.get_data_from_ndef_records([record3, record0])
61+
('23', '14')
62+
>>> NfcHandler.get_data_from_ndef_records([record1, record2])
63+
('23', '14')
64+
>>> NfcHandler.get_data_from_ndef_records([record2, record1])
65+
('23', '14')
66+
"""
67+
68+
spool = None
69+
filament = None
70+
71+
for record in records:
72+
if record.type == NDEF_TEXT_TYPE:
73+
for line in record.text.splitlines():
74+
line = line.split(":")
75+
if len(line) == 2:
76+
if line[0] == SPOOL:
77+
spool = line[1]
78+
if line[0] == FILAMENT:
79+
filament = line[1]
80+
else:
81+
print(f"Read other record: {record}", flush=True)
82+
83+
return spool, filament
84+
85+
def write_to_tag(self, spool: int, filament: int) -> bool:
86+
"""Writes spool & filament info to tag. Returns true if worked."""
87+
88+
self._set_write_info(spool, filament)
89+
90+
if self.write_event.wait(timeout=30):
91+
return True
92+
93+
self._set_write_info(None, None)
94+
95+
return False
96+
97+
def run(self):
98+
"""Run the NFC handler, won't return"""
99+
# Open NFC reader. Will throw an exception if it fails.
100+
with nfc.ContactlessFrontend(self.nfc_device) as clf:
101+
while not self.should_stop_event.is_set():
102+
tag = clf.connect(rdwr={"on-connect": lambda tag: False})
103+
if tag:
104+
self._check_for_write_to_tag(tag)
105+
if tag.ndef is None:
106+
if self.on_nfc_no_tag_present:
107+
self.on_nfc_no_tag_present()
108+
else:
109+
self._read_from_tag(tag)
110+
while clf.connect(rdwr={"on-connect": lambda tag: False}):
111+
if self._check_for_write_to_tag(tag):
112+
self._read_from_tag(tag)
113+
time.sleep(0.1)
114+
else:
115+
time.sleep(0.1)
116+
117+
def stop(self):
118+
"""Call to stop the handler"""
119+
self.should_stop_event.set()
120+
121+
def _write_to_nfc_tag(self, tag, spool: int, filament: int) -> bool:
122+
"""Write given spool/filament ids to the tag"""
123+
try:
124+
if tag.ndef and tag.ndef.is_writeable:
125+
tag.ndef.records = [
126+
ndef.TextRecord(f"{SPOOL}:{spool}\n{FILAMENT}:{filament}\n")
127+
]
128+
return True
129+
self.status = "Tag is write protected"
130+
except Exception as ex: # pylint: disable=W0718
131+
print(ex)
132+
self.status = "Got error while writing"
133+
return False
134+
135+
def _set_write_info(self, spool, filament):
136+
if self.write_lock.acquire(): # pylint: disable=R1732
137+
self.write_spool = spool
138+
self.write_filament = filament
139+
self.write_event.clear()
140+
self.write_lock.release()
141+
142+
def _check_for_write_to_tag(self, tag) -> bool:
143+
"""Check if the tag should be written to and do it"""
144+
did_write = False
145+
if self.write_lock.acquire(): # pylint: disable=R1732
146+
if self.write_spool:
147+
if self._write_to_nfc_tag(tag, self.write_spool, self.write_filament):
148+
self.write_event.set()
149+
did_write = True
150+
self.write_spool = None
151+
self.write_filament = None
152+
self.write_lock.release()
153+
return did_write
154+
155+
def _read_from_tag(self, tag):
156+
"""Read data from tag and call callback"""
157+
if self.on_nfc_tag_present:
158+
spool, filament = NfcHandler.get_data_from_ndef_records(tag.ndef.records)
159+
self.on_nfc_tag_present(spool, filament)

lib/spoolman_client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SPDX-FileCopyrightText: 2024 Sebastian Andersson <sebastian@bittr.nu>
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
"""Spoolman client"""
5+
6+
import json
7+
import requests
8+
9+
10+
# pylint: disable=R0903
11+
class SpoolmanClient:
12+
"""Spoolman Web Client"""
13+
14+
def __init__(self, url: str):
15+
if url.endswith("/"):
16+
url = url[:-1]
17+
self.url = url
18+
19+
def get_spools(self):
20+
"""Get the spools from spoolman"""
21+
url = self.url + "/api/v1/spool"
22+
response = requests.get(url, timeout=10)
23+
if response.status_code != 200:
24+
raise ValueError(f"Request to spoolman failed: {response}")
25+
records = json.loads(response.text)
26+
return records

nfc2klipper-config.json5

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"disable_web_server": true,
3+
4+
/// The address the web server listens to,
5+
/// use 0.0.0.0 for all IPv4
6+
"web_address": "0.0.0.0",
7+
8+
/// The port the web server listens to
9+
"web_port": 5001,
10+
11+
/// Clear the spool & filament info if no tag can be read
12+
"clear-spool": false,
13+
14+
/// Which NFC reader to use, see
15+
/// https://nfcpy.readthedocs.io/en/latest/topics/get-started.html#open-a-local-device
16+
"nfc-device": "ttyAMA0",
17+
18+
/// URL for the moonraker installation
19+
"moonraker-url": "http://mainsailos.local",
20+
21+
/// URL for the moonraker installation
22+
"spoolman-url": "http://mainsailos.local:7912",
23+
}

0 commit comments

Comments
 (0)