Archive for category Software

Random LED Dots: Hardware SPI vs. Data Layout

The LED panel requires multiplexing: turning on one of the PNP transistors activates a single row, with the column shift registers determining which of the 24 LEDs in that row will light up. Because each row remains lit until the next one appears, it will be about 1/8 as bright as a “DC” display.

Random LED Dots - Row Drivers

Random LED Dots – Row Drivers

Although the hardware allows turning on more than one row at a time, that’s a Bad Idea that will produce Bad Results: the column shift registers can’t sink that much current.

Bitmapping the whole array requires 8 x 4 = 32 bytes, which isn’t all that much for an ATmega328 with 2 KB of RAM and nothing else on its mind:

typedef struct {
	byte Row;
	byte ColR;
	byte ColG;
	byte ColB;
} LED_BYTES;

#define NUMROWS 8
#define NUMCOLS 8

LED_BYTES LEDs[NUMROWS] = {
	{0x80,0,0,0},
	{0x40,0,0,0},
	{0x20,0,0,0},
	{0x10,0,0,0},
	{0x08,0,0,0},
	{0x04,0,0,0},
	{0x02,0,0,0},
	{0x01,0,0,0},
};

I decided to use positive logic in the array, then invert the bits on their way to the SPI hardware.

Setting a single LED to a color value requires chopping the color into its three component RGB bits, clearing the appropriate bits in the array, then stuffing the new ones in place:

void SetLED(unsigned long Value) {

byte Row,Col,Color,BitMask;

	Row =   (Value >>  8) & 0x07;
	Col =   (Value >> 16) & 0x07;
	Color = (Value >> 24) & 0x07;

	BitMask = (0x80 >> Col);

//	printf("%u %u %u %u\r\n",Row,Col,Color,BitMask);

	LEDs[Row].ColR &= ~BitMask;
	LEDs[Row].ColR |= (Color & 0x04) ? BitMask : 0;

	LEDs[Row].ColG &= ~BitMask;
	LEDs[Row].ColG |= (Color & 0x02) ? BitMask : 0;

	LEDs[Row].ColB &= ~BitMask;
	LEDs[Row].ColB |= (Color & 0x01) ? BitMask : 0;

}

The Value comes from a radiation-based random number source that produces 32 bits at a time. I suppose you could just slap 24 of the bits into the column values in a row selected by three other bits to update All! The! Dots! in one shot, but it seemed less exciting to update a single LED on each iteration; the update timing is also an interesting random quantity.

Each iteration of the main() loop squirts the (inverted) bits for a single row through the SPI hardware:

void WaitSPIF(void) {
	while (! (SPSR & (1 << SPIF))) {
//		TogglePin(PIN_HEARTBEAT);
		continue;
	}
}

byte SendRecSPI(byte Dbyte) {			// send one byte, get another in exchange
	SPDR = Dbyte;
	WaitSPIF();
	return SPDR;						// SPIF will be cleared
}

void UpdateLEDs(byte i) {

	SendRecSPI(~LEDs[i].ColB);			// low-active outputs
	SendRecSPI(~LEDs[i].ColG);
	SendRecSPI(~LEDs[i].ColR);
	SendRecSPI(~LEDs[i].Row);

	analogWrite(PIN_DIMMING,LEDS_OFF);	// turn off LED to quench current
	PulsePin(PIN_LATCH);				// make new shift reg contents visible
	analogWrite(PIN_DIMMING,LEDS_ON);

}

I don’t do anything with the returned bytes, but perhaps that’ll be a way to get some random numbers into the program later on.

It turned out that all the green LEDs in a column with one lit LED glowed very, very dimly if they weren’t turned off for a while; a few microseconds while pulsing the shift register parallel load clock seems to work reasonably well. I think the glow comes from microamp-level leakage current through the turned-off PNP transistors, but I haven’t tracked it down yet.

The hardware SPI runs at 1 µs/bit with short gaps while cuing up the next byte:

Hardware SPI - SCLK SDAT

Hardware SPI – SCLK SDAT

The last byte out (over on the right) contains the row select bits, of which only one can be active (low) at a time.

The main() loop doesn’t have much else to do, so the rows refresh at 10 kHz:

Hardward SPI - Refresh

Hardward SPI – Refresh

That means the LEDs in each row are active for only 100 µs and, given a whole-panel refresh of 1250 kHz (!), the LEDs appear to shimmer slightly during eye saccades. It’s a much nicer effect than the flicker produced by slower refresh intervals and has much the same eye-magnet attraction as coherent laser light.

The code emits a scope sync pulse just after Row 7 goes out the door, so you can get ready for the next iteration:

	UpdateLEDs(RowIndex);
	if (++RowIndex >= NUMROWS) {
		RowIndex = 0;
		PulsePin(PIN_SYNC);
	}

All in all, it worked right the first time…

1 Comment

Proto Board Holder: 80×110 mm Version

A simple holder for 80×110 mm prototyping boards:

Random LED Dots - circuit board

Random LED Dots – circuit board

It’s similar to the holder for the LED current controller board, minus the center screws, plus nicely rounded corners and a cutout for wires emerging from underneath:

Proto board holder

Proto board holder

Slic3r’s Hilbert Curve infill definitely looks better than the usual straight-line pattern:

Circuit Board Holder - Slic3r preview

Circuit Board Holder – Slic3r preview

The OpenSCAD source code:

// Test support frame for Hall Effect LED Blinky Light
// Ed Nisley KE4ZNU - Sept 2013

Layout = "Fancy";				// Fancy Plain

PlainColor = "LightBlue";

ClampFlange = true;

//- Extrusion parameters - must match reality!

ThreadThick = 0.25;
ThreadWidth = 0.40;

function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);

Protrusion = 0.1;

HoleWindage = 0.2;

//- Screw sizes

inch = 25.4;

Tap4_40 = 0.089 * inch;
Clear4_40 = 0.110 * inch;
Head4_40 = 0.211 * inch;
Head4_40Thick = 0.065 * inch;
Nut4_40Dia = 0.228 * inch;
Nut4_40Thick = 0.086 * inch;
Washer4_40OD = 0.270 * inch;
Washer4_40ID = 0.123 * inch;

