Compare commits

...

34 Commits

Author SHA1 Message Date
Adam Mayer
1b05e9edb5
Merge pull request #4 from macroexp/pdfmanual
Added scanned manual
2022-10-03 19:28:08 -04:00
Mike Shields
67ef6ce139 Added scanned manual 2022-08-11 11:22:30 -04:00
Adam Mayer
e423b7d1a0 fix for unreachable code (should have been looking for a tab, not a newline) 2018-02-10 13:20:43 -05:00
Adam Mayer
3aa063ef79 adding a bit more documentation 2018-01-09 12:36:35 -05:00
Adam Mayer
308c88f67e document SP command 2018-01-03 16:52:53 -05:00
Adam Mayer
b0d6654960 quick note about errors actually being written by the plotter. :) 2018-01-03 16:48:49 -05:00
Adam Mayer
408ae97580 adjust XT/YT to handle all available styles 2018-01-03 13:59:57 -05:00
Adam Mayer
c6d3b86b95 fixing documentation for XT/YT 2018-01-03 13:54:30 -05:00
Adam Mayer
8f1d360a44 fix for draw-after-vp-update 2018-01-03 13:32:06 -05:00
Adam Mayer
0e13833b85 quick cut of xt/yt, needs to handle styles 2018-01-02 21:58:43 -05:00
Adam Mayer
cda0694f04 good enough for text 2018-01-02 21:23:03 -05:00
Adam Mayer
bae6443d5b getting text into plot 2018-01-02 16:04:19 -05:00
Adam Mayer
4872944fd3 starting in on proper font drawing 2018-01-02 14:56:56 -05:00
Adam Mayer
7268a1e872 adding extracted font data for use by simulator 2018-01-02 14:25:42 -05:00
Adam Mayer
b089612d92 adding quick script for ripping fonts in raw format for use in cairo generators 2018-01-02 14:00:22 -05:00
Adam Mayer
af2679bab4 patched up font to make it presentable. fontforge's svg import is... not great 2017-12-27 16:13:39 -05:00
Adam Mayer
8525bee4aa working, if wonky, font 2017-12-27 15:01:26 -05:00
Adam Mayer
2b35d39691 adding model of plotter head 2017-12-27 13:54:40 -05:00
Adam Mayer
2e5109bdbf rough font extract! 2017-12-27 12:35:01 -05:00
Adam Mayer
51cc15f152 get aspect ratio right and clean up 2017-12-27 11:10:56 -05:00
Adam Mayer
81e3a77b36 first pass at SVG font extractor for moving into fontforge 2017-12-27 10:58:59 -05:00
Adam Mayer
6455077236 nailed it 2017-12-26 23:04:48 -05:00
Adam Mayer
1a9c639ca8 Making some progress reversing the font format 2017-12-26 22:58:28 -05:00
Adam Mayer
207763984f
Merge pull request #2 from matthewblain/master
Update commands reference
2017-12-26 13:03:29 -05:00
matthewblain
f603dc0788 Update a few commands
Update a few commands based on info in the user manual.
2017-12-26 08:04:46 -08:00
matthewblain
be764807bc Update a few commands
Update a few commands based on the user manual.
2017-12-26 08:03:51 -08:00
matthewblain
5f8aa51566 Update a few commands
Update a few commands based on the user manual.
2017-12-26 08:01:53 -08:00
matthewblain
b97a1b69d1 Update a few commands
Update a few commands based on info in the user manual.
2017-12-26 07:58:50 -08:00
Adam Mayer
2a92ffd36b rough version of plotter head 2017-12-25 20:35:55 -05:00
Adam Mayer
b98008f113 modelling rough plotter head for printing 2017-12-25 12:37:28 -05:00
Adam Mayer
c87b0b24e3 added PNG output 2017-12-24 13:20:28 -05:00
Adam Mayer
5ff95991ea
Merge pull request #1 from dcarbone/patch-1
Small reference correction
2017-12-24 12:22:45 -05:00
Daniel Carbone
3dfef88012
Small reference correction
First, what you've done here is immensely cool.  I don't have a plotter, nor have I ever used one, but I have been reading this with the upmost interest.

As an old Apple hardware fanboy who has felt decidedly uninterested in Apple as of late, this has made want to dig out my old M00001 and start doing cool stuff again.

Also apologies if the correction is incorrect.  I just assumed some copypasta magic had happened :)
2017-12-24 07:06:38 -05:00
Adam Mayer
53ca795504 switch over to cairo svg renderer (w/ option for png renders further down the line) 2017-12-22 12:42:23 -05:00
18 changed files with 4633 additions and 209 deletions

View File

@ -1,13 +1,6 @@
#!/usr/bin/python3
import serial
import time
from .plot_to_svg import Plotter
def plot_to_svg(instream, outstream):
"Convert a set of plot instructions to an SVG. Works mininmally."
p = Plotter()
p.read(instream)
p.write(outstream)
class Apple410Common:
def __init__(self):

