418 lines
11 KiB
Bash
Executable file
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
|