A small, simple, modern(ish) and incomplete ZMODEM implementation, for use in resource-constrained environments, or wherever you can find a use for it really.
Yes, I am aware that it is the twenty-first century, and ZMODEM is hardly the state of the art. I wrote this for two reasons:
-
I wanted a fun, period-appropriate protocol I could use to load programs into my retro computer (https://github.com/roscopeco/rosco_m68k) without having to pull the ROM chips for each iteration.
-
I haven't implemented Zmodem before and wanted to explore how it worked.
As far as possible, I avoided referring to other ZMODEM implementations and just tried to
implement this using the spec (http://pauillac.inria.fr/~doligez/zmodem/zmodem.txt).
The spec is a bit vague on certain things so I spent quite a bit of time reading old
usenet and forum posts, experimenting, and occasionally peeking at the original zm.c
implementation to see how things were supposed to work.
Right now, this is very limited. For one thing, it can only receive. For another, it doesn't deal with errors in a completely correct way (it does however do the minimum needed to get the other end to resend bad packets).
Expanding it to support sending probably wouldn't be all that much work, but I don't need it right now so I haven't done it.
Improving the error handling is a WIP, but it's 'good enough' for my purposes right now (usage with a 1980's UART).
Other notable things:
- It doesn't do any memory allocation, so it can be used where malloc is unavailable.
- It's sort-of optimised for use in 16/32-bit environments (I wrote it with M68010 as the primary target)
- It doesn't support XON/XOFF flow control (it does enough that it might work with it, but it's not tested and it certainly won't shut up when XOFF tells it to!).
- It doesn't support any of the advanced features of the protocol (compression etc)
Additionally, the included sample has even more limitations, such as:
- It ignores most of the file information in block 0 (it only takes notice of the filename, not the size, mode, etc)
- Related to the above, it doesn't have any support for rejecting files that are too large!
- It doesn't support the (optional) ZSINIT frame and will just ignore it
- It doesn't support resume
- It has a ton of other limitations I'm too lazy to list right now...
- ... but it does work for the simple case of receiving data.
Note that this last set aren't limitations of the library per se - it's more that they aren't provided by the library, and the example program doesn't implement them either.
There are some unit tests included. They don't cover everything, but they're a start, and are likely an improvement over some of the other 30+-year-old code out there.
make test
A sample application is included that will receive a file. You can use sz
or minicom
or
something to send a file, probably using socat
or similar to set up a virtual link.
E.g. start socat:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
You'll probably need to change the source to open the correct device.
To build the application, just do:
make
and then run it:
./rz <device>
Now you can use e.g. sz
to send a file to it:
sz /path/to/somefile > /dev/pts/1 < /dev/pts/1
Be aware that the sample will blindly overwrite files in the current directory if it receives a file with the same name!
To use, you'll need to implement two functions in your code:
ZRESULT zm_recv();
ZRESULT zm_send(uint8_t chr);
These should return one of the codes used in the rest of the library
to indicate an error if one occurs (see ztypes.h
for error codes).
If there isn't a problem, recv
should return the next byte from the serial
link. send
should return OK
.
The ZRESULT
type encodes various things depending on the result of the
function. ztypes.h
defines a few macros that can help with decoding these
results (e.g. IS_ERROR
, IS_FIN
, ZVALUE
etc).
If using this as part of a larger project, you'll probably want to just pull the source into your existing build. If, for some reason, you just want to build the objects for m68k, and you have a cross-compiler, you can do:
make -f Makefile.m68k
I use this mostly for testing that changes will still build using my cross toolchain - it probably isn't actually useful for anything...
When cross-compiling, especially for a freestanding environment, you might want
to define ZEMBEDDED
as this will stop the library pulling in any stdlib
dependencies. In this case, you're expected to provide two functions:
memset
(I might provide a naive default implementation of this later)strcmp
(this will go away in future)
If you define ZDEBUG
and/or ZTRACE
when compiling, you can get
a lot of output from the library as things happen. This can be useful
if things aren't going your way, but they do require a printf
.
If you don't have a printf
but have something similar, edit the
defines at the bottom of ztypes.c
to suit.
The CRC calculation code comes from the pkg-sbbs
project on Github
(https://github.com/ftnapps/pkg-sbbs), and is licensed under the GNU GPL.
Because that code works well, is fast, and I have zero interest in calculating
CRCs myself I took the liberty of grabbing the code and reusing it here.
That code is copyright (c) 2000 Rob Swindell. See the included LICENSE.smblib
for license details.
And obviously much respect to the late Chuck Fosberg, without whom there would be no protocol to implement.