“I cried because I had no salt, until I met a man who had no entropy.”

— Alice and Bob

Dulling the shine


One for Rick C…

Overture PLA Plus/Pro is giving me a much less shiny top surface, at least in the dark blue (love the color, by the way).

Related, while using my heat gun to de-string a print, I noticed that it did a nice job of slightly dulling the extremely shiny finish that I get on the bottom from printing on glass+hairspray. I had it set to 350°F with the fan on high, and kept moving and rotating the parts to avoid melting anything thicker than strings.

Note that this is unrelated to the use of heat guns to restore smooth plastic finishes, which involves reducing the impact of UV and oxidation damage without sanding/polishing off the surface layer.

Oh, and What was I printing? A bag clip, of course. 😁

More specifically, this stl, scaled up from this clip by a designer at Prusa. I found the original wore out too quickly when used to secure twice-folded-over coffee bags, so I scaled it up (a bit more XY, a lot more Z) and printed at 0.3mm, with 5 walls so it printed solid without any infill pattern. The (quite mild) stringing came from testing Cura’s “smart hiding” of layer-start positions with a spool of filament that’s a bit thicker than the nominal 1.75mm.

(picture is unrelated)

Don’t Be A Tool

In the grand tradition of using your 3D printer to print 3D printer accessories, quite a few people have designed little stands to hold all their 3D printer tools and published them to the various “search” sites. With few exceptions, they suck.

Common problems include:

  1. I need seven inches or more: requires at least 180mm in at least two dimensions. My printer’s build area is 255x155x170.

  2. Carve away anything that doesn’t look like an elephant: designed as a solid block of plastic with small holes for tools.

  3. If your love life requires close air support, something has gone very wrong: requires significant supports to print successfully.

  4. Hours will seem like days: all of the above contribute to ridiculous print times.

  5. Where does the third one go?: assumes specific workspace layout (wall-mount, pegboard, attaches to one model of printer, etc).

  6. One ring to rule them all: very-specifically-sized slots for every tool you could possibly need, not just the ones you actually use regularly.

There’s a pretty reasonable one designed specifically for the Dremel 3D45. Except for the part where it mounts to the right side of the printer, which isn’t where I use any of the tools.

File this one at Cults3D under “baffling” (even though it would fit nicely on my printer), because it has prominent storage for seven spare nozzles. Why? Not even “why do you need seven nozzles”, but “why do you have them all out on your workbench gathering dust in unlabeled bins?”.

Right now, I don’t want to print any of them, and I don’t want to spend the time to design my own, so my tool holder will continue to be a $5 box from Michaels. Maybe I’ll make a little organizer insert for it sometime, but honestly, I pretty much just use the scraper, flush cutters, emery boards, and a small sharp knife, and those fit on the lid of the box, with room left over for the calipers.

66%の誘惑

(classical reference)

Next time, Nancy, maybe you shouldn’t buy your souvenir pens from China…

"That's the trouble with godhood: it robs you of your finer

judgement. A deity so rarely has to pay for his mistakes!”

"​...while heroes... heroes have an infinite capacity for

stupidity! Thus are legends born!”

"I'm sorry, did you need that?"


How Disney destroyed Star Wars, catfight edition.

Pixiv: Intersectionality I Can Get Behind


The images for the day are the intersection of the tags “virtual youtuber” (バーチャルYouTuber) and “ass goddess” (尻神様). This is a short NSFW set, just so I could get the joke in.

more...

Nearly Topless


Thickness

I renamed my Cura 0.35mm layer-height preset from Coarse to Thick, because the Custom Printjob Naming plugin tries to abbreviate quality names to their initial letters, and if I have to choose between coarse and chunky, I’ll take chunky (this is not dating advice, but it could be). That makes the full set UAHGMOLTSC, which proves that I’m CACA (Crap At Creating Acronyms).

Mal-Adaptive

