bambu.sh/bambu.sh
2024-07-13 21:57:32 -04:00

418 lines
11 KiB
Bash
Executable file

#!/usr/bin/env bash
CONFIG_PATH="${XDG_CONFIG_HOME:-$HOME/.config}/bambu.conf.sh"
function usage() {
cat <<EOF
Command: $0 [--debug] [--raw-output] <subcommand> [args] ...
Command line client for controlling Bambu Labs 3D printers.
This script relies on a configuration file located at $CONFIG_PATH.
The configuration file should be a Bash script that sets the following variables:
BAMBU_IP
BAMBU_SERIAL
BAMBU_ACCESS_CODE
Subcommands:
help - Print this help message
status - Display current status of the printer
file
ls [--list-only] [--file] [path]
- List <path> on the printer's SD card.
If "--list-only" is specified, only print out the names
of files without metadata (timestamp, etc.).
If "--file" is specified, then <path> is assumed
to be a file, and it is listed on its own instead of
trying to list its children.
get [-o|--output <output_directory>] <remote_file>
- Download <remote_file> to <output_directory> (\$PWD by default)
put <local_file> <remote_directory>
- Upload <local_file> to <remote_directory>
rm [-r|--recursive] <path>
- Delete the remote <path> on the printer.
If "-r" is specified, assume <path> is a directory and
delete all of its children, including itself.
print
start [--timelapse] [--no-bed-leveling] [--no-vibration-calibration]
[--flow-calibration] <remote_file>
- Start a print job with <remote_file>. Only single-plate 3mf
files are supported.
pause|resume|stop
- As names suggest.
check
- Check the current print status from the printer and output
any potential error codes.
EOF
}
function die() {
echo "$1" >&2
exit 1
}
[ ! -f "$CONFIG_PATH" ] && die "Config file $CONFIG_PATH not found"
source "$CONFIG_PATH"
[ -z "$BAMBU_IP" ] && die "Please set BAMBU_IP"
[ -z "$BAMBU_SERIAL" ] && die "Please set BAMBU_SERIAL"
[ -z "$BAMBU_ACCESS_CODE" ] && die "Please set BAMBU_ACCESS_CODE"
PRETTY_OUTPUT=true
DEBUG=false
TMP_DIRS=()
BACKGROUND_PIDS=()
function cleanup_tmp() {
for p in "${BACKGROUND_PIDS[@]}"; do
kill "$p" > /dev/null 2>&1
done
for dir in "${TMP_DIRS[@]}"; do
rm -rf "$dir" > /dev/null 2>&1
done
}
trap "cleanup_tmp" EXIT
function mqtt_command() {
local tmp_sock_dir="$(mktemp -d)"
TMP_DIRS+=("$tmp_sock_dir")
socat UNIX-LISTEN:$tmp_sock_dir/mqtt.sock OPENSSL-CONNECT:$BAMBU_IP:8883,verify=0,no-sni=1 > /dev/null 2>&1 &
BACKGROUND_PIDS+=($!)
local mosquitto_additional_flags=""
if [ "$DEBUG" == "true" ]; then
echo "Sending MQTT payload:" >&2
([ "$PRETTY_OUTPUT" == "true" ] && echo "$1" | jq) || echo "$1" >&2
mosquitto_additional_flags+="-d"
fi
# mosquitto does not send debug messages to stderr, so do our own hack here by redirecting lines that don't look like JSON
# to stderr. Also turn off buffering for stdout so that we can get debug messages quickly.
stdbuf -i0 -o0 -e0 mosquitto_rr $mosquitto_additional_flags --unix $tmp_sock_dir/mqtt.sock -V mqttv31 -u bblp -P $BAMBU_ACCESS_CODE -t "device/$BAMBU_SERIAL/request" -e "device/$BAMBU_SERIAL/report" -m "$1" | {
local seen_json=false
while read line; do
if [[ "$line" =~ ^\{ ]]; then
seen_json=true
fi
if [ "$seen_json" == true ]; then
echo "$line"
else
echo "$line" >&2
fi
done
} | {
([ "$PRETTY_OUTPUT" == "true" ] && [ "$FROM_INTERNAL" != "true" ] && jq) || cat
}
rm -rf "$tmp_sock_dir" > /dev/null 2>&1
}
function curl_ftps_command() {
local directory="$1"
shift
curl --ftp-pasv --insecure --user "bblp:$BAMBU_ACCESS_CODE" "ftps://$BAMBU_IP/$directory" "$@"
}
function preprocess_file_path() {
echo "$1" | sed "s@^/@@"
}
function preprocess_directory_path() {
local directory="$1"
directory="$(preprocess_file_path "$directory")"
[[ "$directory" =~ \/$ ]] || directory="$directory/"
echo "$directory"
}
function bambu_status() {
mqtt_command '{"pushing": { "sequence_id": 1, "command": "pushall"}, "user_id":"1234567890"}'
}
function bambu_file() {
local subcommand="$1"
shift
case "$subcommand" in
ls)
bambu_ls "$@"
;;
put)
bambu_put "$@"
;;
rm)
bambu_rm "$@"
;;
get)
bambu_get "$@"
;;
*)
die "Unknown subcommand $subcommand"
;;
esac
}
function bambu_ls() {
local is_file="false"
local extra_curl_args=""
local path=""
while [ ! -z "$1" ]; do
case "$1" in
-f|--file)
is_file="true"
shift
;;
--list-only)
extra_curl_args+="--list-only"
shift
;;
-*)
die "Unknown option $1"
;;
*)
path="$1"
shift
break
;;
esac
done
if [ "$is_file" == "true" ]; then
path="$(preprocess_file_path "$path")"
curl_ftps_command "$(preprocess_directory_path "$(dirname "$path")")" -s | grep "$(basename "$path")" $extra_curl_args
else
path="$(preprocess_directory_path "$path")"
curl_ftps_command "$(preprocess_directory_path "$path")" $extra_curl_args
fi
}
function bambu_put() {
local directory="$(preprocess_directory_path "$2")"
curl_ftps_command "$directory" -T "$1"
}
function bambu_rm() {
local is_dir="false"
local name=""
while [ ! -z "$1" ]; do
case "$1" in
-r|--recursive)
is_dir=true
shift
;;
-*)
die "Unknown option $1"
;;
*)
name="$1"
shift
break
;;
esac
done
[ -z "$name" ] && die "Please specify file/directory to delete"
if [ "$is_dir" == "true" ]; then
bambu_rm_dir "$name"
else
bambu_rm_file "$name"
fi
}
function bambu_rm_file() {
local file="$(preprocess_file_path "$1")"
curl_ftps_command "" -Q "DELE /$file" > /dev/null 2>&1 || echo "Failed to delete file $file"
}
function bambu_rm_dir() {
local dir="$(preprocess_directory_path "$1")"
[ -z "$dir" ] && die "Please specify directory to be deleted"
local content="$(bambu_ls --list-only "$dir" 2> /dev/null)"
while read file; do
echo "Deleting $dir$file"
bambu_rm_file "$dir$file"
done <<< "$content"
curl_ftps_command "" -Q "RMD /$dir" > /dev/null 2>&1 || echo "Failed to delete directory $dir"
}
function bambu_get() {
local file=""
local output_path="$PWD"
while [ ! -z "$1" ]; do
case "$1" in
-o|--output)
output_path="$2"
shift
shift
;;
-*)
die "Unknown option $1"
;;
*)
file="$1"
shift
;;
esac
done
[ -z "$file" ] && die "Path to file not specified"
curl_ftps_command "$file" -o "$output_path/$(basename "$file")"
}
function bambu_print() {
local subcommand="$1"
shift
case "$subcommand" in
start)
bambu_print_start "$@"
;;
stop)
mqtt_command '{"print": { "sequence_id": 1, "command": "stop", "param": ""}}'
;;
pause)
mqtt_command '{"print": { "sequence_id": 1, "command": "pause", "param": ""}}'
;;
resume)
mqtt_command '{"print": { "sequence_id": 1, "command": "resume", "param": ""}}'
;;
check)
bambu_print_check
;;
*)
die "Unknown subcommand: $subcommand"
;;
esac
}
function bambu_print_check() {
local status="$(FROM_INTERNAL=true bambu_status)"
local err="$(echo "$status" | jq -r '.print.print_error')"
if [ -z "$err" ] || [ "$err" == "0" ]; then
local gcode_state="$(echo "$status" | jq -r '.print.gcode_state')"
if [ "$gcode_state" == "RUNNING" ]; then
echo "Now printing: $(echo "$status" | jq -r '.print.subtask_name')"
echo "Progress: $(echo "$status" | jq -r '.print.mc_percent')%"
fi
echo "No errors"
else
printf "Error: 0x%x\n" "$err"
fi
}
function bambu_print_start() {
local file=""
local timelapse="false"
local bed_leveling="true"
local vibration_cali="true"
local flow_cali="true"
while [ ! -z "$1" ]; do
case "$1" in
--timelapse)
timelapse="true"
shift
;;
--no-bed-leveling)
bed_leveling="false"
shift
;;
--no-vibration-calibration)
vibration_cali="false"
shift
;;
--flow-calibration)
flow_cali="true"
shift
;;
-*)
die "Unknown option $1"
;;
*)
file="$1"
shift
break
;;
esac
done
[ -z "$file" ] && die "Remote file path required"
file="$(echo "$file" | sed "s@^/@@")"
local task_name="$(echo "$file" | rev | cut -d'/' -f1 | rev)"
task_name="${task_name/.3mf/}"
task_name="${task_name/.gcode/}"
local payload=$(cat << EOF
{
"print": {
"sequence_id": "1155",
"command": "project_file",
"url": "file:///sdcard/$file",
"md5": "",
"param": "Metadata/plate_1.gcode",
"subtask_name": "$task_name",
"bed_type": "auto",
"timelapse": $timelapse,
"bed_leveling": $bed_leveling,
"flow_cali": $flow_cali,
"vibration_cali": $vibration_cali,
"layer_inspect": false,
"use_ams": false,
"profile_id": "0",
"project_id": "0",
"subtask_id": "0",
"task_id": "0"
}
}
EOF
)
mqtt_command "$payload"
}
while [ ! -z "$1" ]; do
case "$1" in
-r|--raw-output)
PRETTY_OUTPUT=false
shift
;;
--debug)
DEBUG=true
shift
;;
-*)
die "Unknown option $1"
;;
*)
command="$1"
shift
break
;;
esac
done
case "$command" in
status)
bambu_status "$@"
;;
file)
bambu_file "$@"
;;
print)
bambu_print "$@"
;;
help)
usage
;;
*)
die "Unknown command $command"
;;
esac