A Pi Pico W based controller for Nevermore Max and StealthMax filters. This controller is BlueTooth LE enabled, minimising required wiring and allowing multiple clients to interact with the controller.
-
✓ BLE - Multiple Connections Supported
-
✓ Fan - Tachometer & Automatic Control
-
✓ NeoPixel Support
Refer to the BOM from whatever filter you’re building. This is just a basic guide for what kind of hardware is needed.
Note
|
Sensors are available in various sizes, form factors, and pin orders. Check your filter’s BOM to see which you should obtain. |
Part |
Name |
Quantity |
MCU |
Pico W |
1 |
Sensor |
VOC |
2 |
Temperature |
2 |
|
Humidity |
2 |
Note
|
Many sensor boards are multi-function. e.g. you’ll likely only need one board for temperature/humidity. |
The only hard requirement is a Pico W. Everything else can be omitted at the cost of reduced functionality.
The VOC sensors are critical; they measure the relevant contaminants we wish to filter out.
The temperature & humidity sensors are optional; they serve to improve the accuracy of the VOC sensor. If they’re omitted, the system will assume some sensible defaults instead.
Note
|
In the future, you’ll be able to use Klipper sensors/thermistors to provide these values. This isn’t as precise as dedicated sensors, but it’s better than nothing. e.g. You could use the temperature from your toolhead’s MCU for the intake temperature. |
Sensor |
Measures |
AHT{10, 20, 21} |
Humidity, Temperature |
BME280 |
Humidity, Temperature, Pressure |
BME680, BME688 |
Humidity, Temperature, Pressure [1] |
HTU2xD |
Humidity, Temperature |
SGP40 |
Volatile Organic Compounds |
Display |
Description |
GC9A01 |
Round, 240px^2 |
Name |
Description |
CST816S |
Display Touch Sensor |
NeoPixel |
RGB Bling |
Note
|
Make sure you meet the Klipper requirements. |
Instructions:
-
Build or download the controller
uf2
binary, flash to Pico W.The Pico W’s LED should start flashing once the controller has booted.
-
Execute the following to install the Klipper module:
cd ~ git clone https://github.com/SanaaHamel/nevermore-controller cd nevermore-controller ./install-klipper-module.bash
-
If you’re using Mainsail OS then the install script will ask if you wish to enable BlueTooth. Do so, and then restart your Klipper host. (e.g.
sudo reboot
) -
Add nevermore your printer config. Here’s a trivial configuration example you can use.
-
Update your printer macros.
-
Add
SET_FAN_SPEED FAN=nevermore_fan SPEED=1
to yourprint_start
macro.[2]This’ll let you kick-start the filter before the chamber gets detectably filthy. Example:
[gcode_macro PRINT_START] gcode: ... <SNIP YOUR CURRENT GCODE> ... # Insert right before heating extruder or bed # (e.g. M104, M109, M190, ...) # Override automatic control. # Start filtering before sensors can detect VOCs. SET_FAN_SPEED FAN=nevermore_fan SPEED=1 ... <SNIP YOUR CURRENT GCODE> ...
-
Add
SET_FAN_SPEED FAN=nevermore_fan
to yourTURN_OFF_HEATERS
macro.WarningThis assumes your print_end
macro callsTURN_OFF_HEATERS
. If it doesn’t you’ll want to putSET_FAN_SPEED FAN=nevermore_fan
in yourprint_end
macro to turn off the fan override.Easiest way would be to put this macro wrapper in your config:[3]
# ASSUME: Your `print_end` macro calls `TURN_OFF_HEATERS`. [gcode_macro TURN_OFF_HEATERS] rename_existing: NEVERMORE_CONTROLLER_INNER_TURN_OFF_HEATERS gcode: NEVERMORE_CONTROLLER_INNER_TURN_OFF_HEATERS # Clear the fan control override, we're cooling down # NB: Setting SPEED=0 does *NOT* clear control override. # It instead forces the speed to 0. # Omit `SPEED` argument entirely to clear override. SET_FAN_SPEED FAN=nevermore_fan
-
-
Check your printer’s log file. If everything went well you should see something like:
... BLAH ... BLAH Sending MCU 'mcu' printer configuration... Configured MCU 'mcu' (283 moves) ... BLAH ... BLAH [11:27:13:976834] nevermore - discovered controller 28:CD:C1:09:64:8F [11:27:13:981190] nevermore - connected to controller 28:CD:C1:09:64:8F ... BLAH ... BLAH
-
If you’ve flashed a OTA-capable UF2 to your controller (v0.3+) you can update it wirelessly.
If you’ve flashed a OTA-capable UF2 to your controller (v0.3+) you can update it wirelessly. The process is simple:
# switch to your nevermore-controller installation
cd ~/nevermore-controller
# fetch updates for klipper module and tools
git pull
# download & apply latest controller image
./tools/update_ota.py
The when you run update_ota.py
it will install any missing dependencies.
This can take a while the first time, depending on the machine’s capabilities.
If you have multiple controllers in range, you can specify which to update using --bt-address
. e.g. ./tools/update_ota.py --bt-address XX:XX:XX:XX:XX:XX
See ./tools/update_ota.py --help
for all options.
Note
|
The controller will automatically restart if left idle in bootloader mode for 60 seconds. |
Overall, you should see output similar to the following:
Tool environment seems up to date.
This program will attempt to update a Nevermore controller.
-------------------------------------------------------------------------
discovering Nevermores...
connecting to XX:XX:XX:XX:XX:XX
current revision: v0.7.0
sending reboot-to-OTA command...
connecting to device...
requesting device info...
sync w/ device...
trying to update bootloader...
requesting device info...
img size: 364544
erasing tail [0x10059000, 0x1005a000]...
updating: 100%|██████████████████████████████████████████████████████████████████████| 356k/356k [00:02<00:00, 129kb/s]
# I've already updated this controller, so nothing changed
update modified 0 of 364544 bytes (0.00%)
updating main image...
requesting device info...
img size: 390912
erasing tail [0x100bb000, 0x10200000]...
updating: 100%|██████████████████████████████████████████████████████████████████████| 384k/384k [00:03<00:00, 120kb/s]
update modified 0 of 393216 bytes (0.00%)
finalising...
rebooting...
update complete.
waiting for device to reboot (1 seconds)...
connecting to XX:XX:XX:XX:XX:XX to get installed version
(this may take longer than usual)
NOTE: Ignore logged exceptions about `A message handler raised an exception: 'org.bluez.Device1'.`
This is caused by a bug in `bleak` but should be benign for this application.
previous version: v0.7.0 # whatever version was installed
current version: v0.7.0 # in this example it tried to update to the same version
-
The controller is properly flashed (e.g. the LED is blinking) but my Klipper can’t connect to it.
Make sure the Bluetooth is turned on & working. If you’re using Linux you can use the following to check:
⋊> ~ # ensure BT is on ⋊> ~ bluetoothctl power on Changing power on succeeded ⋊> ~ # scan to see if we see any BT devices ⋊> ~ # if the nevermore controller is powered on then you should see it listed here ⋊> ~ bluetoothctl scan on Discovery started [CHG] Controller XX:XX:XX:XX:XX:XX Discovering: yes [NEW] Device XX:XX:XX:XX:XX:XX <censored> [NEW] Device XX:XX:XX:XX:XX:XX <censored> ^C⏎
If
bluetoothctl
works fine and the scan lists the Nevermore controller then something probably went wrong with the Klipper module. Go find me on the Nevermore Discord for help.
-
I’m using MainsailOS and I’m having trouble with BlueTooth.
This distro disables BlueTooth by default. [4] Please follow this guide to enable BlueTooth. Alternatively, the install script will attempt to apply the changes for you.
Alternatively, you can flash Klipper to the Pico and use it like any other Klipper MCU.
NoteI intend to improve the experience for people using a wired connection instead of wireless (via the Klipper MCU), but have no concrete timeline. -
I’m using the minimal configuration and I only see the VOC plot entry in Mainsail, there’s no 'Nevermore' item.
Verify that your Mainsail version is at least 2.7.1 (first release w/ official support). If that’s fine then double check there isn’t any config errors.
-
-
Fluidd doesn’t show sensor values other than temperature, even with the
class_name_override
hack.Can’t fix due to how Fluidd interprets Klipper state data. There’s a PR fixing the issue, but it hasn’t received much support.
Fluidd is more or less unsupported with regards to sensor display and visualisation. I recommend Mainsail 2.7.1 or later instead.
Touch display support is early in development and currently very limited. For now you can:
-
Long press on the center plot to toggle the fan override on/off
-
Press/drag on the fan power ring to set the fan override to a specific percent
-
Pico-W SDK 1.5.1+
-
CMake 3.20+
-
C+23 compiler, e.g. GCC 12 (tested w/ 12.2.1)
src/config.hpp
contains all user-customisable options.
These options are, for the most part, validated at compile time to prevent mistakes.
Pins assignments can be modified to suit your board/wiring, but are subject to hardware-related constraints. These are constraints are extensively checked at compile time, and will result in a (hopefully) useful error message if violated. If it compiles, it’s a valid configuration.
Warning
|
GPIO 0 and 1 are currently hardcoded for UART. They cannot be used in any pin assignments. |
Warning
|
The default assignments are tentative and will probably change after we get some feedback as to which layouts work best in practice. |
GPIO |
Function |
0 |
UART - TX |
1 |
UART - RX |
2 |
Display - GC9A01 - SPI SCK |
3 |
Display - GC9A01 - SPI TX |
4 |
Display - GC9A01 - SPI RX (not used, for future hardware) |
5 |
Display - GC9A01 - Command |
6 |
Display - GC9A01 - Reset |
7 |
Display - Backlight Brightness PWM |
8 |
Display Touch - CST816S - Interrupt |
9 |
Display Touch - CST816S - Reset |
12 |
NeoPixel - Data |
13 |
Fan - PWM |
15 |
Fan - Tachometer |
18 |
Exhaust - I2C SDA |
19 |
Exhaust - I2C SCL |
20 |
Intake - I2C SDA |
21 |
Intake - I2C SCL |
-
Klipper using Python 3.7+
-
KIAUH-like installation (required by installation script)
TL;DR: If you installed everything using KIAUH, you should be good to go so long as you installed Klipper with Python 3.
This example configuration is intended for quickly getting up and running. You can just copy paste this into your printer’s config.
[nevermore]
# BOM specifies a 16 pixel ring.
# If you don't have LEDs, you can omit the two `led_*` lines entirely
led_colour_order: GBR
led_chain_count: 16
# Optional
# This 'temperature' sensor only serves to draw the intake VOC index on
# Mainsail's temperature plot.
[temperature_sensor nevermore_intake_VOC]
sensor_type: NevermoreSensor
sensor_kind: intake
plot_voc: true
WS2812 pixel strips can be used just like any other WS2812 pixel strip connected to your Klipper instance. This includes support for LED effects.
# led-effects are supported, here's an example:
[led_effect panel_idle]
autostart: true
frame_rate: 24
leds:
nevermore
layers:
comet 1 0.5 add (0.0, 0.0, 0.0),(1.0, 0.0, 0.0),(1.0, 1.0, 0.0),(1.0, 1.0, 1.0)
breathing 2 1 top (0,.25,0)
Warning
|
Don’t copy-paste this into your config/ It won’t give you a working setup. Follow the setup guide if you have any doubts. |
This section lists all options and defaults. Some minor examples are also provided.
Note
|
The values shown here are either the default for that option or a placeholder. |
Note
|
If you don’t care about a setting, leave it unset. Suggested defaults will change over time based on user feedback. |
# DON'T JUST COPY PASTE THIS INTO YOUR CONFIG.
# READ THE SETUP GUIDE.
[nevermore]
# Can omit if you have only one nevermore in range.
# See <<Finding The BT Address>> for more info.
# NOTE: Providing an address will make startup slightly faster.
# (If no address is provided then the system must spend extra time
# verifying that there's only one nearby Nevermore.)
# example - `bt_address: 43:43:A2:12:1F:AC`
bt_address: <optional, omitted by default>
# seconds, 0 to disable, how long to wait before reporting that the Nevermore is missing.
# If disabled (set to 0) the module will keep trying to connect in the background.
# Disabling this requires that `bt_address` is set.
#
# WARNING: Do not disable unless you've already tested that it can connect to the Nevermore.
# WARNING: If you set this < 10 you will likely have trouble connecting to the Nevermore.
# NOTE: The module quietly keeps trying to reconnect if connection is lost after startup.
# NOTE: It takes some amount of time to reliably scan & connect to Nevermore.
# This varies on a few factors outside of your control, so the system
# will reject unfeasibly small timeout values to keep you from screwing
# yourself over.
connection_initial_timeout: <default varies based on whether `bt_address` is set>
# LED
# For the optional LED ring feature.
# Members generally behaves like the WS2812 klipper module.
# (e.g. supports heterogenous pixel chains)
led_colour_order: GRB
led_chain_count: 0
# Fan Options
# Various settings for the fan.
# float \in [0, 1] - Fan power used when the automatic fan policy is active.
fan_power_automatic: 1
# float \in [0, 1] - Coefficient applied to the fan power.
# i.e. Limits the maximum speed of the fan. Useful for things like managing noise.
# e.g. At 0.75, requesting 100% power will run the fan at 75% power.
fan_power_coefficient: 1
# Fan Policy
# Controls how/when the fan turns on automatically.
# seconds, how long to keep filtering after the policy would otherwise stop
fan_policy_cooldown: 900
# voc index, 0 to disable, filter if any sensor meets this threshold
fan_policy_voc_passive_max: 125
# voc index, 0 to disable, filter if the intake exceeds exhaust by at least this much
fan_policy_voc_improve_min: 25
# Fan Policy - Thermal Limit
# Controls how/when the fan power is throttled down if the temperature is too high.
# See Fan Control section for details.
# float, Celsius, temperature at which point thermal limiting starts being applied
fan_thermal_limit_temperature_min: 50
# float, Celsius, temperature at which point thermal limiting is fully applied
fan_thermal_limit_temperature_max: 60
# float \in [0, 1], -1 to disable the thermal limiter entirely
fan_thermal_limit_coefficient: 0
# Misc. Sensor Options
# If temperature, humidity, etc, is unavailable on one side of the filter then
# report the value from the other side (if available).
# Useful for builds where you only have one temperature or humidity sensor,
# and you want to use it for both intake/exhaust.
sensors_fallback: false
# Use the MCU's temperature as an exhaust temperature fallback.
# Only useful for filters which have the MCU in the exhaust airflow (e.g. StealthMax)
# and don't have any dedicated temperature sensors.
sensors_fallback_exhaust_mcu: false
# MOSTLY OBSOLETE.
# Mainsail 2.7.1 introduced dedicated support for Nevermore controllers, simply having
# `[nevermore]` is sufficient to display sensor values in the 'Temperatures' panel.
#
# Only remaining useful behaviour for `temperature_sensors` is the `plot_voc` option
# which allows drawing the VOC index values for intake/exhaust in the temperature plot.
[temperature_sensor <name>]
sensor_type: NevermoreSensor # fixed, must be `NevermoreSensor`
# valid values: `intake`, `exhaust`
sensor_kind: <required, no defaults>
# Mainsail 2.7.1 doesn't recognise `NevermoreSensor` as sensor it should plot.
# This hacky option allows overriding the class name with one it does recognise
# as something that should be plotted.
# Using `bme280` is strongly suggested.
class_name_override: <optional, not set by default>
# Pretends the VOC index is a temperature, allowing it to be plotted in Mainsail/Fluidd.
# Setting this to `true` will suppress the all other readings for this sensor object.
# (e.g. temperature, pressure, etc)
plot_voc: false
If you have only one Nevermore controller in range then you can omit the bt_address
option in your printer configuration and ignore this section entirely.
If you have multiple BlueTooth (BT) devices in range that look like candidates for a Nevermore controller, then you have to specify which one to use. This is done by specifying their 'address' in the printer config using bt_address: <address>
.
On Linux and Windows hosts, this address looks like XX:XX:XX:XX:XX:XX
, where X
is a hexadecimal digit.
On MacOS hosts, this address is a randomly assigned UUID specific to that host.
Note
|
It is possible, but very rare, for the address to change when a new uf2 is flashed onto the Pico. This has been observed once after updating the Pico SDK.
|
An error will be raised if there are multiple controllers in range. The error message will list all the available controllers' addresses.
Pick one from the list and stuff that into the nevermore
section’s bt_address
.
For example, given this log:
...
...
[11:06:36:535560] nevermore - multiple nevermore controllers discovered.
specify which to use by setting `bt_address: <insert-address-here>` in your klipper config.
discovered controllers (ordered by signal strength):
address | signal strength
-----------------------------------
FA:KE:AD:RE:SS:01 | -38 dBm
FA:KE:AD:RE:SS:00 | -57 dBm
Config error
Traceback (most recent call last):
File "~/klipper/klippy/klippy.py", line 180, in _connect
cb()
File "~/klipper/klippy/extras/nevermore.py", line 793, in _handle_connect
raise self.printer.config_error("nevermore failed to connect - timed out")
configparser.Error: nevermore failed to connect - timed out
...
...
We could use bt_address: FA:KE:AD:RE:SS:01
or bt_address: FA:KE:AD:RE:SS:00
.
In this case I’d plug in FA:KE:AD:RE:SS:01
since that device has the strongest signal, i.e. closest-ish to the Klipper host.
Note
|
Only works on Linux. Yes, I know you didn’t read the title. |
-
Make sure your Nevermore controller is powered and the LED is blinking. (Indicates it is active.)
-
In a terminal, run:
bluetoothctl
This’ll open a REPL interface.
⋊> ~ bluetoothctl Agent registered [CHG] Controller FA-KE-AD-RE-SS-FF Pairable: yes [bluetooth]#
-
Run:
scan on
, wait a few seconds (~5 or 6 is plenty)Starts background scan for devices. This isn’t a blocking command, you can issue other commands as it scans in the background.
[bluetooth]# scan on Discovery started [CHG] Controller FA-KE-AD-RE-SS-FF Discovering: yes [NEW] Device FA:KE:AD:RE:SS:05 <censored> [NEW] Device FA:KE:AD:RE:SS:00 Nevermore [CHG] Device FA:KE:AD:RE:SS:05 RSSI: -53 [CHG] Device FA:KE:AD:RE:SS:04 ManufacturerData Key: 0x004c ... [DEL] Device FA:KE:AD:RE:SS:04 FA-KE-AD-RE-SS-04 [NEW] Device FA:KE:AD:RE:SS:04 FA-KE-AD-RE-SS-04 ...
WarningIf you wait too long (~15-20 seconds), the scan ends, and the host will forget about the devices it discovered. -
Run:
devices
[bluetooth]# devices Device FA:KE:AD:RE:SS:05 <censored> Device FA:KE:AD:RE:SS:01 Nevermore Device FA:KE:AD:RE:SS:04 FA-KE-AD-RE-SS-04 Device FA:KE:AD:RE:SS:00 Nevermore Device FA:KE:AD:RE:SS:02 FA-KE-AD-RE-SS-02 Device FA:KE:AD:RE:SS:03 FA-KE-AD-RE-SS-03
Look for the entries named "Nevermore" or "Nevermore Controller", and copy their address into the printer configuration.
In this example, we could use
bt_address: FA:KE:AD:RE:SS:00
orbt_address: FA:KE:AD:RE:SS:01
.
Warning
|
If you’re hosting Klipper on MacOS then you cannot use this approach and must use Method A - Check the Klipper Log. |
nRF Connect is an app by Nordic Semi. It’s meant for debugging/exploring BLE devices, but we can (ab)use to find the BT addresses.
Load the app, scan for BLE devices. The controllers will all be named "Nevermore", and their BT addresses will be listed below.
There are two modes of operation:
-
Automatic - Fan power is managed by the controller based on its fan policy (see here).
-
Manual - Fan power is overridden and will run at the specified power until the override is cleared.
In both cases, the fan power is scaled by two factors:
-
The
fan_power_coefficient
setting scales in all cases. Useful for limiting noise since the StealthMax recommended fans are more powerful than strictly needed. -
Thermal Limiting scales the actual fan power applied based on the maximum of the intake and exhaust temperatures. This is intended to improve the carbon’s effective lifespan, which degrades at high temperatures. This feature can be disabled by setting
fan_thermal_limit_coefficient: -1
.
From within Klipper, the fan can be controlled much like any other fan:
; override automatic fan control, full speed ahead
SET_FAN_SPEED FAN=nevermore_fan SPEED=1
; not specifying `SPEED=` disables fan override and returns to automatic fan control
SET_FAN_SPEED FAN=nevermore_fan
Warning
|
Setting the fan speed to 0 in Mainsail/Fluidd UI does not clear the control override. It just sets it to zero. (i.e. disables the fan) |
If you would like to limit the maximum speed of the fan, e.g. to reduce noise, set fan_power_coefficient
to a value < 1.
-
Julian Schill - installation script (derived)
-
Bosch Sensors - BME280, BME68x library (included)
-
https://github.com/sciosense/ENS160_driver [ScioSense] - ENS160 library (referenced)
-
Sensirion - SGP30 library (referenced)
-
Sensirion - SGP40 gas index library (included)
-
https://github.com/klipper3d/klipper [Klipper] - AHTxx library (referenced)
-
Apache Nuttx - I2C software reset (derived)
-
0ndsk4 - Donated hardware for testing
print_start
probably does a lot of things (homing, QGL, purge, etc).
TURN_OFF_HEATERS
is a built-in macro, and should never be overridden w/o calling the replaced macro, so it doesn’t matter if another macro ends up wrapping this wrapper.