Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
Category: Software
General-purpose computers doing something specific
I recently exhumed an Iomega 500 GB Home Network Hard Drive (model MDHD-500-N) from the Big Box o’ Drives, with the intent of dumping video files from the Sony HDR-AS30 helmet camera thereupon.
Remember Iomega of ZIP Drive fame? Seems EMC Borged ’em a while back, collided with Lenovo, discarded all the old hardware support, and that’s the end of that story.
Exhuming the setup password from my backup stash wasn’t worth the effort, so I experimentally determined that holding the Reset switch closed while turning the drive on blows away the existing configuration. It woke up, asked for an IP address, got 192.168.1.52 from the DHCP server (you can find that by checking the router’s tables), and popped up the administration console at 192.168.1.52:80 as you’d expect.
The userid will always be admin, but you can change the password from admin to whatever you like; you may safely assume I have done somewhat better than what you see below.
Twiddling the configuration through the IOmega web-based console:
Device name: IOMEGA-500MB (for lack of anything more creative)
Group name: WHATSMYNET
Password: not-admin
Drag the date/time into the current millennium
Time Zone: GMT-5:00
Time Server: 0.us.pool.ntp.org
Static IP: 192.168.1.10 (suitable for my network)
Gateway & DNS as appropriate
Windows File Sharing enabled for the PUBLIC directory
FTP turned off
Sleep time: 10 minutes
Changing either the IP address or the password requires logging in again, of course.
I reformatted the drive, just to be sure.
Then, after a bit of Googling to remember how all this works…
A line in /etc/hosts (left over from the last time I did this) gives the new static IP address:
192.168.1.10 nasty
Install the cifs-utils package to enable mounting the drive.
Create a mount point:
sudo mkdir /mnt/video
Create a file (/root/.nas-id) holding the super-secret credentials used to gain access to the drive:
domain=WHATSMYNET
username=ed
password=not-admin
Then restrict the file to the eyes of the root user:
sudo chmod 700 /root/.nas-id
It’s not clear that the username or domain really make any difference in this situation, but there they are.
Define where and how to mount the network drive with a new line at the bottom of /etc/fstab, which refers to the aforementioned super-secret credentials file:
Mounting it with my userid gives the shared directories & files proper permissions for me (and nobody else, not that anybody else around here cares).
So the manual mounting process looks like this:
sudo mount /mnt/video
Adding the user mount option would eliminate the sudo, but manual mounting won’t be necessary after a normal boot when the automagic startup script does the deed.
The drive must have the noauto attribute to prevent the upstart Pachinko machine from trying to mount the network drives before the network comes up. Actually mounting the drive at the proper time requires an additional line in /etc/init/local.conf:
description "Stuff that should be in /etc/rc.local"
author "Ed Nisley - KE4ZNU"
start on (local-filesystems and net-device-up IFACE=em1)
stop on shutdown
emits nfs-mounted
script
logger Starting local init...
logger Mounting NFS (and CIFS) filesystems
mount /mnt/bulkdata
mount /mnt/userfiles
mount /mnt/diskimages
mount /mnt/music
mount /mnt/video
initctl emit nfs-mounted
logger Ending local init
end script
The reason the drive wound up in the Big Box o’ Hard Drives was its lethargic transfer speed; copying a 4 GB video file from either the MicroSDXC card (via an SD adapter) or the previous 750 GB USB-attached hard drive to the IOmega NAS trundles along at a little over 6 MB/s. The camera stores 25 Mb/s = 3 MB/s of data in 1080p @ 60 fps, so figure 1/2 hour of copying per hour of riding. The USB drive can write data from the aforementioned MicroSDXC card at 18 MB/s, so the card and USB interface aren’t the limiting factors.
I’m not (generally) in a big hurry while copying files from the camera’s SD card, because that’s now automated:
#!/bin/sh
thisdate=$(date --rfc-3339=date)
echo Date is [$thisdate]
# IOmega NASalready mounted as /mnt/video in fstab
mkdir /mnt/video/$thisdate
sudo mount -o uid=ed /dev/sdb1 /mnt/part
rsync -ahu --progress /mnt/part/MP_ROOT/100ANV01/ /mnt/video/$thisdate
if [ $? -eq 0 ] ; then
rm /mnt/part/MP_ROOT/100ANV01/*
sudo umount /mnt/part
fi
I’ve been discarding the oldest month of videos as the USB hard drive fills up, which will happen a bit more often than before: the drive’s 466 GB can hold barely 35 hours of ride video.
Another nine hours of printing produced a second 9×13 link chain mail armor sheet that simply begged to be joined with the first. Snipping a connecting link on one sheet and attempting to thread it through the armor button on the other didn’t work nearly as well as I expected, because the pillars on the open links don’t quite pass through the slot in the side of the armor button links:
Chain Mail Armor – 4 sided
So I summoned joiner links from the digital deep:
Chain Mail Armor – Sheet Joiners
Those are standard armor button links, split at the cross bar level, then laid out along the Y axis. The cap bridges across the link just as it does on the chain mail sheets, so, when they’re glued back together, the result should be exactly like a solid link. There’s no room for alignment pins and, frankly, I wouldn’t fiddle with two dozen filament snippets anyway.
The OpenSCAD code below produces joiners that work for the square arrangement, not the diamond, but that’s in the nature of fine tuning.
When I saw them pasted to the platform, just like the model:
Chain Mail Armor – joiners on platform
It occurred to me that I could pop the caps off, then lay the sheets in position, aligned on the underlying joiner half-links. Here’s the first sheet over the left set of bars:
Chain Mail Armor – sheet and joiners on platform
Then glue the armor caps in place:
Chain Mail Armor – joiner with solvent glue
Four dots of IPS #4 solvent glue, dispensed from a fine copper tube serving as a pipette, wet the four pillars of the joiner’s two bottom bars. I dotted each pillar to begin softening the PLA, paused for a breath, wet them again to leave enough solvent to bite into the bottom of the armor cap, pressed the cap in place, tweaked the alignment with tweezers, then pressed downward for maybe five seconds. Although the joiner link has no inherent alignment features, there’s also not much room to slide around and it worked surprisingly well.
Repeat that trick dozen times and you’re done. The aggravation scales as the square root of the overall sheet size, so it’s not as awful as assembling every single link, but it’s definitely a task for the low-caffeine part of the day.
One bottom bar came loose when I showed the result at the MHVLUG meeting, but the bar reappeared and I glued it again easily enough. I’ve now printed several spare joiners, Just In Case.
The bottom bars aren’t firmly affixed to the platform after it cools and they dislodge fairly easily: that’s how I get larger models off: let everything cool, then simply lift the plastic off. If I were joining sheets on a regular basis, I’d conjure a fixture to hold the sheets and joiner caps in position, probably with the sheets upside down, then glue the bars atop the inverted caps. That could get messy.
Perhaps a special holder to capture the bars in the proper alignment, maybe with pins matching the square openings at the corners, would help?
This is a trial fit before gluing that’s visually indistinguishable from the final product:
Chain Mail Armor – joined sheets on platform
It’s not actually fabric, but it’s sufficiently bendy to cover a hand:
Chain Mail Armor – joined sheet draped on hand
The thing just cries out to be fondled…
There’s a quarter kilogram of plastic in that 8×12 inch = 200×310 mm sheet that almost used up the last of the black PLA spool.
Remember: you must tweak the OpenSCAD code to match your extruder settings, export a suitable STL file, get really compulsive about platform alignment, use hairspray / glue stick to boost platform adhesion, and have no qualms about an all-day print run. You can’t just slice a random STL file produced for a different printer, because the link dimensions come directly from the printer’s capabilities: one size does not fit all.
The OpenSCAD source code [Update: This is the refactored version.]:
// Chain Mail Armor Buttons
// Ed Nisley KE4ZNU - December 2014
Layout = "Build"; // Link Button LB Joiner Joiners Build
//-------
//- Extrusion parameters must match reality!
// Print with 1 shell and 2+2 solid layers
ThreadThick = 0.20;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1*ThreadThick; // make holes end cleanly
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//-------
// Dimensions
//- Set maximum sheet size
SheetSizeX = 50; // 170 for full sheet on M2
SheetSizeY = 60; // 230
//- Diamond or rectangular sheet?
Diamond = false; // true = rotate 45 degrees, false = 0 degrees for square
BendAround = "X"; // X or Y = maximum flexibility *around* designated axis
Cap = true; // true = build bridge layers over links
Armor = true && Cap; // true = build armor button atop (required) cap
ArmorThick = IntegerMultiple(6,ThreadThick); // height above cap surface
// Link bar sizes
BarWidth = 6 * ThreadWidth;
BarThick = 4 * ThreadThick;
BarClearance = 5*ThreadThick; // vertical clearance above & below bars
//-- Compute link sizes from those values
// Absolute minimum base link: bar width + corner angle + build clearance around bars
// rounded up to multiple of thread width to ensure clean filling
BaseSide = IntegerMultiple((4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth)),ThreadWidth);
BaseHeight = 2*BarThick + BarClearance; // both bars + clearance
echo(str("BaseSide: ",BaseSide," BaseHeight: ",BaseHeight));
echo(str(" Base elements: ",4*BarWidth,", ",2*BarWidth/sqrt(2),", ",3*(2*ThreadWidth)));
echo(str(" total: ",(4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth))));
BaseOutDiagonal = BaseSide*sqrt(2) - BarWidth;
BaseInDiagonal = BaseSide*sqrt(2) - 2*(BarWidth/2 + BarWidth*sqrt(2));
echo(str("Outside diagonal: ",BaseOutDiagonal));
//- On-center distance measured along coordinate axis
// the links are interlaced, so this is half of what you think it should be...
LinkOC = BaseSide/2 + ThreadWidth;
LinkSpacing = Diamond ? (sqrt(2)*LinkOC) : LinkOC;
echo(str("Base spacing: ",LinkSpacing));
//- Compute how many links fit in sheet
MinLinksX = ceil((SheetSizeX - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
MinLinksY = ceil((SheetSizeY - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
echo(str("MinLinks X: ",MinLinksX," Y: ",MinLinksY));
NumLinksX = ((0 == (MinLinksX % 2)) && !Diamond) ? MinLinksX + 1 : MinLinksX;
NumLinksY = ((0 == (MinLinksY % 2) && !Diamond)) ? MinLinksY + 1 : MinLinksY;
echo(str("Links X: ",NumLinksX," Y: ",NumLinksY));
//- Armor button base
CapThick = 4 * ThreadThick; // at least 3 layers for solid bridging
ButtonHeight = BaseHeight + BarClearance + CapThick;
echo(str("ButtonHeight: ",ButtonHeight));
//- Armor ornament size & shape
// Fine-tune OD & ID to suit the number of sides...
ArmorSides = 4;
ArmorAngle = true ? 180/ArmorSides : 0; // true -> rotate half a side for best alignment
TotalHeight = ButtonHeight + ArmorThick;
echo(str("Overall Armor Height: ",TotalHeight));
ArmorOD = 1.1 * BaseSide; // tune for best base fit
ArmorID = 10 * ThreadWidth; // make the tip blunt & strong
//-------
module ShowPegGrid(Space = 10.0,Size = 1.0) {
RangeX = floor(95 / Space);
RangeY = floor(125 / Space);
for (x=[-RangeX:RangeX])
for (y=[-RangeY:RangeY])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
//-------
// Create link with armor button as needed
module Link(Topping = false) {
LinkHeight = (Topping && Cap) ? ButtonHeight : BaseHeight;
render(convexity=3)
rotate((BendAround == "X") ? 90 : 0)
rotate(Diamond ? 45 : 0)
union() {
difference() {
translate([0,0,LinkHeight/2]) // outside shape
intersection() {
cube([BaseSide,BaseSide,LinkHeight],center=true);
rotate(45)
cube([BaseOutDiagonal,BaseOutDiagonal,LinkHeight],center=true);
}
translate([0,0,(BaseHeight + BarClearance - Protrusion)/2])
intersection() { // inside shape
cube([(BaseSide - 2*BarWidth),
(BaseSide - 2*BarWidth),
(BaseHeight + BarClearance + Protrusion)],
center=true);
rotate(45)
cube([BaseInDiagonal,
BaseInDiagonal,
(BaseHeight + BarClearance + Protrusion)],
center=true);
}
translate([0,0,((BarThick + 2*BarClearance)/2 + BarThick)]) // openings for bars
cube([(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
(2*BaseSide),
BarThick + 2*BarClearance],
center=true);
translate([0,0,(BaseHeight/2 - BarThick)])
cube([(2*BaseSide),
(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
BaseHeight],
center=true);
}
if (Topping && Armor)
translate([0,0,(ButtonHeight - Protrusion)]) // sink slightly into the cap
rotate(ArmorAngle)
cylinder(d1=ArmorOD,
d2=ArmorID,
h=(ArmorThick + Protrusion),
$fn=ArmorSides);
}
}
//-------
// Create split buttons to join sheets
module Joiner() {
translate([-LinkSpacing,0,0])
difference() {
Link(false);
translate([0,0,BarThick + BarClearance + TotalHeight/2 - Protrusion])
cube([2*LinkSpacing,2*LinkSpacing,TotalHeight],center=true);
}
translate([LinkSpacing,0,0])
intersection() {
translate([0,0,-(BarThick + BarClearance)])
Link(true);
translate([0,0,TotalHeight/2])
cube([2*LinkSpacing,2*LinkSpacing,TotalHeight],center=true);
}
}
//-------
// Build it!
ShowPegGrid();
if (Layout == "Link") {
Link(false);
}
if (Layout == "Button") {
Link(true);
}
if (Layout == "LB") {
Link(true);
translate([LinkSpacing,LinkSpacing,0])
Link(false);
}
if (Layout == "Build")
for (ix = [0:(NumLinksX - 1)],
iy = [0:(NumLinksY - 1)]) {
x = (ix - (NumLinksX - 1)/2)*LinkSpacing;
y = (iy - (NumLinksY - 1)/2)*LinkSpacing;
translate([x,y,0])
color([(ix/(NumLinksX - 1)),(iy/(NumLinksY - 1)),1.0])
if (Diamond)
Link((ix + iy) % 2); // armor at odd,odd & even,even points
else
if ((iy % 2) && (ix % 2)) // armor at odd,odd points
Link(true);
else if (!(iy % 2) && !(ix % 2)) // connectors at even,even points
Link(false);
}
if (Layout == "Joiner")
Joiner();
if (Layout == "Joiners") {
NumJoiners = max(MinLinksX,MinLinksY)/2;
for (iy = [0:(NumJoiners - 1)]) {
y = (iy - (NumJoiners - 1)/2)*2*LinkSpacing + LinkSpacing/2;
translate([0,y,0])
color([0.5,(iy/(NumJoiners - 1)),1.0])
Joiner();
}
}
As a reward for reading all the way to the bottom, some further thoughts:
A mask array could control what type of link goes where, which cap style goes on each armor button, and whether to print the link at all. That way, you could produce customized armor buttons in non-rectangular (albeit coarsely pixelized) fabric sheets.
You could produce an armor sheet sporting cubic caps, then intersect the whole sheet with a model built from a height-map image to spread a picture across the sheet. The complexity of that model would probably tie OpenSCAD in knots, but perhaps an external program could intersect two properly aligned STL / AMF files.
The bars could be a thread or two thinner, shaving a few millimeters off the basic link. The printer’s ability to bridge the link to form the flying bars and cap limits making the links much larger.
Starting from the improved chain mail link design, extend the top bars enough to clear the cross links, then bridge across them to form a flat cap:
Chain Mail – Armor and Link
The OpenSCAD code makes the links as small as they can possibly be, based on the bar size and clearances, then rounds up to a multiple of the thread width so the flat cap will fill properly. Given the extrusion thread dimensions and the bar sizes, the OpenSCAD code computes everything else: the link model matches the slicer settings that define the printer’s output.
Given:
Thread: 0.4 mm wide x 0.2 mm thick
Bar: 6 thread wide x 4 thread thick = 2.4 x 0.8 mm
Clearances: 2 thread horizontal x 5 thread vertical = 0.8 x 1.0 mm
All the links measure 15.6 mm from side to side, the short connecting links are 2.6 mm tall, and the flat caps are 4.4 mm tall. Interlinked links sit 8.2 mm on center = half the link side plus one thread width clearance, which is 16.4 mm on center for adjacent links.
Duplicated appropriately, the caps resemble turtle armor:
Chain Mail – Flat Armor
Which look about the same in real life, minus the cheerful colors:
Armor Buttons – on platform – side
Now, however, you can plunk an armor button atop the cap:
Chain Mail Armor – 4 sided
With any number of sides:
Chain Mail Armor – 6 sided
Up to a truncated cone:
Chain Mail Armor – 24 sided
The flat tip makes the button more durable and user-friendly, but you can make it a bit more pointy if you favor that sort of thing. The button adds 6 mm to the link base, making armor links 10.4 mm tall.
Other printable stuff could fit on that cap: letters, decorations, widgets, whatever.
I think square armor buttons look ever so imposing when they’re arrayed in a sheet:
Chain Mail Armor – square – 4 sided
The general idea being that you could attach the armor sheet to a cloth / leather backing to form a gauntlet or greave; the border of bottom links around the button array should serve for that purpose.
The plastic prints just like the model and pops off the M2’s platform ready to use, with no finishing required:
Chain Mail Armor – square on desk
The two-color effect came from hot-swapping black filament as the red PLA ran out. The 6×6 armor button array and the 7×7 connecting link array holding it together required 14 meters of filament and I guesstimated the red spool held 9 meters: I was ready when the last of the red vanished just after completing the bridging layer under the flat caps. Filament swaps work reasonably well; I’d hate to do that on a production basis.
If you don’t mind my saying so, everybody thinks it’s spectacular:
Chain Mail Armor – square on arm
The sheet has a definite “grain” defined by the orientation of the bottom links, making it far more bendy in one direction than the other:
Chain Mail Armor – square rolled
The sheet layout orients the more-bendy direction along the M2’s (longer) Y axis, so that sheets can wrap snugly around your arm (or leg) and extend straight-ish along the bones in the other direction. That should be configurable, I think.
There’s an option to rotate the links by 45° to produce diamond-theme arrays:
Chain Mail Armor – diamond – 8 sided
Which would make good patch armor, if you’re into that sort of thing:
Chain Mail Armor – diamond on hand
Those have octagonal buttons, which IMHO don’t look nearly as crisp as the four-sided version.
Ah! I should generalize the diamond rotation option to select all four useful rotations.
The 6×6 square sheet requires three hours on the M2, with the intial print time estimates being low by nearly a factor of two. The M2 has a 200×250 mm platform and I’ll definitely try a full-size array just to see how it works.
The OpenSCAD source code, which stands badly in need of refactoring:
// Chain Mail Armor Buttons
// Ed Nisley KE4ZNU - November 2014
Layout = "Build"; // Link Button LB Build
//-------
//- Extrusion parameters must match reality!
// Print with 1 shell and 2+2 solid layers
ThreadThick = 0.20;
ThreadWidth = 0.40;
HoleWindage = 0.2;
Protrusion = 0.1; // make holes end cleanly
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
//-------
// Dimensions
//- Set maximum sheet size
SheetSizeX = 70;
SheetSizeY = 80;
//- Diamond or rectangular sheet?
Diamond = false; // true = rotate 45 degrees, false = 0 degrees for square
ArmorButton = true; // true = build button atop cap
// Link bar sizes
BarWidth = 6 * ThreadWidth;
BarThick = 4 * ThreadThick;
BarClearance = 5*ThreadThick; // vertical clearance above & below bars
//-- Compute link sizes from those values
// Absolute minimum base link: bar width + corner angle + build clearance around bars
// rounded up to multiple of thread width to ensure clean filling
BaseSide = IntegerMultiple((4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth)),ThreadWidth);
BaseHeight = 2*BarThick + BarClearance; // both bars + clearance
echo(str("BaseSide: ",BaseSide," BaseHeight: ",BaseHeight));
BaseOutDiagonal = BaseSide*sqrt(2) - BarWidth;
BaseInDiagonal = BaseSide*sqrt(2) - 2*(BarWidth/2 + BarWidth*sqrt(2));
echo(str("Outside diagonal: ",BaseOutDiagonal));
//- On-center distance measured along coordinate axis
LinkOC = BaseSide/2 + ThreadWidth;
LinkSpacing = Diamond ? (sqrt(2)*LinkOC) : LinkOC;
echo(str("Base spacing: ",LinkSpacing));
//- Compute how many links fit in sheet
MinLinksX = ceil((SheetSizeX - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
MinLinksY = ceil((SheetSizeY - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
echo(str("MinLinks X: ",MinLinksX," Y: ",MinLinksY));
NumLinksX = ((0 == (MinLinksX % 2)) && !Diamond) ? MinLinksX + 1 : MinLinksX;
NumLinksY = ((0 == (MinLinksY % 2) && !Diamond)) ? MinLinksY + 1 : MinLinksY;
echo(str("Links X: ",NumLinksX," Y: ",NumLinksY," Total: ",NumLinksX*NumLinksY));
//- Armor button base
CapThick = BarThick;
ButtonHeight = BaseHeight + BarClearance + CapThick;
echo(str("ButtonHeight: ",ButtonHeight));
//- Armor ornament size & shape
ArmorSides = 4;
ArmorAngle = true ? 180/ArmorSides : 0; // rotate half a side?
ArmorThick = IntegerMultiple(6,ThreadThick); // keep it relatively short
ArmorOD = 1.1 * BaseSide; // tune for best fit at base
ArmorID = 10 * ThreadWidth; // make the tip wide & strong
//-------
module ShowPegGrid(Space = 10.0,Size = 1.0) {
RangeX = floor(95 / Space);
RangeY = floor(125 / Space);
for (x=[-RangeX:RangeX])
for (y=[-RangeY:RangeY])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
//-------
// Create base link
module BaseLink() {
render()
rotate(Diamond ? 45 : 90) // 90 = more bendy around X axis
difference() {
translate([0,0,BaseHeight/2]) {
difference(convexity=2) {
intersection() { // outside shape
cube([BaseSide,BaseSide,BaseHeight],center=true);
rotate(45)
cube([BaseOutDiagonal,BaseOutDiagonal,BaseHeight],center=true);
}
intersection() { // inside shape
cube([(BaseSide - 2*BarWidth),
(BaseSide - 2*BarWidth),
(BaseHeight + 2*Protrusion)],
center=true);
rotate(45)
cube([BaseInDiagonal,
BaseInDiagonal,
(BaseHeight +2*Protrusion)],
center=true);
}
}
}
translate([0,0,(BaseHeight/2 + BarThick)])
cube([(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
(2*BaseSide),
BaseHeight],
center=true);
translate([0,0,(BaseHeight - BaseHeight/2 - BarThick)])
cube([(2*BaseSide),
(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
BaseHeight],
center=true);
}
}
//-------
// Create button link
module ButtonLink() {
render()
rotate(Diamond ? 45 : 90) // 90 = more bendy around X axis
union() {
difference() {
translate([0,0,ButtonHeight/2]) // outside shape
intersection() {
cube([BaseSide,BaseSide,ButtonHeight],center=true);
rotate(45)
cube([BaseOutDiagonal,BaseOutDiagonal,ButtonHeight],center=true);
}
translate([0,0,(BaseHeight + BarClearance - Protrusion)/2])
intersection() { // inside shape
cube([(BaseSide - 2*BarWidth),
(BaseSide - 2*BarWidth),
(BaseHeight + BarClearance + Protrusion)],
center=true);
rotate(45)
cube([BaseInDiagonal,
BaseInDiagonal,
(BaseHeight + BarClearance + Protrusion)],
center=true);
}
translate([0,0,((BarThick + 2*BarClearance)/2 + BarThick)])
cube([(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
(2*BaseSide),
BarThick + 2*BarClearance],
center=true);
translate([0,0,(BaseHeight/2 - BarThick)])
cube([(2*BaseSide),
(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
BaseHeight],
center=true);
}
if (ArmorButton)
translate([0,0,(ButtonHeight - Protrusion)]) // armor on cap
rotate(ArmorAngle)
cylinder(d1=ArmorOD,
d2=ArmorID,
h=(ArmorThick + Protrusion),
$fn=ArmorSides);
}
}
//-------
// Build it!
ShowPegGrid();
if (Layout == "Link") {
BaseLink();
}
if (Layout == "Button") {
ButtonLink();
}
if (Layout == "LB") {
ButtonLink();
translate([LinkSpacing,LinkSpacing,0])
BaseLink();
}
if (Layout == "Build") {
for (ix = [0:(NumLinksX - 1)],
iy = [0:(NumLinksY - 1)])
assign(x = (ix - (NumLinksX - 1)/2)*LinkSpacing,
y = (iy - (NumLinksY - 1)/2)*LinkSpacing)
translate([x,y,0])
color([(ix/(NumLinksX - 1)),(iy/(NumLinksY - 1)),1.0])
if (Diamond)
if ((ix + iy) % 2) // armor at odd,odd & even, even points
ButtonLink();
else
BaseLink(); // connectors otherwise
else
if ((iy % 2) && (ix % 2)) // armor at odd,odd points
ButtonLink();
else if ((!(iy % 2) && !(ix % 2))) // connectors at even,even points
BaseLink();
}
The rectangular posts in my chain mail resemble Zomboe’s original design, but with dimensions computed directly from the bar (and, thus, thread) widths and thicknesses to ensure good fill and simple bridging:
Chain Mail Link
They fit together well, but the angled post edges make the bridge threads longer than absolutely necessary along the outside edge of each link:
Chain Mail Sheet – detail
A bit of fiddling produces a squared-off version:
Chain Mail Link – Improved Posts
Which nest together like this:
Chain Mail – Improved Posts – Bottom View
Now all the bridge threads have the same length, which should produce better results.
Everybody likes chain mail, so I made a few big sheets:
Chain Mail Sheet
That’s a nominal 150 mm on the X axis and 200 mm on the Y, which pretty well fills the M2’s 8×10 inch platform after Slic3r lays a few skirt threads around the outside. All 192 links require a bit under four hours to print: all those short movements never let the platform get up to full speed.
Look no further for a brutal test of platform alignment and adhesion. The platform is slightly too high in the left front corner and, no surprise, slightly too low in the right rear. The skirt thread varies from 0.15 to 0.27 around the loop.
Hairspray works wonder to glue down all those little tiny links. They pop off the platform quite easily after it cools under 50 °C, with no need for any post-processing.
This version of the OpenSCAD code correctly figures the number of links to fill a given width & length; the old code didn’t get it quite right.
Coloring the links makes the whole thing easier to look at:
Chain Mail Sheet – detail
The real world version comes out in red PLA that saturates Sony imagers:
Because the ET227 transistor acts as a current limiter, the motor current waveform has flat tops at the level set by the DAC voltage. However, the current depends strongly on the temperature of all those transistor junctions, with some commutation noise mixed in for good measure, so the firmware must measure the actual current to know what’s going on out there.
Here’s one way to pull that off:
Motor current – ADC sample timing
The upper waveform shows the motor current sporting flat tops at 650 mA.
The lower waveform marks the current measurement routine, with samples taken just before the falling edge of the first nine pulses. The (manually tweaked) delay between the samples forces them to span one complete cycle of the waveform, but they’re not synchronized to the power line. Remember that the motor runs from a full wave rectifier, so each “cycle” in that waveform is half of a normal power line cycle.
Given an array containing those nine samples, the routine must return the maximum value of the waveform, ignoring the little glitch at the start of the flat top and taking into consideration that the waveform won’t have a flat top (or much of a glitch) when the current “limit” exceeds the maximum motor current.
After a bit of fumbling around with the scope and software, the routine goes like this:
Collect samples during one current cycle
Sort in descending order
Ignore highest sample
Return average of next two highest samples
Given that the array has only nine samples, I used a quick-and-dirty bubble sort. The runt pulse at the end of the series in the bottom waveform brackets the sort routine, so it’s not a real time killer.
Seeing as how this is one of the very few occasions I’ve had to sort anything, I wheeled out the classic XOR method of exchanging the entries. Go ahead, time XOR against swapping through a temporary variable; it surely doesn’t make any difference at all on an 8-bit microcontroller.
The sampling code, with all the tracing stuff commented out:
//------------------
// Sample current along AC waveform to find maximum value
// this is blocking, so don't call it every time around the main loop!
#define NUM_I_SAMPLES 9
unsigned int SampleCurrent(byte PinNum) {
unsigned int Samples[NUM_I_SAMPLES];
unsigned int AvgSample;
byte i,j;
// digitalWrite(PIN_SYNC,HIGH);
for (i=0; i < NUM_I_SAMPLES; i++) { // collect samples
// digitalWrite(PIN_SYNC,HIGH);
Samples[i] = ReadAI(PinNum);
// digitalWrite(PIN_SYNC,LOW);
delayMicroseconds(640);
}
// digitalWrite(PIN_SYNC,LOW);
// digitalWrite(PIN_SYNC,HIGH); // mark start of sorting
for (i=0; i < (NUM_I_SAMPLES - 1); i++)
for (j=0 ; j < (NUM_I_SAMPLES - 1 - i); j++)
if (Samples[j] < Samples[j+1]) {
Samples[j] ^= Samples[j+1]; // swap entries!
Samples[j+1] ^= Samples[j];
Samples[j] ^= Samples[j+1];
}
// digitalWrite(PIN_SYNC,LOW); // mark end of sorting
// printf("Samples: ");
// for (i=0; i < NUM_I_SAMPLES; i++)
// printf("%5d,",Samples[i]);
AvgSample = (Samples[1] + Samples[2])/2; // discard highest sample
// printf(" [%5d]\r\n",AvgSample);
return AvgSample;
}