//- PCB sizes

PCBSize = [110.0,80.0,1.5];
PCBShelf = 2.0;

Clearance = 2*[ThreadWidth,ThreadWidth,0];

WallThick = IntegerMultiple(5.0,ThreadWidth);
FrameHeight = 8.0;

ScrewOffset = 0.0 + Clear4_40/2;

OAHeight = FrameHeight + Clearance[2] + PCBSize[2];

FlangeExtension = 5.0;
FlangeThick = IntegerMultiple(2.0,ThreadThick);
Flange = PCBSize
			+ 2*[ScrewOffset,ScrewOffset,0]
			+ 2*[Washer4_40OD,Washer4_40OD,0]
			+ [2*FlangeExtension,2*FlangeExtension,(FlangeThick - PCBSize[2])]
			;

echo("Flange: ",Flange);
NumSides = 4*5;

WireChannel = [Flange[0],15.0,3.0 + PCBSize[2]];
WireChannelOffset = [Flange[0]/2,25.0,( + FrameHeight + PCBSize[2] - WireChannel[2]/2)];

//- Adjust hole diameter to make the size come out right

module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes

  Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);

  FixDia = Dia / cos(180/Sides);

  cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
}

//- Put peg grid on build surface

module ShowPegGrid(Space = 10.0,Size = 1.0) {

  RangeX = floor(100 / Space);
  RangeY = floor(125 / Space);

	for (x=[-RangeX:RangeX])
	  for (y=[-RangeY:RangeY])
		translate([x*Space,y*Space,Size/2])
		  %cube(Size,center=true);

}

//- Build it

ShowPegGrid();

difference() {
	union() {									// body block and screw bosses
		translate([0,0,OAHeight/2])
			color(PlainColor)
			cube(PCBSize + Clearance + [2*WallThick,2*WallThick,FrameHeight],center=true);
		for (x=[-1,1], y=[-1,1]) {
			translate([x*(PCBSize[0]/2 + ScrewOffset),
						y*(PCBSize[1]/2 + ScrewOffset),
						0])
				color((Layout == "Fancy") ? "Orchid" : PlainColor)
				cylinder(r=Washer4_40OD,h=OAHeight,$fn=NumSides);
		}
		if (ClampFlange)
			color((Layout == "Fancy") ? "SeaGreen" : PlainColor)
			linear_extrude(height=Flange[2])
				hull()
					for (i=[-1,1], j=[-1,1]) {
						translate([i*(Flange[0]/2 - Washer4_40OD/2),j*(Flange[1]/2 - Washer4_40OD/2)])
							circle(d=Washer4_40OD,$fn=NumSides);
					}
	}

	for (x=[-1,1], y=[-1,1]) {				// screw holes and washer recesses
		translate([x*(PCBSize[0]/2 + ScrewOffset),
					y*(PCBSize[1]/2 + ScrewOffset),
					-Protrusion])
			rotate((x-1)*90)
			PolyCyl(Tap4_40,(OAHeight + 2*Protrusion));
		translate([x*(PCBSize[0]/2 + ScrewOffset),
					y*(PCBSize[1]/2 + ScrewOffset),
					OAHeight - PCBSize[2]])
			PolyCyl(1.2*Washer4_40OD,(PCBSize[2] + Protrusion),NumSides);
	}

	translate([0,0,OAHeight/2])					// through hole below PCB
		cube(PCBSize - 2*[PCBShelf,PCBShelf,0] + [0,0,2*OAHeight],center=true);

	translate([0,0,(OAHeight - (PCBSize[2] + Clearance[2])/2 + Protrusion/2)])	// PCB pocket on top
		cube(PCBSize + Clearance + [0,0,Protrusion],center=true);

	translate(WireChannelOffset)									// clearance for cable on solder side
		cube(WireChannel + [0,0,Protrusion],center=true);
}

, ,

3 Comments

HP 7475A Plotter: Chiplotle Supershape

In this day and age, a pen plotter isn’t going to be doing anything useful, because we have better ways to draw schematics and make presentation graphics, but it can produce Algorithmic Art:

HP 7475A - Chiplotle Supershape plotting

HP 7475A – Chiplotle Supershape plotting

Well, granted, that’s a rather small value of Art, but it does show that the plotter can draw 10 k points using serial port hardware handshaking.

That’s one of an infinite variety of Supershapes produced by the Chiplotle geometry.shapes.supershape() function:

from chiplotle import *
import math
plt=instantiate_plotters()[0]
plt.set_origin_center()
plt.write(hpgl.VS(5))
ss=geometry.shapes.supershape(3900,3900,5.3,0.4,1,1,point_count=10*1000,travel=10*2*math.pi)
plt.select_pen(1)
plt.write(ss)
plt.select_pen(0)

The plotter uses absolute plotter units that range from (0,0) to (10365,7962). Telling the plotter to put its origin in the middle of the page makes perfect sense, because that automagically centers the figure.

Dialing the speed back to 5 cm/s works much better with the Sakura pens than the default 38.1 cm/s = 15.0 inch/s; hand-drawing pens just don’t have the flow rate for prolonged vigorous scribbling. HP was obviously on the edge of converting to metric engineering units in the early 1980s, with the HP 7475A designed before the transition and shipped afterward.

The supershape parameters:

  • 3900,3900 sets the maximum coordinate value along each axis. The plot may or may not exceed that value, depending on how weird the supershape turns out, but it’s generally pretty close
  • 5.3,0.4,1,1 correspond to coefficients m, n1, n2, n3
  • By default, a=1 and b=1, but you can change those as you like
  • point_count=10*1000 sets how many total points appear in the plot
  • travel=10*2*math.pi sets the number of complete cycles, in units of 2π

The function spits out a list of Cartesian XY coordinates, not the polar rΦ coordinates you might expect.

Slightly non-integer values, particularly for m, produce more interesting patterns. Other than that, there’s just no telling.

Use io.view(ss) to get an idea of what you got, it’s much faster than plotting!

Chiplotle Supershape preview

Chiplotle Supershape preview

You may find the online superformula explorers better suited to rapid prototyping, though. There’s a list at the bottom of the Wikipedia article, although some links seem defunct.

