Skip to content
Newer
Older
100755 637 lines (536 sloc) 20.1 KB
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
1 #!/usr/bin/env python
2
3 # Copyright 2009 Steve Conklin
4 # steve at conklinhouse dot com
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
20
21 # This software emulates the external floppy disk drive used
22 # by the Brother Electroknit KH-930E computerized knitting machine.
23 # It may work for other models, but has only been tested with the
24 # Brother KH-930E
25 #
26 # This emulates the disk drive and stores the saved data from
27 # the knitting machine on the linux file system. It does not
28 # read or write floppy disks.
29 #
30 # The disk drive used by the brother knitting machine is the same
31 # as a Tandy PDD1 drive. This software does not support the entire
32 # command API of the PDD1, only what is required for the knitting
33 # machine.
34 #
35
36 #
37 # Notes about data storage:
38 #
39 # The external floppy disk is formatted with 80 sectors of 1024
40 # bytes each. These sectors are numbered (internally) from 0-79.
41 # When starting this emulator, a base directory is specified.
42 # In this directory the emulator creates 80 files, one for each
43 # sector. These are kept sync'd with the emulator's internal
44 # storage of the same sectors. For each sector, there are two
45 # files, nn.dat, and nn.id, where 00 <= nn <= 79.
46 #
47 # The knitting machine uses two sectors for each saved set of
48 # information, which are referred to in the knitting machine
49 # manual as 'tracks' (which they were on the floppy disk). Each
50 # pair of even/odd numbered sectors is a track. Tracks are
51 # numbered 1-40. The knitting machine always writes sectors
52 # in even/odd pairs, and when the odd sector is written, both
53 # sectors are concatenated to a file named fileqq.dat, where
54 # qq is the sector number.
55 #
56
57 # The Knitting machine does not parse the returned hex ascii values
58 # unless they are ALL UPPER CASE. Lower case characters a-f appear
59 # to parse az zeros.
60
61 # You will need the (very nice) pySerial module, found here:
62 # http://pyserial.wiki.sourceforge.net/pySerial
63
64 import sys
65 import os
66 import os.path
67 import string
68 from array import *
69 import serial
70
71 version = '1.0'
72
73 #
74 # Note that this code makes a fundamental assumption which
75 # is only true for the disk format used by the brother knitting
76 # machine, which is that there is only one logical sector (LS) per
77 # physical sector (PS). The PS size is fixed at 1280 bytes, and
78 # the brother uses a LS size of 1024 bytes, so only one can fit.
79 #
80
81 class DiskSector():
82 def __init__(self, fn):
83 self.sectorSz = 1024
84 self.idSz = 12
85 self.data = ''
86 self.id = ''
87 #self.id = array('c')
88
89 dfn = fn + ".dat"
90 idfn = fn + ".id"
91
92 try:
93 try:
94 self.df = open(dfn, 'r+')
95 except IOError:
96 self.df = open(dfn, 'w')
97
98 try:
99 self.idf = open(idfn, 'r+')
100 except IOError:
101 self.idf = open(idfn, 'w')
102
103 dfs = os.path.getsize(dfn)
104 idfs = os.path.getsize(idfn)
105
106 except:
107 print 'Unable to open files using base name <%s>' % fn
108 raise
109
110 try:
111 if dfs == 0:
112 # New or empty file
113 self.data = ''.join([chr(0) for num in xrange(self.sectorSz)])
114 self.writeDFile()
115 elif dfs == self.sectorSz:
116 # Existing file
117 self.data = self.df.read(self.sectorSz)
118 else:
119 print 'Found a data file <%s> with the wrong size' % dfn
120 raise IOError
121 except:
122 print 'Unable to handle data file <%s>' % fn
123 raise
124
125 try:
126 if idfs == 0:
127 # New or empty file
128 self.id = ''.join([chr(0) for num in xrange(self.idSz)])
129 self.writeIdFile()
130 elif idfs == self.idSz:
131 # Existing file
132 self.id = self.idf.read(self.idSz)
133 else:
134 print 'Found an ID file <%s> with the wrong size, is %d should be %d' % (idfn, idfs, self.idSz)
135 raise IOError
136 except:
137 print 'Unable to handle id file <%s>' % fn
138 raise
139
140 return
141
142 def __del__(self):
143 return
144
145 def format(self):
146 self.data = ''.join([chr(0) for num in xrange(self.sectorSz)])
147 self.writeDFile()
148 self.id = ''.join([chr(0) for num in xrange(self.idSz)])
149 self.writeIdFile()
150
151 def writeDFile(self):
152 self.df.seek(0)
153 self.df.write(self.data)
154 self.df.flush()
155 return
156
157 def writeIdFile(self):
158 self.idf.seek(0)
159 self.idf.write(self.id)
160 self.idf.flush()
161 return
162
163 def read(self, length):
164 if length != self.sectorSz:
165 print 'Error, read of %d bytes when expecting %d' % (length, self.sectorSz)
166 raise IOError
167 return self.data
168
169 def write(self, indata):
170 if len(indata) != self.sectorSz:
171 print 'Error, write of %d bytes when expecting %d' % (len(indata), self.sectorSz)
172 raise IOError
173 self.data = indata
174 self.writeDFile()
175 return
176
177 def getSectorId(self):
178 return self.id
179
180 def setSectorId(self, newid):
181 if len(newid) != self.idSz:
182 print 'Error, bad id length of %d bytes when expecting %d' % (len(newid), self.id)
183 raise IOError
184 self.id = newid
185 self.writeIdFile()
e9a2d93 Bump version, print it on start
Steve Conklin authored
186 print 'Wrote New ID: ',
187 self.dumpId()
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
188 return
189
190 def dumpId(self):
191 for i in self.id:
192 print '%02X ' % ord(i),
193 print
194
195 class Disk():
196 def __init__(self, basename):
197 self.numSectors = 80
198 self.Sectors = []
199 self.filespath = ""
200 # Set up disk Files and internal buffers
201
202 # if absolute path, just accept it
203 if os.path.isabs(basename):
204 dirpath = basename
205 else:
206 dirpath = os.path.abspath(basename)
207
208 if os.path.exists(dirpath):
209 if not os.access(dirpath, os.R_OK | os.W_OK):
210 print 'Directory <%s> exists but cannot be accessed, check permissions' % dirpath
211 raise IOError
212 elif not os.path.isdir(dirpath):
213 print 'Specified path <%s> exists but is not a directory' % dirpath
214 raise IOError
215 else:
216 try:
217 os.mkdir(dirpath)
218 except:
219 print 'Unable to create directory <%s>' % dirpath
220 raise IOError
221
222 self.filespath = dirpath
223 # we have a directory now - set up disk sectors
224 for i in range(self.numSectors):
225 fname = os.path.join(dirpath, '%02d' % i)
226 ds = DiskSector(fname)
227 self.Sectors.append(ds)
228 return
229
230 def __del__(self):
231 return
232
233 def format(self):
234 for i in range(self.numSectors):
235 self.Sectors[i].format()
236 return
237
238 def findSectorID(self, psn, id):
239 for i in range(psn, self.numSectors):
240 sid = self.Sectors[i].getSectorId()
241 if id == sid:
242 return '00' + '%02X' % i + '0000'
243 return '40000000'
244
245 def getSectorID(self, psn):
246 return self.Sectors[psn].getSectorId()
247
248 def setSectorID(self, psn, id):
249 self.Sectors[psn].setSectorId(id)
250 return
251
252 def writeSector(self, psn, lsn, indata):
253 self.Sectors[psn].write(indata)
254 if psn % 2:
255 filenum = ((psn-1)/2)+1
256 filename = 'file-%02d.dat' % filenum
257 # we wrote an odd sector, so create the
258 # associated file
259 fn1 = os.path.join(self.filespath, '%02d.dat' % (psn-1))
260 fn2 = os.path.join(self.filespath, '%02d.dat' % psn)
261 outfn = os.path.join(self.filespath, filename)
262 cmd = 'cat %s %s > %s' % (fn1, fn2, outfn)
263 os.system(cmd)
264 return
265
266 def readSector(self, psn, lsn):
267 return self.Sectors[psn].read(1024)
268
269 class PDDemulator():
270
271 def __init__(self, basename):
272 self.verbose = True
273 self.noserial = False
274 self.ser = None
275 self.disk = Disk(basename)
276 self.FDCmode = False
277 # bytes per logical sector
278 self.bpls = 1024
279 self.formatLength = {'0':64, '1':80, '2': 128, '3': 256, '4': 512, '5': 1024, '6': 1280}
280 return
281
282 def __del__(self):
283 return
284
285 def open(self, cport='/dev/ttyUSB0'):
286 if self.noserial is False:
287 self.ser = serial.Serial(port=cport, baudrate=9600, parity='N', stopbits=1, timeout=1, xonxoff=0, rtscts=0, dsrdtr=0)
288 if self.ser == None:
289 print 'Unable to open serial device %s' % cport
290 raise IOError
291 return
292
e9a2d93 Bump version, print it on start
Steve Conklin authored
293 def close(self):
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
294 if self.noserial is not False:
295 if ser:
296 ser.close()
297 return
298
299 def dumpchars(self):
300 num = 1
301 while 1:
302 inc = self.ser.read()
303 if len(inc) != 0:
304 print 'flushed 0x%02X (%d)' % (ord(inc), num)
305 num = num + 1
306 else:
307 break
308 return
309
310 def readsomechars(self, num):
311 sch = self.ser.read(num)
312 return sch
313
314 def readchar(self):
315 inc = ''
316 while len(inc) == 0:
317 inc = self.ser.read()
318 return inc
319
320 def writebytes(self, bytes):
321 self.ser.write(bytes)
322 return
323
324 def readFDDRequest(self):
325 inbuf = []
326 # read through a carriage return
327 # parameters are seperated by commas
328 while 1:
329 inc = self.readchar()
330 if inc == '\r':
331 break
332 elif inc == ' ':
333 continue
334 else:
335 inbuf.append(inc)
336
337 all = string.join(inbuf, '')
338 rv = all.split(',')
339 return rv
340
341 def getPsnLsn(self, info):
342 psn = 0
343 lsn = 1
344 if len(info) >= 1 and info[0] != '':
345 val = int(info[0])
346 if psn <= 79:
347 psn = val
348 if len(info) > 1 and info[1] != '':
349 val = int(info[0])
350 return psn, lsn
351
352 def readOpmodeRequest(self, req):
353 buff = array('b')
354 sum = req
355 reqlen = ord(self.readchar())
356 buff.append(reqlen)
357 sum = sum + reqlen
358
359 for x in range(reqlen, 0, -1):
360 rb = ord(self.readchar())
361 buff.append(rb)
362 sum = sum + rb
363
364 # calculate ckecksum
365 sum = sum % 0x100
366 sum = sum ^ 0xFF
367
368 cksum = ord(self.readchar())
369
370 if cksum == sum:
371 return buff
372 else:
373 if self.verbose:
374 print 'Checksum mismatch!!'
375 return None
376
377 def handleRequests(self):
378 synced = False
379 while True:
380 inc = self.readchar()
381 if self.FDCmode:
382 self.handleFDCmodeRequest(inc)
383 else:
384 # in OpMode, look for ZZ
385 #inc = self.readchar()
386 if inc != 'Z':
387 continue
388 inc = self.readchar()
389 if inc == 'Z':
390 self.handleOpModeRequest()
391 # never returns
392 return
393
394 def handleOpModeRequest(self):
395 req = ord(self.ser.read())
e9a2d93 Bump version, print it on start
Steve Conklin authored
396 print 'Request: 0X%02X' % req
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
397 if req == 0x08:
398 # Change to FDD emulation mode (no data returned)
399 inbuf = self.readOpmodeRequest(req)
400 if inbuf != None:
401 # Change Modes, leave any incoming serial data in buffer
402 self.FDCmode = True
403 else:
404 print 'Invalid OpMode request code 0X%02X received' % req
405 return
406
407 def handleFDCmodeRequest(self, cmd):
408 # Commands may be followed by an optional space
409 # PSN (physical sector) range 0-79
410 # LSN (logical sector) range 0-(number of logical sectors in a physical sector)
411 # LSN defaults to 1 if not supplied
412 #
413 # Result code information (verbatim from the Tandy reference):
414 #
415 # After the drive receives a command in FDC-emulation mode, it transmits
416 # 8 byte characters which represent 4 bytes of status code in hexadecimal.
417 #
418 # * The first and second bytes contain the error status. A value of '00'
419 # indicates that no error occurred
420 #
421 # * The third and fourth bytes usually contain the number of the physical
422 # sector where data is kept in the buffer
423 #
424 # For the D, F, and S commands, the contents of these bytes are different.
425 # See the command descriptions in these cases.
426 #
427 # * The fifth-eighth bytes usual show the logical sector length of the data
428 # kept in the RAM buffer, except the third and fourth digits are 'FF'
429 #
430 # In the case of an S, C, or M command -- or an F command that ends in
431 # an error -- the bytes contain '0000'
432 #
433
434 if cmd == '\r':
435 return
436
437 if cmd == 'Z':
438 # Hmmm, looks like we got the start of an Opmode Request
439 inc = self.readchar()
440 if inc == 'Z':
441 # definitely!
e9a2d93 Bump version, print it on start
Steve Conklin authored
442 print 'Detected Opmode Request in FDC Mode, switching to OpMode'
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
443 self.FDCmode = False
444 self.handleOpModeRequest()
445
446 elif cmd == 'M':
447 # apparently not used by brother knitting machine
448 print 'FDC Change Modes'
449 raise
450 # following parameter - 0=FDC, 1=Operating
451
452 elif cmd == 'D':
453 # apparently not used by brother knitting machine
454 print 'FDC Check Device'
455 raise
456 # Sends result in third and fourth bytes of result code
457 # See doc - return zero for disk installed and not swapped
458
459 elif cmd == 'F'or cmd == 'G':
460 #rint 'FDC Format',
461 info = self.readFDDRequest()
462
463 if len(info) != 1:
464 print 'wrong number of params (%d) received, assuming 1024 bytes per sector' % len(info)
465 bps = 1024
466 else:
467 try:
468 bps = self.formatLength[info[0]]
469 except KeyError:
470 print 'Invalid code %c for format, assuming 1024 bytes per sector' % info[0]
471 bps = 1024
472 # we assume 1024 because that's what the brother machine uses
473 if self.bpls != bps:
474 print 'Bad news, differing sector sizes'
475 self.bpls = bps
476
477 self.disk.format()
478
479 # But this is probably more correct
480 self.writebytes('00000000')
481
482 # After a format, we always start out with OPMode again
483 self.FDCmode = False
484
485 elif cmd == 'A':
486 # Followed by physical sector number (0-79), defaults to 0
487 # returns ID data, not sector data
488 info = self.readFDDRequest()
489 psn, lsn = self.getPsnLsn(info)
e9a2d93 Bump version, print it on start
Steve Conklin authored
490 print 'FDC Read ID Section %d' % psn
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
491
492 try:
493 id = self.disk.getSectorID(psn)
494 except:
495 print 'Error getting Sector ID %d, quitting' % psn
496 self.writebytes('80000000')
497 raise
498
499 self.writebytes('00' + '%02X' % psn + '0000')
500
501 # see whether to send data
502 go = self.readchar()
503 if go == '\r':
dde56d2 Fix an error in response to a request for track ID
Steve Conklin authored
504 self.writebytes(id)
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
505
506 elif cmd == 'R':
507 # Followed by Physical Sector Number PSN and Logical Sector Number LSN
508 info = self.readFDDRequest()
509 psn, lsn = self.getPsnLsn(info)
e9a2d93 Bump version, print it on start
Steve Conklin authored
510 print 'FDC Read one Logical Sector %d' % psn
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
511
512 try:
513 sd = self.disk.readSector(psn, lsn)
514 except:
515 print 'Failed to read Sector %d, quitting' % psn
516 self.writebytes('80000000')
517 raise
518
519 self.writebytes('00' + '%02X' % psn + '0000')
520
521 # see whether to send data
522 go = self.readchar()
523 if go == '\r':
524 self.writebytes(sd)
525
526 elif cmd == 'S':
527 # We receive (optionally) PSN, (optionally) LSN
528 # This is not documented well at all in the manual
529 # What is expected is that all sectors will be searched
530 # and the sector number of the first matching sector
531 # will be returned. The brother machine always sends
532 # PSN = 0, so it is unknown whether searching should
533 # start at Sector 0 or at the PSN sector
534 info = self.readFDDRequest()
535 psn, lsn = self.getPsnLsn(info)
e9a2d93 Bump version, print it on start
Steve Conklin authored
536 print 'FDC Search ID Section %d' % psn
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
537
538 # Now we must send status (success)
539 self.writebytes('00' + '%02X' % psn + '0000')
540
541 #self.writebytes('00000000')
542
543 # we receive 12 bytes here
544 # compare with the specified sector (formatted is apparently zeros)
545 id = self.readsomechars(12)
e9a2d93 Bump version, print it on start
Steve Conklin authored
546 print 'checking ID for sector %d' % psn
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
547
548 try:
549 status = self.disk.findSectorID(psn, id)
550 except:
551 print "FAIL"
552 status = '30000000'
553 raise
554
e9a2d93 Bump version, print it on start
Steve Conklin authored
555 print 'returning %s' % status
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
556 # guessing - doc is unclear, but says that S always ends in 0000
557 # MATCH 00000000
558 # MATCH 02000000
559 # infinite retries 10000000
560 # infinite retries 20000000
561 # blinking error 30000000
562 # blinking error 40000000
563 # infinite retries 50000000
564 # infinite retries 60000000
565 # infinite retries 70000000
566 # infinite retries 80000000
567
568 self.writebytes(status)
569
570 # Stay in FDC mode
571
572 elif cmd == 'B' or cmd == 'C':
573 # Followed by PSN 0-79, defaults to 0
574 # When received, send result status, if not error, wait
575 # for data to be written, then after write, send status again
576 info = self.readFDDRequest()
577 psn, lsn = self.getPsnLsn(info)
e9a2d93 Bump version, print it on start
Steve Conklin authored
578 print 'FDC Write ID section %d' % psn
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
579
580 self.writebytes('00' + '%02X' % psn + '0000')
581
582 id = self.readsomechars(12)
583
584 try:
585 self.disk.setSectorID(psn, id)
586 except:
587 print 'Failed to write ID for sector %d, quitting' % psn
588 self.writebytes('80000000')
589 raise
590
591 self.writebytes('00' + '%02X' % psn + '0000')
592
593 elif cmd == 'W' or cmd == 'X':
594 info = self.readFDDRequest()
595 psn, lsn = self.getPsnLsn(info)
e9a2d93 Bump version, print it on start
Steve Conklin authored
596 print 'FDC Write logical sector %d' % psn
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
597
598 # Now we must send status (success)
599 self.writebytes('00' + '%02X' % psn + '0000')
600
601 indata = self.readsomechars(1024)
602 try:
603 self.disk.writeSector(psn, lsn, indata)
604 except:
605 print 'Failed to write data for sector %d, quitting' % psn
606 self.writebytes('80000000')
607 raise
608
609 self.writebytes('00' + '%02X' % psn + '0000')
610
611 else:
612 print 'Unknown FDC command <0x02%X> received' % ord(cmd)
613
614 # return to Operational Mode
615 return
616
617 # meat and potatos here
618
619 if len(sys.argv) < 3:
620 print '%s version %s' % (sys.argv[0], version)
621 print 'Usage: %s basedir serialdevice' % sys.argv[0]
622 sys.exit()
623
624 print 'Preparing . . . Please Wait'
625 emu = PDDemulator(sys.argv[1])
626
627 emu.open(cport=sys.argv[2])
628
e9a2d93 Bump version, print it on start
Steve Conklin authored
629 print 'PDDtmulate Version 1.1 Ready!'
dde56d2 Fix an error in response to a request for track ID
Steve Conklin authored
630 try:
631 while 1:
632 emu.handleRequests()
633 except (KeyboardInterrupt):
e9a2d93 Bump version, print it on start
Steve Conklin authored
634 pass
febe4fc @brianredbeard initial checkin including test setup for becky's knitting machine and
brianredbeard authored
635
636 emu.close()
Something went wrong with that request. Please try again.