BBC Software Applications Page
A Single-Path SDR Engine Example
(Last Mod: 27 November 2010 21:37:20 )
The purpose of this example is to show how simple blocks can be implemented to create a single-path radio. Recalling that the term "radio" is merely a label and that the SDR Engine can be used to construct more generalized processing engines, we will underscore that point by implementing an engine that implements a Caesar Shift Cipher.
When finished, the program will ask the user a shift amount (i.e., the encryption key) and for two file names (an input and an output file). It will then build an SDR radio that will read the data from the file and encipher it by shifting each letter in the file by the amount specified. In other words, if the shift is 3 then all 'A's in the file will be replaced by 'D's, all 'B's will be replaced by 'E's, and so forth, wrapping around so that all 'Z's are replaced by 'C's.
While this is a workable description of the cryptographic algorithm, there are additional issues that our engine must deal with. For instance, what if the input file contains characters other than uppercase letters? We need our engine to discard any character that is not a letter and to convert all lowercase letters to upper case. By doing this, the ciphertext is a stream of uppercase characters with no hint of word boundaries or capitalization, which would give an attacker a huge advantage in attempting to break the cipher (not that a Caesar shift cipher is hard to break, but its the principle that counts).
The output will consist of five letter groups, all upper case, with at most five groups on a given line. A format such as this greatly aids any humans that would have to work with the ciphertext since humans have difficulty working with long, unbroken strings of characters. For instance, image a Morse code operator that had to transmit the ciphertext; the structured grouping makes it much easier for them to keep track of where they are in the message.
Finally, the output will be padded so that the last block is a complete set of five characters. The padding will consist of appending a 'Z' to the last of the ciphertext and then appending 'X's to fill out the remainder of the final block. Note that the ciphertext is padded, not the plaintext; padding the plaintext in this manner would immediately reveal the key to an attacker. A rule such as this might help a human in a couple of ways: first, it would allow them to always work with blocks of five characters, which might avoid throwing a radio operator off their stride; second, it offers a degree of assurance that the entire message was received, as opposed to simply losing radio reception partway through. These potential benefits notwithstanding, the primary reason for including this rule here is to add a more complex processing task to the engine and, as we shall discover, this is by far the most difficult task in this example.
We will implement the radio using blocks that are very narrowly defined. To the degree possible, each block will perform one very specific task and nothing more.
Here are the tasks assigned to each block:
Each of these blocks is a single-input, single-output block and therefore the overall radio has a single data path and the very simple structure shown below.
Note that some of these blocks could easily be combined, such as adding the shift amount and reducing the result modulo 26. However, in addition to getting more practice implementing blocks, a block that does nothing but add an offset to a stream of data and a block that does nothing but modular reduction on a stream of data are each more likely to be usable in another context than one that does both. Similarly, it would be simple to construct a block that read in an arbitrary ASCII data stream and output only numbers between 0-25 for each alphabetic character encountered. But, once again, three blocks that are each more narrowly defined are much more likely to find other applications that one that is more broadly defined.
Another thing to notice is that the blocks can be greatly simplified if they are allowed to assume that the blocks before them have done their job. However, clearly taking advantage of this is counter to the idea of making blocks that are easily reused. For instance, a block that converts alphabetic characters to uppercase relying on a trick that can be played if the input data is assumed to be alphabetic is not as easily used elsewhere as one that doesn't make this assumption. However, this is a tradeoff that is endemic with most signal processing, embedded, and/or real time systems. As long as the limitations of a block are clearly understood and documented, then later users can easily modify it to remove some of those limitations, if needed. In our radio here, we will leverage all of the tricks we can, if for no other reason than to expose people the kinds of games that can be played. Later, we may look at how using a different set of tricks can simplify the processing if we reorder some of the tasks.
Although not shown here, the Source and Sink blocks started off very different, and much simpler, than the final result. Initially, the Source block did nothing except output the ASCII codes for the printable characters one-by-one. Similarly, the Sink block initially did nothing more than pop a character from its input FIFO, if one was available, and out it to the screen. That was more than sufficient to develop all of the code for the remainder of the blocks without ever having to mess with reading and writing files.
Now that we have the basic architecture of our radio laid out, we are in a
position to write a simple "driver" program to take our radio for a test drive
by instantiating the radio and running it. We will place all of the code for our radio is its
own file and provide the user with a function called caesar_build()
that will accept the input parameters and return a pointer to a radio that is
ready to run. If there are any errors during the build process, we will print
out some basic diagnostic information, delete the radio, and return a NULL
pointer. Although it would be good practice for a programmer using our build
function to test that it returned a valid pointer before attempting to run the
radio, the radio's functions are all at least safe to run with a NULL radio
pointer.
With our build function to draw from, the file containing our main()
function becomes very lean, indeed.
#include <stdio.h> #include <stdlib.h> #include "caesar.h"
int main(void)
{SDR *radio;
int shift; key = 3; radio = caesar_build(key, "plaintext.txt", "ciphertext.txt");
SDR_run(radio);
SDR_delete(radio); radio = caesar_build(-key, "ciphertext.txt", "decipheredtext.txt");
SDR_run(radio);
SDR_delete(radio);
printf("\n\nHit RETURN to exit");
getc(stdin); return EXIT_SUCCESS;
}
Although we stated in our problem description that our program was going to ask the user to enter this information, there is nothing to be gained by cluttering up our code with those routine tasks and so we have chosen to simply pass typical values to the build function instead. Hint: Don't try to play this game when turning in homework!
As a test that our radio is doing what it should, our driver actually builds two radios: the first encrypts the original plaintext and the second decrypts the ciphertext by using the fact that encrypting with a key that is the negative of the original key will undo the original encryption. However, notice that the radio doesn't know that it is decrypting and so it not only doesn't undo the original padding, but it adds its own! However, this is easy to spot in the deciphered text file (easier, in fact, than reading the recovered plaintext). It will be left as an exercise for the interested reader to see how the radio might be modified to strip the padding when it is decrypting a message. Hint: the fact that the key is negative can tell the radio that it is decrypting. Now consider what would happen if a block were added immediately after the source block that, when decrypting, held the last five characters in a buffer so that it could detect the end of the stream before it passes them on and, once the end of the stream is found, examines the last five and only pushes the ones that are not padding characters. Of course, the normal padding block would also have to be tweaked so that it knew not to pad a decrypted message.
Before looking at the caesar_build()
code, we will first
look at the structure we have chosen to use for the radio's data block, shown
below. The build function will take the file names and convert them to
appropriate FILE pointers that will be stored in this data block. Remember, the
radio's data block is stored at the top radio level and all blocks within the
radio have access to it. Don't confuse this with each block's individual data
blocks, which are only accessible by that block (at least without doing some
pretty involved coding gymnastics).
//---------------------------------------------------------------------------
// Radio Data Block
//---------------------------------------------------------------------------
typedef struct CAESAR_DB CAESAR_DB;
struct CAESAR_DB
{
FILE *fp_in;
FILE *fp_out;
int key;
};
If the user passes a NULL pointer for the input file name, then input will be
taken from the keyboard. Similarly, if NULL is passed for the output file name,
the output will go to the screen. If we want to tell the build function to use
the simple ASCII code generator as a source, we will pass it an empty string as
an input file name. If we pass an empty string as the output file name, the sink
block will simply discard the data and not output anything. The following is the code for the caesar_build()
function.
Notice that the first half is simply dealing with opening up the necessary
files, which is why the initial cut at the radio used much simpler source and
sink blocks. The remainder of the function is really nothing more than a minor modification of the main()
function in the Overview section of the main page.
SDR *caesar_build(int key, char *infilename, char *outfilename) { SDR *radio; CAESAR_DB *db; // Radio-level Data Block //----------------------------------------------------------------------- // Prepare the radio-level data block
//----------------------------------------------------------------------- db = malloc(sizeof(CAESAR_DB)); if (!db) return NULL; // Copy the key into the data block db->key = key; // Set up the input file pointer if (NULL == infilename) // NULL: Use the keyboard db->fp_in = stdin; else if (0 == *infilename) // "": Use the internal test generator db->fp_in = NULL; else // Open the specified file db->fp_in = fopen(infilename, "rt"); if ((infilename)&&(0!=*infilename)&&(!db->fp_in)) // File didn't open { free(db); return NULL; } // Set up the output file pointer if (NULL == outfilename) // NULL: Send output to screen db->fp_out = stdout; else if (0 == *outfilename) // "": Suppress output completely db->fp_out = NULL; else // Open the specified file db->fp_out = fopen(outfilename, "wt"); if ((outfilename)&&(!db->fp_out)) // File didn't open { free(db); if ((db->fp_in)&&(!(stdin == db->fp_in))) fclose(db->fp_in); return NULL; } //----------------------------------------------------------------------- // Build the Radio //----------------------------------------------------------------------- // Instantiate the radio chassis radio = SDR_new(db, NULL); // Define Blocks SDR_add_block(radio, "source", source); SDR_add_block(radio, "alpha", alpha); SDR_add_block(radio, "upper", upper); SDR_add_block(radio, "tonum", tonum); SDR_add_block(radio, "shift", shift); SDR_add_block(radio, "mod26", mod26); SDR_add_block(radio, "tochar", tochar); SDR_add_block(radio, "pad5", pad5); SDR_add_block(radio, "group5", group5); SDR_add_block(radio, "line5", line5); SDR_add_block(radio, "sink", sink); // Connect Blocks SDR_connect(radio, "source:0", "alpha:0", 1, 16); SDR_connect(radio, "alpha:0", "upper:0", 1, 16); SDR_connect(radio, "upper:0", "tonum:0", 1, 16); SDR_connect(radio, "tonum:0", "shift:0", 1, 16); SDR_connect(radio, "shift:0", "mod26:0", 1, 16); SDR_connect(radio, "mod26:0", "tochar:0", 1, 16); SDR_connect(radio, "tochar:0", "pad5:0", 1, 16); SDR_connect(radio, "pad5:0", "group5:0", 1, 16); SDR_connect(radio, "group5:0", "line5:0", 1, 16); SDR_connect(radio, "line5:0", "sink:0", 1, 16); // Tell the radio which block to monitor for end-of-run SDR_add_monitor(radio, "sink"); //Reset the radio SDR_reset(radio); // If there have been any errors, output the SDR structure report, // delete the radio and return a NULL pointer if (SDR_errors(radio)) { SDR_report(radio, stdout); SDR_delete(radio); radio = NULL; } return radio; }
The source block was implemented in three stages. As described previously, the first stage was simply a hardcoded character generator that produced the ASCII codes for each of the printing characters. This was more than adequate to serve as the source throughout the remainder of the radio's development. Once the entire radio was up and running, the source block was revisited twice: once to accept the plaintext from the keyboard and a second time to read the plaintext from a text file. This section discusses the initial, hardcoded plaintext source.
An examination of the ASCII chart shows that all of the printing characters are grouped together and begin with the space character (' ' = ASCII code 0x20 = 32) and end with the tilde ('~' = ASCII code 0x7E = 126). Thus our simple generator merely needs to initialize a variable to 0x20 and, on each iteration, test to see if there is room in the output port for one more character and, if there is, push the value presently stored in the variable, increment it, and test to see if the new value is still a printing character. If it isn't, then the block is finished and should mark itself as inactive.
int source_old(SDRB *p, int when)
{
int retvalue;
unsigned char *cp;
retvalue = 0;
switch (when)
{
case SDRB_INI:
cp = malloc(sizeof(char)); // Storage for one character.
cp = SDRB_set_data(p, cp); // Make it the data block.
if (!cp)
retvalue = 1; // Flag the allocation error.
else
*cp = ' '; // First printing character
break;
case SDRB_RST:
break;
case SDRB_RUN:
cp = SDRB_get_data(p); // Assume it is valid.
if (!SDRB_is_full(p, 0)) // Is there room for output
{
SDRB_push(p, 0, cp); // Output current character
(*cp)++; // and advance to next
}
if (*cp > '~') // Past the printing characters?
SDRB_set_active(p, FALSE); // If so, deactivate the block
break;
case SDRB_DEL:
free(SDRB_get_data(p)); // Free the data block and keep
SDRB_set_data(p, NULL); // the radio from also freeing it.
break;
default:
retvalue = 1;
}
return retvalue;
}
Notice that most of the code is taken directly from the block function template. There are few things worth noting in the above code, particularly for those not familiar with working with pointers and/or dynamic memory allocation.
Normally, good programming practice dictates that a pointer be NULL checked before being dereferenced. It might appear that we fail to do this with the SDR structure pointer passed to the function, however all of the SDR_*() and SDRB_*() functions perform these checks and will not dereference a NULL pointer. We could add an explicit check which would have the benefit of assuring someone looking through the code that it hadn't been overlooked. Peforming such a check is harmless, beyond consuming some processor cycles. However, since this is modeling a real-time system, we are going to pay attention to trying to trim unnecessary cycle usage, particularly in the runtime sections of our block functions.
We do perform a NULL pointer check in the initialization phase on the memory that was allocated prior to dereferencing the pointer to it, but we do so in a way that, at first glance, may not make sense.
cp = malloc(sizeof(char));
cp = SDRB_set_data(p, cp);
if (!cp)
retvalue = 1;
The reason that we overwrite the value of the pointer with the return value
of the
SDRB_set_data(p, cp)
function is so that we ensure that we perform
the NULL check on the value of the pointer actually saved, and not just the
value of the pointer that we intended to save.
Another thing that might raise an eyebrow is that we do not perform a similar check when directly dereferencing this same pointer in the runtime phase. We could, and arguably should. The rationale here is that a failure to allocate the memory in the initialization phase was flagged as a radio error (due to the non-zero return value) and the programmer becomes responsible for ensuring that the radio is simply not run under these conditions. In fact, unless the programmer is digging around under the hood in the code, the normal radio construction process ensures that this is the case. Therefore we choose to minimize the housekeeping burden that we place on our runtime procedures. There is certainly going to be a school of thought that says that you go ahead and accept the additional burden in order to better bulletproof your code, at least until you actually get to a point where you simply can't afford the overhead. This argument has considerable merit.
One line of code above bears particular mentioning for those unfamiliar with working with pointers. Since new programmers are generally taught that a dereferenced pointer can be used just like a variable of the data type being pointed to, it would seem natural to work with integer associated with a pointer as follows:
int *ptr;
...
*ptr = 4;
*ptr = *ptr + 1;
*ptr += 1;
*ptr++;
...
While the first statement would correctly set the integer equal to 4, the second statement would increment it to 5, and the third would increment it to 6, the final statement would not have the desired effect. The value of the integer would not be altered. The reason is that the increment operator and the structure dereference operator have the same operator precedence, but are right associative, meaning that the final statement binds as:
*(ptr++);
Thus the value of the pointer, not what it points to, will be incremented and, if that same pointer is dereferenced further on, it would no longer be a valid pointer. The compiler may be nice and throw a warning since the statement is dereferencing a pointer and not doing anything with the resulting value; it's legal, but it makes no sense, so you might get a warning. To get this statement to bind correctly, you simply need to force the compiler to dereference the pointer before incrementing the result by writing it as follows:
(*ptr)++;
The lesson here is that you should never be afraid to throw lots of parentheses at code that is doing anything out of the ordinary -- don't let the compiler do too much of your thinking for you -- and never ignore compiler warnings: just because the code compiles and runs doesn't mean it's correct. Almost all warnings are, in fact, logic errors of some kind.
The original Sink block was extremely simple in that all it did was check to see if any data was available from its input port and, if there was, it popped one character from the port and put it to the screen. The Sink block is even easier to implement, the only function we need to write is the run function and all it will do is pop one byte from its input port and output it to the screen.
int sink_old(SDRB *p, int when)
{
int retvalue;
retvalue = 0; // Set retvalue to 1 to indicate errors. switch (when) { case SDRB_INI: // Initialation Behavior break; case SDRB_RST: // Reset Behavior break; case SDRB_RUN: // Runtime Behavior if (!SDRB_is_empty(p, 0)) putc(*(char *)SDRB_pop(p, 0, NULL), stdout); break; case SDRB_DEL: // Deletion Behavior break; default: // Should never reach this point. retvalue = 1; } return retvalue;
}
The only thing that has been added to the template block function is the single if() statement in the RUN section. For those to whom the first argument to the putc() function appears difficult to comprehend, recall that the SDRB_pop() function returns a pointer to the data being popped from the FIFO. Passing a NULL data pointer to this function prevents it from copying and data from the FIFO to the data block, but since it returns a pointer to the data within the FIFO, the user can access it directly. Since that data can be of any type, the pointers within the FIFO are stored as void pointers and must therefore be type cast prior to being dereferenced. An equivalent way of writing this if() block would have been the following:
char c;
...
if (!SDRB_is_empty(p, 0))
{
SDRB_pop(p, 0, &c);
putc(c, stdout);
}
...
Depending on how good the compiler is at optimizing code, the original
version may run quite a bit faster because it does not need to copy data to any
automatic variables on the stack. Also, when a pointer is supplied to the
SDRB_pop()
function, the function copies the contents of the
FIFO's memory into the block pointed to.
The final source block represents a substantial upgrade to its original
capabilities. As described previously, it can now accept input from a text file,
per the original problem statement, or can accept text one character at a time
directly from the keyboard. It should be noted that this capability requires
using functions that are not ANSI-C compliant, namely the _kbhit()
and _getch()
functions from <console.h>
.
While these functions and this header file are extremely common, they are not
supported by all compilers and are not always supported the same way, even when
they are. Thus using them represents a reduction in the portability of the
block.
Although the value of the input file pointer stored in the radio source block
is sufficient to determine where to get data during the runtime phase, this
information is determined once during the initialization phase and the result
stored in the mode
element of the source's data block reduce
runtime overhead.
Finally, the starting and stopping values for the hardcoded runtime generator are stored in the source's data block. If programmer wishes to change the range of values produced, as they might want to do when developing blocks that rely on certain characters being filtered out by previous blocks, they can easily change these values, although they do have to change the code to do so. This was purely a quick and dirty development feature that was maintained in the final version simply because there was no compelling reason to remove it.
typedef struct SOURCE_DB SOURCE_DB;
struct SOURCE_DB
{
int mode;
FILE *fp;
unsigned char c;
char start, stop;
};
int source(SDRB *p, int when)
{
int retvalue;
SOURCE_DB *db;
unsigned char c;
int c_int;
SDR *radio;
CAESAR_DB *radio_db;
retvalue = 0;
switch (when)
{
case SDRB_INI:
// Create a Data Block if one doesn't already exist.
db = SDRB_get_data(p);
if (!db)
{
db = malloc(sizeof(SOURCE_DB));
SDRB_set_data(p, db);
// Default (Use internal generator)
db->mode = 0;
db->fp = NULL;
db->start = ' ';
db->stop = '~';
db->c = db->start;
// Update with data in the radio's data block
radio = SDRB_radio(p);
radio_db = SDR_datablock(radio);
if (radio_db)
{
if (radio_db->fp_in)
{
db->fp = radio_db->fp_in;
if (stdin == db->fp)
db->mode = 1;
else
db->mode = 2;
}
}
}
// Error if one doesn't exist now.
retvalue = !SDRB_get_data(p);
break;
case SDRB_RST:
break;
case SDRB_RUN:
db = SDRB_get_data(p);
switch (db->mode)
{
case 0: // Fixed pattern from Start to Stop
if (db->c <= db->stop)
{
if (!SDRB_is_full(p, 0))
{
SDRB_push(p, 0, &(db->c));
(db->c)++;
}
}
else
SDRB_set_active(p, FALSE);
break;
case 1: // Keyboard input - ^Z to stop
if (_kbhit() && !SDRB_is_full(p, 0))
{
c = _getch();
if (('Z'&0x1F) == c)
SDRB_set_active(p, FALSE);
else
SDRB_push(p, 0, &c);
}
break;
case 2: // Input from text file
if (!SDRB_is_full(p, 0))
{
c_int = fgetc(db->fp);
c = (unsigned char) c_int;
if (EOF == c_int)
SDRB_set_active(p, FALSE);
else
SDRB_push(p, 0, &c);
}
break;
default:
retvalue = 1;
}
break;
case SDRB_DEL:
db = SDRB_get_data(p);
if (db)
{
if ((db->fp)&&(stdin!=db->fp))
fclose(db->fp);
free(db);
SDRB_set_data(p, NULL);
}
break;
default:
retvalue = 1;
}
return retvalue;
}
The final Sink block is only slightly more involved than the quick-and-dirty version. The one important feature to note in this code is that if the output file pointer is NULL, indicating that the user entered an empty string as the output file name and, therefore, wishes to simply suppress output, that the block must still monitor the input port and, when it has data, pop it out. Failure to do this will cause the FIFO to fill up and stall the block feeding it and, eventually, the entire radio.
//--------------------------------------------------------------------------- // Sink Data Block and Function //---------------------------------------------------------------------------
int sink(SDRB *p, int when)
{
int retvalue; FILE *fp;
retvalue = 0; // Set retvalue to 1 to indicate errors. switch (when) { case SDRB_INI: // Initialation Behavior break; case SDRB_RST: // Reset Behavior break; case SDRB_RUN: // Runtime Behavior fp = ((CAESAR_DB *)SDR_datablock(SDRB_radio(p)))->fp_out; if (SDRB_is_active(p)) { if (!SDRB_is_empty(p, 0)) if (fp) putc(*(char *)SDRB_pop(p, 0, NULL), fp); else SDRB_pop(p, 0, NULL); } else { if ((fp)&&(stdout != fp)) fclose(fp); } break; case SDRB_DEL: // Deletion Behavior break; default: // Should never reach this point. retvalue = 1; } return retvalue;
}
Most of the pipeline blocks, i.e., blocks that have both input and output ports, are extremely simple. In fact, most of them add only one or two lines of code to the RUN section of the template block function. By far, the most complicated one is the one that pads the data stream with a 'Z' and then as many 'X's as are required for the total count to be a multiple of 5. The comments should be sufficient to understand how most of the blocks work. The padding function is left uncommented as a puzzle for the interested reader, as they say.
//---------------------------------------------------------------------------
// Pipeline Block Functions
//---------------------------------------------------------------------------
// Alphabetic ASCII characters are in the ranges [0x41:0x5A] and [0x61:0x7A]
int alpha(SDRB *p, int when)
{
int retvalue;
char c;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
if ((0x41 <= c)&&(c <= 0x5A)||(0x61 <= c)&&(c <= 0x7A))
SDRB_push(p, 0, &c);
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
// Given an alphabetic ASCII code,
// clearing bit 5 (0x20) will make it to uppercase
int upper(SDRB *p, int when)
{
int retvalue;
char c;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
c &= ~0x20;
SDRB_push(p, 0, &c);
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
// Given an upper case alphabetic ASCII code,
// subtracting 'A' will make it a number from 0 to 25
int tonum(SDRB *p, int when)
{
int retvalue;
char c;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
c -= 'A';
SDRB_push(p, 0, &c);
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
// Add the shift amount to each number (don't worry about wraparound)
int shift(SDRB *p, int when)
{
int retvalue;
char c;
int key;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
SDRB_set_data(p, malloc(sizeof(int)));
if (!SDRB_get_data(p))
return 1;
key = ((CAESAR_DB *)SDR_datablock(SDRB_radio(p)))->key;
if (key < 0)
key = 26+key;
*((int *) SDRB_get_data(p)) = key;
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
c += *((int *) SDRB_get_data(p));
SDRB_push(p, 0, &c);
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
// Reduce a number between 0 and 51 modulo 26
int mod26(SDRB *p, int when)
{
int retvalue;
char c;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
if (c >= 26)
c -= 26;
SDRB_push(p, 0, &c);
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
// Given a number from 0 to 25,
// adding 'A' will make it an upper case alphabetic ASCII code,
int tochar(SDRB *p, int when)
{
int retvalue;
char c;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
c += 'A';
SDRB_push(p, 0, &c);
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
int pad5(SDRB *p, int when)
{
int retvalue;
char c;
int *count;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
SDRB_set_data(p, malloc(sizeof(int)));
if (!SDRB_get_data(p))
return 1;
*((int *) SDRB_get_data(p)) = 9;
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
count = (int *) SDRB_get_data(p);
if (SDRB_is_active(p))
{
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
SDRB_push(p, 0, &c);
if (*count > 5)
(*count)--;
else
(*count) = 9;
}
}
else
{
if (*count >= 5)
{
if (!SDRB_is_full(p, 0))
{
c = 'Z';
SDRB_push(p, 0, &c);
(*count) = *count-5;
}
SDRB_set_active(p, TRUE);
}
else
{
if (*count)
{
if (!SDRB_is_full(p, 0))
{
c = 'X';
SDRB_push(p, 0, &c);
(*count)--;
}
SDRB_set_active(p, TRUE);
}
}
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
// After five characters, generate a space instead of copying the input.
int group5(SDRB *p, int when)
{
int retvalue;
char c;
int *count;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
SDRB_set_data(p, malloc(sizeof(int)));
if (!SDRB_get_data(p))
return 1;
*((int *) SDRB_get_data(p)) = 5;
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
count = (int *) SDRB_get_data(p);
if (*count) // copy character
{
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
SDRB_push(p, 0, &c);
(*count)--;
}
}
else
{
if (!SDRB_is_full(p, 0))
{
c = ' ';
SDRB_push(p, 0, &c);
(*count) = 5;
}
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
//---------------------------------------------------------------------------
// Replace every fifth space with a newline character.
int line5(SDRB *p, int when)
{
int retvalue;
char c;
int *count;
retvalue = 0; // Set retvalue to 1 to indicate errors.
switch (when)
{
case SDRB_INI: // Initialation Behavior
SDRB_set_data(p, malloc(sizeof(int)));
if (!SDRB_get_data(p))
return 1;
*((int *) SDRB_get_data(p)) = 4;
break;
case SDRB_RST: // Reset Behavior
break;
case SDRB_RUN: // Runtime Behavior
count = (int *) SDRB_get_data(p);
if ((!SDRB_is_empty(p, 0))&&(!SDRB_is_full(p, 0)))
{
SDRB_pop(p, 0, &c);
if (' ' == c)
{
if (*count)
(*count)--;
else
{
c = '\n';
*count = 4;
}
}
SDRB_push(p, 0, &c);
}
break;
case SDRB_DEL: // Deletion Behavior
break;
default: // Should never reach this point.
retvalue = 1;
}
return retvalue;
}
The observant reader may have noticed that, when inserting the spaces so that we have five characters in a block, we count down from 5 but, when inserting the newlines so that we have five blocks on a row, we only count down from 4. The reason for the difference is that counting down from 5 gives six events (including 0) and we output six characters per block because we are adding the space character to the stream. However, when inserting the newline characters we are replacing a space character that is already in the stream, hence we are only dealing with five events per line (four spaces and one newline) and hence need to count down from 4.