Notice that the end of the plot doesn’t quite reach the beginning over on the far right, which is a consequence of how Python produces sequences. Adding one more point does the trick:

ss=geometry.shapes.supershape(3900,3900,5.3,0.4,1,1,point_count=1+10*1000,travel=10.001*2*math.pi)
Chiplotle Supershape preview - closed

Chiplotle Supershape preview – closed

I’ll try remembering that the next time around…

14 Comments

HP 7475A Plotter: Hacking Chiplotle For Hardware Handshaking

Chiplotle seems like a good way to drive the HP 7475A plotter, but some preliminary tinkering showed that the plotter pen paused quite regularly while drawing. The plotter wakes up with hardware handshaking enabled, Chiplotle has a config file that lets you specify hardware handshaking, the cable has all the right connections for hardware handshaking, but peering at Der Blinkenlights showed hardware handshaking never happened: the data didn’t overrun, the buffer never filled up, and DTR remained solidly on.

Come to find out that Chiplotle sends data in half-buffer-size chunks (all code from baseplotter.py):

class _BasePlotter(object):
   def __init__(self, serial_port):
      self.type = '_BasePlotter'
      self._logger = get_logger(self.__class__.__name__)
      self._serial_port = serial_port
      self._hpgl = commands
      self._margins = MarginsInterface(self)
      self.maximum_response_wait_time = get_config_value('maximum_response_wait_time')

      #this is so that we don't pause while preparing and sending
      #full buffers to the plotter. By sending 1/2 buffers we assure
      #that the plotter will still have some data to plot while
      #receiving the new data
      self.buffer_size = int(self._buffer_space / 2)
      self.initialize_plotter( )

Every time something goes out to the plotter, this happens:

   def _write_string_to_port(self, data):
      ''' Write data to serial port. data is expected to be a string.'''
      #assert type(data) is str
      if not isinstance(data, basestring):
         raise TypeError('string expected.')
      data = self._filter_unrecognized_commands(data)
      data = self._slice_string_to_buffer_size(data)
      for chunk in data:
         self._sleep_while_buffer_full( )
         self._serial_port.write(chunk)

In order to figure out whether the plotter has enough room, Chiplotle must ask it:

   def _sleep_while_buffer_full(self):
      '''
         sleeps until the buffer has some room in it.
      '''
      if self._buffer_space < self.buffer_size:
         while self._buffer_space < self.buffer_size:
            time.sleep(0.01)

The self._buffer_space method contains the complete handshake:

   def _buffer_space(self):
      self._serial_port.flushInput()
      self._serial_port.write(self._hpgl.B().format)
      bs = self._read_port()
      return int(bs)

Assuming that Python can actually meter out a 0.01 second sleep, that’s a mere 10 ms; call it 10 character times at 9600 b/s. By and large, Chiplotle hammers away at the poor plotter while the buffer drains.

Now, that would be just ducky, except that the HP 7475A plotter dates back to slightly after microcontrollers were invented. The MC6802 trundles along at 1 MHz from a 4 MHz crystal, because it needed a quadrature clock, and takes a while to get things done. Responding to the buffer space request (a three-character sequence: ␛.B) requires the plotter to:

  • Stop plotting 
  • Answer the phone
  • Figure out what to do
  • Compose a reply
  • Drop it in the serial buffer
  • Resume plotting

Which take enough time to produce a distinct hitch in the gitalong. Some crude print debugging showed most of the delay happens between the write() and the read() tucked inside _buffer_space.

Linux handles serial port hardware handshaking far below the Python level, so the simplest fix was to rip out the line that checks for enough buffer space:

   def _write_string_to_port(self, data):
      ''' Write data to serial port. data is expected to be a string.'''
      #assert type(data) is str
      if not isinstance(data, basestring):
         raise TypeError('string expected.')
      data = self._filter_unrecognized_commands(data)
      data = self._slice_string_to_buffer_size(data)
      for chunk in data:
#         self._sleep_while_buffer_full( )
         self._serial_port.write(chunk)

And then the plotter races along without pauses, drawing as fast as it possibly can, with the DTR output blinking like crazy as Chiplotle dumps the character stream into the output buffer and the serial port hardware (*) managing the data flow. Apparently, detecting a buffer-full situation and dropping the DTR output requires only a few 6802 CPU cycles, which is what makes hardware handshaking such a good idea.

There’s a movie about that…

Hooray for Der Blinkenlights!

HP 7475A - serial port adapters - hardcore

HP 7475A – serial port adapters – hardcore

(*) Which is, of course, a USB-to-RS232 converter. I paid extra to get one that reports an FTDI chipset, which may mean the counterfeiters have upped their game since the Windows driver disaster. I actually tried it on the Token Windows box and it still works, so maybe it’s Genuine FTDI.

Leave a comment

HP 7475A Plotter: Roland Knife Stabilizer, Improved

The objective being to wrap a nose around the cutter blade to allow some control over the cut depth, I lengthened the cylinder around the cutter body and modeled a discrete glue-on cap:

Roland knife stabilizer and nose - show

Roland knife stabilizer and nose – show

Which, with an additional 80 g of ballast, worked fine in the double-thick vinyl:

Roland knife stabilizer - nut weight

Roland knife stabilizer – nut weight

The pen-lift spring can just barely manage to heave that load off the vinyl, but it’s obviously running at the limit of its ability and this can’t possibly be a Good Thing for the mechanism in the long run.

After a bit more fiddling around, I noticed that the stabilizer wasn’t sitting flat on the pen holder and that there really wasn’t any good reason to have a separate cap, so I did one more revision:

Roland knife stabilizer with nose - side view

Roland knife stabilizer with nose – side view

The cutaway view shows the knife model now has tapered transition from the body to the grossly enlarged blade, so the model will build without supports inside the cylinder.

A little cutout on one wall lets the plate sit flat on the pen holder and a barely visible recess in the cylinder gives the carousel pen-capping actuator a bit more clearance:

Roland knife stabilizer with nose - Slic3r preview

Roland knife stabilizer with nose – Slic3r preview

It works about as well as the version shown above, minus the tedious gluing, so I’ll call it a success… even though it’s obviously not going to get much use. I don’t see any way to apply enough downforce to make the cutter work; the mechanical changes just aren’t worthwhile.