It seems Cura’s adaptive layer-height mode is experimental for a good reason. I printed a folding tablet holder that’s a good test of how well you’ve dialed in your filament settings, and it had a bunch of small holes in the top flat surfaces. Why? Because Cura calculated the number of top layers required for a 0.8mm surface based on the nominal layer height of 0.2mm, but because of nearby curved elements, printed them at 0.05mm instead. Four of those doesn’t make much of a surface. The stand is fully functional, just a bit moth-eaten on top.

This explains some other minor flaws I was running into with this feature, so I’ve stripped it out of my config set for now. UHGMOLTSC.

Reborn In Another World As A 3D Printer


I’d Rather Summon The Mazoku Musume

Lacking anything else to watch, I made it most of the way through the first season of How Not To Summon A Demon Lord. The main elf girl has ridiculous gag boobs made of pudding while the main catgirl is pure AAA-cup angst, but most of the other females are somewhere in between, which is refreshing after Highschool DxD.

It’s better than those other fan-service comedies I’ve attempted to watch recently, although Our Demon Lord’s social-anxiety freakouts get old fast. It seems to run at the usual light-novel adaptation pace of four episodes per book, which is too fast if the cast is large and there’s any non-trivial world-building. In this case it only feels slightly rushed and sparse, as if the original author hadn’t gotten around to doing those things yet in the first three books.

Note that the original title is a bit more direct: 異世界魔王と召喚少 女の奴隷魔術 = “other world demon lord and summoning-girl slave magic”. Pretty much what it says on the tin, although the Crunchyroll version restricts itself to pokies and not-quite-sexual climaxes. That said, the flatcat girl did take an offscreen finger in the last episode I watched.

Season 2 coming in April.

Cutting the strings

When I switched back from PETG to PLA and tried to print the mini traffic cone that was filled with spiders, I still got some spider-webs. Fortunately, I’ve been keeping my WIP Dremel configs in source control, and determined that I’d inadvertently deleted an important option from my top-level definition file:

"infill_before_walls": { "default_value": false }

The default inherited from fdmprinter.def.json is true, and a cone is pretty much the worst-case scenario for this, cutting across the circle from the end position of one layer to the start of the next, smaller layer. Adding this back eliminated every single bit of stringing inside the cone.

Tarball updated. Several times, actually; among other things, retraction_extra_prime_amount is something that can’t be overridden on a per-filament basis. You have to create a whole set of filament-specific quality overrides, which I generated with a script, because I’m up to 10 quality settings now. Each one contains a dozen lines of boilerplate and one actual setting line.

Which reminds me that I’m due for a rant on the fact that Cura uses three completely different file formats to store configuration information: XML, JSON, and Python’s ConfigParser (which is more-or-less Microsoft Windows INI format). Machine and extruder definitions are in JSON, quality settings and variants (like different nozzle sizes) are ConfigParser, and filament definitions are in XML. Some settings are valid in any file, some only in specific ones, and the only documentation is some help strings in fdmprinter.def.json. If there’s any good documentation, it’s not on Ultimaker’s support site or Github repo wikis.

Then there are settings that I couldn’t find in any file, including some that caused Cura to decide that all my configs had been corrupted and needed to be wiped, after I added or removed things from its directory. I let it do the reset once, to clean out a bunch of cruft; downloading filament profiles from their marketplace was a mistake, and saved custom settings are “messy”, to put it gently. Since then, I’ve kept tarball snapshots as I tinker.

Note that their contribution policy is to only accept new printer definitions from the actual manufacturer, so even if I (or the other guy) produce really awesome config files for the Dremel, they won’t merge a pull request.

