CablePipeSnake


iStrain

Instead of incorporating any of the existing methods of fixing their long-standing problem with poor strain relief on cables, Apple patented a new one.

In other DifferentThink news, Apple has declared the word ’Asian’ out of bounds for kid-safe browsing on iPads and iPhones. No, seriously.

A Simple, Well-Made Thing

It’s a clone of a well-known and much-copied commercial product, but this folding pipe holder is correctly and cleanly adapted to 3D printing. No supports required, and the print-in-place hinge didn’t even require any break-in to work.

(and with the revised acceleration/jerk settings I mentioned yesterday, the actual print time was 99.5% of the slicer’s estimate)

I don’t currently have any fancy filament that would look cool for showing off some of my dad’s pipes, but it was refreshing to see someone do the job right.

Unlike the item I was looking at yesterday that had all kinds of curves and angles and overhangs running in all directions, forcing you to add 50% to the time and materials to support it all. It’s a complete replacement for an injection-molded part, with bits added to partially solve a problem that doesn’t come up that often. It would have been faster and easier to design something that attached to the existing part rather than replacing it, and the result would have been easier to print. Sigh.

I Speak Gcode To My Dremel, Python Edition

(script significantly updated and reformatted)

#!/usr/bin/env python3
"""
Connect to a networked Dremel 3D45 to manage print jobs and query
for job status and printer information.

Commands:
    info, status, preheat, print, cancel, pause, resume
"""

import sys
import os
import argparse
import configparser
import datetime
import requests

COMMAND = "http://%s/command"
UPLOAD = "http://%s/print_file_uploads"
WEBCAM = "http://%s:10123/?action=stream"
IPADDR = "192.168.0.1"


def api_cmd(command):
    try:
        r = requests.post(COMMAND % IPADDR, data=command, timeout=5)
        r.raise_for_status()
    except requests.exceptions.HTTPError as http_err:
        sys.exit("Printer %s (%s): %s" % (PRINTERNAME, IPADDR, http_err))
    except Exception as err:
        sys.exit("Printer %s (%s) offline or unhappy: %s" % (PRINTERNAME, IPADDR, err))
    return r.json()


def api_upload(file, jobname):
    try:
        gcode = {"print_file": (jobname, open(file, "rb").read())}
    except Exception as err:
        sys.exit("Printer %s (%s): %s" % (PRINTERNAME, IPADDR, err))
    try:
        r = requests.post(UPLOAD % IPADDR, files=gcode, timeout=300)
        r.raise_for_status()
    except HTTPError as http_err:
        sys.exit("Printer %s (%s): %s" % (PRINTERNAME, IPADDR, http_err))
    except Exception as err:
        sys.exit("Printer %s (%s) offline or unhappy: %s" % (PRINTERNAME, IPADDR, err))
    return r.json()


parser = argparse.ArgumentParser(
    description="Network utility for Dremel 3D45 printers",
    formatter_class=argparse.RawTextHelpFormatter,
    epilog="""
Requires a file ~/.pydremel containing at least one entry like this:
    [default]
    name=Autobot
    [Autobot]
    ip_address=192.168.0.200
(or you can change IPADDR at the top of the script)

* If you print the same filename twice in a row with different
  contents, the printer will remember some metadata from the first
  job, and calculate completion progress incorrectly.
""",
)
parser.add_argument(
    "-p",
    "--printer",
    default="default",
    help="named printer in your .pydremel config file",
)
parser.add_argument(
    "-r",
    "--raw",
    action="store_true",
    help="print raw JSON for info or status output",
)
parser.add_argument(
    "command",
    choices=["info", "status", "preheat", "print", "cancel", "pause", "resume"],
)
parser.add_argument("filename", nargs="?", help="gcode file to be printed")
args = parser.parse_args()

# load printer info from config unless user edited the script
if IPADDR == "192.168.0.1":
    config = configparser.RawConfigParser()
    config_file = os.path.join(os.path.expanduser("~"), ".pydremel")
    try:
        config.read(config_file)
    except:
        sys.exit("No config file ~/.pydremel")

    if args.printer == "default":
        if config.has_section("default"):
            PRINTERNAME = config.get("default", "name")
        else:
            sys.exit("No default printer in ~/.pydremel")
    else:
        PRINTERNAME = args.printer
    if config.has_section(PRINTERNAME):
        IPADDR = config.get(PRINTERNAME, "ip_address")
    else:
        sys.exit("No IP address for printer %s" % PRINTERNAME)