The OpenSCAD source code, which includes some tweaks and outright kludges since the first version, builds adapters for Sakura pens (which work just fine) as well as this knife stabilizer:

// HP 7475A plotter pen adapters
// Ed Nisley KE4ZNU April 2015

Layout = "BuildStabilizer";
							// ShowBody BuildBody BodyPoly
							// ShowPen ShowPenAdapter BuildPenAdapter Plug Pen PenPoly
							// ShowKnife BuildKnife KnifeAdapter Knife
							// ShowStabilizer Stabilizer BuildStabilizer

//- Extrusion parameters must match reality!

ThreadThick = 0.25;
ThreadWidth = 0.40;

HoleWindage = 0.2;

Protrusion = 0.1;			// make holes end cleanly

inch = 25.4;

function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);

//----------------------
// Dimensions
// Z=0 at pen tip!

NumSides = 8*4;						// number of sides on each "cylinder"

RADIUS = 0;							// subscript for radius values
HEIGHT = 1;							//   ... height above Z=0

//-- Original HP plotter pen, which now serves as a body for the actual pen

BodyOutline = [						// X values = (measured diameter)/2, Y as distance from tip
	[0.0,0.0],						//  0 fiber pen tip
//	[2.0/2,1.4],					//  1 ... taper (not buildable)
	[1.0/2,0.005],					//  1 ... faked point to remove taper
	[2.0/2,0.0],[2.0/2,2.7],		//  2 ... cylinder
	[3.7/2,2.7],[3.7/2,4.45],		//  4 tip surround
	[4.8/2,5.2],					//  6 chamfer
	[6.5/2,11.4],					//  7 rubber seal face
	[8.9/2,11.4],					//  8 cap seat
	[11.2/2,15.9],					//  9 taper to body
	[11.5/2,28.0],					// 10 lower body
	[13.2/2,28.0],[16.6/2,28.5],	// 11 lower flange = 0.5
	[16.6/2,29.5],[13.2/2,30.0],	// 13 flange rim = 1.0
	[11.5/2,30.0],					// 15 upper flange = 0.5
	[11.5/2,43.25],					// 16 upper body
	[0.0,43.25]						// 17 lid over reservoir
	];

TrimHeight = BodyOutline[9][HEIGHT];		// cut off at top of lower taper
SplitHeight = (BodyOutline[11][HEIGHT] + BodyOutline[14][HEIGHT])/2;	// middle of flange

FlangeOD = 2*BodyOutline[13][RADIUS];
FlangeTop = BodyOutline[15][HEIGHT];

BodyOD = 2*BodyOutline[16][RADIUS];
BodyOAL = BodyOutline[17][HEIGHT];

echo(str("Trim: ",TrimHeight));
echo(str("Split: ",SplitHeight));

BuildSpace = FlangeOD;

//-- Sakura Micron fiber-point pen

ExpRP = 0.15;						// expand critical sections (by radius)

//-- pen locates in holder against end of outer body

PenOutline = [
	[0,0],							//  0 fiber pen tip
	[0.6/2,0.0],[0.6/2,0.9],		//  1  ... cylinder
	[1.5/2,0.9],[1.5/2,5.3],		//  3 tip surround
	[4.7/2,5.8],					//  5 chamfer
	[4.9/2,12.3],					//  6 nose
//	[8.0/2,12.3],[8.0/2,13.1],		//  7 latch ring
//	[8.05/2,13.1],[8.25/2,30.5],	//  9 actual inner body
	[8.4/2 + ExpRP,12.3],[8.4/2 + ExpRP,30.5],	//  7 inner body - clear latch ring
	[9.5/2 + ExpRP,30.5],			//  9 outer body - location surface!
	[9.8/2 + ExpRP,50.0],			// 10 outer body - length > Body
	[7.5/2,50.0],					// 11 arbitrary length
	[7.5/2,49.0],					// 12 end of reservoir
	[0,49.0]						// 13 fake reservoir
	];

PenNose = PenOutline[6];
PenLatch = PenOutline[7];

PenOAL = PenOutline[11][HEIGHT];

//-- Plug for end of cut-off pen body
//   you need two plugs...

PlugOutline = [
	[0,0],							// 0 center of lid
	[9.5/2,0.0],[9.5/2,1.0],		// 1 lid rim <= body OD
	[7.9/2,1.0],					// 3 against end of pen
	[7.6/2,6.0],					// 4 taper inside pen body
	[5.3/2,6.0],					// 5 against ink reservoir
	[4.0/2,1.0],					// 6 taper to lid
	[0.0,1.0]						// 7 flat end of taper
	];

PlugOAL = PlugOutline[5][HEIGHT];

//   cap locates against end of inner body at latch ring
//-- cap origin is below surface to let pen tip be at Z=0

CapGap = 1.0;						// gap to adapter body when attached
CapGripHeight = 2.0;				// thickness of cap grip flange
CapTipClearance = 1.0;				// clearance under fiber tip

CapOffset = -(CapGripHeight + CapTipClearance);	// align inside at pen tip Z=0

CapOutline = [
	[0,CapOffset],									// 0 base
	[FlangeOD/2,CapOffset],							// 1 finger grip flange
	[FlangeOD/2,CapOffset + CapGripHeight],			// 2  ... top
	[BodyOD/2,CapOffset + CapGripHeight],			// 3 shaft
	[BodyOD/2,TrimHeight - CapGap],					// 4  ... top with clearance
	[PenLatch[RADIUS],TrimHeight - CapGap],			// 5 around pen latch ring
	[PenLatch[RADIUS],PenNose[HEIGHT]],				// 6  ... location surface!
	[PenNose[RADIUS] + ExpRP,PenNose[HEIGHT]],		// 7 snug around  nose
	[PenNose[RADIUS] + ExpRP,-CapTipClearance],		// 8 clearance around tip
	[0,-CapTipClearance],							// 9  ... bottom
	];

//-- Roland drag knife bearing assembly

ExpRK = 0.30;						// expand critical sections (by radius)
AdjLen = 2.0;						// allowance for adjustment travel