Useful Cura plugins

  1. RawMouse: enables support for 3DConnexion 3D controllers. I’ve been using mine extensively with PrusaSlicer, and kinda-sorta with OpenSCAD (where it doesn’t work very well). 3d mice are worth every penny, if all your core apps support them. Cura doesn’t, and doesn’t offer a plugin in the marketplace, so this one’s on Github.

  2. Z Offset Setting: tweak the Gcode output up or down to compensate for nozzle height and material differences.

  3. Auto Orientation: try to orient models to reduce the amount of support needed to print them.

  4. Custom Printjob Naming: simple, flexible way to embed useful information in Gcode file names.

  5. Startup Optimizer: hide the cruft (dozens of printers and materials you don’t have) so Cura doesn’t try to load it all.

  6. Setting Visibility Set Creator: lets you override the standard basic/expert/advanced modes to expose only the settings you actually tinker with.

  7. Export HTML Cura Settings: only useful for A/B testing, but really useful for that. This is how I diffed two sets of Dremel configs.

  8. Barbarian Units: most STL files use millimeters as units, but every once in a while some application gives you Love American Style.

Set for the long weekend

With all the testing I’ve been doing, I’m finally running low on the Hatchbox Grey PLA, so I started to order some more, and then remembered that I’ve got five spools of Dremel PLA in assorted colors. I do need some more neutral gray for babydai koma at some point (good contrast against most thread colors), but that’s to make sets for other people; I’ve got mine.

Reduced stringing...


Flossy!

Unless you work with embroidery floss or other fine thread, you won’t have any practical use for this floss bobbin STL file. It is, however, small, flat, and requires only 1.5 grams of filament to print, making it an excellent choice for testing adhesion, elephant’s foot compensation, and top/bottom quality.

’cause rasterized is best!

The English word raster pretty much only refers to converting vectors to bitmaps (displaying text/graphics on a monitor, etc). The German word raster means “grid”, which leads to some confusion when someone says their 3D model is rasterized. I always interpret it as “has blocky pixels like Minecraft”, but they mean “sized to a specific X/Y/Z grid”.

“Asking for a friend…”

Does this picture of Mayumi Yamanaka count as Tenga cosplay?

(raise shields and disable javascript before clicking on the picture)

InDirecTV

I’ve received email and a text message informing me that DirecTV has finally acknowledged that I returned their equipment over two months ago, and it’s no longer being charged to my account. I won’t really believe it until it shows up on paper, and even then, I doubt they’ll ever send the $8 they owe me.

Dremelizing Cura, Chainsaw Edition


Here’s a tarball of my first pass at completely overhauling these Dremel 3D45 Cura configs. In order to test them side-by-side, I changed the vendor name in all the references from “Dremel” to “DR”.

The most frustrating aspect of creating this set was that when Cura barfed on my configs, all it said in the logs was “Error when loading container: ‘int’ object is not iterable”. Nothing useful like the specific file or line number. The actual error is: machine definition files can only have values in the form {"default_value": ... } or {"value": ...}. I had copied actual values out of the material and quality files as part of my effort to maximize the use of inheritance and calculated values. And that’s a half-hour of my life I want back.

My primary motive for building these was being able to adjust acceleration, jerk, and speed, and have all the other related values get calculated relative to them. Dremel’s original configs didn’t do this, and the linked Github repo is pretty much copied directly from those, with ongoing cleanup.

My secondary motive was expanding the list of recommended quality settings. I’d added a bunch of different layer heights (0.15, 0.25, 0.35, 0.4, and adaptive), but they didn’t show up in the top-level menu; I always had to go into the custom view to select from them, and then I lost the convenient UI for infill and supports. Defining a whole new printer with custom settings fixed that, and also automatically keeps overrides (like z offset) when I switch settings.

As part of the fun, I tried to come up with names for the various quality settings that matched the scheme Dremel used. Theirs were Ultra (0.05mm), High (0.1mm), Medium (0.2mm), Low (0.3mm), and High Speed (0.34mm plus some speed overrides). I added Good (0.15mm), Okay (0.25mm), Coarse (0.35mm), Chunky (0.4mm), and Adaptive (0.05-0.35mm). All of them inherit my revised acceleration/jerk settings, so the estimated print times are within 5%.

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.

“Need a clue, take a clue,
 got a clue, leave a clue”