else:
    PRINTERNAME = "3D45"

if args.command == "info":
    s = api_cmd("GETPRINTERINFO")
    if args.raw:
        print(str(s).replace("'", '"'))
    else:
        print("%s" % s["machine_type"])
        print(
            "(SN=%s, firmware=%s, API=%s)"
            % (s["SN"], s["firmware_version"], s["api_version"])
        )
        if s["ethernet_connected"] == 1:
            print("IP Address %s (wired)" % s["ethernet_ip"])
        if s["wifi_connected"] == 1:
            print("IP Address %s (wireless)" % s["wifi_ip"])

elif args.command == "status":
    # note that 'layer' and 'fanSpeed' do not contain valid data
    s = api_cmd("GETPRINTERSTATUS")
    if args.raw:
        print(str(s).replace("'", '"'))
    else:
        if s["message"] == "success":
            print(
                "%.1f%% %s %s\n  %s/%s; %d°/%d° (chamber %d°)"
                % (
                    s["progress"],
                    datetime.timedelta(seconds=s["remaining"]),
                    s["jobname"],
                    s["status"],
                    s["jobstatus"],
                    s["temperature"],
                    s["platform_temperature"],
                    s["chamber_temperature"],
                )
            )
            # 'elaspedtime' (sic) starts counting when the nozzle
            # reaches temperature and moves to start printing, and
            # stops when jobstatus == 'completed'
            if s["totalTime"] > 0 and s["jobstatus"] == "completed":
                delta = s["elaspedtime"] - s["totalTime"]
                if abs(delta) > s["totalTime"] * 0.01:
                    print(
                        "  (estimate %s, actual %s%s (%.1f%%))"
                        % (
                            datetime.timedelta(seconds=s["totalTime"]),
                            "+" if delta > 0 else "-",
                            datetime.timedelta(seconds=abs(delta)),
                            abs(delta) / s["totalTime"] * 100
                        )
                    )

# there's no point in preheating the nozzle, since it will cool
# down by the time the auto-leveling is finished.
elif args.command == "preheat":
    s = api_cmd("PLATEHEAT")
    if s["message"] == "success":
        print("Bed heating to preset temperature (printer will beep twice)")

# jobname passed to printer cannot contain path or space characters
# and must end in '.gcode'
elif args.command == "print":
    if not args.filename:
        sys.exit("usage: %s print filename.gcode" % sys.argv[0])
    filename = args.filename
    if not os.path.isfile(filename):
        sys.exit("Error: file '%s' not found" % filename)
    jobname = os.path.basename(filename).replace(" ", "_")
    if os.path.isfile(filename):
        s = api_upload(filename, jobname)
    if s["message"] == "success":
        s = api_cmd("PRINT=%s" % jobname)
    if s["message"] == "success":
        print("Success! Watch your job print at:")
        print(WEBCAM % IPADDR)
    else:
        sys.exit("Error: couldn't print '%s': %s" % (filename, s["message"]))

# cancel will not take effect until after auto-leveling completes
elif args.command == "cancel":
    s = api_cmd("GETJOBSTATUS")
    if s["message"] == "success":
        jobname = s["jobname"]
    if jobname == "":
        print("No active job")
    else:
        s = api_cmd("CANCEL=%s" % jobname)
        if s["message"] == "success":
            print("Print job %s canceled" % jobname)
        else:
            print("Cancel failed: %s" % s["message"])

elif args.command == "pause":
    s = api_cmd("GETJOBSTATUS")
    if s["message"] == "success":
        jobname = s["jobname"]
    if jobname == "":
        print("No active job")
    else:
        s = api_cmd("PAUSE=%s" % jobname)
        if s["message"] == "success":
            print("Print job %s paused" % jobname)
        else:
            print("Pause failed: %s" % s["message"])

elif args.command == "resume":
    s = api_cmd("GETJOBSTATUS")
    if s["message"] == "success":
        jobname = s["jobname"]
    if jobname == "":
        print("No active job")
    else:
        s = api_cmd("RESUME=%s" % jobname)
        if s["message"] == "success":
            print("Print job %s resumed" % jobname)
        else:
            print("Resume failed: %s" % s["message"])

Oxy Morons in the House

Dear Dems, either they’re facts or they’re allegations. Calling them “factual allegations” kind of makes Trump’s point that you’re just blowing smoke up our asses.


Comments via Isso

Markdown formatting and simple HTML accepted.

Sometimes you have to double-click to enter text in the form (interaction between Isso and Bootstrap?). Tab is more reliable.