"but seriously, that artwork is a reflection of US global cultural hegemony. it's not that hard to see this stuff if you know how to look at the world."

"Hi, artist here! It's about cute girl eating burger"

— Sasoura corrects a pompous imbecile

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.

Thicc Cam


Sticktoitivity

Accepted wisdom is that the maximum practical layer height for a given nozzle size is ~80% of its diameter, or 0.32mm for the common 0.4mm nozzles. I was tinkering with Cura’s adaptive layer height feature (which does not offer the customizability of PrusaSlicer’s, and is hidden away as an experimental feature), and it happened to generate some layers all the way up at 0.4. So I figured, what the hell, let’s try it on something that doesn’t have to be sturdy, and that would still work just fine if the top half fell off. Or even the top 2/3.

Turned out just fine, with some very light stringing on the inside of the tube that was easy to shave. Fits nicely, too. I’m sure there are plenty of models where 0.4mm layers wouldn’t work, but it’s a time-saver when it does.

(and, yes, if I want to really speed things up, MicroSwiss does make 0.6mm and 0.8mm nozzles for the 3D45, as well as replacement 0.4mm ones; all of them are also rated for abrasive filaments, something I’ve had no reason to try yet)

My miter box cam pin has finally been indexed on Thingiverse, and shows up both in searches and in my profile. It took two months.

Found a Zorkmid

Well, printed one, anyway, as a Cura test. Came out pretty decent, printed on edge on a raft.

No soup for you!


No pie for you!

Corona-chan apparently took down the local Pizza Hut a few weeks ago. Permanently? Still showed as closed on Friday night at dinnertime. If I wanted good pizza I’d make it myself, but first I’d have to go buy pepperoni and cheese. I tend to order from PH if I don’t want to go out, because it’s better than Domino’s, half the price of Round Table, and won’t make me sick like Mountain Mike’s does.

No cut for you!

The local chain haircut joint was shut down by the governor (again) at the beginning of December, and despite him opening things back up a little in a likely-futile effort to stop the petition to shitcan his l’il-dictator ass, they haven’t reopened yet. It looks like their location in Seaside is back online, and I know the folks from the Salinas shop were working shifts over there, so maybe I’ll make the 20-mile drive today or tomorrow.

No kids for you!

Episode 4 of That Spider Show was a bit more interesting, because it didn’t advance the B ArkArc.

(spider is unrelated)

No shows for you!

I haven’t found anything else worth watching for days. Last thing I managed to make it through was Matt Smith’s final arc in Doctor Who.

Time for a Cofee Brick!


Amusing Note

Shady web sites that redirect you to malware are still using the “your Flash Player is out of date” con. And it probably still works about as well as it ever did.

This Just In

Remember the “insurrection” that Trump supposedly incited with his speech, that was used to take down Parler despite being coordinated on Twitter and Facebook? Y’know, the one where police opened the doors to let protesters into the building before killing an unarmed woman who wasn’t threatening anyone? The one where they found pipe bombs that the FBI just revealed were planted the night before?

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