//- Knife tweaked for pen adapter
/*
KnifeOutline = [
	[0,0],							//  0 blade point (actually 0.25 mm offset)
	[1.0/2,0.0],					//  1  ... blunt end
	[1.0/2,4.0],					//  2  ... cylinder
	[2.0/2,4.0],					//  3 blade shank
	[2.0/2,5.9],					//  4  .. at bearing
	[6.0/2,5.9],					//  5 holder - shell

	[7.3/2 + ExpRK,8.3],			//  6 holder - taper to body
	[7.3/2 + ExpRK,21.0 - AdjLen],	//  7 holder body

	[8.8/2 + ExpRK,22.0 - AdjLen],	//  8 holder - threads bottom
	[8.8/2 + ExpRK,25.0],[9.0/2 + ExpRK,26.0],		//  9 clear threads to reduce friction
	[9.0/2 + ExpRK,32.0],[8.8/2 + ExpRK,33.0],		// 11  ... end clearance
	[8.8/2 + ExpRK,42.5 - AdjLen],	// 13 holder - threads top = locknut bottom
	[12.5/2,42.5 - AdjLen],			// 14 knurled locknut - adjustment travel
	[12.5/2,45.8],					// 15 knurled locknut - top
	[11.0/2,45.8],					// 16 holder - adjusting knurl
	[11.0/2,52.0],					// 17 holder - top surface
	[3.0/2,52.0],[3.0/2,57.2],		// 18 spring post
	[0.0,57.2]						// 19 end of post
	];
*/

//- Knife tweaked for stabilizer

KnifeOutline = [
	[0,0],							//  0 blade point (actually 0.25 mm offset)
	[3.0/2,0.0],					//  1  ... blunt end
	[3.0/2,4.0],					//  2  ... cylinder
	[3.0/2,4.0],					//  3 blade shank
	[6.0/2,5.9],					//  4  .. at bearing (taper to support nose)
	[6.0/2,5.9],					//  5 holder - shell

	[7.3/2 + ExpRK,8.3],			//  6 holder - taper to clear threads
	[7.3/2 + ExpRK,21.0 - AdjLen],	//  7  ..

	[8.8/2 + ExpRK,22.0 - AdjLen],	//  8 holder - threads bottom
	[8.8/2 + ExpRK,25.0],[9.0/2 + ExpRK,26.0],		//  9 clear threads to reduce friction
	[9.0/2 + ExpRK,32.0],[8.8/2 + ExpRK,33.0],		// 11  ... end clearance

	[8.8/2 + ExpRK,42.5 - AdjLen],	// 13 holder - threads top = locknut bottom
	[12.5/2,42.5 - AdjLen],			// 14 knurled locknut - adjustment travel
	[12.5/2,45.8],					// 15 knurled locknut - top
	[11.0/2,45.8],					// 16 holder - adjusting knurl
	[11.0/2,52.0],					// 17 holder - top surface
	[3.0/2,52.0],[3.0/2,57.2],		// 18 spring post
	[0.0,57.2]						// 19 end of post
	];

ThreadStart = KnifeOutline[8][HEIGHT];
ThreadOD = 2*KnifeOutline[11][RADIUS];

//-- Plotter pen holder stabilizer

HolderPlateThick = 3.0;				// thickness of plate atop holder
RimHeight = 5.0;					// rim around sides of holder
RimThick = 2.0;						// wall thickness

HolderOrigin = [17.0,12.2,0.0];		// center of pen tip relative to polygon coordinates

HolderHeight = 30.0;				// top of holder to platen in pen-down position
HolderTopThick = 1.7;				// top of holder to top of pen flange

HolderNoseLength = 4.0;				// length of nose taper
HolderKnifeOffset = -3.0;			// additional downward adjustment range

LockScrewInset = 3.0;				// from right edge of holder plate
LockScrewOD = 2.0;					// tap for 2.5 mm screw

UncapperHeight = -17.0;				// uncapping actuator arm from top of pen holder
UncapperOD = 11.0;					//  ... max OD that clears uncapper

// Beware: update hardcoded subscripts in Stabilizer() when adding / deleting point entries 

HolderPlate = [
	[8.6,18.2],[8.6,23.9],			// 0 lower left corner of pen recess
	[13.9,23.9],[13.9,30.0],		// 2
//	[15.5,30.0],[15.5,25.0],		// 4 omit middle of support beam
//	[20.4,25.0],[20.4,30.0],		// 6
	[22.7,30.0],[22.7,27.5],		// 4
	[35.8,27.5],[35.8,20.7],		// 6 spring box corner
	[43.0,20.7],					// 8
	[31.5,0.0],						// 9
//	[24.5,0.0],[24.5,8.0],			// 10 omit pocket above pen clamp
//	[22.5,10.0],[22.5,16.5],		// 12
//	[20.5,18.2]						// 14
	[13.6,0.0],						// 10
	[8.6,5.0]						// 11
	];

BeamWidth = HolderPlate[4][0] - HolderPlate[2][0];		// rear support beam

TabWidth = HolderPlate[1][1] - HolderPlate[0][1];		// tab extending left beyond pen recess
TabClear = 3.0;											// maximum rim height over tab

HolderCylinderOutline = [
	[0,0],											//  0 center of nose
	[6.0/2,0.0],									//  1 flat nose surface OD
	[BodyOD/2,HolderNoseLength],			//  2 taper to cylinder OD
	[BodyOD/2,HolderHeight],				//  3 cylinder to top of holder plate
	[0,HolderHeight]								//  4 flat top
	];

//----------------------
// Useful routines

module PolyCyl(Dia,Height,ForceSides=0) {			// based on nophead's polyholes

  Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);

  FixDia = Dia / cos(180/Sides);

  cylinder(r=(FixDia + HoleWindage)/2,
           h=Height,
	   $fn=Sides);
}

//- Locating pin hole with glue recess
//  Default length is two pin diameters on each side of the split

PinOD = 1.75;
PinOC = BodyOD / 2;

module LocatingPin(Dia=PinOD,Len=0.0) {

	PinLen = (Len != 0.0) ? Len : (4*Dia);

