What and why
Bee-Slicer-CLI is a small Linux CLI wrapper around beedriver for BEETHEFIRST / BEETHEFIRST+ printers.
I made it to run printer operations directly from terminal (print, load/unload, monitor, calibrate) without keeping a Docker stack around.
Quickstart
git clone https://github.com/CMDR-Shrine/Bee-Slicer-CLI.git
cd Bee-Slicer-CLI
chmod +x print.sh
Set USB access once:
# Arch
sudo usermod -a -G uucp "$USER"
# Debian/Ubuntu
sudo usermod -a -G dialout "$USER"
sudo cp config/99-beeverycreative.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
Run:
# Menu (print, load, unload, monitor, calibrate)
./print.sh
# Direct print
./print.sh /path/to/file.gcode
# Direct calibrate
./print.sh calibrate
Full source code
print.sh
#!/bin/bash
#
# BEETHEFIRST Standalone Printer CLI
# No Docker required - uses Miniconda Python 2.7 environment (x86_64)
# or system Python 2.7 + virtualenv (ARM64/Raspberry Pi)
#
# Usage: ./print.sh [gcode_file]
# ./print.sh (shows menu)
#
set -e
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_NAME="beethefirst"
MINICONDA_DIR="$HOME/.config/miniconda3"
VENV_DIR="$SCRIPT_DIR/.venv_py27"
# Detect architecture
ARCH=$(uname -m)
# Show menu if no arguments provided
if [ -z "$1" ]; then
echo "============================================================"
echo "BEETHEFIRST STANDALONE PRINTER CLI"
echo "============================================================"
echo ""
echo "Select an option:"
echo " 1) Print from G-code file"
echo " 2) Load filament (heat + extrude)"
echo " 3) Unload filament (heat + retract)"
echo " 4) Monitor print progress (passive)"
echo " 5) Calibrate Printer (Bed Leveling)"
echo " 6) Exit"
echo ""
read -p "Choice [1-6]: " CHOICE
echo ""
case $CHOICE in
1)
read -p "Enter G-code file path: " GCODE_FILE
if [ -z "$GCODE_FILE" ]; then
echo "ERROR: No file specified"
exit 1
fi
if [ ! -f "$GCODE_FILE" ]; then
echo "ERROR: File not found: $GCODE_FILE"
exit 1
fi
MODE="print"
;;
2)
MODE="load"
;;
3)
MODE="unload"
;;
4)
MODE="monitor"
;;
5)
MODE="calibrate"
;;
6|q|Q)
echo "Goodbye!"
exit 0
;;
*)
echo "Invalid choice"
exit 1
;;
esac
else
# Direct usage: ./print.sh <gcode_file> or ./print.sh calibrate
if [ "$1" = "calibrate" ]; then
MODE="calibrate"
else
GCODE_FILE="$1"
if [ ! -f "$GCODE_FILE" ]; then
echo "ERROR: File not found: $GCODE_FILE"
exit 1
fi
MODE="print"
fi
fi
echo "============================================================"
if [ "$MODE" = "print" ]; then
echo "BEETHEFIRST STANDALONE PRINTER"
echo "============================================================"
echo "G-code file: $(basename "$GCODE_FILE")"
elif [ "$MODE" = "load" ]; then
echo "FILAMENT LOADER"
elif [ "$MODE" = "unload" ]; then
echo "FILAMENT UNLOADER"
elif [ "$MODE" = "monitor" ]; then
echo "PRINT MONITOR"
elif [ "$MODE" = "calibrate" ]; then
echo "PRINTER CALIBRATION"
fi
echo "Architecture: $ARCH"
echo "============================================================"
echo ""
# Check for processes using the printer (skip for monitor mode)
if [ "$MODE" != "monitor" ]; then
echo "[0/2] Checking for conflicting processes..."
CONFLICTING_PROCS=$(ps aux | grep -E "(beeweb|beesoft|simple_print|gcode_sender)" | grep -v grep | grep -v "$$" || true)
else
# Monitor mode is passive and doesn't need to check
CONFLICTING_PROCS=""
fi
if [ -n "$CONFLICTING_PROCS" ]; then
echo ""
echo "WARNING: Found processes that may be using the printer:"
echo "$CONFLICTING_PROCS" | awk '{printf " [PID %s] %s\n", $2, $11}'
echo ""
# Check if Docker container is running
DOCKER_RUNNING=$(docker ps --filter "name=beeweb-server" --format "{{.Names}}" 2>/dev/null || true)
if [ -n "$DOCKER_RUNNING" ]; then
echo " Docker container 'beeweb-server' is running"
fi
echo ""
read -p "Kill these processes and continue? [y/N] " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Stopping conflicting processes..."
# Stop Docker container if running
if [ -n "$DOCKER_RUNNING" ]; then
echo " Stopping beeweb-server container..."
docker stop beeweb-server >/dev/null 2>&1 || true
fi
# Kill other processes
echo "$CONFLICTING_PROCS" | awk '{print $2}' | while read pid; do
if [ -n "$pid" ]; then
echo " Killing PID $pid..."
kill "$pid" 2>/dev/null || true
fi
done
# Wait a moment for processes to die
sleep 2
echo "Processes stopped. Continuing..."
echo ""
else
echo "Aborted. Please manually stop processes using the printer."
exit 1
fi
elif [ "$MODE" != "monitor" ]; then
echo " No conflicting processes found."
fi
# ARM64 (Raspberry Pi) - use system Python 2.7 with virtualenv
if [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
if [ "$MODE" = "monitor" ]; then
echo "[1/2] Using system Python 2.7 (ARM64 platform)..."
else
echo "[1/3] Using system Python 2.7 (ARM64 platform)..."
fi
# Check if Python 2.7 is installed
if ! command -v python2.7 &> /dev/null; then
echo "ERROR: Python 2.7 not found!"
echo ""
echo "Install it with:"
echo " sudo apt update"
echo " sudo apt install -y python2.7 python-pip-whl python-setuptools-whl"
echo ""
exit 1
fi
# Check if virtualenv is available
if ! python2.7 -m virtualenv --version &> /dev/null 2>&1; then
echo "ERROR: virtualenv not found for Python 2.7!"
echo ""
echo "Install it with:"
echo " sudo apt install -y python-virtualenv"
echo ""
echo "Or via pip (if not in a virtualenv):"
echo " pip2 install virtualenv"
echo ""
exit 1
fi
# Create virtualenv if it doesn't exist
if [ ! -d "$VENV_DIR" ]; then
echo "[SETUP] Creating Python 2.7 virtualenv..."
python2.7 -m virtualenv "$VENV_DIR"
echo "[SETUP] Installing dependencies..."
source "$VENV_DIR/bin/activate"
pip install pyusb==1.0.2 pyserial==2.7
deactivate
echo "[SETUP] Virtualenv created successfully!"
fi
# Activate virtualenv
source "$VENV_DIR/bin/activate"
PYTHON_VERSION=$(python --version 2>&1)
echo " Python: $PYTHON_VERSION"
# Install missing dependencies if needed
if ! python -c "import serial" 2>/dev/null; then
echo " Installing pyserial..."
pip install -q pyserial==2.7
fi
if ! python -c "import usb" 2>/dev/null; then
echo " Installing pyusb..."
pip install -q pyusb==1.0.2
fi
# x86_64 - use Miniconda as before
else
# Check if miniconda is installed
if [ ! -d "$MINICONDA_DIR" ]; then
echo "ERROR: Miniconda not found at $MINICONDA_DIR"
echo "Please install Miniconda first or update MINICONDA_DIR in this script"
exit 1
fi
# Initialize conda
source "$MINICONDA_DIR/etc/profile.d/conda.sh"
# Check if environment exists
if ! conda env list | grep -q "^$ENV_NAME "; then
echo "[SETUP] Creating Python 2.7 environment '$ENV_NAME'..."
conda create -y -n "$ENV_NAME" python=2.7
echo "[SETUP] Installing dependencies..."
conda activate "$ENV_NAME"
pip install pyusb==1.0.2 pyserial==2.7
conda deactivate
echo "[SETUP] Environment created successfully!"
fi
# Activate environment
if [ "$MODE" = "monitor" ]; then
echo "[1/2] Activating Python 2.7 environment..."
else
echo "[1/3] Activating Python 2.7 environment..."
fi
conda activate "$ENV_NAME"
# Check Python version
PYTHON_VERSION=$(python --version 2>&1)
echo " Python: $PYTHON_VERSION"
# Install missing dependencies if needed
if ! python -c "import serial" 2>/dev/null; then
echo " Installing pyserial..."
pip install -q pyserial==2.7
fi
fi
# Run the appropriate script based on mode
if [ "$MODE" = "monitor" ]; then
echo "[2/2] Running $MODE script..."
else
echo "[2/3] Running $MODE script..."
fi
echo ""
case $MODE in
print)
python "$SCRIPT_DIR/src/print.py" "$GCODE_FILE"
;;
load)
python "$SCRIPT_DIR/src/load.py"
;;
unload)
python "$SCRIPT_DIR/src/unload.py"
;;
monitor)
python "$SCRIPT_DIR/src/monitor.py"
;;
calibrate)
python "$SCRIPT_DIR/src/calibrate.py"
;;
esac
# Deactivate environment
if [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]]; then
deactivate
else
conda deactivate
fi
echo ""
echo "Done!"
src/print.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
Standalone BEETHEFIRST Printer Script
No Docker required - uses local Python 2.7 environment
Key discovery: The BEETHEFIRST firmware converts filenames to LOWERCASE
when using M23! So we must send lowercase filenames.
Workflow:
1. Transfer G-code file to SD card
2. Heat nozzle to target temperature
3. Send M23 with LOWERCASE filename to select file
4. Send M24 to start printing
Usage:
python2 print.py <gcode_file>
"""
import sys
import os
import time
import re
# Add beedriver to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'beedriver'))
try:
import beedriver.connection as conn
except ImportError as e:
print("ERROR: Failed to import beedriver!")
print("Error: {}".format(e))
print("\nMake sure you run this via the print.sh wrapper script!")
sys.exit(1)
# Check args
if len(sys.argv) < 2:
print("Usage: python2 print.py <gcode_file>")
sys.exit(1)
gcode_file = sys.argv[1]
if not os.path.exists(gcode_file):
print("ERROR: File not found: {}".format(gcode_file))
sys.exit(1)
print("="*60)
print("BEETHEFIRST STANDALONE PRINTER")
print("="*60)
print("File: {}".format(gcode_file))
print("")
# Step 1: Connect to printer
print("[1/7] Connecting to printer...")
c = conn.Conn()
if not c.connectToFirstPrinter():
print("ERROR: Failed to connect to printer!")
sys.exit(1)
cmd = c.getCommandIntf()
if cmd is None:
print("ERROR: Failed to get command interface!")
print("This usually means USB permission issues.")
print("")
print("Fix with:")
print(" sudo usermod -a -G dialout $USER")
print(" (then log out and back in)")
print("")
print("Or run with sudo:")
print(" sudo ./print.sh gcode/case.gcode")
sys.exit(1)
print(" Connected!")
# Step 2: Check firmware mode
print("\n[2/7] Checking printer mode...")
mode = cmd.getPrinterMode()
print(" Mode: {}".format(mode))
if mode != 'Firmware':
print(" Going to firmware mode...")
cmd.goToFirmware()
# Wait for device to reset and re-enumerate
print(" Waiting for device to reset...")
time.sleep(5)
# Reconnect to the printer
print(" Reconnecting...")
if not c.reconnect():
print("ERROR: Failed to reconnect after firmware switch!")
sys.exit(1)
# Get new command interface
cmd = c.getCommandIntf()
if cmd is None:
print("ERROR: Failed to get command interface after reconnect!")
sys.exit(1)
print(" Reconnected successfully!")
mode = cmd.getPrinterMode()
print(" New mode: {}".format(mode))
# Clear any shutdown flag from previous print
status = cmd.getStatus()
if status == 'Shutdown':
print(" Printer in Shutdown mode, clearing flag...")
cmd.clearShutdownFlag()
time.sleep(2)
status = cmd.getStatus()
print(" New status: {}".format(status))
print(" In firmware mode!")
# Step 3: Analyze G-code file
print("\n[3/7] Analyzing G-code file...")
target_temp = 200 # default
gcode_line_count = 0
with open(gcode_file, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith(';'):
continue # Skip empty lines and comments
gcode_line_count += 1
# Extract temperature from M104/M109 commands
# Only accept temps > 150C (ignore M104 S0 which turns off heater)
if line.startswith('M104') or line.startswith('M109'):
if ' S' in line:
try:
temp_str = line.split(' S')[1].split()[0].split(';')[0]
temp = int(float(temp_str))
if temp > 150: # Ignore heater-off commands
target_temp = temp
print(" Found temperature: {}C".format(target_temp))
except:
pass
print(" G-code lines: {}".format(gcode_line_count))
print(" Target temperature: {}C".format(target_temp))
# Step 4: Transfer file to SD card
print("\n[4/7] Transferring file to SD card...")
basename = os.path.basename(gcode_file)
print(" File: {}".format(basename))
# Always use "ABCDE" as the SD filename (matches official BeeSlicer)
# This prevents file accumulation - each print overwrites the previous one
print(" SD filename: ABCDE (fixed, prevents file accumulation)")
cmd.transferSDFile(fileName=gcode_file, sdFileName="ABCDE")
# Step 5: Monitor transfer progress
print("\n[5/7] Monitoring transfer...")
last_progress = -1
while cmd.isTransferring():
progress = cmd.getTransferCompletionState()
if progress is not None and progress != last_progress:
# Convert to float if it's a string
try:
progress_num = float(progress)
print(" Transfer: {:.2f}%".format(progress_num))
last_progress = progress
except (ValueError, TypeError):
pass
time.sleep(1)
print(" Transfer complete!")
# Step 6: Heat nozzle
print("\n[6/7] Heating nozzle to {}C...".format(target_temp))
cmd.sendCmd('M104 S{}\n'.format(target_temp))
max_wait = 300 # 5 minutes
start_time = time.time()
last_reported_temp = -999
while time.time() - start_time < max_wait:
current_temp = cmd.getNozzleTemperature()
if current_temp is not None:
# Report temperature every 5 degrees change
if abs(current_temp - last_reported_temp) >= 5:
print(" Current: {:.1f}C / Target: {}C".format(current_temp, target_temp))
last_reported_temp = current_temp
# Check if target reached
if current_temp >= target_temp - 2: # Within 2 degrees
print(" Target temperature reached: {:.1f}C!".format(current_temp))
break
time.sleep(2)
# Step 7: Start print
print("\n[7/7] Starting print...")
# Use "abcde" as the SD filename (lowercase for M23 command)
# This matches what we transferred as "ABCDE" (firmware converts to lowercase)
sd_filename_lower = "abcde"
print(" SD filename: {}".format(sd_filename_lower))
# Initialize SD card
print(" Sending M21 (Init SD card)...")
response = cmd.sendCmd('M21\n')
print(" M21: {}".format(response.strip() if response else 'No response'))
time.sleep(1)
# Select file with M23 (LOWERCASE filename!)
print(" Sending M23 {} (Select SD file)...".format(sd_filename_lower))
response = cmd.sendCmd('M23 {}\n'.format(sd_filename_lower))
print(" M23: {}".format(response.strip() if response else 'No response'))
if 'error' in response.lower():
print(" ERROR: M23 failed to select file!")
sys.exit(1)
# Start autonomous SD printing with M33 (BEETHEFIRST custom command)
# Based on official BeeSlicer software - just "M33" alone, no filename!
# NOTE: Official software does NOT send G28 before print - printer homes from G-code
time.sleep(1)
print(" Sending M33 (Start autonomous SD print)...")
response = cmd.sendCmd('M33\n')
print(" M33: {}".format(response.strip() if response else 'No response'))
# Wait for print to initialize and check status using M32 (official method)
print(" Waiting 5 seconds for print to initialize...")
time.sleep(5)
print(" Checking print status with M32 (print session variables)...")
print(" M32 returns: A<estimated> B<elapsed> C<totalLines> D<currentLine>")
print("")
is_printing = False
for i in range(6): # Check 6 times over 30 seconds
# M32 returns print session variables - official BeeSlicer method
response = cmd.sendCmd('M32\n')
response_str = response.strip() if response else 'No response'
print(" M32: {}".format(response_str))
# Check if we're getting print progress data (indicates printing)
if response and ('A' in response or 'B' in response or 'D' in response):
is_printing = True
print(" ✓ Print session active!")
break
# Also check M625 status
status_response = cmd.sendCmd('M625\n')
print(" M625: {}".format(status_response.strip() if status_response else 'No response'))
# s:5 means printing state
if status_response and 's:5' in status_response:
is_printing = True
print(" ✓ Printer status: s:5 (Printing)")
break
if i < 5: # Don't sleep on last iteration
print(" Waiting... ({}/{})".format(i+1, 6))
time.sleep(5)
if not is_printing:
print("\n WARNING: Print status unclear after 30 seconds")
print(" The print may still start successfully.")
print(" Check the printer display to verify.")
print("\n" + "="*60)
print("PRINT STARTED!" if is_printing else "WAITING FOR PRINT...")
print("="*60)
# Monitor print status
print("\nMonitoring status (Ctrl+C to exit)...")
print("="*60)
print("")
try:
while True:
time.sleep(7)
try:
temps = cmd.getTemperatures()
nozzle = temps.get('Nozzle', 'N/A')
status = cmd.getStatus()
is_printing = cmd.isPrinting()
print("[{}] Temp: {}C | Status: {} | Printing: {}".format(
time.strftime("%H:%M:%S"), nozzle, status, is_printing))
if status == 'Shutdown' or not is_printing:
print("\nPrint completed or stopped.")
break
except Exception as e:
print("Error reading status: {}".format(e))
break
except KeyboardInterrupt:
print("\n\nMonitoring stopped by user.")
print("\nClosing connection...")
c.close()
print("Done!")
For the complete project (including load.py, unload.py, monitor.py, calibrate.py, driver modules, and udev rules), see: github.com/CMDR-Shrine/Bee-Slicer-CLI