Connecting a Creality K1 to a Raspberry Pi
Summary of this thread in Discord - huuuge credit to exor for figuring this all out. Just trying to keep a log here as a note-to-self for what to do once I get the appropriate hardware and time.
Prerequisites
- Make sure your K1 mainboard has a populated microusb header. This is very risky to do unless you have a way to recover your K1 to stock. See here for Creality's recovery flashing instructions.
- You should already be using a probe supported by Simple AF because stock Klipper does not support the K1's multiple load cells, and the bed/prtouch MCU is not used here. This guide assumes that you are using a Cartographer over USB, but most probes should work.
- You will need an SBC that supports USB OTG mode. This guide assumes that you'll be using a Raspberry Pi 4, but it's technically possible with other devices.
Hardware
Important power considerations
The Raspberry Pi will potentially attempt to backfeed power to the K1 if there is continuity on the VCC+ line of the USB cable. This is very risky, and can cause all sorts of hard-to-debug issues.
At the same time, the Pi and K1 must still share ground. If you cut the VCC+ line and the GND line on the USB cable, data lines D+/D‑ will see undefined reference potentials. Ensure the USB shielding and ground wires remains intact if you're doing any cable surgery.
You will need to
- Use the Pi's USB-C port to connect to the printer (it's the only port on the Pi that supports OTG mode)
- Disconnect the VCC+ line on the USB cable connecting your Pi to the printer to prevent backfeeding
- Still find a way to provide power the Pi while its USB-C port is being used for OTG mode
This is still an area of investigation.
Preventing backfeeding
Options include:
- using Kaptom tape on the VCC+ pin of the USB cable plugged into the printer
- cable surgery to snip the VCC+ wire on the USB cable connecting the Pi to the printer
- using a device to block USB Power while allowing USB Data
- print a jig to block the VCC+ pin on the USB-A cable
- using JST connectors to wire to the K1 mainboard's USB header and removing/snipping the VCC+ cable there
Powering the Raspberry Pi
This can be a little tricky since we need to use its USB-C port for OTG. Options include:
- using a power/data splitter so you can still power the Pi over USB Type C (Still needs VCC+ Cut/Disabled)
- powering the Raspberry Pi with a PoE adapter or HAT
- providing appropriately regulated voltage to its GPIO power pins if you know exactly what you're doing
There are likely special concerns for the Raspberry Pi 5 because of its somewhat unique power requirements and software-side checks for appropriate power. The USB Type C power splitter OTG cable above is capable of providing the rated power with an appropriate power supply without extra config aside from making sure the VCC+ pin is disabled.
Data cables
- Connect Raspberry Pi's USB Type C port to the K1's USB header, making sure you've addressed the VCC+ continuity issue above!. Shielded cables are highly recommended.
- Simple: USB Type A Male to USB Type C Male to plug into the front USB port of the K1
- Neat: JST to USB plugged into the K1 mainboard's USB header
- Combining some options above
- power + data splitter for Pi
- JST to USB cable plugged into the Pi and the K1 mainboard
- snip the power wire on the JST to USB cable (usually red)
- Connect Cartographer to the Pi over USB, using the included Cartographer USB cable
- Recommended: Unplug the Camera cable from the K1, adapt it from JST to USB, and plug into the Raspberry Pi
- Optional: Redirect the front USB port on your printer to your Raspberry Pi by adapting its cable from JST to USB and plugging it into an open port on the Raspberry Pi
Note, you may need some male/male or female/female JST adapter cables as well!
Software
Raspberry Pi Side
Simple AF for RPi
- Install Simple AF for RPi
- after cloning the repo, but before running the installer, run this command to get the appropriate printer profiles loaded
- note: if/when these profiles land in
main, this step won’t be necessary, and this guide will be updated to reflect that
- note: if/when these profiles land in
- if you have a K1 or K1 Max and arent sure if it's a 2023 or 2024 variant, you can run the following on your K1. If it returns 1, then its 2024. Otherwise its 2023.
- continue installing Simple AF for RPi, using the appropriate profile for or printer
- after cloning the repo, but before running the installer, run this command to get the appropriate printer profiles loaded
Exposing your Pi as a USB Gadget
Next, we will Set your Raspberry Pi to boot as a USB gadget so the K1 can see it and interact with it. This disables host‑mode on the Pi's USB‑C port. Devices that are normally powered from that port stop will stop working, but you should still have the VCC+ pin disabled on your USB connection to the K1 to prevent backfeeding power.
- set the appropriate boot flags
- Create a script to expose the Raspberry Pi to the K1 as a USB device using it's USB-C OTG functionality by writing the following to
/usr/bin/setup_usb_gadget.sh
- Make the script executable
- Set up a launch service to automatically run the script by creating
/etc/systemd/system/usb-gadget-init.service
- Enable the service and run it now in one command using:
sudo systemctl enable --now usb-gadget-init.service
K1 Side
Instead of using Klipper on the K1's embedded Linux system, we want the K1's Linux system to forward every bit of data from the printer's MCU hardware to the Raspberry Pi over the USB connection.
- If it's not already installed, install Simple AF firmware on your K1
- Disable Klipper and Moonraker services by stopping them from being picked up at init:
- Install required utilities to expose MCU devices to the USB OTG device and manage the USB lifecycle
- Set up a service that uses
socatby creating this file:/etc/init.d/S99rpi-bridge-init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
#!/bin/bash # OTG gadget info VENDOR_ID="1d6b" PRODUCT_ID="0104" DEV_PREFIX="/dev/ttyACM" # mcu paths MCU="/dev/ttyS7" NOZZLE_MCU="/dev/ttyS1" SOCAT_OPTS="raw,echo=0,b230400" PIDFILE_MCU="/var/run/socat_mcu.pid" PIDFILE_NOZZLE_MCU="/var/run/socat_nozzle_mcu.pid" LOG_FILE="/tmp/rpi_bridge.log" RETRY_DELAY=5 MAX_RETRIES=12 MCU_RESET_BIN="/usr/bin/mcu_reset.sh" USBRESET_BIN="/opt/bin/usbreset" SOCAT_BIN="/opt/bin/socat" # Function to log to /tmp/rpi_bridge.log log_message() { if [ -f "$LOG_FILE" ] || touch "$LOG_FILE" 2>/dev/null; then echo "$(date '+%Y-%m-%d %H:%M:%S') rpi-bridge-init: $1" >> "$LOG_FILE" else echo "$(date '+%Y-%m-%d %H:%M:%S') rpi-bridge-init: $1" > /dev/null fi } # Function to find ttyACM devices find_tty_devices() { ls ${DEV_PREFIX}* 2>/dev/null | sort -V } # Function to wait for USB device wait_for_usb_device() { while ! lsusb -d "$VENDOR_ID:$PRODUCT_ID" >/dev/null 2>&1; do log_message "Waiting for USB device $VENDOR_ID:$PRODUCT_ID to appear..." sleep 1 done log_message "USB device $VENDOR_ID:$PRODUCT_ID detected" } # Function to perform usbreset usbreset_device() { local usb_dev usb_dev=$(lsusb -d "$VENDOR_ID:$PRODUCT_ID" | awk '{print $2 "/" $4}' | sed 's/:$//') if [ -n "$usb_dev" ]; then log_message "Resetting USB device: $usb_dev" /opt/bin/usbreset $usb_dev 2>/dev/null return $? else log_message "USB device $VENDOR_ID:$PRODUCT_ID not found for reset" return 1 fi } # Function to check devices and select the first two valid ACM devices check_and_handle_devices() { local devices local valid_devices="" local count=0 devices=$(find_tty_devices) if [ -z "$devices" ]; then log_message "No ttyACM devices found" return 1 fi for dev in $devices; do # Check if valid character device if [ -c "$dev" ]; then valid_devices="$valid_devices $dev" count=$((count + 1)) [ $count -eq 2 ] && break else log_message "Invalid device node $dev, removing" rm -f "$dev" fi done if [ $count -lt 2 ]; then log_message "Found only $count valid /dev/ttyACM devices, need 2" return 1 fi TTY_0=$(echo "$valid_devices" | awk '{print $1}') TTY_1=$(echo "$valid_devices" | awk '{print $2}') return 0 } setup_bridge() { if ! check_and_handle_devices; then return 1 fi if ! usbreset_device; then log_message "USB reset failed, retrying process" return 1 fi log_message "Waiting $RETRY_DELAY seconds to recheck ACM devices after USB reset" sleep $RETRY_DELAY if ! check_and_handle_devices; then log_message "Failed to reconfirm ACM devices after USB reset" return 1 fi log_message "Starting socat bridge: $MCU <-> $TTY_0" start-stop-daemon --start --background --pidfile "$PIDFILE_MCU" --make-pidfile --exec /opt/bin/socat -- "$MCU,$SOCAT_OPTS" "$TTY_0,raw,echo=0" if [ $? -eq 0 ]; then log_message "Socat bridge for $MCU started" else log_message "Failed to start socat bridge for $MCU" return 1 fi log_message "Starting socat bridge: $NOZZLE_MCU <-> $TTY_1" start-stop-daemon --start --background --pidfile "$PIDFILE_NOZZLE_MCU" --make-pidfile --exec /opt/bin/socat -- "$NOZZLE_MCU,$SOCAT_OPTS" "$TTY_1,$SOCAT_OPTS" if [ $? -eq 0 ]; then log_message "Socat bridge for $NOZZLE_MCU started" else log_message "Failed to start socat bridge for $NOZZLE_MCU" return 1 fi log_message "Executing MCU reset script" /usr/bin/mcu_reset.sh if [ $? -eq 0 ]; then log_message "MCU reset completed successfully" return 0 else log_message "Failed to execute MCU reset script" return 1 fi } stop_socat() { if [ -f "$PIDFILE_MCU" ]; then log_message "Stopping socat bridge for $MCU" start-stop-daemon --stop --pidfile "$PIDFILE_MCU" --retry 5 if [ $? -eq 0 ]; then rm -f "$PIDFILE_MCU" log_message "Socat bridge for $MCU stopped" else log_message "Failed to stop socat bridge for $MCU" fi fi if [ -f "$PIDFILE_NOZZLE_MCU" ]; then log_message "Stopping socat bridge for $NOZZLE_MCU" start-stop-daemon --stop --pidfile "$PIDFILE_NOZZLE_MCU" --retry 5 if [ $? -eq 0 ]; then rm -f "$PIDFILE_NOZZLE_MCU" log_message "Socat bridge for $NOZZLE_MCU stopped" else log_message "Failed to stop socat bridge for $NOZZLE_MCU" fi fi } case "$1" in start) for x in "$USBRESET_BIN" "$SOCAT_BIN" "$MCU_RESET_BIN"; do if ! [ -x "$x" ]; then echo "Error: '$x' not found or not executable" >&2 log_message "Error: '$x' not found or not executable" exit 1 fi done log_message "Starting rpi-bridge-init (max retries: $MAX_RETRIES)" attempt=0 while [ $attempt -lt $MAX_RETRIES ]; do wait_for_usb_device if setup_bridge; then log_message "rpi-bridge-init started successfully" exit 0 fi attempt=$((attempt + 1)) log_message "Attempt $attempt failed, retrying after $RETRY_DELAY seconds..." sleep $RETRY_DELAY done log_message "Failed to start rpi-bridge" exit 1 ;; stop) log_message "Stopping rpi-bridge-init" stop_socat exit 0 ;; restart|reload) stop_socat sleep 1 $0 start exit $? ;; *) echo "Usage: $0 {start|stop|restart|reload}" >&2 exit 1 ;; esac exit 0
- Make it executable with
chmod +x /etc/init.d/S99rpi-bridge-init - Restart your K1
Post install testing
At this point turning on the k1 and pi at the same time should result in the MCUs being available to the pi klipper setup as if they are local, after about 30s or so. A good indicator of successful setup is the printer's LEDs turning off and then on again as the last setup of the k1 init script runs. You may need to check the klippy.log for any configuration errors if it just says Unknown or any other non-specific message.
-K1 setup script logs to /tmp/rpi_bridge.log, and a normal boot should look something like:
-Pi doesn't need any active script running, but a good indicator that it's setup properly is the following output in dmesg:
Usage note:
This script's intended flow is that the pi and k1 turn on at the same time, and never reboot independently of eachother. You will have to manually trigger things if you do this in order to restart the link.
Optional steps:
- Screen
- If you want to keep the stock screen on the K1, you can connect grumpyscreen on the K1 to the Raspberry Pi's IP:
- Open the configuration file
/usr/data/guppyscreen/guppyscreen.json - look for
"moonraker_host": "127.0.0.01",near the bottom - Change this to your pi's IP to make the screen point to that klipper instance
- Open the configuration file
- Your Raspberry Pi will be running Klipperscreen by default. You can either uninstall Klipperscreen, or use it by connecting an HDMI touchscreen to your Pi.
- If you want to keep the stock screen on the K1, you can connect grumpyscreen on the K1 to the Raspberry Pi's IP:
- You can print a nice mount for both the mainboard and a Raspberry Pi once you get things working, if you want a nice all-in-one package!
Switching between Raspberry Pi and SAF k1 or stock k1:
- Disable
S99rpi-bridge-initon the k1 by renaming it to prefix it with a_ - Re-enable stock klipper and moonraker by removing the
_ - If the guppyscreen IP was changed then revert it back to
127.0.0.1 - Power off both devices and switch the cables back to standard configuration
- You can use both the Pi and the k1 stock/saf at the same time, such as for camera or other services, without changes.
- You can disable klipper and moonraker on the pi.
Future work
Generic Serial Port
Instead of (or in addition to) exposing just the MCUs to the Pi over serial, we can also expose a serial port to the K1 SBC itself.
- K1 side could be simplified to just connect a serial console to
ACM0whenever it sees it come up - Pi side can repeatedly attempt to connect over
GS0 - Both sides can have retry logic to make sure they auto-reconnect after power cycles or transient disconnections
- When the devices see each other, the Pi can then just do all of the detection and setup over the serial link
Initial USB Negotiation
Initial USB negotiation can be flaky. Need to investigate USB race conditions between the Pi and K1. It's likely that we can update both ends of the script to be able to properly negotiate the connection
- mostly fixed with newer init script, but reconnection logic is not present on the k1 side.
TCP moonraker tunnel over serial
A TCP tunnel can be implemented to run over the 3rd exposed serial port as an alternative to using as a console, as described above, and "export" the moonraker server back over to the k1 so no changes are required to use the screen and it doesn't require a network connection. This is possible and has been implemented but it adds to the fragility of the setup, and a network connection is always required to the k1 for configuration purposes anyways. Example script for anyone interested in exploring that route: serial_tunnel.py