	translate([0,0,-ThreadThick])
		PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);

	translate([0,0,-2*ThreadThick])
		PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);

	translate([0,0,-(Len/2 + ThreadThick)])
		PolyCyl(Dia,(Len + 2*ThreadThick),4);

}

module LocatingPins(Length) {
	for (i=[-1,1])
	translate([0,i*PinOC/2,0])
		rotate(180/4)
		LocatingPin(Len=Length);
}

//----------------------
// Basic shapes

//-- HP plotter pen body

module ShowPolygon(pts) {
	polygon(pts);
}

module Body() {
	render(convexity=3)
		rotate_extrude($fn=NumSides)
			polygon(points=BodyOutline);
}

//-- Sakura drawing pen body

module Pen() {
	rotate_extrude($fn=NumSides)
		polygon(points=PenOutline);
}

//-- Plug for top of Sakura pen

module Plug() {
	render(convexity = 2)
		rotate_extrude($fn=NumSides)
			polygon(points=PlugOutline);
}

//-- Cap for tip of Sakura pen

module Cap() {
	render(convexity = 2)
		rotate_extrude($fn=NumSides)
			polygon(points=CapOutline);
}

//-- Sakura pen adapter

module PenAdapter() {

	render(convexity=3)
		difference() {
			Body();
			Pen();
			translate([0,0,TrimHeight/2])
				cube([2*FlangeOD,2*FlangeOD,TrimHeight],center=true);
		}
}

//-- Roland knife body

module Knife() {
	render(convexity=3)
		rotate_extrude($fn=NumSides)
			polygon(points=KnifeOutline);
}

//-- Roland knife adapter

module KnifeAdapter(TrimZ = false) {

Trans = TrimZ ? - TrimHeight : 0;

	render(convexity=5)
		translate([0,0,Trans])
			difference() {
				Body();
				Knife();
				translate([0,0,TrimHeight/2])
					cube([2*FlangeOD,2*FlangeOD,TrimHeight],center=true);
			}
}

//-- nose cap for stabilizer cylinder

module StabilizerNose() {
	render(convexity = 2)
		rotate_extrude($fn=NumSides)
			polygon(points=StabilizerNoseOutline);
}

//-- Roland knife stabilizer atop pen holder
//   the trim blocks have offsets with magic numbers from the HolderPlate outline &c

module Stabilizer(SeeKnife = false) {

Cutout = (Layout == "ShowStabilizer") ? 0 : 1;

	difference() {
		union() {
			translate(-HolderOrigin)													// put center of pen at origin
			translate([0,0,-RimHeight])													// put top of holder at Z=0
				difference() {
						render(convexity=4)
						linear_extrude(height=(HolderPlateThick + RimHeight))			// overall flange around edges
							offset(r=RimThick)
									polygon(points=HolderPlate);

						render(convexity=4)
						translate([0,0,-Protrusion])									// recess for pen holder plate
							linear_extrude(height=(RimHeight + Protrusion))
								polygon(points=HolderPlate);

						translate([HolderPlate[7][0] - Protrusion,HolderPlate[7][1] - Protrusion,-Protrusion])	// trim spring box from top plate
							cube([30,20,(RimHeight + HolderPlateThick + 2*Protrusion)]);

						translate([27.0,HolderPlate[6][1] - Protrusion,-Protrusion])	// trim pivot plate clearance
							cube([30,20,(RimHeight + HolderPlateThick + 2*Protrusion)]);

						translate([HolderPlate[2][0],20,-Protrusion])					// trim left support beam
							cube([BeamWidth,20,(RimHeight + Protrusion)]);

						translate([0,HolderPlate[0][1],-(TabClear + Protrusion)])		// trim tab behind pen recess
							cube([(HolderPlate[0][0] + Protrusion),TabWidth,RimHeight + Protrusion]);

						translate([HolderPlate[9][0] - LockScrewInset,RimThick,RimHeight - HolderTopThick - LockScrewOD/2])												// lock screw on front edge
							rotate([90,0,0])
								rotate(180/4)
									PolyCyl(LockScrewOD,3*RimThick);					// punch out hold-down screw hole
				}

			difference() {
				translate([0,0,-HolderHeight])											// cylinder and nose
					rotate_extrude($fn=NumSides)
						polygon(points=HolderCylinderOutline);
				translate([-HolderOrigin[0],-(BodyOD + Cutout*UncapperOD/2),(UncapperHeight - HolderHeight)])	// uncapper clearance
					cube([2*HolderOrigin[0],BodyOD,HolderHeight]);
			}
		}

		translate([0,0,-HolderHeight + HolderKnifeOffset])
			if (SeeKnife)
#				Knife();
			else
				Knife();

	}
}

//----------------------
// Build it

if (Layout == "Pen")
	Pen();

if (Layout == "Knife")
	Knife();

if (Layout == "Stabilizer")
	Stabilizer();

if (Layout == "ShowBody")
	Body();

if (Layout == "BodyPoly") {
	ShowPolygon(BodyOutline);
	Body();
}

if (Layout == "PenPoly") {
	ShowPolygon(PenOutline);
	Pen();
}

if (Layout == "BuildBody") {
	Spacing = 0.75*BuildSpace;
	difference() {
		union() {
			translate([Spacing,0,-SplitHeight])
				Body();
			rotate([180,0,0])
				translate([-Spacing,0,-SplitHeight])
					Body();
		}
		translate([0,0,-BodyOAL])
			cube(2*BodyOAL,center=true);
		for (i = [-1,1])
			translate([i*Spacing,0,0])
				LocatingPins(5.0);
	}
}

if (Layout == "Plug")
	Plug();

if (Layout == "KnifeAdapter")
	KnifeAdapter();

if (Layout == "ShowPen") {
	color("AntiqueWhite") {
		Pen();
		translate([-1.5*BodyOD,0,0])
			Pen();
	}
	color("Magenta",0.35) {
		translate([0,0,PlugOAL + PenOAL + 3.0])
			rotate([180,0,0])
				Plug();
		PenAdapter();
		Cap();
	}
	color("Magenta") {
		translate([1.5*BodyOD,0,PlugOAL + PenOAL + 3.0])
			rotate([180,0,0])
				Plug();
		translate([1.5*BodyOD,0,0]) {
			PenAdapter();
			Cap();
		}
	}
}

