Skip to content

SanaaHamel/nevermore-controller

Repository files navigation

Nevermore Controller

Features

Required Hardware

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 the filter’s BOM to see which you should obtain.
Table 1. Recommended Hardware

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 RP2040 board. 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 the toolhead’s MCU for the intake temperature.

Supported Hardware

Table 2. Supported Sensors

Sensor

Measures

Notes

AHT{10, 20, 21}

Humidity, Temperature

BMP280

Temperature, Pressure

Do not use; does not measure humidity. [1]

BME280

Humidity, Temperature, Pressure

BME680, BME688

Humidity, Temperature, Pressure

Cannot use gas sensor. [2]

HTU2xD

Humidity, Temperature

SGP30

Volatile Organic Compounds

Deprecated. [3]

SGP40

Volatile Organic Compounds

SHT4x

Humidity, Temperature

Table 3. Supported Displays

Display

Description

GC9A01

Round, 240px^2

Table 4. Supported Miscellaneous Hardware

Name

Description

CST816S

Display Touch Sensor

NeoPixel

RGB Bling

Setup Guide For Dummies

Note
Make sure you meet the Klipper requirements.

Instructions:

  1. Build or download the controller uf2 binary, flash to the RP2040 board (e.g. Pico W).

    If the board has a simple on-board LED, then it should start flashing once the controller has booted.

  2. SSH into the printer. Execute the following to install the Klipper module:

    cd ~
    git clone https://github.com/SanaaHamel/nevermore-controller
    cd nevermore-controller
    ./install-klipper-module.bash
  3. If you’re using Mainsail OS then the install script will ask if you wish to enable BlueTooth. Do so, and then restart the Klipper host. (e.g. sudo reboot)

  4. Add nevermore to the printer config. Here’s a trivial configuration example you can use.

  5. Verify Klipper managed to connect to the controller(s) by checking the printer’s logs:

    The log should contain lines similar to:

    Starting Klippy...
    ... BLAH
    ... BLAH
    ===== Config file =====
    ... BLAH
    ... BLAH
    =======================
    Extruder max_extrude_ratio=... BLAH
    mcu 'mcu': Starting CAN connect
    ... BLAH
    ... BLAH
    # lines saying discovered controller & connected
    [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
  6. Calibrate the sensors. See the calibration section in the VOC Guide.

  7. You can apply updates the controller without removing it from the filter. See the Updating Guide.

Configured Nevermores will automatically turn on/off depending on whether any extruder heaters are active. They will also turn on/off depending on sensor values and configured fan policies.

Updating Guide For Dummies

If you’ve flashed a OTA-capable UF2 to the controller (v0.3+) you can update it without removing it from the filter. The process is simple:

# switch to the 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.

If you’re using serial, use --serial <same path specified in the klipper cfg> instead of --bt-address.

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

Uploading Specific Versions & Custom Builds

You may specify --tag <release-tag> to upload a specific release instead of the latest. e.g. ./tools/update_ota.py --tag v0.15.1 to download v0.15.1.

You can also upload custom builds using --file. These builds must include a PicoWOTA bootloader; by convention these UF2 files are prefixed with picowota_ota-.

If Sanaa sends you a custom build via Discord you can usually apply it as follows:

  1. Right click on download → "Copy link"

  2. Open a SSH shell and run:

cd ~/nevermore-controller
wget -O picowota_ota-custom.uf2 "<paste link, make sure it is quoted>"
./tools/update_ota.py <serial or bt-address> --file "./picowota_ota-custom.uf2"
Warning
Custom builds are often unstable and can break the bootloader. If something goes wrong you may have to flash an official release using USB & the boot button.

Getting Console Logs Via USB / UART

If you run into any problems that look hardware related, you can plug the controller via USB or use UART (pins 0, 1) to get logs. In rare cases USB output might not work, but UART always should. If you have a debug build, this will also work in bootloader mode.

Warning
When using UART, always connect a shared ground pin between the UART adapter and the Pico before connecting the UART pins. Failure to have a shared 0v can result in hardware damage.

The following assume you’re on Linux (you can use the printer’s Klipper host) and using USB. Using UART should be identical, just use the UART adapter’s serial device instead of the Nevermore directly.

  1. If you’re using UART instead of USB then connect a shared ground pin before doing anything else. See the big note/warning above that you ignored.

  2. Plug in the controller using a USB cable.

    The controller should now be visible as a serial device at /dev/serial/by-id/usb-Nevermore_Filter_<board>_<device-id>_if00.

Note
You want the first interface (ends with _if00), not the second (ends with _if02).
  1. Open a terminal and run minicom -c on -b 115200 -O timestamp=extended -D /dev/serial/by-id/usb-Nevermore_Filter_<board>_<device-id>_if00.

    You will probably get a screen that looks like this:

    Welcome to minicom 2.8
    
    OPTIONS: I18n
    Port /dev/serial/by-id/usb-Raspberry_Pi_Pico_Nevermore_E6616408432C432E-if00, 15:36:28
    
    Press CTRL-A Z for help on special keys
Note
Want to save this log to a file? (e.g. You’re debugging a periodic crash.) Add -C controller.log to the command line arguments to save a copy of the log in controller.log.
Note
Need a long term log? Use tmux (or equiv) to keep minicom alive even if SSH disconnects.
  1. Restart the controller using one of the following:

    1. Use the reset button (if the board has one).

    2. Reboot it via NEVERMORE_REBOOT or directly via BLE.

    3. Unplug the controller and plug it back in (assuming it is powered by USB only).

  2. The minicom session should now look like this:

Welcome to minicom 2.8

OPTIONS: I18n
Port /dev/serial/by-id/usb-Raspberry_Pi_Pico_Nevermore_E6616408432C432E-if00, 15:36:28

Press CTRL-A Z for help on special keys

Checking settings slot #0
corrupt settings: size=0xffffffff not in range [0x0000000c, 0x00001000]
Checking settings slot #1
Checking settings slot #2
corrupt settings: size=0xffffffff not in range [0x0000000c, 0x00001000]
Checking settings slot #3
corrupt settings: size=0xffffffff not in range [0x0000000c, 0x00001000]
Restored settings from slot #1 (CRC: 0x4a1427d1)
DEBUG - SQUARE WAVE pin=10 w/ 30 hz @ 50.00% duty
        div=63.10 top=65487 level=32744
I2C bus 0 running at 399361 baud/s (requested 400000 baud/s)
I2C bus 1 running at 399361 baud/s (requested 400000 baud/s)
SPI bus 0 running at 62500000 baud/s (requested 62500000 baud/s)
[Warn]  (1.017, +1017)   lv_init: Style sanity checks are enabled that uses more RAM    (in lv_obj.c line #181)
BLE GATT - ready; address is 28:CD:C1:0B:7B:63
Waiting 100 ms for sensor init
I2C0 - initializing sensors...
ERR - [I2C0 ***] *** - write failed; len=*** result=-2  # expect lots of these lines
I2C1 - initializing sensors...
ERR - [I2C1 ***] *** - write failed; len=*** result=-2  # expect lots of these lines
...

I2C errors during startup are generally normal and expected; that’s how the system probes for sensors. If you see !! No sensors found?, however, you probably have a problem (unless there are no sensors connected).

When a sensor is found, there will be a line saying so (e.g. Found SGP30, or Found BME280).

FAQ / Known Issues

  • The controller’s LED is blinking very quickly and I can’t connect to it.

    The controller is in bootloader mode. If the image isn’t corrupted it’ll restart in application mode in about 60 seconds if you leave it alone. If it is corrupted, it won’t reboot and will stay in bootloader mode to let you upload a valid image using the update tool.

  • The controller is properly flashed (e.g. the LED is blinking) but Klipper can’t connect to it using BlueTooth.

    There are several possible causes:

    1. Verify sure BlueTooth is turned on & working. If you’re using Linux, you can use the following to verify:

      ⋊> ~ # ensure BT is on
      ⋊> ~ bluetoothctl power on
      Changing power on succeeded
      ⋊> ~ # scan to see if we see any BT devices
      ⋊> ~ 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 doesn’t work or the scan doesn’t list any BlueTooth devices then there’s something wrong with the OS’s configuration and/or BlueTooth adapter. You’ll need to fix that first (see other FAQ entries for some ideas).

    2. Verify that the BlueTooth adapter can connect to the device. If you’re on Linux, follow this procedure to find and connect directly to the controller.

    3. Verify that both the Klipper installation and the controller are the same release version.

      If the printer log has exceptions similar to:

      Exception: 4553d138-1d00-4b6f-bc42-955a89cf8c36 (Handle: 67): Unknown doesn't have exactly N characteristic(s) 00002b04-0000-1000-8000-00805f9b34fb with properties ...

      Then you probably have a mismatch between the controller and Klipper module.

    If you’ve checked all of the above and you still have exceptions in the printer log then you may go find Sanaa on the Nevermore Discord for help.

  • I’m having trouble getting a reliable connection using BlueTooth to the controller. Sometimes it works, sometimes it just doesn’t connect.

    (This is specifically for the case where the printer log does not show any exceptions mentioning bluetooth characteristics; otherwise see below.)

    There might be interference on the 2.4 GHz wireless band. Verify the following:

    • If the Klipper host is connected via WiFi make sure it’s using 5.0 GHz, or use Ethernet instead.

    • If the Klipper host is a Raspberry Pi, make sure the Pi’s USB C port is not used. It is not properly shielded and emits EMI.

      You can test to see if the problem is specific to the Klipper host by connecting with another machine, such as a pocket supercomputer.

  • The printer log or nevermore tools show exceptions/errors mentioning missing or unknown 'characteristics' and it can’t connect to the controller.

    If you encounter an exception or error talking about 'characteristics', such as:

    Exception: <UUID> (Handle: <number>): Unknown has no characteristic <UUID> with properties ...

    Try the following, in order:

    1. Update the controller using OTA. The controller might be too old for the Klipper module you’re using. If you know it’s up to date, or can’t connect via OTA, continue to 2.

    2. Disable and remove BlueZ GATT caches.

      BlueZ (Linux’s BlueTooth subsystem) has a known bug where it can store corrupt BLE attribute caches. [4] You can disable and clear this cache to work around this bug:

      1. Disable Caching

        Run sudo nano /etc/bluetooth/main.conf and in the [GATT] section change #Cache = always to Cache = no. If main.conf doesn’t have a [GATT] section, add it and Cache = no. e.g.

        [GATT]
        Cache = no

        Reboot the machine to apply the change.

      2. Remove Existing Caches

        Run sudo bluetoothctl power off.

        Get the addresses of all controllers with sudo ls /var/lib/bluetooth. They will be of the form xx:xx:xx:xx:xx:xx.

        Run for each controller sudo rm -rf /var/lib/bluetooth/<controller-address>/cache. (Not all controllers will necessarily have a cache.)

        Reboot the machine to ensure the BlueZ doesn’t persist any cache in memory.

  • I’m using MainsailOS and I’m having trouble with BlueTooth.

    This distro disables BlueTooth by default. [5] 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.

    Note
    I 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/Fluidd, there’s no 'Nevermore' item.

    Mainsail must be version >= 2.7.1. Fluidd must be version >= 1.31.0. If that’s fine then double check there isn’t any config errors.

  • Only the intake/exhaust side shows values in Mainsail/Fluidd, the other side only shows ---.

    1. Run ./tools/pin-config.py --reset-default.

      This fixes a known bug when updating to 0.14+ from older versions that would corrupt the pin config for I2C0 (intake). If this does fix the problem and it was on the exhaust side, then the intake/exhaust I2C lines are swapped.

    2. Double check the wiring.

      You can quickly test this by swapping the working side’s sensors with the problematic one. If problematic side starts working then the issue is with the sensors you pulled, otherwise the wiring is the problem.

Display Support

There are a handful of UIs available. You can select them using the display_ui Klipper option.

Supported Display UI
Figure 1. Supported Display UIs

Touch Support

Touch display support is early in development and currently very limited. For now you can:

  • Long press on the center area to toggle the fan override on/off

  • Press/drag on the fan power ring to set the fan override to a specific percent

Software Build Requirements

  • Pico-W SDK 1.5.1+

  • CMake 3.20+

  • C+23 compiler, e.g. GCC 12 (tested w/ 12.2.1)

Controller Customisation

src/config.hpp contains all user-customisable options. These options are, for the most part, validated at compile time to prevent mistakes.

Pin Assignments

Pins assignments can be customised, but are subject to hardware-related constraints. These are constraints are extensively checked at compile time and runtime, and will result in a (hopefully) useful error message if violated. If it compiles, it’s a valid configuration.

Custom Assignments

The recommended way to customise pin assignments is to use the pin-config.py tool:

# update the pin configuration. follow the on-screen instructions.
~/nevermore-controller/tools/pin-config.py

Changes will only take effect after a reboot of the controller.

You can reset the configuration to the board defaults using --reset-default. See --help for more options.

Default Assignments

Warning
GPIO 0 and 1 are reserved for UART. They cannot be used in any pin assignments.
Table 5. Default Pin Assignments - Pico W

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

10

Photocatalytic Control (PWM)

12

NeoPixel - Data

13

Fan - PWM

14

Vent Servo PWM

15

Fan - Tachometer

18

Exhaust - I2C SDA

19

Exhaust - I2C SCL

20

Intake - I2C SDA

21

Intake - I2C SCL

Table 6. Default Pin Assignments - Waveshare RP2040 Zero

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

11

Photocatalytic Control (PWM)

12

NeoPixel - Data

13

Vent Servo PWM

14

Fan - Tachometer

15

Fan - PWM

26

Intake - I2C SDA

27

Intake - I2C SCL

28

Exhaust - I2C SDA

29

Exhaust - I2C SCL

Table 7. Default Pin Assignments - Waveshare Touch LCD 1.28"

GPIO

Function

16

Intake - I2C SDA

17

Intake - I2C SCL

26

NeoPixel - Data

27

Fan - Tachometer

28

Fan - PWM

Persistent Settings

The controller will save most settings and calibrations to built-in flash periodically. To minimise wear & tear, settings are written every 10 minutes (if they’ve changed), and sensor calibrations are checkpointed every 24h. Settings are also immediately written (if changed) before any reboot requests.

The current implementation doesn’t distinguish between user customised values and default ones. Consequently, if default settings change they won’t be updated automatically unless the settings are reset. This can be done using NEVERMORE_RESET, if you are connected via Klipper.

Klipper

Requirements

TL;DR: If you installed everything using KIAUH, you should be good to go so long as you installed Klipper with Python 3.

Configuration

Configuration is typically done using a Klipper instance (e.g. the one on the printer) connected to the controller. Changes to settings are then persisted to flash after ~10 seconds.

Note
If you have a non-Klipper printer then you can use a temporary Klipper instance to configure the controller, disconnect it from Klipper, and use it in the non-Klipper printer.

Minimal Example

This example configuration is intended for quickly getting up and running. You can just copy paste this into the printer’s config.

Check out the full documentation section (just after this) after you’ve tested everything works with the minimal configuration; there are many useful options for customisation.

[nevermore]
# If you're using USB instead of BT, uncomment and specify the correct serial device.
# WARNING: Make sure it's the `-if02` interface, not `-if00`.
#serial: /dev/serial/by-id/usb-Nevermore_Filter_<board>_<device-id>-if02

# 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

# These `fan_power_*` entries are for a DELTA BFB0712HF (StealthMax BOM)
# If you have a different fan then play with these numbers to the satisfaction.
# See full config documentation for details.
# (e.g. See `fan_power_automatic` if you'd prefer very quiet background filtering.)
fan_power_coefficient: 0.8  # lower max power to keep things much more quiet

# Optional
# This 'temperature' sensor only serves to draw the intake VOC index on
# Mainsail/Fluidd's temperature plot.
[temperature_sensor nevermore_intake_VOC]
sensor_type: NevermoreSensor
sensor_kind: intake
plot_voc: true

WS2812 Example (NeoPixel)

WS2812 pixel strips can be used just like any other WS2812 pixel strip connected to the Klipper instance. This includes support for LED effects. See Klipper Object Naming if you have a non-default named Nevermore.

# 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)

Full Documentation

Warning
Don’t simply copy-paste this into the config. It won’t give you a working setup. Follow the setup guide.

This section lists all options and their defaults. Some minor examples are also provided. Use multiple [nevermore …​] sections if you have multiple Nevermores.

Note
The values shown here are either the default for that option or a placeholder.
Warning
Leave an option unset if you don’t need a value different than the default. Setting an option to the same value as the default will prevent you from getting new defaults from future updates.
Warning
Using multiple Nevermores over BLE is experimental and may take longer to connect.
# DON'T JUST COPY PASTE THIS INTO THE PRINTER'S KLIPPER CONFIGURATION.
# 1) Read the setup guide.
# 2) *Don't uncomment default values unless you explicitly wish to change them.*
#    Doing so will prevent you from getting new defaults from future updates.

# If name is omitted, will default to just `nevermore`.
# You may specify multiple `[nevermore ...]` sections to define multiple filters.
[nevermore custom_names_allowed]
# Can omit if you have only one nevermore in range, but it is recommended you
# specify the address.
# 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, recommended, omitted by default>

# Use a serial connection instead of BLE.
# Mutually exclusive w/ `bt_address`.
# WARNING:  Make sure you're using the 2nd interface (path ends with '-if02'),
#           *not* one which ends w/ `-if00`.
serial: <device path>

# seconds, 0 to disable, how long to wait at startup before failing if Klipper can't connect
# If disabled (set to 0) the module will not error on startup if it cannot connect.
# Disabling this requires that `bt_address` is set.
# Cannot be used w/ `serial`.
#
# WARNING:  **Do not disable unless you've fully tested everything in the filter.**
#           i.e. it should be ready for a serial # request on the Discord.
#           Disabling makes it difficult to decern if a problem is caused by connection issues
#           or something else.
# WARNING:  If you set this < 10 seconds you will likely have trouble connecting.
# NOTE:     **After** startup module will always quietly keep trying to reconnect if connection,
#           regardless of what value is set for `connection_initial_timeout`.
# 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 policy nor overridden
#fan_power_passive: 0

# float \in [0, 1] - Fan power used when the automatic fan policy is active.
# Useful if you'd prefer slower but quieter background/automatic filtering.
#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 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
# NB: if <= 200 then fan will engage when in the 'nominal' region (see VOC guide)
#fan_policy_voc_passive_max: 250
# voc index, 0 to disable, filter if the intake exceeds exhaust by at least this much
# Not recommended; `voc_passive_max` is generally more reliable and useful.
#fan_policy_voc_improve_min: 0

# 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
# 0 to disable the fan at max temp
# 0.5 to half the fan speed at max temp
# 1 to effectively disable the thermal limiter (no scaling at max temp)
#fan_thermal_limit_coefficient: 0


# Sensor Settings

# voc index \in [175, 500], threshold where the system stops adjusting the
# calibration because the air is "unusually dirty". (AKA 'gating')
# VOC emissions can significantly vary between different filament materials and
# brands.
# Set this threshold to the 'typical' VOC index observed mid print.
# Setting this *too* low will prevent the system from adjusting to normal
# air quality variations. Advised not to set < 225.
# If you print with multiple materials/brands, see the G-Code command
# `NEVERMORE_VOC_GATING_THRESHOLD_OVERRIDE`.
#voc_gating_threshold: 250


# Display Options

# float \in [0, 1] - display backlight PWM %
#display_brightness: 1

# enum - display UI
# Valid enums:
#   GC9A01_CLASSIC      - full sized VOC plot
#   GC9A01_SMALL_PLOT   - smaller plot w/ explicit labels
#   GC9A01_NO_PLOT      - no plot, largest text size
#
# NB: Changing will take effect when the controller reboots.
#     You can reboot the controller using `NEVERMORE_REBOOT`. See G-Code Commands section.
#display_ui: GC9A01_CLASSIC


# Vent Servo
# NOTE: To reverse direction set `vent_servo_pulse_width_max` < `vent_servo_pulse_width_min`

# seconds \in (0, 0.02), duration of pulse when requested 0%
#vent_servo_pulse_width_min: 0.001
# seconds \in (0, 0.02), duration of pulse when requesting 100%
#vent_servo_pulse_width_max: 0.002


# 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+ and Fluidd 1.31.0+ both have dedicated support for Nevermores.
# 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>

# full Klipper object name of the Nevermore instance to use as a source
nevermore: <omitted, e.g. `nevermore custom_names_allowed`>

# 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

Klipper Object Naming

Nevermore instances have two kinds of names:

  • Short names: Used by GCode commands (i.e. NEVERMORE=<short name>).

  • Full Klipper names: Used by the Klipper config files.

Note
Full Klipper full names are case and whitespace sensitive.
Table 8. Nevermore Object Names

Klipper Config Declaration

Short Name

Full Klipper Name

LED Effect Name

[nevermore]

nevermore

nevermore

nevermore

[nevermore Foo_Bar]

Foo_Bar

nevermore Foo_Bar

nevermore:Foo_Bar

When referring to a Nevermore for LED effects, use the full Klipper name and replace any spaces with :.

G-Code Commands

The following command can be used to influence behaviour at runtime.

These typically have an optional NEVERMORE= parameter to specify which Nevermore to interact with. If no NEVERMORE= argument is provided then the command will apply to all Nevermores.

NEVERMORE_VENT_SERVO_SET

Command:

NEVERMORE_VENT_SERVO_SET [NEVERMORE=<name>] [PERCENT=<float \in [0, 1]>] [HOLD_FOR=<seconds > 0, optional>]

Set the vent’s servo pulse to the specified % between . Omitting PERCENT disables the servo. Specifying HOLD_FOR disables the servo after the specified # of seconds. HOLD_FOR requires a PERCENT.

NEVERMORE_STATUS

Command:

NEVERMORE_STATUS [NEVERMORE=<name>]

Prints the Nevermores' current status to the console. Not terribly useful for most things, but helpful if you’re not sure it’s connected yet. (e.g. when used with connection_initial_timeout: 0)

NEVERMORE_REBOOT

Command:

NEVERMORE_REBOOT [NEVERMORE=<name>]

Reboots Nevermores, if connected. Persistent settings will be saved.

Probably easier than power cycling the whole printer.

NEVERMORE_RESET

Warning
This command should not be used unless directed by Someone Who Knows What They’re Doing.

Command:

NEVERMORE_RESET FLAGS=<int> [NEVERMORE=<name>]

Resets persistent settings to defaults. It is deliberately under-documented to dissuade causal use.

Policy settings can can be reset to default using FLAGS=2.

NEVERMORE_VOC_CALIBRATION

Command:

NEVERMORE_VOC_CALIBRATION ENABLED={0, 1} [NEVERMORE=<name>]
Warning
Calibration is automatically suspended by the Klipper module when any extruders have a target temperature. It is resumed when no extruders have a target temperature. You should not have to explicitly use this command in typical scenarios.

Enables/disables the VOC sensor calibration. Sensor calibration should be enabled whenever the printer isn’t printing.

Sensor calibration should only be disabled when the printer is printing. Doing this prevents the VOC sensor from mistaking low VOC emissions for sensor drift and implicitly compensating for it.

This should be used in conjunction with NEVERMORE_VOC_GATING_THRESHOLD_OVERRIDE to automatically enable/disable VOC calibration if the air is still dirty post-print.

VOC sensor calibration is always enabled when the controller powers on.

NEVERMORE_VOC_GATING_THRESHOLD_OVERRIDE

Command:

NEVERMORE_VOC_GATING_THRESHOLD_OVERRIDE [NEVERMORE=<name>] [THRESHOLD=<int \in [175, 500]>]

Overrides the VOC gating threshold (see voc_gating_threshold in the Klipper config). Omit the THRESHOLD parameter to clear any existing override.

This is intended for setups where the slicer specifies the filament type using a user-defined G-Code macro (e.g. SET_MATERIAL ABS), and you would like to temporarily set the VOC gating threshold for a specific material/filament.

Unlike the voc_gating_threshold, this is setting is not persisted and will be lost when the controller restarts.

NEVERMORE_SENSOR_CALIBRATION_CHECKPOINT

Command:

NEVERMORE_SENSOR_CALIBRATION_CHECKPOINT [NEVERMORE=<name>]

Force sensors to checkpoint their calibration. The checkpoints will be persisted after a brief delay (under 20 seconds).

Useful if you must save the current calibration immediately instead of waiting for the usual 24h periodic checkpoint. e.g. After a short baseline calibration.

NEVERMORE_SENSOR_CALIBRATION_RESET

Command:

NEVERMORE_SENSOR_CALIBRATION_RESET [NEVERMORE=<name>]

Resets the sensor calibrations. Does not immediately persist this reset calibration, but it will eventually be applied when the checkpoint process triggers.

Useful when moving the printer to a new environment.

Finding The BT Address

If you have only one Nevermore controller in range then you can omit the bt_address option in the 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.

Method A - Check the Klipper Log

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 the 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.

Method B - Linux Only - bluetoothctl

Note
Only works on Linux. Yes, I know you didn’t read the title.
  1. Make sure the Nevermore controller is powered and the LED is blinking. (Indicates it is active.)

  2. 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]#
  3. 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
    ...
    Warning
    If you wait too long (~15-20 seconds), the scan ends, and the host will forget about the devices it discovered.
  4. 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", "Nevermore Controller", or "picowota" [6], and copy their address into the printer configuration.

    In this example, we could use bt_address: FA:KE:AD:RE:SS:00 or bt_address: FA:KE:AD:RE:SS:01.

  5. You should try connecting to the controller to verify that there’s no significant interference:

    Run: connect <BT address>

    [bluetooth]# connect FA:KE:AD:RE:SS:00
    Attempting to connect to FA:KE:AD:RE:SS:00
    [CHG] Device FA:KE:AD:RE:SS:00 Connected: yes
    Connection successful
    <lots of of new services/characteristics announced>

    If connecting fails, or momentarily succeeds and then connection is lost, then there might be interference from the WiFi adapter. See this FAQ for details.

Method C - Use A Phone + nRF Connect

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" (or "picowota", if in bootloader more), and their BT addresses will be listed below.

nRC Connect Screenshot
Figure 2. nRF Connect displays device names & addresses

You can test if the controller is accepting new connections by pressing the 'connect' button.

"Print Mode"

The Klipper module enters/exits "print mode" based on whether any extruder is turned on (i.e. has a non-zero target temperature).

This allows Nevermores to immediately begin filtering before VOC levels exceed thesholds and helps prevent calibration drift that could occur with long prints using low-VOC filaments.

On entering "print mode":

On exiting "print mode":

  • Nevermores return to automatic control.

  • VOC calibration is resumed.

These commands are automatically re-issued to a controller when they reconnect after losing connection.

Fan Control & Macros

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.

Credits

  • Julian Schill - installation script (derived)

  • Bosch Sensors - BMP280, BME280, BME68x library (included)

  • ScioSense - ENS160 library (referenced)

  • Sensirion - SGP30 library (referenced)

  • Sensirion - SGP40 gas index library (included)

  • Klipper - AHTxx library (referenced)

  • Apache Nuttx - I2C software reset (derived)

  • Gary S. Brown - CRC32 table (included)

  • Ursula K. Le Guin

Thanks To

  • 0ndsk4 - Donated hardware for testing

  • Appri (Nevermore Discord) - Testing Volunteer

  • BO_Andy (Nevermore Discord) - Testing Volunteer

  • Central 3D Printing - Donated hardware for testing

  • Drevic (Nevermore Discord) - Testing Volunteer

Generous Donors


1. Only supported to detect when someone inadvertently uses a BMP280 instead of a BME280.
2. This specific multi-sensor has a gas sensor, but does not reliably detect VOCs relevant to 3D printing.
3. SGP40s are preferred, but SGP30s should still be functional.
4. Observed in versions up to 5.66.
5. Mainsail OS disabled BlueTooth to enable hardware UART on Raspberry Pi SBCs.
6. This is the name it uses when in bootloader mode. Unfortunately BlueZ is too aggressive about caching device names.