“I feared that the committee would decide to go with their previous decision unless I credibly pulled a full tantrum.”

— dmr@alice.UUCP

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.

Tactical DungeonScript


Tactical Stealth Police, you say?

Pretty sure there’s no tactics involved in opening a beer.

Women In Engineering?

Wow, Japan was really ahead of the curve in getting women into STEM!

Yes, my Kindle recommendations are currently filled with old samurai and ninja books, randomly categorized.

Can you spin a good yarn?

It’s all fun and games until someone beats the weft.

Monster Manual

I’ve always suspected Javascript was an Aberration…

“Thank you for the mental picture!”

Actual headline:

Pornhub Announces ‘Biometric Technology’ to Verify Users

DNA sample optional.

Dialing It In

After tinkering with acceleration and jerk settings in both Cura and PrusaSlicer (dropping them to 1000 and 5, respectively), I kicked off a job with an estimated runtime of 22,873 seconds. Actual completion time: 23,478 seconds. 2.6% over is plenty good enough for me, so hopefully it will be pretty consistent from now on.

(the actual before/after print times were almost identical in my testing, so this is just adjusting the slicer to match the actual behavior of the printer; next step: tweaking overall speed up and down, to see if the new estimates are still in the ballpark)

Reminder: early reports are almost never true

It’s extremely common for “on-the-scene” reports of an event to be false or misleading, and the few surviving serious journalists know this, but the convenient lie will always get more coverage than the pedestrian truth. It must have sickened a CNN “journalist” to print these words about Teh Insurrection:

“Medical examiners did not find signs that the officer sustained any blunt force trauma, so investigators believe that early reports that he was fatally struck by a fire extinguisher are not true.”

Still no word on why Ashli Babbitt was shot dead by a cop.

What's the nicest thing I can say about...


High School DxD

I made it farther (5 episodes) than I did with Senran Kagura (2/3), only because the fan-service was so overwhelming and badly done that it was funny, particularly the completely out-of-character stripperific ED animation. Unfortunately, that’s not enough to make the show watchable, especially given how annoying Our Hero is. But what really killed it for me was Sailor Goon showing up in episode 6; pick a direction, guys.

Apple

They’re really working hard to make Microsoft look like a well-run company with a solid operating system and decent QA. Surprisingly, so is Microsoft.

Corona-chan

The masks coming off, and I don’t mean the sneeze-guards people have been forced to wear for most of the past year.

Google/Twitter/Facebook

Well, they’re not serial killers. Pretty Sure. Okay, kinda sure. Maybe?

LinkedIn

They haven’t completely lost focus on being a professional networking tool, although they sure have buried it under a mountain of irrelevant social-network/data-harvesting/certification bullshit. And they accidentally helped a new job get me last year.

DirecTV/AT&T

Their efforts to fraudulently extract money from former customers probably aren’t felonious. If only because they have really good contract lawyers.

Democrats

They’re not all career criminals. I mean, there’s Tulsi Gabbard, who I disagree with on almost every issue but am willing to trust when it counts because she actually likes America.

And, um, give me a minute, I’m sure I’ll think of another one.

Poorly seasoned...


Boonie Dungeon

Not watching, but spotted an interesting review of the original light novels, where someone commented that they found it rather offputting that the author kept laughing at his own jokes. That explains a lot, actually.

(and, no, most fan-artists aren’t watching this show, either)

Wet season, dry season

Yup, there’s nothing on. This season is so weak that I’ve started watching the uncensored version of High School DxD on Funimation. So far, the only two female characters without giant gag boobs are the kitten named “Kitten” and the white girl named Asia. Their figures would be spectacular for real-life high-school-age girls not named “Ai Shinozaki”, while the bowling balls the rest of the girls are attached to should leave them all crippled with pain, even with demonic powers keeping their permanently-stiff nipples at unnatural angles. Dunno how many episodes I can keep this up for…

(I had to go looking for this, because out of more than 25,000 saved Pixiv pics, I didn’t have a single one from this series)

Coming soon…

Next season, the only things that look watchable so far are That Slime Spinoff and the long-overdue Zombieland Saga: Revenge.

Unrelated, you’ll never guess what Apple did!

…unless you guess that they locked my account again with no explanation.

Surprisingly, I’ve only had to log in once on each device to restore functionality. So far.

Speaking of Evil

Remember when I canceled DirecTV in November and returned my equipment the same day, only to have them try to bill me for it at the beginning of January? Remember how I pulled out my carefully-saved receipt and read the confirmation number over the phone to a rep who confirmed that it was received and the bill would be canceled?

DirecTV doesn’t. They just tried again. Really, really glad I removed my credit card from the account before I called them last month.

30 frustrating minutes later…

It only took about five minutes of ads from a company that’s trying to rip me off for $150 to get through to a human, who agreed that yes, this should have been fixed last time, but I was talking to the wrong department, and needed to talk to the warehouse to confirm that the equipment I had a return receipt for actually arrived.

I pointed out that the fact of my receipt and its presence in her system made it Not My Problem, but she insisted that since it wasn’t a billing issue, she had to transfer me to the warehouse rep. She couldn’t give me a reference number or a direct phone number, but happily waited on hold with me to ensure I was correctly connected and the call was not dropped. Sweet time-waster for someone paid by the hour, says I.

After fifteen minutes of increasingly hard-to-understand ads, I was finally connected with someone who I could not understand at all. Not because of her accent, because approximately 50% of the voice packets were missing. Fortunately, she could hear me just fine, so I could give her my callback number. Unfortunately, it took another several minutes for her to convey the message that she couldn’t disconnect and call me back until I hung up. Yeah, whatever.

To my surprise, she actually did call me back right away on a clean line, confirm that the ticket was being escalated, and give me a reference number and approximate ETA: 10-15 business days.

Joy.

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