if (Layout == "ShowPenAdapter") {
	color("AntiqueWhite") {
		translate([0.00*BodyOD,0,0])
			Pen();
		translate([-2.75*BodyOD,0,0])
			Pen();
	}

	translate([-1.50*BodyOD,0,0])
		color("SandyBrown")
			Body();

	translate([0.00*BodyOD,0,0])
		color("SandyBrown",0.35)
			PenAdapter();

	translate([3.00*BodyOD,0,0])
		color("SandyBrown")
			PenAdapter();

	translate([1.50*BodyOD,0,0])
		difference() {
			color("SandyBrown")
				PenAdapter();
			translate([-BodyOD,-2*BodyOD,0])
				cube([2*BodyOD,2*BodyOD,PenOAL]);
		}

}

if (Layout == "ShowKnife") {

	color("Goldenrod") {
		Knife();
		translate([-1.5*BodyOD,0,0])
			Knife();
	}
	color("Magenta",0.35)
		KnifeAdapter();
	color("Magenta") {
		translate([1.5*BodyOD,0,0])
			KnifeAdapter();
	}

}

if (Layout == "BuildPenAdapter") {

	if (false) {
		for (j = [-1,1])
			translate([j*BuildSpace/2,-0.7*BuildSpace,0])
			Plug();

		translate([0,0,-CapOffset])
			Cap();
	}
	else {
		Plug();
	}

	difference() {
		union() {
			translate([1.20*BuildSpace,0,-SplitHeight])
				PenAdapter();
			rotate([180,0,0])
				translate([-1.20*BuildSpace,0,-SplitHeight])
					PenAdapter();
		}
		translate([0,0,-BodyOAL])
			cube(2*BodyOAL,center=true);
	}

}

if (Layout == "BuildKnife") {

	difference() {
		union() {
			translate([0.7*BuildSpace,0,-SplitHeight])
				KnifeAdapter(false);
			rotate([180,0,0])
				translate([-0.7*BuildSpace,0,-SplitHeight])
					KnifeAdapter(false);
		}
		translate([0,0,-BodyOAL])
			cube(2*BodyOAL,center=true);
	}

}

if (Layout == "BuildStabilizer") {

	translate([0,0,HolderPlateThick])
		rotate([0,180,0])
			Stabilizer(false);
}

if (Layout == "ShowStabilizer") {

	translate([BuildSpace/2,0,HolderHeight])
			Stabilizer(true);
	translate([-BuildSpace/2,0,HolderKnifeOffset])
		Knife();
}

,

5 Comments

HP 7475A Plotter: Roland Knife Holder-Stabilizer

Somewhat encouraged by the results of the height-map cap atop the plotter’s pen holder, I figured a unified knife adapter and stabilizer cap would work even better. That requires enough accuracy to build a real solid model, rather than just sketch a height map…

Print out the the grid-overlaid image of the pen holder, then doodle coordinates & measurements all over the poor thing:

HP 7475A Pen Holder - gridded doodle

HP 7475A Pen Holder – gridded doodle

Now I can toss that piece of paper…

That, plus a bit of digital caliper work, produces a flurry of dimensions & an array of vertices:

//-- Plotter pen holder stabilizer

HolderPlateThick = 3.0;				// thickness of plate atop holder
RimHeight = 5.0;					// rim around sides of holder
RimThick = 2.0;

HolderOrigin = [17.0,12.2,0.0];		// center of pen relative to polygon coordinates

HolderZOffset = 30.0;				// top of holder in pen-down position
HolderTopThick = 1.7;				// top of holder to top of pen flange
HolderCylinderLength = 17.0;		// length of pen support structure

HolderKnifeOffset = -2.0;			// additional downward adjustment range (not below top surface)

LockScrewInset = 3.0;				// from right edge of holder plate
LockScrewOD = 2.0;					// tap for 2.5 mm screw

// Beware: update hardcoded subscripts in Stabilizer() when adding / deleting point entries 

HolderPlate = [
	[8.6,18.2],[8.6,23.9],			// 0 lower left corner of pen recess
	[13.9,23.9],[13.9,30.0],		// 2
//	[15.5,30.0],[15.5,25.0],		// 4 omit middle of support beam
//	[20.4,25.0],[20.4,30.0],		// 6
	[22.7,30.0],[22.7,27.5],		// 4
	[35.8,27.5],[35.8,20.7],		// 6 spring box corner
	[43.0,20.7],					// 8
	[31.5,0.0],						// 9
//	[24.5,0.0],[24.5,8.0],			// 10 omit pocket above pen clamp
//	[22.5,10.0],[22.5,16.5],		// 12
//	[20.5,18.2]						// 14
	[13.6,0.0],						// 10
	[8.6,5.0]						// 11
	];

BeamWidth = HolderPlate[4][0] - HolderPlate[2][0];

The general idea is to extrude the overall shape of the stabilizer cap, carve out chunks to fit it onto the pen holder, then add a cylinder around the knife bearing:

HP7475A - Roland knife stabilizer - bottom - thrown together view

HP7475A – Roland knife stabilizer – bottom – thrown together view

The OpenSCAD source code contains a bunch of magic numbers and indexes that pull values from the vertex array:

module Stabilizer(SeeKnife = false) {

  difference() {
    union() {
      translate(-HolderOrigin)                                            // put center of pen at origin
        difference() {
            render(convexity=4)
            linear_extrude(height=(HolderPlateThick + RimHeight))         // overall flange around edges
              offset(r=RimThick)
                  polygon(points=HolderPlate);

            render(convexity=4)
            translate([0,0,-Protrusion])                                  // recess for pen holder plate
              linear_extrude(height=(RimHeight + Protrusion))
                polygon(points=HolderPlate);

            translate([HolderPlate[7][0] - Protrusion,HolderPlate[7][1] - Protrusion,-Protrusion])  // trim spring box from top plate
              cube([30,20,(RimHeight + HolderPlateThick + 2*Protrusion)]);

            translate([27.0,HolderPlate[6][1] - Protrusion,-Protrusion])  // trim pivot plate clearance
              cube([30,20,(RimHeight + HolderPlateThick + 2*Protrusion)]);

            translate([HolderPlate[2][0],20,-Protrusion])                 // trim left support beam
              cube([BeamWidth,20,(RimHeight + Protrusion)]);

            translate([HolderPlate[9][0] - LockScrewInset,RimThick,RimHeight - HolderTopThick - LockScrewOD/2])                        // lock screw on front edge
              rotate([90,0,0])
                rotate(180/4)
                  PolyCyl(LockScrewOD,3*RimThick);                        // hold-down screw hole
        }

      translate([0,0,(RimHeight - HolderCylinderLength + Protrusion)])
        cylinder(d=BodyOD,h=HolderCylinderLength + Protrusion,$fn=NumSides);  // surround knife threads
    }

    translate([0,0,-HolderZOffset + HolderKnifeOffset])
      if (SeeKnife)
#        Knife();
      else
        Knife();
  }
}

