/nevermore-controller

Primary LanguageCGNU Affero General Public License v3.0AGPL-3.0

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

Supported Hardware

Table 2. Supported Sensors

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

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 Pico W.

    The Pico W’s LED should start flashing once the controller has booted.

  2. 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 your Klipper host. (e.g. sudo reboot)

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

  5. Update your printer macros.

    1. Add SET_FAN_SPEED FAN=nevermore_fan SPEED=1 to your print_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> ...
    2. Add SET_FAN_SPEED FAN=nevermore_fan to your TURN_OFF_HEATERS macro.

      Warning
      This assumes your print_end macro calls TURN_OFF_HEATERS. If it doesn’t you’ll want to put SET_FAN_SPEED FAN=nevermore_fan in your print_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
  6. 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
  7. If you’ve flashed a OTA-capable UF2 to your controller (v0.3+) you can update it wirelessly.

Updating Guide For Dummies

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

FAQ / Known Issues

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

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

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

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 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.
Table 5. Default Pin Assignments

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

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

Minimal Example

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 Example (NeoPixel)

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)

Full Documentation

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: 5

# 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

Finding The BT Address

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.

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

Method B - Linux Only - bluetoothctl

Note
Only works on Linux. Yes, I know you didn’t read the title.
  1. Make sure your 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" 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 or bt_address: FA:KE:AD:RE:SS:01.

Method C - Use Your 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", and their BT addresses will be listed below.

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

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


1. This specific multi-sensor has a gas sensor, but does not reliably detect VOCs relevant to 3D printing.
2. I suggest adding gcode rather than a macro wrapper because you want the filter to start when the extruder/bed heats up, and your print_start probably does a lot of things (homing, QGL, purge, etc).
3. Wherever you’d like.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.
4. Mainsail OS disabled BlueTooth to enable hardware UART on Raspberry Pi SBCs.