Fun

Cash Leveling


“Tomorrow, we have to decide which country we want to buy”

Went through the first pass on my taxes last night, and came out better than expected. That is, the two different employers, large severance check, and stock sales (options, ESPP, RSU) were effectively balanced by the extra amount I had withheld and the amount my new job over-withheld for Social Security, etc, leaving me with my usual close-to-breaking-even little refund checks. I’ll let it bake a week in case some late paperwork or surprise changes arrive, but it looks like the money I carefully saved for potential tax payments goes into my pocket instead. And there’s a semi-annual bonus on the way.

But I’ve already said that I’m not buying a new car this year, or a new computer, or a new camera, no matter how Sony tempts me. Oh, my. Wait, this one has gigabit ethernet?!?

We haven’t finalized the dates on the re-re-re-rescheduled Japan trip, but it’s currently “three weeks in November”, with enough time in Tokyo for me to spend way too much money. Probably not on camera gear, unless I find myself in a shop full of classic Minolta lenses…

Maybe this will be the trip where we take a few overnight trips off the beaten path. I’ve already penciled in a day trip to Kamakura and a whistle-stop tour of Nagoya (mostly for the Totoro House) on the way from Tokyo to Kyoto, but Nikko and Amanohashidate would benefit from full days, and a return visit to Ise would be cool, too. My sister’s not a big fan of cable-car rides, but she might go for an overnight stay at one of the many temples on Mt. Koya.

Solo Leveling Offline

File under peculiar the fact that the first half of the Solo Leveling light novel is out today in paperback only. No Kindle edition listed at all. I’d have to wait an entire day before I could start reading it. I think I’ll hold off for a week or so to see if the ebook turns up.

Which reminds me to check up on the online comic, which was supposed to start back up this year.

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.

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.

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.

bash dose not exists


On CallCallCallCall

Yesterday was the beginning of my first week of on-call since I started the new job at the end of August (pause for hysterical laughter at the thought of only one week at a time). When Baby’s First Alert went off, PagerDuty simultaneously sent email, SMS, a push notification, and a phone call; it sounded like my phone was having an audiogasm. My first priority was trimming it to one alert at a time.

And, yes, one of the Nagios errors I’ve been paged over was a log-scanner that flagged an unfamiliar error message. It turns out that “X dose not exists” is a surprisingly popular typo, in a variety of contexts.

Technically not a lens!

Despite the high risk of higher taxes this year, I decided to stretch my no-new-cameras-or-lenses rule slightly to buy a Nikon-to-Sony tilt/shift adapter. +/-10mm of shift, 10° of tilt, and 360° of rotation with click-stops so you can use them in any direction. It’s not a true view camera adapter, but it also doesn’t cost $850 and weigh considerably more than the camera.

A quick test with my really old Nikon 35mm f/2.8 and 50mm f/1.4 allowed full movements with no vignetting on my A6500. It’s something I’d only pack on days where I was planning to visit a castle (or if I ever made it waaay out to the Yamato museum in Kure to get some pics of the 1:10 scale model), but it does something software simply can’t.

(and, yes, there are half a dozen 3d-printed tilt/shift adapter designs out there, but most of them have the wrong mount on one or both sides, and are clunky and/or hard to print. Also, I’m not really willing to trust my lenses to their design skills. I’ve had a camera fall four feet onto concrete due to a defective strap; trusting a 3d-printed connector seems worse)

(and, yes, my current straps are very sturdy)

Also not a lens!

Somewhere I have a very nice pinhole adapter for my 4x5 camera, which would be great if I had a darkroom, or a high-end digital back. Since I don’t, I was idly looking at the various body-cap adapters, which range from actual body caps to nicely-made adapters that take lens caps and filters.

That Amazon listing didn’t include the effective aperture size, which is kinda useful information, so I went to their official site, and the very first sample picture from the product was:

Because nothing says “infinite depth of field” like a giant replica of a masturbation device.

Monster Girl Doctor

Sometime last year I read the first two light novels for this and lost interest, so the anime wasn’t high on my list. After idly watching the first few episodes, I’m not terribly interested in more, unless they focus on the cyclops girl Memé.

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