A bottom view shows all the cutouts:

HP7475A - Roland knife stabilizer - build layout

HP7475A – Roland knife stabilizer – build layout

The little hole in the front fits a screw that will pass under the top plate of the pen holder and prevent the cutting forces from pushing it off.

As with the Sakura pen adapter, the knife point sits at (0,0,0) with the stabilizer cap positioned at the (estimated) top of the pen holder:

Roland knife stabilizer - show layout

Roland knife stabilizer – show layout

After a few print-and-try iterations to align all the fiddly cutouts:

HP 7475A - Roland knife stabilizer - installed

HP 7475A – Roland knife stabilizer – installed

The slope-topped block protruding toward you from the bottom right actuates the carousel’s pen capping mechanism; it doesn’t quite touch the side of an HP pen and is well clear of the knife holder.

Because there’s still no depth control surrounding the knife blade, this won’t work well at all, but it should suffice for better measurements.

The full source code is back at the beginning.

, ,

Leave a comment

HP 7475A Plotter: Pen Holder Height Map Cap

The “pen holder” in an HP 7475A plotter carries the pen across the width of the paper:

HP 7475A - Pen Holder - overview

HP 7475A – Pen Holder – overview

Given that it was designed to carry pens, not knives, I wasn’t surprised that the spring-loaded finger clamping the knife adapter didn’t apply enough force to hold the adapter in place against the cutting forces. I figured a quick test of a gizmo to stabilize the adapter would be in order, even though I knew:

  • The pen holder doesn’t apply enough downward force
  • The knife adapter doesn’t have a depth-of-cut shroud around the blade

In order to build the gizmo, I need the carrier’s dimensions…

An overhead photo of the pen holder shows the layout in the XY plane:

HP7475A - pen holder - top view

HP7475A – pen holder – top view

I shouldn’t have used graph paper as a background, because the next step was to remove the background and isolate the carrier:

HP7475A - pen holder - top view - isolated

HP7475A – pen holder – top view – isolated

The carrier measures 26.8 mm front-to-back, so scaling a grid to match that dimension provides a coordinate system overlay:

HP7475A - pen holder - top view - 1 mm grid

HP7475A – pen holder – top view – 1 mm grid

The (0,0) origin sits at the lower left, so you can read off all the relevant coordinates as needed.

However, rather than go full-frontal digital, I resized the isolated image to 20 pixel/mm, turned it into a height map, and treated it like a chocolate mold or cookie cutter with gray values scaled to the desired height:

  • Black = background to be removed
  • Dark gray = 2.5 mm thick
  • Medium gray = 3.5 mm
  • Light gray = 7 mm
  • White = 10 mm

Drawing the walls with a 40 pixel diameter pen makes them 2 mm wide at 20 pixel/mm:

HP7475A - knife stabilizer

HP7475A – knife stabilizer

It’s painfully obvious why I don’t do much freehand drawing, although the knife adapter hole is supposed to be oval.

As with cookie cutters and chocolate molds, there’s no need for that much resolution, so I rescaled it to 4 pixel/mm, saved that tiny image as a PNG file, and handed it to OpenSCAD’s surface() function to get a solid model. This being a one-off, I typed this OpenSCAD source code directly into the OpenSCAD editor on the fly, then remembered to save it (!) before shutting down:

rotate([0,180,0])
mirror([0,0,1])
scale([0.25,0.25,10/100])
difference() {
  translate([0,0,-2.0]) render(convexity=10)
    surface("/long-and-tedious-path/HP7475A - knife stabilizer - scaled.png",center=true);
  translate([0,0,-200])
    cube(400,center=true);
}

The mirror() transformation inverts the model top-to-bottom along the Z axis, compensating for the flip from drawing the height map as though the walls rise upward from the pen carrier, after which the flip() transformation puts the flat side down to make it buildable.

The height map image conversion produces a bazillion irrelevant faces, but it’s quick and easy:

HP7475A - Roland knife stabilizer - height map model

HP7475A – Roland knife stabilizer – height map model

I’ve been using Slic3r’s Hilbert Curve  pattern for top & bottom infill to get a nice textured result:

Roland knife stabilizer - height map - Slic3r preview

Roland knife stabilizer – height map – Slic3r preview

Which printed just about like you’d expect:

HP 7475A - Roland knife adapter and stabilizer - height map - bottom view

HP 7475A – Roland knife adapter and stabilizer – height map – bottom view

I reamed out the hole with a step drill (the HP pens are close enough to 7/16 as to make no difference here) to get the knife adapter to fit, but the walls and suchlike came out close enough.

Then it just snapped into place:

HP 7475A - Roland knife adapter and stabilizer - height map

HP 7475A – Roland knife adapter and stabilizer – height map

Actually, no, it didn’t just snap into place: some (dis)assembly was required.

First, remove the brass knife bearing from the adapter, push the knife adapter shell into the pen holder, slide the stabilizer cap down over the adapter, press it firmly around the pen holder, reinstall the brass knife bearing, then it’s ready.

The cuts in the green vinyl just to the left of the knife blade (in a window decoration sheet I spotted in a trash can) show that the blade can cut, albeit with some finger pressure, but the fancy red stabilizer didn’t stay stuck on the pen carrier nearly as well as I expected. A screw attachment will help with that, which calls for going all digital on those coordinates.

But it was quick & easy…

,

6 Comments