View File

@ -47,7 +47,7 @@ def svg2plot():
f = open(args.SVG)
plot_svg(f,center=args.center)
from .plot_to_svg import Plotter
from .plot_to_cairo import CairoPlotter
def plot2svg():
parser = argparse.ArgumentParser("plot2svg",
@ -58,7 +58,21 @@ def plot2svg():
f = sys.stdin
else:
f = open(args.PLOT)
p = Plotter()
p = CairoPlotter(sys.stdout.buffer)
p.read(f)
p.write(sys.stdout)
p.write()
def plot2png():
parser = argparse.ArgumentParser("plot2png",
description="Convert a set of Apple 410 Color Plotter commands to a PNG")
parser.add_argument('PLOT', help='The plotter file to process. "-" will read plotter commands from standard input.')
args = parser.parse_args()
if args.PLOT == '-':
f = sys.stdin
else:
f = open(args.PLOT)
p = CairoPlotter(sys.stdout.buffer,'PNG')
p.read(f)
p.write()

Binary file not shown.

Binary file not shown.

View File

@ -2,6 +2,8 @@
import sys
import math
import cairo
import pkg_resources
import pickle
def coordlist(line,expected=-1):
l = list(map(float,line.split(',')))
@ -20,11 +22,64 @@ W = 2394
H = 1759
LW = 5
class Font:
def __init__(self,path):
f = open(pkg_resources.resource_filename('apple410',path),'rb')
self.char_map = pickle.load(f)
def unpack_byte(b):
"convert two 4-bit signed packed numbers to a tuple"
# this packing is... unusual.
x = b >> 4
y = b % 16
if y > 8: # now the weird
x -= 1
y -= 16
return (x,y)
def render_char(self, ctx, char, xoff, yoff):
d = list(self.char_map[char])
while d:
cmd = d.pop(0)
cn, ca = cmd >> 4, cmd % 16
for _ in range(ca):
(x1, y1) = Font.unpack_byte(d.pop(0))
x = x1 + xoff
y = y1 + yoff
if cn == 0:
ctx.move_to(x,y)
elif cn == 2:
ctx.line_to(x,y)
def render_string(self,ctx,s):
xoff = 0
yoff = 0
ctx.set_line_width(1)
for c in s:
if c == '\r' or c == '\n':
xoff = 0
yoff += 10
elif c == '\t':
xoff += 20
else:
self.render_char(ctx,c,xoff,yoff)
xoff += 10
class CairoPlotter:
def __init__(self, path):
self.surface = cairo.SVGSurface(path, W, H)
def __init__(self, path, surftype = 'SVG'):
self.surftype = surftype
if surftype == 'SVG':
self.surface = cairo.SVGSurface(path, W, H)
elif surftype == 'PNG':
self.path = path
self.surface = cairo.ImageSurface(cairo.Format.RGB24,W,H)
self.context = cairo.Context(self.surface)
if surftype == 'PNG':
self.context.rectangle(0,0,W,H)
self.context.set_source_rgb(1.0,1.0,1.0)
self.context.fill()
#self.context.scale(W,H)
self.context.select_font_face("monospace")
self.pennum = 0
@ -32,7 +87,11 @@ class CairoPlotter:
self.window = (0, 0, W, H)
self.text_theta = 0
self.text_size = 1
self.context.set_line_cap(cairo.LINE_CAP_ROUND)
self.context.set_line_join(cairo.LINE_JOIN_ROUND)
self.update_ctm()
self.char_font = Font('data/a410_chars.pickle')
# A quick review of coordinate systems:
# Cairo native: origin at upper left, x+ to right, y+ down.
@ -67,11 +126,13 @@ class CairoPlotter:
self.context.move_to(cp[0],cp[1])
def vp(self, params):
self.finish_path()
l=coordlist(params,4)
self.viewport=(l[0],l[1],l[2],l[3])
self.update_ctm()
def wd(self, params):
self.finish_path()
l=coordlist(params,4)
self.window=(l[0],l[1],l[2],l[3])
self.update_ctm()
@ -108,6 +169,52 @@ class CairoPlotter:
p=self.context.get_current_point()
self.context.arc(p[0],p[1],r,0.0,math.pi*2)
def yt(self, params):
l=coordlist(params,5)
left = l[3]
right = l[4]
num = int(l[2])
style = int(l[0])
if style > 3:
sys.stderr.write("Unrecognized tick mark style {}\n".format(style))
return
if style:
dist = l[1]
tw = dist/num
else:
dist = l[1]*num
tw = l[1]
first = style >> 1
(x,y) = self.context.get_current_point()
self.context.line_to(x,y+dist)
for i in range(first,num+1):
y1 = y + tw*i
self.context.move_to(x+left,y1)
self.context.line_to(x-right,y1)
def xt(self, params):
l=coordlist(params,5)
above = l[3]
below = l[4]
num = int(l[2])
style = int(l[0])
if style > 3:
sys.stderr.write("Unrecognized tick mark style {}\n".format(style))
return
if style & 0x01:
dist = l[1]
tw = dist/num
else:
dist = l[1]*num
tw = l[1]
first = style >> 1
(x,y) = self.context.get_current_point()
self.context.line_to(x+dist,y)
for i in range(first,num+1):
x1 = x + tw*i
self.context.move_to(x1,y+above)
self.context.line_to(x1,y-below)
def ac(self,params):
l=coordlist(params)
r=l[0]
@ -136,11 +243,20 @@ class CairoPlotter:
def pl(self,params):
self.finish_path()
(x,y) = self.context.get_current_point()
sys.stderr.write("text at {} {}\n".format(x,y))
self.context.show_text(params)
self.context.save()
self.context.translate(x,y)
self.context.rotate(math.radians(self.text_theta))
self.context.scale(self.text_size/10.0, self.text_size/8.0)
self.char_font.render_string(self.context,params)
self.context.restore()
def write(self,out):
def write(self):
self.finish_path()
if self.surftype == 'PNG':
if self.path == '-':
self.surface.write_to_png(sys.stdout)
else:
self.surface.write_to_png(self.path)
self.surface.finish()
def process_cmd(self,command):
@ -157,7 +273,7 @@ class CairoPlotter:
self.process_cmd(line)
if __name__=='__main__':
p = CairoPlotter('foo.svg')
p = CairoPlotter(sys.stdout.buffer)
p.read(sys.stdin)
p.write(sys.stdout)
p.write()

View File

@ -1,175 +0,0 @@
#!/usr/bin/python3
import sys
import svgwrite
import math
def coordlist(line,expected=-1):
l = list(map(float,line.split(',')))
if expected > -1:
assert len(l) == expected
return l
pens = [
svgwrite.rgb(0, 0, 0, '%'),
svgwrite.rgb(100, 0, 0, '%'),
svgwrite.rgb(0, 100, 0, '%'),
svgwrite.rgb(0, 0, 100, '%') ]
# Default W/H
W = 2394
H = 1759
class Plotter:
def __init__(self):
self.pennum = 0
self.viewport = (0, 0, W, H)
self.window = (0, 0, W, H)
self.cur_g = None
self.cur_d = None
self.d = svgwrite.Drawing(size=(W,H))
self.text_theta = 0
self.text_size = 1
self.pos = (0,0)
self.clip = None
self.clipno = 0
def get_g(self):
"Return a valid group for this viewport/window"
if not self.cur_g:
self.cur_g = self.d.g(style='clip-path: url(#cl{})'.format(self.clipno))
(v,w) = (self.viewport,self.window)
a = (v[2]-v[0])/(w[2]-w[0])
d = (v[3]-v[1])/(w[3]-w[1])
e = v[0] - w[0]*a
f = v[1] - w[1]*d
# everything needs to flip on the Y axis.
f = H-f
d = -d
self.cur_g.matrix(a,0,0,d,e,f)
self.clip = self.d.clipPath(id='cl{}'.format(self.clipno))
self.clip.add(self.d.rect((w[0],w[1]),(w[2],w[3])))
self.d.defs.add(self.clip)
self.clipno += 1
return self.cur_g
def finish_g(self):
if self.cur_g:
self.d.add(self.cur_g)
self.cur_g = None
def finish_path(self):
if self.cur_d:
path=self.d.path(
d=self.cur_d,
stroke=pens[self.pennum])
path.fill(opacity=0)
self.get_g().add(path)
self.cur_d = None
def add_to_path(self, addition):
if not self.cur_d:
self.cur_d = "M{} {}".format(self.pos[0],self.pos[1])
self.cur_d = self.cur_d + " " + addition
def invalidate_window(self):
self.finish_path()
self.finish_g()
def vp(self, params):
self.invalidate_window()
l=coordlist(params,4)
self.viewport=(l[0],l[1],l[2],l[3])
def wd(self, params):
self.invalidate_window()
l=coordlist(params,4)
self.window=(l[0],l[1],l[2],l[3])
def ma(self, params):
l=coordlist(params,2)
self.pos = (l[0],l[1])
def mr(self, params):
l=coordlist(params,2)
self.pos = (self.pos[0]+l[0],self.pos[1]+l[1])
def da(self, params):
l=coordlist(params)
self.add_to_path("L{}".format(" ".join(map(str,l))))
self.pos = (l[-2],l[-1])
def dr(self, params):
l=coordlist(params)
self.add_to_path("l{}".format(" ".join(map(str,l))))
self.pos = (l[-2],l[-1])
def ca(self, params):
self.finish_path()
l=coordlist(params)
r=l[0]
if len(l) > 2:
p=(l[1],l[2])
else:
p=self.pos
c=self.d.circle(center=p,r=r)
c.fill(opacity=0)
c.stroke(color=pens[self.pennum],opacity=100,width=2)
self.get_g().add(c)
def ac(self,params):
l=coordlist(params)
r=l[0]
if len(l) > 3:
p=(l[3],l[4])
else:
p=self.pos
t2, t1 = math.radians(l[1]),math.radians(l[2])
x1, y1 = p[0] + (r*math.cos(t1)), p[1] + (r*math.sin(t1))
x2, y2 = p[0] + (r*math.cos(t2)), p[1] + (r*math.sin(t2))
ds="M {} {} A {} {} 0 0 0 {} {}".format(x1,y1,r,r,x2,y2)
self.add_to_path(ds)
def lr(self,params):
self.text_theta = float(params)
def ls(self,params):
self.text_size = float(params)
def ps(self,params):
self.finish_path()
self.pennum = int(params) - 1
def pl(self,params):
self.finish_path()
flip_pos = (self.pos[0], -self.pos[1])
t = self.d.text(params,insert=flip_pos)
t.rotate(self.text_theta,center=flip_pos)
t['font-size'] = self.text_size
g = self.d.g()
g.add(t)
g.matrix(1,0,0,-1,0,0)
self.get_g().add(g)
def write(self,out):
self.invalidate_window()
self.d.write(out)
def process_cmd(self,command):
cmd_code = command[0:2].lower()
params = command[2:]
try:
getattr(self,cmd_code)(params)
except AttributeError:
sys.stderr.write("Unrecognized command code {}\n".format(cmd_code))
def read(self,inf):
for line in inf.readlines():
line = line.strip()
self.process_cmd(line)
if __name__=='__main__':
p = Plotter()
p.read(sys.stdin)
p.write(sys.stdout)

View File

@ -132,6 +132,14 @@ specified the circle will be centered at the current position.
Params: 1 or 3
### Letter Initialize (LI)
```
LI
```
Reset the text format.
### Letter size (LS)
```
@ -176,14 +184,15 @@ Params: 1
### Line type (LT)
```
LTn
LTp(,k)
```
Select the line style for subsequent draw operations. `n` is a value in the
Select the line style for subsequent draw operations. `p` is a value in the
range 1-9:
| n | Line style |
|---|------------------|
| 0 | Custom pattern (see UL) |
| 1 | Solid |
| 2 | Dotted |
| 3 | Short dash |
@ -194,7 +203,18 @@ range 1-9:
| 8 | Line w/ two dots |
| 9 | Line w/ two short dashes |
Params: 1
`k` specifies the repeat length in mesurement units. The default is 100, but the value will persist.
Params: 1 or 2
```
ULd1,m1(,d2,m3,..d6,m6)
```
Create a custom pattern for use with subsequent LT0 operations.
`d` specifies a draw length, `m` specifies a move length.
### X-axis ticks (XT)
@ -214,9 +234,15 @@ is interpreted.
| a | length of tick mark above the axis |
| b | length of tick mark below the axis |
If the `s` parameter is set to 1, then the distance is interpreted as
the length of the entire axis. If `s` is set to 2, then the distance is
interpretes as the length of the space between adjacent tick marks.
The `s` parameter determines how the `d` parameter is interpreted and whether
the first tick mark (at the "0" point on the axis) is drawn or not:
| style parameter | description |
|------------------|-------------------------------------|
| 0 | d is distance between ticks |
| 1 | d is total length of axis |
| 2 | d is distance between ticks, first tick mark is not drawn |
| 3 | d is total length of axis, first tick mark is not drawn |
Params: 5
@ -299,7 +325,7 @@ WDx1,y1,x2,y2
The window into which to draw. See the introduction for an explanation of how the window
is used. `x1,y1` specify the upper left hand corner
of the viewport; `x2,y2` specify the lower right hand of the viewport.
of the window; `x2,y2` specify the lower right hand of the window.
Params: 4
@ -329,20 +355,58 @@ After the arc is drawn, the endpoint of the arc is considered to be the new posi
Params: 3 or 5
### Unknown (IM)
### Error action (IM)
```
IMa,b
IMa(,b)
```
IM0,31 turns on error checking; IM0,0 turns it off?
This is referenced obliquely in part II of the manual.
Selecting error action
`a` specifies the mode: 0 = turn on error light; 64 = reject commands ; 192 = write message.
| a | Mode |
|---|------------------|
| 0 | Turn on error light |
| 64 | Reject command |
| 192 | Write error message |
Do note that "write message" does not mean to send a response over the serial port; it means
that it will literally write the error message out on the plotter.
`b` specifies the errors to filter. Default is 31.
| a | Error type |
|---|------------------|
| 1 | Command not recognized |
| 2 | Numbers outside range |
| 4 | Not enough numbers for command |
| 8 | Bad syntax |
| 16 | Pen would go outside window |
### Size Paper (SP)
```SPn```
Sets the current paper size. This is essentially a preset "VP" command. The table below of
drawing area sizes is approximate, based on measuring a rectangle drawn at the extent of the
entire viewport.
| n | measurement (in mm) |
|---|---------------------|
| 0 | 241 x 175 |
| 1 | 178 x 240 |
| 2 | 394 x 240 |
| 3 | 282 x 280(?) |
| 4 | 259 x 170 |
| 5 | 171 x 280(?) |
| 6 | 326 x 217 |
| 7 | 218 x 142 |
| 8 | 143 x 217 |
## Unknown commands
LI - ??? - no params/arb ???
PK - ??? - no params/arb
UL - ??? - no params/arb ? 1 param 0-9?
SP - ??? - 1 param ??
LF - ??? - 1 param ? 0-9
PK - ??? - no params/arb (Not in user reference)
LF - ??? - 1 param ? 0-9 Letter Font

412
docs/Apple 410 Plotter.md Normal file
View File

@ -0,0 +1,412 @@
# The Apple 410 Color Plotter
## Quick introduction and apology
Everything below (other than the dip switch settings) was sussed out by
ripping the ROM, extracting the self-test script and the command lookup table, and
trial and error. Now that the manual is included in this folder, this could possibly
be updated.
## General concepts
The way the 401 interprets a coordinate at any given time depends on the current
viewport and window settings. The "viewport" is a rectangle specified in hardware
absolute coordinates and defines the physical area in which coordinates are resolved.
The "window" defines the virtual coordinates of the drawing area, which are then
mapped to the viewport.
All drawing operations are clipped to the current viewport and window; parts of
arcs, circles, lines, etc. that would be rendered outside the viewport are not
drawn.
If the size of the window has a different aspect ratio than the size of the viewport,
the elements drawn will be stretched to fit. You can use this mechanism to draw ellipses
or stretched text.
Commands sent to the 401 are terminated by the ASCII "end of text" delimiter, 0x03.
A semicolon or newline may also be used in some situations; however it's safest to
terminate a text-writing command (PL) with 0x03.
## Units
The base units for the viewport (as used in the VP command) are approximately 1 unit
= 0.1mm. (In practice, this is slightly off, so I don't recommend drawing any rulers
with this machine.)
### Serial connection
The 8 DIP switches (SW1-SW8) on the back of the plotter are used to configure the
serial interface.
SW1 selects the data bits. ON = 7 bits, OFF = 8 bits.
SW2 selects whether parity is used. ON = no parity bit, OFF = use a parity bit.
If parity is selected, SW3 determines the type of parity bit. ON = odd parity,
OFF = even parity.
SW4 and SW5 select the number of stop bits:
| SW4 | SW5 | Stop bits |
|------|------|---------------|
| OFF | OFF | 2 stop bits |
| ON | OFF | 1.5 stop bits |
| OFF | ON | 1 stop bit |
| ON | ON | invalid |
SW6, SW7, and SW8 select the baud rate:
| SW6 | SW7 | SW8 | Baud |
|-----|-----|-----|------------|
| OFF | OFF | OFF | 75 baud |
| OFF | OFF | ON | 150 baud |
| OFF | ON | OFF | 300 baud |
| OFF | ON | ON | 600 baud |
| ON | OFF | OFF | 1200 baud |
| ON | OFF | ON | 2400 baud |
| ON | ON | OFF | 4800 baud |
| ON | ON | ON | 9600 baud |
**The 401 uses hardware handshaking.** It never returns data to the computer
over the serial line and sets DSR when its internal buffer is full. Sending even
one byte after the DSR line has been set will corrupt the 401's command buffer
and trigger an error. Most USB to RS232 cables will attempt to bundle up several
bytes before transmission, so unless you're sure that your cable can handle it
I recommend flushing the serial connection after every byte and checking the DSR
line yourself.
## Command Reference
### Move Absolute (MA)
```
MAx,y
```
Raise the pen and move the plotter head to the position specified by the x,y coordinates.
Params: 2
### Move relative (MR)
```
MRx,y
```
Raise the pen and move the plotter head to a position offset x,y from its
current position.
Params: 2
### Draw absolute (DA)
```
DAx,y(,x,y..)
```
Lower the pen and draw a line from the current position to the position
specified by the x,y coordinates. If more than one coordinate pair is
specified, continues to draw lines to the subsequent points as well.
Params: 2(+2*n)
### Draw relative (DR)
```
DRx,y(,x,y..)
```
Lower the pen and draw a line from the current position to the position
offset x,y for its current position. If more than one coordinate pair is
specified, continues to draw lines to the subsequent offset points as well.
Params: 2(+2*n)
### Circle (CA)
```
CAr(,x,y)
```
Lower the pen and draw a circle of radius r centered at x,y. If x,y are not
specified the circle will be centered at the current position.
Params: 1 or 3
### Letter Initialize (LI)
```
LI
```
Reset the text format.
### Letter size (LS)
```
LSs
```
Sets the font size to s (?? in what units? relative to what system of coordinates? ??)
Params: 1
### Letter rotation (LR)
```
LRθ
```
Sets the angle to draw subsequent text at θ degrees clockwise.
Params: 1
### Print letters (PL)
```
PLtext
```
Lower the pen and draw the specified text at the current position,
using the currently active letter size and rotation settings.
Params : 1 (string)
### Pen select (PS)
```
PSi
```
Select pen i (where i is in the range 1-4).
Params: 1
### Line type (LT)
```
LTp(,k)
```
Select the line style for subsequent draw operations. `p` is a value in the
range 1-9:
| n | Line style |
|---|------------------|
| 0 | Custom pattern (see UL) |
| 1 | Solid |
| 2 | Dotted |
| 3 | Short dash |
| 4 | Medium dash |
| 5 | Long dash |
| 6 | Line w/ dots |
| 7 | Line w/ one short dash |
| 8 | Line w/ two dots |
| 9 | Line w/ two short dashes |
`k` specifies the repeat length in mesurement units. The default is 100, but the value will persist.
Params: 1 or 2
```
ULd1,m1(,d2,m3,..d6,m6)
```
Create a custom pattern for use with subsequent LT0 operations.
`d` specifies a draw length, `m` specifies a move length.
### X-axis ticks (XT)
```
XTs,d,n,a,b
```
Draw an X axis with tick marks as specified. There are two "styles"
`s` of input available, which determine how the distance `d` parameter
is interpreted.
| parameter | function |
|-----------|---------------------------------------|
| s | style |
| d | distance |
| n | number of tick marks |
| a | length of tick mark above the axis |
| b | length of tick mark below the axis |
The `s` parameter determines how the `d` parameter is interpreted and whether
the first tick mark (at the "0" point on the axis) is drawn or not:
| style parameter | description |
|------------------|-------------------------------------|
| 0 | d is distance between ticks |
| 1 | d is total length of axis |
| 2 | d is distance between ticks, first tick mark is not drawn |
| 3 | d is total length of axis, first tick mark is not drawn |
Params: 5
### Y-axis ticks (YT)
```
YTs,d,n,a,b
```
Draw a Y axis with tick marks as specified. The parameters are handled
in the same manner as the `XT` command (see above).
Params: 5
### Clear/Reset (CH)
```
CH
```
Raise the pen and move the plotter head to its original position; usually used
at the end of a plot.
Params: 0
### Point mark (PM)
```
PMt
```
Draw a marker at the current position. The marker type is specified by the `t`
parameter. The marker size is determined by the font size setting specified by the
`LS` command; consider the markers as just special characters.
The various marker shapes are illustrated in the images below.
![Image of PM1-5](pm1-5.jpg) ![Image of PM6-10](pm6-10.jpg) ![Image of PM11-15](pm11-15.jpg)
Params: 1
### Pen velocity (PV)
```
PVv
```
Sets the drawing velocity `v`, on a scale of 1-10, where 1 is slowest, 10 is fastest.
The default speed is 10.
Params: 1
### Slant lettering (SL)
```
SLθ
```
Slants subsequent text by an angle specified by `θ` degrees.
Params: 1
### Viewport (VP)
```
VPx1,y1,x2,y2
```
Specifies the viewport area, in absolute coordinates, to draw in. See the introduction
for an explanation of how the viewport is used. `x1,y1` specify the upper left hand corner
of the viewport; `x2,y2` specify the lower right hand of the viewport.
Params: 4
### Window (WD)
```
WDx1,y1,x2,y2
```
The window into which to draw. See the introduction for an explanation of how the window
is used. `x1,y1` specify the upper left hand corner
of the window; `x2,y2` specify the lower right hand of the window.
Params: 4
### Reset (RS)
```
RS
```
Resets the error bit. If the error LED is lit, it should be extinguished.
RS will apparently accept some parameters, but it's unknown if this does anything.
Params: 0 or 1
### Arc (AC)
```
ACr,θ1,θ2(,x,y)
```
Draws a circular arc of radius `r` from `θ1` to `θ2`. If x,y are specified, they
determined the center of the arc's circle. If they are not specified, the current
position is considered to be the center of the arc's circle.
After the arc is drawn, the endpoint of the arc is considered to be the new position.
Params: 3 or 5
### Error action (IM)
```
IMa(,b)
```
Selecting error action
`a` specifies the mode: 0 = turn on error light; 64 = reject commands ; 192 = write message.
| a | Mode |
|---|------------------|
| 0 | Turn on error light |
| 64 | Reject command |
| 192 | Write error message |
Do note that "write message" does not mean to send a response over the serial port; it means
that it will literally write the error message out on the plotter.
`b` specifies the errors to filter. Default is 31.
| a | Error type |
|---|------------------|
| 1 | Command not recognized |
| 2 | Numbers outside range |
| 4 | Not enough numbers for command |
| 8 | Bad syntax |
| 16 | Pen would go outside window |
### Size Paper (SP)
```SPn```
Sets the current paper size. This is essentially a preset "VP" command. The table below of
drawing area sizes is approximate, based on measuring a rectangle drawn at the extent of the
entire viewport.
| n | measurement (in mm) |
|---|---------------------|
| 0 | 241 x 175 |
| 1 | 178 x 240 |
| 2 | 394 x 240 |
| 3 | 282 x 280(?) |
| 4 | 259 x 170 |
| 5 | 171 x 280(?) |
| 6 | 326 x 217 |
| 7 | 218 x 142 |
| 8 | 143 x 217 |
## Unknown commands
PK - ??? - no params/arb (Not in user reference)
LF - ??? - 1 param ? 0-9 Letter Font

BIN
docs/Apple410Plotter.PDF Executable file

Binary file not shown.

58
models/plotter_head.scad Normal file
View File

@ -0,0 +1,58 @@
5notch_width = 2.5;
$fa = 6;
$fs = 0.5;
module peg_hole() {
union() {
difference() {
union() {
cylinder(h=21.5,d1=7.8,d2=7.5,center=false);
// Chamfer
cylinder(h=1.25,d1=9.2,d2=7.7,center=false);
}
translate([3.3, -5, -0.1]) {
cube([10,10,30]);
}
}
translate([0,-notch_width/2,0]) {
cube([4.5,notch_width,3.4]);
}
}
}
module shell() {
union() {
cylinder(h=12,d=32.0,center=false);
translate([0,0,8.1]) {
cylinder(h=3.4,d=34,center=false);
}
translate([0,0,11.5]) {
cylinder(h=11.4,d1=34,d2=21,center=false);
}
}
}
module pen(which) {
rotate([0,0,which*90]) {
translate([10,0,4]) rotate([0,90,0]) {
cylinder(h=10,d=4,center=false);
}
}
}
module pens() {
for (i=[0:4]) {
pen(i);
}
}
//peg_hole();
difference() {
union() {
shell();
}
pens();
translate([0,0,-0.005]) {
peg_hole();
}
}

3684
roms/Apple410.sfd Normal file

File diff suppressed because it is too large Load Diff

BIN
roms/Apple410.ttf Normal file

Binary file not shown.

View File

@ -8,3 +8,11 @@ consolidated ROM image built by concatenating the three individual roms.
`test_extractor.py` is a script for extracting the self-test commands from the consolidated
ROM file.
'font_extract.py' extracts the font as a set of SVG files. 'font_to_pickled_dict.py' generates
two pickle files, one with the font and another with the point markers. Each pickle file contains
a single dictionary mapping characters to the raw sequence of bytes that represents the glyph in
the Apple 410's native format.
'ff_import.py' is a rough script for importing the SVGs into fontforge. The generated font will
require a lot of manual cleanup.

37
roms/ff_import.py Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/python
import fontforge
import tempfile
import os
import re
font = fontforge.font()
font.fontname = "Apple410"
base = "alpha"
matcher = re.compile(r"{}_([0-9a-f][0-9a-f]).svg".format(base))
for path in os.listdir('.'):
m = matcher.match(path)
if m:
idx = int(m.group(1),16)
print "{} : {}".format(idx,path)
# fontforge can't handle % colors, so we need to hack those out. Luckily, these are the only %
# characters in the files, so...
tf = tempfile.NamedTemporaryFile(suffix='.svg')
inf = open(path)
for c in inf.read():
if c != '%':
tf.write(c)
tf.flush()
font.createChar(idx)
font[idx].importOutlines(tf.name)
tf.close()
inf.close()
print "glyph count {}".format(len(font))
#print font.createChar(0x41)
#font[0x41].importOutlines("alpha_41.svg")
font.save("Apple410-new.sfd")

71
roms/font_extract.py Executable file
View File

@ -0,0 +1,71 @@
#!/usr/bin/python3
import sys
import math
import cairo
import struct
alpha_ft_start = 0x2569
point_ft_start = 0x3153
f = open("ROM.bin","rb")
data = f.read()
def eot(ft, off):
"Returns true if the offset is at the end of the table."
# Presumes that every table terminates in 0xff
return data[ft + off*2] == 0xff
def get_char(ft,off):
achar = chr(off+0x20)
o1 = ft + (off*2)
o2 = (data[o1+1]*256) + data[o1]
o3 = o2
while data[o3] != 0xff:
o3 += 1
return data[o2:o3]
def unpack_byte(b):
"convert two 4-bit signed packed numbers to a tuple"
# this packing is... unusual.
x = b >> 4
y = b % 16
if y > 8: # now the weird
x -= 1
y -= 16
return (x,y)
# H:W for a char is 3:2
def unpack_coords(b,xscale=66,yscale=100,xoff=25,yoff=25):
"convert two 4-bit signed packed numbers to cairo coordinates"
(x,y) = unpack_byte(b)
return (x*xscale + xoff, (8 - y)*yscale + yoff)
def build_char_file(path, ft, offset):
surf = cairo.SVGSurface(path, 800, 1280)
c = cairo.Context(surf)
d = list(get_char(ft, offset))
#c.set_source_rgb(0.0, 0.0, 0.0)
c.set_line_width(40)
c.set_line_cap(cairo.LINE_CAP_ROUND)
c.set_line_join(cairo.LINE_JOIN_ROUND)
while d:
cmd = d.pop(0)
cn, ca = cmd >> 4, cmd % 16
for _ in range(ca):
(x,y) = unpack_coords(d.pop(0))
if cn == 0:
c.move_to(x,y)
elif cn == 2:
c.line_to(x,y)
c.stroke()
surf.finish()
def dump_font(path_base, ft, charoff):
off = 0
while not eot(ft, off):
build_char_file("{}_{:2x}.svg".format(path_base,charoff+off),ft,off)
off += 1
dump_font("alpha",alpha_ft_start,0x20)

92
roms/font_test.py Executable file
View File

@ -0,0 +1,92 @@
#!/usr/bin/python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
import math
font_table_start = 0x2569
first_char = 0x2627
f = open("ROM.bin","rb")
data = f.read()
from gi.repository import Gtk
import cairo
import math
char_off = 0
# ! : 01 08 21 02 01 00 21 00
# " : 01 28 21 26 01 68 21 66
def get_char(ft,off):
achar = chr(off+0x20)
o1 = ft + (off*2)
o2 = (data[o1+1]*256) + data[o1]
o3 = o2
while data[o3] != 0xff:
o3 += 1
#print("Looking up '{}' at {:x}; len {}".format(achar,o2,o3-o2))
print("{} : {}".format(achar, " ".join(map(hex,data[o2:o3]))))
return data[o2:o3]
def bytecoords(b):
rx, ry = math.floor(b/16), b%16
if ry > 8:
ry = ry - 16
ry = 8 - ry # inverted y axis
return ( 50 + rx*16, 50 + ry*16 )
def OnDraw(w, cr):
global char_off
cr.set_source_rgb(0, 0, 0)
np = True
cr.set_line_width(5)
cr.set_line_cap(cairo.LINE_CAP_ROUND)
data = list(get_char(font_table_start, char_off))
pos = (0,0)
while data:
c = data.pop(0)
if c == 0x01:
# move
(x,y) = bytecoords( data.pop(0) )
cr.move_to(x,y)
elif c > 0x20:
segments = c - 0x20
for s in range(segments):
# draw to
(x,y) = bytecoords( data.pop(0) )
cr.line_to(x,y)
cr.stroke()
def OnKey(w, event):
global char_off
n = Gdk.keyval_name(event.keyval)
if n == 'Right':
char_off += 1
#get_char(font_table_start, char_off)
#coords.append((x,y))
w.queue_draw()
elif n == 'Left':
char_off -= 1
#get_char(font_table_start, char_off)
w.queue_draw()
elif n == 'q':
print("QUIT")
Gtk.main_quit()
w = Gtk.Window()
w.set_default_size(640, 480)
a = Gtk.DrawingArea()
w.add(a)
w.connect('destroy', Gtk.main_quit)
a.connect('draw', OnDraw)
w.connect('key_press_event', OnKey)
w.show_all()
Gtk.main()

48
roms/font_to_pickled_dict.py Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/python3
"""
Extract the fonts and create pickled dictionaries mapping characters to the raw binary
strings representing each entry. Parsing these strings is done in the client code.
"""
import struct
import pickle
alpha_ft_start = 0x2569
point_ft_start = 0x3153
f = open("ROM.bin","rb")
data = f.read()
def eot(ft, off):
"Returns true if the offset is at the end of the table."
# Presumes that every table terminates in 0xff
return data[ft + off*2] == 0xff
def get_char(ft,off):
"Returns the character data (sans terminating 0xff)"
achar = chr(off+0x20)
o1 = ft + (off*2)
o2 = (data[o1+1]*256) + data[o1]
o3 = o2
while data[o3] != 0xff:
o3 += 1
return data[o2:o3]
def build_char_dict(ft, first_char=0):
offset = 0
d = {}
while not eot(ft,offset):
cur_char = chr(offset+first_char)
d[cur_char] = get_char(ft,offset)
offset += 1
return d
def pickle_font(path, ft, first_char=0):
f = open(path,"wb")
d = build_char_dict(ft,first_char)
pickle.dump(d,f)
pickle_font("a410_chars.pickle",alpha_ft_start,ord(' '))
pickle_font("a410_points.pickle",point_ft_start)

View File

@ -43,7 +43,8 @@ setup(
# You can just specify the packages manually here if your project is
# simple. Or you can use find_packages().
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
packages=['apple410'],
package_data={'apple410' : ['data/*.pickle']},
install_requires=['pyserial'],
python_requires='>=3',
@ -55,6 +56,7 @@ setup(
'apple410=apple410.cmdline:main',
'a410svg2plot=apple410.cmdline:svg2plot',
'a410plot2svg=apple410.cmdline:plot2svg',
'a410plot2png=apple410.cmdline:plot2png',
],
},
)